Agent auth-cache hardening, global cache mode, worker CI gate & inject crash-recovery#2
Merged
Conversation
…erialize prompts
Fixes from a security review of the agent's Touch ID auth caches:
- Dual-clock expiry (CacheExpiry): macOS Instant reads CLOCK_UPTIME_RAW,
which pauses during sleep, so a grant issued before lid-close stayed
live on wake. Entries are now valid only while BOTH the monotonic and
wall clocks are within the TTL (mono caps awake time and bounds NTP
backwards steps; wall counts sleep).
- Idle timeout judged on both clocks too (a nap-happy laptop could never
accumulate 30 awake minutes), and idle now flushes both auth caches
unconditionally — previously the flush was gated on SSH keys being
loaded, so decrypt-only agents never idle-flushed grants.
- New cache watcher (5s tick): flushes grants on screen lock and on
wake-from-sleep (wall/mono divergence >= 30s), sudo-timestamp
semantics. Skips the WindowServer poll while both caches are empty.
- All 7 human-prompt paths (sign, decrypt@vt, auth@vt, run@vt, sign@vt)
now go through authenticate_serialized: run@vt's Semaphore(1)
generalized so a hostile VT_AUTH peer cannot stack N dialogs, and the
blocking auth chain moved to spawn_blocking so concurrent prompts
cannot pin tokio workers. The sign no-context path now reports
Unavailable instead of flattening it to Rejected.
- decrypt@vt: empty batch is rejected as BadRequest before any prompt
("decrypt 0 secrets" spam); after a fresh approval the whole batch is
granted (strict-TTL, no sliding refresh) so entries that expired while
the prompt sat on screen are refreshed.
- Docs: CLAUDE.md agent auth-cache section + CLI help warning that
forwarded-agent (ssh -A) requests share the local session's cache
context within the TTL.
Clock-injected regression tests replace the sleep-based ones
(dual-clock expiry, idle_exceeded, watcher_should_clear).
…t notices; fix macOS shim recursion Four changes, each closing a gap found while dogfooding the auth cache: - AuthCacheMode::Global: one shared context for the whole agent, no TTY or session requirement. Orchestrated callers (AI agents / CI / make) spawn TTY-less commands with a fresh session per call, so per-session/per-app resolve to None and can never cache; global is the opt-in escape hatch for exactly that workload. Coarsest blast radius (any socket reacher within the TTL, incl. forwarded sessions) — documented in CLAUDE.md and the CLI help. - Prompt-queue cache double-check: cached paths now re-check the cache after acquiring the global prompt permit, and grants land while the permit is still held. N concurrent requests for the same key/records collapse to one Touch ID; the waiters resolve as silent cache hits instead of stacking N sequential dialogs. - Agent cache-hit 免审批 notice (cf-worker): audit-ingest rows with outcome=cache_hit now fan out the same one-line notice as the Worker DEK-cache hit (Pushover / Slack / Slack App / Feishu), with the note 「缓存命中,免 Touch ID」, throttled DO-side to one per (op_kind, host) per 60s. The dispatch is a shared pushCacheHitNotices used by both the DEK-cache and agent paths; buildCacheHitLines gains an optional note and drops the count segment when salts=0. - fix(hook): vt shims looped to the recursion limit on macOS. current_exe() there returns the invoked symlink path (unlike Linux's /proc/self/exe), so basename(vt_binary()) equalled the shimmed tool name, the "bare vt runs as-is" guard matched every shim, and the bare-name exec resolved straight back to the shim. vt_binary() now canonicalizes (canonical_self_exe); and resolve_real fails fast with a clear error when every PATH candidate is a vt shim instead of letting execvp loop.
vt ssh connect is also the transport for multi-host fan-outs (tssh h1 h2 ...), where v1's always-prompt policy produced one Touch ID per host. sign@vt now routes through check_or_prompt_auth, sharing the standard sign cache — same (context, fingerprint) key, same strict-TTL / dual-clock / lock-wake-idle-flush / prompt-queue-re-check semantics, same opt-in (--ssh-auth-cache-mode, default none = old behaviour). The v1 design's "cache-context escalation" objection (a standard-sign grant silently authorizing sign@vt for the same key, and vice versa) is accepted deliberately: both operations sign an arbitrary challenge with the same key, so a cached grant on either path already concedes the capability for the TTL; a per-(host, op) namespace would just re-add one prompt per host on fan-outs. docs/sign-vt-design.md §2/§5/§6/§9 updated to record the reversal. AuthDecision::Unavailable now carries UnavailableReason so sign@vt keeps its structured ErrKind mapping (SessionLocked/NoGuiSession) instead of flattening to Generic.
Three high-leverage improvements from the project review, plus doc gaps. cf-worker CI gate (was: zero automated checks on half the system): - New CI job type-checks (tsc --noEmit) and unit-tests the worker on every PR — a TS error in the DO / WebAuthn / Access path can no longer reach `wrangler deploy`. - vitest with 18 tests over the runtime-free pure logic (b64u, challengeHash domain separation, ctEq, replay window, credentials parse/lookup). - justfile: `test`, `check-worker`, `ci` recipes. vt inject crash-recovery (was: reboot/SIGKILL left plaintext exposed indefinitely with no record): - A 0600 sidecar (~/.local/state/vt/inject/<rand>.json, paths + deadline, no secret) is written before any plaintext hits disk and deleted on every normal restore path (parent immediate-restore + supervisor post-timeout). - `vt inject --recover` (no auth — only moves the ciphertext backup back) sweeps the dir and restores entries whose backup survives and whose window has elapsed (+5s grace so a live supervisor is never raced). Pure `plan_recovery` decision is unit-tested; end-to-end verified. Docs/product-gap cleanup: - README: document run / hook / fido2 / inject --recover; add a Linux platform note to Quick Start (bootstrap is macOS-only). - CLAUDE.md: crash-recovery section replaces the old "known gap"; record the no-master-key-rotation limitation as accepted. - docs/structured-errors.md: flip status proposal -> shipped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four commits from a security + code-health review of the Touch ID auth cache and surrounding tooling. Each was adversarially reviewed (codex-expert) before landing.
1. Auth-cache TTL survives sleep; flush on lock/wake/idle; serialize prompts (
c02cdeb)InstantreadsCLOCK_UPTIME_RAW, which pauses during sleep — a grant issued before lid-close stayed live on wake. Entries are now valid only while both the monotonic and wall clocks are within the TTL.Semaphore(1)+spawn_blockingso a hostile peer can't stack dialogs or pin tokio workers.2. Global cache mode, prompt-queue cache re-check, cache-hit notices (
9027185)AuthCacheMode::Global: one shared context for the whole agent, no TTY/session requirement — the only mode that serves orchestrated callers (AI agents / CI) whose TTY-less, fresh-session spawns can never hit per-session/per-app.cache_hitrows fan out the same notice as the Worker DEK-cache hit (Pushover / Slack / Slack App / Feishu), throttled per (op_kind, host).3. sign@vt shares the sign auth cache (
f4be887)vt ssh connectis the transport for multi-host fan-outs (tssh h1 h2 …), where the v1 always-prompt policy meant one Touch ID per host. sign@vt now shares the standard sign cache (same key, opt-in, defaultnone). Design doc §6 updated to record the accepted tradeoff.4. Worker CI gate, inject crash-recovery, docs cleanup (
e8d6b37)tsc --noEmit) and unit-tests the worker on every PR — a TS error in the DO / WebAuthn / Access path can no longer reachwrangler deploy. vitest with 18 assertions over the runtime-free pure logic.vt inject --recover: a 0600 sidecar (paths + deadline, no secret) written before plaintext hits disk lets recovery restore ciphertext for any file a crashed/rebooted supervisor left decrypted. Pureplan_recoverydecision is unit-tested.Test gates
cargo test: 223 passedcargo checkhost +x86_64-unknown-linux-gnuboundary greentsc --noEmit+ 18 vitest assertions green🤖 Generated with Claude Code