hive-ag3nt: scrub turn.rs cookies (#716 batch 7)

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
This commit is contained in:
iris 2026-05-31 17:29:20 +02:00
commit f8e0061f3f

View file

@ -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/<agent>/.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<PathBuf> {
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<PathBuf> {
/// 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 {