From f8e0061f3fb5413c908749e73dac70eea1aa2a8a Mon Sep 17 00:00:00 2001 From: iris Date: Sun, 31 May 2026 17:29:20 +0200 Subject: [PATCH] hive-ag3nt: scrub turn.rs cookies (#716 batch 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 12 → 0 cookies in turn.rs (claude --print pump + stream-json + compaction + wait_for_login). Cookie scrub only — substantive prose (auth-failed detection, mtime-snapshot resumption, DirSnapshot two-axis design, AuthFailed turn outcome) is already documented in docs/turn-loop.md::The loop and the Turn outcomes table. Rustdocs trimmed to point at the doc where useful (AUTH_FAIL_MARKERS, wait_for_login). - AUTH_FAIL_MARKERS doc: drop #658, #419 attribution - write_settings inner comment: drop #555 attribution - write_system_prompt doc: drop #519 attribution - TurnOutcome::AuthFailed doc: drop #419 attribution - wait_for_login doc: drop #542 attribution, point at docs - wait_for_login inner emit_status comment: drop #563 attribution - Test comments: drop #542 and argus #545 attribution --- hive-ag3nt/src/turn.rs | 71 +++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 39 deletions(-) diff --git a/hive-ag3nt/src/turn.rs b/hive-ag3nt/src/turn.rs index 5982764f..ad0e58c6 100644 --- a/hive-ag3nt/src/turn.rs +++ b/hive-ag3nt/src/turn.rs @@ -20,7 +20,7 @@ use crate::mcp; // `--settings` JSON is read at runtime from // `$HIVE_ASSETS_DIR/prompts/claude-settings.json` via -// `hive_sh4re::assets::claude_settings()` (#555). We turn off claude's +// `hive_sh4re::assets::claude_settings()`. We turn off claude's // in-session auto-compaction and its cross-session auto-memory because // hyperhive owns those concerns (`/compact` on overflow, notes // persistence under `/state`). Unknown keys are silently ignored by @@ -47,15 +47,16 @@ const RATE_LIMIT_MARKERS: &[&str] = &[ ]; /// Substrings that indicate the Anthropic API rejected the request as -/// unauthenticated — the OAuth session in `$HOME/.claude/` (post-#658 -/// `/home//.claude`, previously `/root/.claude`) has expired or -/// been revoked. Surfaced as `TurnOutcome::AuthFailed`, which the +/// unauthenticated — the OAuth session in `$HOME/.claude/` has expired +/// or been revoked. Surfaced as `TurnOutcome::AuthFailed`, which the /// harness uses to flip the container into `needs_login_idle` so the -/// dashboard's re-auth flow takes over (closes #419). Matched against -/// both stdout JSON `error` events and stderr; the markers come from -/// claude-code's `api_retry` events (`{"error":"authentication_failed", +/// dashboard's re-auth flow takes over. Matched against both stdout +/// JSON `error` events and stderr; the markers come from claude-code's +/// `api_retry` events (`{"error":"authentication_failed", /// "error_status":401,...}`) and the human-readable /// "Failed to authenticate. API Error: 401" line claude prints on giveup. +/// See [`docs/turn-loop.md::The loop`](../../docs/turn-loop.md) for the +/// re-auth resumption path. const AUTH_FAIL_MARKERS: &[&str] = &[ "\"error\":\"authentication_failed\"", "\"error_status\":401", @@ -159,7 +160,7 @@ pub async fn write_settings(_socket: &Path) -> Result { let parent = crate::paths::config_dir(); tokio::fs::create_dir_all(&parent).await.ok(); let path = parent.join("claude-settings.json"); - // #555: source-of-truth is `$HIVE_ASSETS_DIR/prompts/claude-settings.json`; + // Source-of-truth is `$HIVE_ASSETS_DIR/prompts/claude-settings.json`; // copy through the per-agent runtime dir so claude reads it from the // same socket-adjacent location every time and so a future override // (per-agent settings JSON layer) drops in cleanly. @@ -177,8 +178,8 @@ pub async fn write_settings(_socket: &Path) -> Result { /// Thin re-export of [`crate::prompt::write_system_prompt`] for /// callers that already import this module. The actual rendering + -/// marker-block logic lives in `prompt.rs` (closes #519); this is -/// just the public entry point the binaries call. +/// marker-block logic lives in `prompt.rs`; this is just the public +/// entry point the binaries call. /// /// # Errors /// @@ -211,7 +212,7 @@ pub enum TurnOutcome { /// The Anthropic API rejected the request with 401 (OAuth session /// expired or revoked). The serve loop should flip the container /// into `needs_login_idle` and stop driving turns until the - /// operator re-auths via the per-agent web UI (closes #419). + /// operator re-auths via the per-agent web UI. AuthFailed, Failed(anyhow::Error), } @@ -477,17 +478,12 @@ pub fn emit_turn_end(bus: &Bus, outcome: &TurnOutcome) { /// Block until the bound `~/.claude/` dir contains a session that /// post-dates this call, polling on a `poll_ms` interval (min 2s). /// Flips `state` to `Online` when login lands; caller resumes its -/// serve loop. -/// -/// **Mtime-progress, not bare existence (closes #542)**: an -/// existence-only check (the pre-#542 behaviour) immediately returns -/// after a 401 because the stale `credentials.json` is still on disk -/// — the next turn then 401s on the same tokens and the harness -/// loops forever. We snapshot the newest file mtime in `claude_dir` -/// at entry and only resume when something has been written since -/// that snapshot (the operator's `/login/code` flow lands a refreshed -/// credentials file, bumping its mtime). First-time login (empty -/// dir → `None` snapshot) still flips on the first file appearing. +/// serve loop. Snapshots the dir at entry and only resumes when the +/// snapshot advances (mtime OR file-count change), avoiding the +/// infinite-401 loop a bare-existence check would produce when stale +/// credentials are already on disk. Mtime-snapshot resumption rationale +/// + DirSnapshot two-axis design: see +/// [`docs/turn-loop.md::The loop`](../../docs/turn-loop.md). /// /// # Panics /// @@ -502,16 +498,13 @@ pub async fn wait_for_login( claude_dir = %claude_dir.display(), "no claude session — staying in partial-run mode (web UI only)" ); - // #563: announce `needs_login_idle` to the bus so the sentinel file + // Announce `needs_login_idle` to the bus so the sentinel file // (`{state_dir}/hyperhive-needs-login`) gets written on every entry // path — cold-boot, 401-mid-turn, and `/api/logout`. The host's - // `auth_failed_sentinel` reads that file to surface `needs_login` on - // the dashboard; prior to this call the cold-boot + 401 paths flipped - // `LoginState::NeedsLogin` in memory but never touched the sentinel, - // so the dashboard kept rendering `online` for a parked harness. - // Idempotent — `emit_status` is a `write` on a small empty file, so - // re-entering this function after a transient operator action is a - // no-op for the on-disk state. + // `auth_failed_sentinel` reads that file to surface `needs_login` + // on the dashboard. Idempotent — `emit_status` is a `write` on a + // small empty file, so re-entering this function after a transient + // operator action is a no-op for the on-disk state. bus.emit_status("needs_login_idle"); let snapshot = snapshot_dir(claude_dir); let probe = Duration::from_millis(poll_ms.max(2000)); @@ -867,7 +860,7 @@ mod tests { #[test] fn session_refreshed_first_login_flips_on_any_file() { // Empty-dir snapshot → any file appearing means a fresh - // login landed. Pre-#542 semantics for first-time login. + // login landed. First-time login semantics. let dir = tempfile::tempdir().unwrap(); let snapshot = snapshot_dir(dir.path()); assert!(!session_refreshed(snapshot, snapshot_dir(dir.path()))); @@ -877,9 +870,9 @@ mod tests { #[test] fn session_refreshed_stale_creds_dont_flip_immediately() { - // The #542 repro: stale credentials.json already exists at - // entry; wait_for_login must NOT immediately return — it - // would loop straight into another 401-failing turn. + // Stale credentials.json already exists at entry; wait_for_login + // must NOT immediately return — it would loop straight into + // another 401-failing turn. let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("credentials.json"), b"{}").unwrap(); let snapshot = snapshot_dir(dir.path()); @@ -917,11 +910,11 @@ mod tests { #[test] fn session_refreshed_count_change_flips_when_mtime_unreadable() { - // Defensive (argus #545 nit): if all files have unreadable - // `meta.modified()` (exotic fs / NFS), newest_mtime stays - // `None` forever — but file_count axis still catches new - // files appearing. Simulated here by forging a snapshot with - // file_count=1 + no mtime, then writing a second file. + // Defensive: if all files have unreadable `meta.modified()` + // (exotic fs / NFS), newest_mtime stays `None` forever — but + // file_count axis still catches new files appearing. Simulated + // here by forging a snapshot with file_count=1 + no mtime, then + // writing a second file. let dir = tempfile::tempdir().unwrap(); fs::write(dir.path().join("a"), b"{}").unwrap(); let forged = DirSnapshot {