From a5448c81c40dffe17cf117a05de2d5722c46efea Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 10 Oct 2026 00:36:47 +0200 Subject: [PATCH] agent-modules: hand an api-key agent's subagent daemon the backend file A claude agent with useApiKey gets ANTHROPIC_API_KEY through backendEnvironmentFile, but only the harness unit loaded that file; hive-subagent-daemon loaded it for ACP agents alone. The daemon's nested claude inherits the daemon's environment (hive-claude adds to it, never clears it), and an api-key agent has no OAuth session in ~/.claude, so its claude subagents started with no credentials. hive-subagent-daemon now loads backendEnvironmentFile when the agent is ACP (unchanged) or has useApiKey set. Module-eval covers the api-key claude arm; the existing claude arm (file set, useApiKey unset) still asserts no EnvironmentFile. Closes #5088 --- docs/turn-loop/config.md | 4 +++- nix/agent-modules/agent-service.nix | 4 +++- nix/agent-modules/mcp.nix | 9 +++++---- nix/module-eval/agent-runtime.nix | 15 ++++++++++++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/turn-loop/config.md b/docs/turn-loop/config.md index 36d3cf60..902b6a12 100644 --- a/docs/turn-loop/config.md +++ b/docs/turn-loop/config.md @@ -269,7 +269,9 @@ is a no-op without the other: reads from the environment — typically `ANTHROPIC_API_KEY` and `ANTHROPIC_BASE_URL`. Loaded as an _optional_ `EnvironmentFile` (leading `-`), so setting the option before the file exists doesn't - strand the harness at boot. + strand the harness at boot. With `useApiKey` set, the subagent daemon + (`hive-subagent-daemon`) loads the same file, so the agent's claude + subagents run on the same key. Provision the file once, out of band (never through nix — an API key in the store is world-readable and travels with the flake closure): diff --git a/nix/agent-modules/agent-service.nix b/nix/agent-modules/agent-service.nix index a8254a28..4662b04e 100644 --- a/nix/agent-modules/agent-service.nix +++ b/nix/agent-modules/agent-service.nix @@ -209,7 +209,9 @@ in before creating the file, or a fresh host rebuild before state is restored — makes systemd skip it rather than refuse to start the harness. See `services.hyperhive.agent.useApiKey`'s doc for the option this one is - paired with. + paired with. The subagent daemon loads the same file on an ACP agent + and on an agent with `useApiKey` set, so its subagents authenticate + the same way the harness does. On an ACP agent using the `opencode` preset, a provider key this file leaves unset is read from the swarm secret store at diff --git a/nix/agent-modules/mcp.nix b/nix/agent-modules/mcp.nix index d41e92fb..77bb8068 100644 --- a/nix/agent-modules/mcp.nix +++ b/nix/agent-modules/mcp.nix @@ -494,13 +494,14 @@ in // lib.optionalAttrs ( - config.services.hyperhive.agent.runtime == "acp" + (config.services.hyperhive.agent.runtime == "acp" || config.services.hyperhive.agent.useApiKey) && config.services.hyperhive.agent.backendEnvironmentFile != null ) { - # An ACP subagent authenticates to its provider with the same - # credentials as the harness's ACP agent. Claude subagents are - # not handed this file. + # A subagent authenticates with the same credentials as the + # harness: an ACP subagent to its provider, a claude subagent of + # an api-key agent with `ANTHROPIC_API_KEY`, as it has no OAuth + # session to fall back on. EnvironmentFile = "-${config.services.hyperhive.agent.backendEnvironmentFile}"; }; }; diff --git a/nix/module-eval/agent-runtime.nix b/nix/module-eval/agent-runtime.nix index 6fca89be..3e1ba768 100644 --- a/nix/module-eval/agent-runtime.nix +++ b/nix/module-eval/agent-runtime.nix @@ -38,6 +38,13 @@ let claude = agentOn "claude"; acp = agentOn "acp"; + claudeApiKey = agentWith { + services.hyperhive.agent = { + runtime = "claude"; + useApiKey = true; + backendEnvironmentFile = backendEnv; + }; + }; # The opencode preset, with and without a secret store. opencodeWith = @@ -148,9 +155,15 @@ let name = "an ACP agent's subagent daemon loads the backend credentials"; ok = (subagent acp).serviceConfig.EnvironmentFile or null == "-${backendEnv}"; } + { + # The nested `claude` inherits the daemon's environment, so this file + # is where its `ANTHROPIC_API_KEY` comes from. + name = "an api-key claude agent's subagent daemon loads the backend credentials"; + ok = (subagent claudeApiKey).serviceConfig.EnvironmentFile or null == "-${backendEnv}"; + } { # Unset is what `RuntimeSpec` reads as claude, and a claude subagent - # is not handed the backend file. + # of an agent without `useApiKey` is not handed the backend file. name = "a claude agent's subagent daemon has no runtime selection and no backend file"; ok = lib.all (var: (subagent claude).environment.${var} or null == null) runtimeVars