diff --git a/docs/turn-loop/config.md b/docs/turn-loop/config.md index a6765679..d7e2b15d 100644 --- a/docs/turn-loop/config.md +++ b/docs/turn-loop/config.md @@ -239,64 +239,3 @@ left untouched. Set to `false` for agents that parse cargo's JSON output programmatically and do not pass `--message-format json` themselves. -## API-key backend (`useApiKey` / `backendEnvironmentFile`) - -```nix -hyperhive.useApiKey = true; # default: false -hyperhive.backendEnvironmentFile = - "/agents/myagent/harness/openrouter.env"; # default: null -hyperhive.model = "anthropic/claude-3.5-sonnet"; # provider-specific model string -``` - -Runs this agent's `claude` against an API-key backend (e.g. OpenRouter) -instead of a Claude subscription via OAuth. Two options, paired — each -is a no-op without the other: - -- **`useApiKey`** tells the harness itself not to wait for a Claude OAuth - session: at boot, `LoginState::from_dir` reports `Online` without - checking `~/.claude/` (`hive_agent::login::using_api_key`, reads - `HIVE_USE_API_KEY`), and the fact is stamped into the consolidated - harness state file so the operator dashboard also stops reading this - agent's empty `~/.claude/` as "needs login". An api-key agent that hits - a real 401 (the key itself is bad) still surfaces `needs_login` — only - the boot-time "have I ever logged in" check is bypassed, not the - auth-failure path. -- **`backendEnvironmentFile`** points at an operator-managed file - (outside the nix store, one `KEY=value` per line, systemd - `EnvironmentFile` syntax) supplying the credentials `claude` itself - 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. - -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): - -```sh -# on the host, once per agent that should use an api-key backend -sudo install -m 0600 -o root /dev/stdin \ - /var/lib/hyperhive/agents//harness/openrouter.env < Self { - // API-key backends (OpenRouter etc.) never populate `~/.claude/` — - // there is no OAuth flow to complete, `claude` reads - // `ANTHROPIC_BASE_URL`/`ANTHROPIC_API_KEY` from the environment - // instead. Checked first: an api-key agent with a stale or absent - // credentials dir must report `Online`, not park the turn loop - // waiting for a login that will never happen. - if using_api_key() || has_session(dir) { + if has_session(dir) { Self::Online } else { Self::NeedsLogin @@ -117,16 +111,6 @@ impl LoginState { } } -/// Whether this agent is configured to authenticate to its backend with an -/// API key rather than a Claude OAuth session — set by -/// `hyperhive.useApiKey` via the `HIVE_USE_API_KEY` env var -/// (`nix/agent-modules/agent-service.nix`). Read fresh on every call rather -/// than cached: it's an env var, not a value worth a `OnceLock` for. -#[must_use] -pub fn using_api_key() -> bool { - std::env::var("HIVE_USE_API_KEY").is_ok_and(|v| v == "1") -} - /// Baseline for [`has_fresh_credentials`] when the caller has no specific /// prior-failure instant to compare against (the cold-boot call site, where /// `has_session` already established no credential file exists yet — so diff --git a/hive-agent/src/main.rs b/hive-agent/src/main.rs index b6a8028e..d4e8aa94 100644 --- a/hive-agent/src/main.rs +++ b/hive-agent/src/main.rs @@ -559,10 +559,6 @@ async fn serve_main(socket: &Path, poll_ms: u64) -> Result<()> { let claude_dir = login::default_dir(); let initial = LoginState::from_dir(&claude_dir); tracing::info!(state = ?initial, claude_dir = %claude_dir.display(), "harness boot"); - // Config fact, stamped once — see `harness_state::write_api_key_mode`'s - // doc for why hive-c0re needs this to stop reporting `needs_login` for - // an agent whose `~/.claude/` is empty by design. - harness_state::write_api_key_mode(login::using_api_key()); let login_state = Arc::new(Mutex::new(initial)); let bus = Bus::new(); // Set by the web UI's `/api/cancel` on a successful SIGINT, read-and- diff --git a/hive-c0re/src/container_view.rs b/hive-c0re/src/container_view.rs index ef7d5459..445ccaac 100644 --- a/hive-c0re/src/container_view.rs +++ b/hive-c0re/src/container_view.rs @@ -130,23 +130,9 @@ pub async fn build_all(hive: &crate::coordinator::HiveEnv) -> Vec // (boot-time / fresh container) OR the harness wrote the auth-failed // sentinel because a turn hit 401. Cleared for stopped containers — // stale sentinel state is not meaningful when the harness isn't up. - // - // The first half doesn't apply to an api-key agent - // (`hyperhive.useApiKey`, stamped as `api_key_mode` in the - // consolidated state file by `hive_agent::harness_state::write_api_key_mode`): - // its `~/.claude/` is empty by design (no OAuth flow to complete), - // so an empty dir there means nothing — only the auth-failed - // sentinel (a real 401, meaning the configured key itself is bad) - // still counts. - // - // One `read_harness_flags` call for both fields, not a wrapper per - // field: this is the only call site that needs more than one, and - // a wrapper each would read the file twice per agent per sweep for - // no reason. - let (_, needs_login_sentinel, is_api_key) = read_harness_flags(&logical); let needs_login = running - && ((!is_api_key && !claude_has_session(&Coordinator::agent_claude_dir(&logical))) - || needs_login_sentinel); + && (!claude_has_session(&Coordinator::agent_claude_dir(&logical)) + || auth_failed_sentinel(&logical)); // Read the active model from the harness state file. Only surfaced // when the container is running — stale model info from a stopped // agent is misleading (the model may change on next boot). @@ -194,14 +180,11 @@ pub fn claude_has_session(dir: &Path) -> bool { .any(|e| e.file_type().is_ok_and(|t| t.is_file())) } -/// Read `rate_limited` + `needs_login` (auth-failed sentinel) + -/// `api_key_mode` from the consolidated `hyperhive-harness.json`. Falls -/// back to the legacy individual sentinel files written by older harness -/// builds so in-place upgrades don't lose state during the transition -/// window — `api_key_mode` has no legacy equivalent (it postdates the -/// consolidated file), so that fallback just says `false`, the correct -/// answer for any harness build old enough to have never written it. -fn read_harness_flags(name: &hive_types::Ident) -> (bool, bool, bool) { +/// Read `rate_limited` + `needs_login` (auth-failed sentinel) from +/// the consolidated `hyperhive-harness.json`. Falls back to the legacy +/// individual sentinel files written by older harness builds so in-place +/// upgrades don't lose state during the transition window. +fn read_harness_flags(name: &hive_types::Ident) -> (bool, bool) { let dir = Coordinator::agent_notes_dir(name); if let Ok(raw) = std::fs::read_to_string(dir.join("hyperhive-harness.json")) && let Ok(v) = serde_json::from_str::(&raw) @@ -214,16 +197,16 @@ fn read_harness_flags(name: &hive_types::Ident) -> (bool, bool, bool) { .get("needs_login") .and_then(serde_json::Value::as_bool) .unwrap_or(false); - let akm = v - .get("api_key_mode") - .and_then(serde_json::Value::as_bool) - .unwrap_or(false); - return (rl, nl, akm); + return (rl, nl); } // Legacy fallback: presence of individual sentinel files. let rate_limited = dir.join("hyperhive-rate-limited").exists(); let needs_login = dir.join("hyperhive-needs-login").exists(); - (rate_limited, needs_login, false) + (rate_limited, needs_login) +} + +fn auth_failed_sentinel(name: &hive_types::Ident) -> bool { + read_harness_flags(name).1 } /// Read the agent's free-text status and the Unix timestamp when it was last set diff --git a/nix/agent-modules/agent-service.nix b/nix/agent-modules/agent-service.nix index dd2226ed..21671fb5 100644 --- a/nix/agent-modules/agent-service.nix +++ b/nix/agent-modules/agent-service.nix @@ -106,65 +106,6 @@ in ''; }; - options.hyperhive.useApiKey = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Authenticate this agent's `claude` invocations with an API key - (`ANTHROPIC_API_KEY`/`ANTHROPIC_BASE_URL`, e.g. OpenRouter) rather - than a Claude OAuth session. Sets `HIVE_USE_API_KEY=1`, which the - harness reads (`hive_agent::login::using_api_key`) to report - `LoginState::Online` at boot without checking `~/.claude/` — an - api-key agent has no OAuth session to wait for, so it must never - park the turn loop expecting one. Also stamped into the consolidated - harness state file so hive-c0re's dashboard stops reading an empty - `~/.claude/` as "needs login" for this agent (see - `hive_c0re::container_view`'s `needs_login` computation). - - Set this AND `hyperhive.backendEnvironmentFile` together — this - option changes what the harness believes about its own login state, - the other actually supplies the credentials `claude` reads. Neither - is useful alone. - ''; - }; - - options.hyperhive.backendEnvironmentFile = lib.mkOption { - type = lib.types.nullOr lib.types.str; - default = null; - example = "/agents/myagent/harness/openrouter.env"; - description = '' - Path (outside the nix store) to a systemd `EnvironmentFile` loaded - by the harness service — the mechanism for supplying - `ANTHROPIC_API_KEY`/`ANTHROPIC_BASE_URL` (or any other backend - credential `claude`/the harness reads from the environment) without - baking a secret into the nix store. - - The file must use systemd `EnvironmentFile` syntax: one `KEY=value` - pair per line, no `export`, no shell quoting needed for simple - values. Example contents: - - ``` - ANTHROPIC_API_KEY=sk-or-v1-... - ANTHROPIC_BASE_URL=https://openrouter.ai/api/v1 - ``` - - Place the file inside the agent's bind-mounted **harness** dir (e.g. - `/agents//harness/openrouter.env`, `$HYPERHIVE_HARNESS_DIR`), - not `state/` — `harness/` survives container rebuilds exactly like - `state/` does, but is never bind-mounted into a parent agent's - container (unlike `state/`, which a parent gets read-write for child - recovery — see `docs/persistence.md`'s "Parent access to child - state"), so this credential is reachable by nothing but this agent - and the host. Permissions should be `0600`, owned by the agent's - unix user. Loaded with a leading `-` (optional `EnvironmentFile`), - so a path that doesn't exist yet — an operator setting this option - 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 `hyperhive.useApiKey`'s doc for the option this one is - paired with. - ''; - }; - options.hyperhive.extraWebProxies = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = { }; @@ -290,11 +231,6 @@ in # reverse-proxies. See `hyperhive.extraWebProxies` option # and `web_ui/proxy.rs::extra_proxy_service`. HIVE_EXTRA_WEB_PROXIES = builtins.toJSON config.hyperhive.extraWebProxies; - } - // lib.optionalAttrs config.hyperhive.useApiKey { - # Tells the harness not to wait for a Claude OAuth session — see - # `hyperhive.useApiKey`'s own description for the full mechanism. - HIVE_USE_API_KEY = "1"; }; serviceConfig = { ExecStart = "${config.hyperhive.packages.hive-agent}/bin/${binary}"; @@ -310,11 +246,6 @@ in RuntimeDirectory = "hive-config"; User = userName; Group = userName; - } - // lib.optionalAttrs (config.hyperhive.backendEnvironmentFile != null) { - # See `hyperhive.backendEnvironmentFile`'s own description for - # the file shape and the leading-`-` rationale. - EnvironmentFile = "-${config.hyperhive.backendEnvironmentFile}"; }; }; };