nix: runtime option, acp.* and an opencode preset
services.hyperhive.agent.runtime ("claude" default | "acp") and
acp.{command,args,env}, rendered into HIVE_RUNTIME / HIVE_ACP_* only
for acp, so a claude agent's unit is unchanged. acp implies useApiKey.
acp.presets.opencode runs `opencode acp` from nixpkgs against an
OpenAI-compatible provider from acp.opencode.{provider,model,
contextWindow,outputLimit}: the config is rendered to the store with
the API key as an {env:VAR} reference, so the key is read at runtime
from backendEnvironmentFile. OPENCODE_PERMISSION denies opencode's
built-in bash, task, todowrite and websearch, and makes webfetch ask.
Refs #4391
This commit is contained in:
parent
f0110be76c
commit
1b24edf4b4
1 changed files with 215 additions and 0 deletions
|
|
@ -11,6 +11,51 @@
|
|||
let
|
||||
userName = config.services.hyperhive.agent.user.name;
|
||||
homeDir = "/home/${userName}";
|
||||
acp = config.services.hyperhive.agent.acp;
|
||||
isAcp = config.services.hyperhive.agent.runtime == "acp";
|
||||
preset = if acp.preset == null then null else acp.presets.${acp.preset} or null;
|
||||
oc = acp.opencode;
|
||||
|
||||
# Tools opencode must not offer, mirroring claude's built-in allow-list
|
||||
# (hive_sh4re::permissions): no built-in shell (shell is `mcp__bash__run`),
|
||||
# no nested agents, no in-session todo list, no web search. `webfetch`
|
||||
# asks, and the harness answers per the `web_tools` tool group. Passed as
|
||||
# OPENCODE_PERMISSION because opencode merges that over every config file,
|
||||
# including ones the agent can write.
|
||||
opencodePermission = {
|
||||
bash = "deny";
|
||||
task = "deny";
|
||||
todowrite = "deny";
|
||||
websearch = "deny";
|
||||
webfetch = "ask";
|
||||
};
|
||||
opencodeConfig = pkgs.writeText "opencode.json" (
|
||||
builtins.toJSON {
|
||||
"$schema" = "https://opencode.ai/config.json";
|
||||
autoupdate = false;
|
||||
share = "disabled";
|
||||
model = "${oc.provider.id}/${oc.model}";
|
||||
provider.${oc.provider.id} = {
|
||||
npm = "@ai-sdk/openai-compatible";
|
||||
inherit (oc.provider) name;
|
||||
options = {
|
||||
baseURL = oc.provider.baseUrl;
|
||||
# Substituted by opencode from its environment at startup, so the
|
||||
# key stays in backendEnvironmentFile and out of the store.
|
||||
apiKey = "{env:${oc.provider.apiKeyEnv}}";
|
||||
};
|
||||
# `limit.context` is what opencode reports as the window in its
|
||||
# `usage_update`, i.e. the harness's ctx %.
|
||||
models.${oc.model} = {
|
||||
name = oc.model;
|
||||
limit = {
|
||||
context = oc.contextWindow;
|
||||
output = oc.outputLimit;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
options.services.hyperhive.agent.model = lib.mkOption {
|
||||
|
|
@ -165,6 +210,136 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
options.services.hyperhive.agent.runtime = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"claude"
|
||||
"acp"
|
||||
];
|
||||
default = "claude";
|
||||
example = "acp";
|
||||
description = ''
|
||||
What drives this agent's turns. `"claude"` runs `claude --print`.
|
||||
`"acp"` runs the Agent Client Protocol agent described by
|
||||
`services.hyperhive.agent.acp`, as one long-lived child of the
|
||||
harness, and turns `services.hyperhive.agent.useApiKey` on by default:
|
||||
the agent authenticates to its own provider, so there is no Claude
|
||||
login to wait for.
|
||||
|
||||
On `"acp"`, `model`, `effortLevel` and `autoCompact` have no effect
|
||||
(the model is whatever the agent is configured with), and neither the
|
||||
web UI's cancel button nor `/compact` works yet.
|
||||
'';
|
||||
};
|
||||
|
||||
options.services.hyperhive.agent.acp = {
|
||||
preset = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "opencode";
|
||||
description = ''
|
||||
Name of an entry in `services.hyperhive.agent.acp.presets` whose
|
||||
`command`, `args` and `env` become the defaults of the options of
|
||||
the same name here.
|
||||
'';
|
||||
};
|
||||
|
||||
command = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
example = lib.literalExpression ''"''${pkgs.opencode}/bin/opencode"'';
|
||||
description = "Program the harness spawns as its ACP agent (`HIVE_ACP_COMMAND`).";
|
||||
};
|
||||
|
||||
args = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
example = [ "acp" ];
|
||||
description = "Arguments for `command` (`HIVE_ACP_ARGS`, as JSON).";
|
||||
};
|
||||
|
||||
env = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
description = ''
|
||||
Environment for the ACP agent only, on top of the harness's own
|
||||
(`HIVE_ACP_ENV`, as JSON). Rendered into the nix store: never put a
|
||||
credential here. Put it in `services.hyperhive.agent.backendEnvironmentFile`,
|
||||
which the agent inherits through the harness.
|
||||
'';
|
||||
};
|
||||
|
||||
presets = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
command = lib.mkOption { type = lib.types.str; };
|
||||
args = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
};
|
||||
env = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
description = ''
|
||||
Named ACP agent setups `services.hyperhive.agent.acp.preset` can
|
||||
select. `opencode` runs `opencode acp` against the OpenAI-compatible
|
||||
provider set in `services.hyperhive.agent.acp.opencode`.
|
||||
'';
|
||||
};
|
||||
|
||||
opencode = {
|
||||
provider = {
|
||||
id = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "provider";
|
||||
example = "hetzner";
|
||||
description = "Provider id in opencode's config; the model is addressed as `<id>/<model>`.";
|
||||
};
|
||||
name = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = oc.provider.id;
|
||||
defaultText = lib.literalExpression "config.services.hyperhive.agent.acp.opencode.provider.id";
|
||||
description = "Display name of the provider.";
|
||||
};
|
||||
baseUrl = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "https://inference.hetzner.com/api/v1";
|
||||
description = "Base URL of the OpenAI-compatible API.";
|
||||
};
|
||||
apiKeyEnv = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "ACP_PROVIDER_API_KEY";
|
||||
description = ''
|
||||
Environment variable opencode reads the provider's API key from.
|
||||
Set it in `services.hyperhive.agent.backendEnvironmentFile`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
model = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
example = "Qwen/Qwen3.6-35B-A3B-FP8";
|
||||
description = "Model id, as the provider's API names it.";
|
||||
};
|
||||
contextWindow = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = 131072;
|
||||
example = 262144;
|
||||
description = "The model's context window in tokens; the harness's ctx % is measured against it.";
|
||||
};
|
||||
outputLimit = lib.mkOption {
|
||||
type = lib.types.ints.positive;
|
||||
default = 32768;
|
||||
description = "Maximum output tokens per model response.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
options.services.hyperhive.agent.extraWebProxies = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
default = { };
|
||||
|
|
@ -209,8 +384,41 @@ in
|
|||
+ "services.hyperhive.agent.availableModels ([ ${lib.concatStringsSep " " config.services.hyperhive.agent.availableModels} ]) "
|
||||
+ "— add it to the list or change the model.";
|
||||
}
|
||||
{
|
||||
assertion = acp.preset == null || preset != null;
|
||||
message =
|
||||
"services.hyperhive.agent.acp.preset (\"${toString acp.preset}\") names no entry in "
|
||||
+ "services.hyperhive.agent.acp.presets ([ ${lib.concatStringsSep " " (lib.attrNames acp.presets)} ]).";
|
||||
}
|
||||
{
|
||||
assertion = !isAcp || acp.command != "";
|
||||
message = "services.hyperhive.agent.runtime = \"acp\" needs services.hyperhive.agent.acp.command (or acp.preset).";
|
||||
}
|
||||
{
|
||||
assertion = acp.preset != "opencode" || (oc.provider.baseUrl != null && oc.model != null);
|
||||
message = "services.hyperhive.agent.acp.preset = \"opencode\" needs acp.opencode.provider.baseUrl and acp.opencode.model.";
|
||||
}
|
||||
];
|
||||
|
||||
services.hyperhive.agent.useApiKey = lib.mkIf isAcp (lib.mkDefault true);
|
||||
|
||||
services.hyperhive.agent.acp = {
|
||||
presets.opencode = {
|
||||
command = "${pkgs.opencode}/bin/opencode";
|
||||
args = [ "acp" ];
|
||||
env = {
|
||||
OPENCODE_CONFIG = "${opencodeConfig}";
|
||||
OPENCODE_PERMISSION = builtins.toJSON opencodePermission;
|
||||
# Keeps a config file in the agent's working directory from
|
||||
# overriding the one above.
|
||||
OPENCODE_DISABLE_PROJECT_CONFIG = "1";
|
||||
};
|
||||
};
|
||||
command = lib.mkIf (preset != null) (lib.mkDefault preset.command);
|
||||
args = lib.mkIf (preset != null) (lib.mkDefault preset.args);
|
||||
env = lib.mkIf (preset != null) (lib.mkDefault preset.env);
|
||||
};
|
||||
|
||||
# HIVE_DEFAULT_MODEL seeds the initial model selection when no
|
||||
# persisted model choice exists in the state dir.
|
||||
environment.variables = {
|
||||
|
|
@ -295,6 +503,13 @@ in
|
|||
# Tells the harness not to wait for a Claude OAuth session — see
|
||||
# `services.hyperhive.agent.useApiKey`'s own description for the full mechanism.
|
||||
HIVE_USE_API_KEY = "1";
|
||||
}
|
||||
// lib.optionalAttrs isAcp {
|
||||
# Read by `hive_runtime::RuntimeSpec`; unset means claude.
|
||||
HIVE_RUNTIME = "acp";
|
||||
HIVE_ACP_COMMAND = acp.command;
|
||||
HIVE_ACP_ARGS = builtins.toJSON acp.args;
|
||||
HIVE_ACP_ENV = builtins.toJSON acp.env;
|
||||
};
|
||||
serviceConfig = {
|
||||
ExecStart = "${config.services.hyperhive.agent.packages.hive-agent}/bin/${binary}";
|
||||
|
|
|
|||
Loading…
Reference in a new issue