feat(#493): api-key backend support (useApiKey + backendEnvironmentFile)

This commit is contained in:
damocles 2026-08-26 22:22:47 +02:00 committed by mara
commit 535ba0c11c
6 changed files with 197 additions and 14 deletions

View file

@ -161,6 +161,30 @@ pub(crate) fn write_harness_state(
write_harness_json(&v);
}
/// Stamp `api_key_mode` into the consolidated state file — a config fact
/// (does this agent authenticate to its backend with an API key rather
/// than a Claude OAuth session?), not runtime state, so it's written once
/// at harness startup and never toggled again for the life of the
/// container. Lives in the same file as the toggling fields rather than a
/// dedicated marker file: this codebase already consolidated two ad-hoc
/// sentinel files into one JSON for exactly the reason a new one would
/// re-create (`hive-c0re` paying a stat call per extra file per sweep) —
/// see this module's own doc comment above.
///
/// hive-c0re reads it to stop reporting `needs_login` for an agent whose
/// `~/.claude/` dir is empty by design (`container_view::api_key_mode`) —
/// without this, the host's own naive "does the credentials dir have
/// files in it" check has no way to tell "never going to log in" apart
/// from "hasn't logged in yet".
pub(crate) fn write_api_key_mode(enabled: bool) {
let _guard = HARNESS_JSON_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut v = read_harness_json();
v["api_key_mode"] = enabled.into();
write_harness_json(&v);
}
/// Compiled-in fallback model used when neither `HIVE_DEFAULT_MODEL` nor a
/// persisted runtime override is present.
pub const DEFAULT_MODEL: &str = "haiku";

View file

@ -103,7 +103,13 @@ pub enum LoginState {
impl LoginState {
#[must_use]
pub fn from_dir(dir: &Path) -> Self {
if has_session(dir) {
// 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) {
Self::Online
} else {
Self::NeedsLogin
@ -111,6 +117,16 @@ 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

View file

@ -559,6 +559,10 @@ async fn serve_main<S: Surface>(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-