Update documentation with 33 changed files (#4575) - #4612
Conversation
Close the TOCTOU between engineer worktree discovery/reuse and the concurrent GC/reaper that aborted goal-session cycles with a bare missing-workspace fault (issue #4578). Add a side-effect-free, fail-closed presence probe EngineerWorktree::is_present() (single claim-sentinel read) and apply it at the three reuse/report sites in advance_goal and its per-cycle consumers: - Assigned-engineer short-circuit (typed_goal_session): present -> keep the engineer; absent -> warn, clear assigned_to, drop the stale map entry, and re-provision instead of failing permanently. - Discovery reuse (typed_goal_session): re-read the claim sentinel before reporting Succeeded; on absence, warn and fall through to allocate(). - Stored-map consumers (subordinate heartbeat path, cycle worker_present): require the checkout to still be on disk so a reaped worktree no longer counts as a live worker. Absence is surfaced via structured tracing only (engineer_worktree.reaped_before_reuse) and always becomes a clean re-provision, never a crash. Additive and non-breaking. Adds regression tests for is_present() and the reuse-after-reap / stored-map-staleness paths, plus a reference doc. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rysweet
left a comment
There was a problem hiding this comment.
🔍 Comprehensive Code Review — Step 17b
Scope: #4578 engineer-worktree presence guard (TOCTOU fix). 9 files, +887/−33.
Verdict: ✅ Approve — no blocking issues. High-quality, well-tested, additive fix. Notes below are non-blocking.
Review checklist
- Code quality & standards — idiomatic Rust; structured
tracing(noprint!/println!); consistentevent = "engineer_worktree.reaped_before_reuse"telemetry across all sites. - Test coverage — real fault injection (
allocate()+ out-of-bandremove_dir_all), not mocks. Covers happy path, own-cleanup, out-of-band reap, sentinel-only removal, idempotency, discovery-vs-stored-map, and managed-root layout anchor. - No TODOs / stubs / swallowed exceptions — none introduced; presence probe fails closed.
- No unimplemented functions —
is_present()fully implemented. - Logic correctness — see analysis below; fix is sound.
- Edge cases — dir reaped, sentinel gone/corrupted, partial reap all handled → not-present → re-provision.
Strengths
- Right-sized seam.
EngineerWorktree::is_present()= one fail-closed claim read.read_engineer_claim_fullalready fails closed when the dir is gone, so no separatelstatneeded — correctly justified in the doc-comment. - All five sites covered consistently. Discovery reuse (
typed_goal_session, viaPathBuf), assigned-goal reuse, stored-map reuse (subordinate.rs), and the live-worker signal (cycle.rs) each re-verify presence before trustingpath(). - Honest type handling. Discovery holds only a
PathBuf, so it re-reads the claim directly rather than forcing anEngineerWorktree— avoids a false abstraction. - Recovery, not just detection. Stale entries are dropped + re-provisioned instead of returning a stale
Succeededthat crashes the next cycle.
Non-blocking observations
- Semantic change worth flagging (
typed_goal_session.rsL960-964 removed). Thealready_assignedbranch previously returned a permanentErr("goal already has an assigned engineer"). It now returns idempotentSucceededwhen the worktree is present, or clears the assignment and re-provisions when reaped. This is intentional and is precisely what breaks the crashloop (an assigned-but-reaped goal could never recover before), but it is a behavior change beyond a pure null-check — reviewers should be aware the old permanent-error path is gone. - Residual TOCTOU window (inherent). A gap still exists between the presence read and the checkout's actual use next cycle. This is unavoidable for filesystem TOCTOU; the fail-closed → re-provision design is the correct mitigation and the doc acknowledges it. No action needed.
- Per-cycle I/O in
gather_per_goal_cycle_ctx(cycle.rs).worker_presentchanged from an O(1)HashMap::contains_keytois_present()(a claim-file read) per goal per cycle. Negligible at cycle cadence, but it does add a syscall on a hot path — worth a mental note if goal counts grow large. - Mutex held across FS I/O (
typed_goal_session.rs).lock_stateis nowmutand spans theis_present()read plus the assignment/map mutation. Justified — the check+mutate must be atomic against concurrent state access — but note the lock now covers a syscall. - Minor evidence inconsistency. The assigned-present branch synthesizes
session_id: format!("engineer-{goal_id}")while the discovery branch derives it frompath.file_name(). Cosmetic (evidence label only); could derive from the stored worktree path for consistency.
Verification performed
- CI: 18/18 checks pass (incl.
pre-commit12m,coverage7m50s,install-real, cargo-audit/deny/vet). mergeable: MERGEABLE; clean re-export path forread_engineer_claim_full(pub useinmod.rs).- Confirmed scope is limited to the #4578 fix; coverage-gate (#4523/#4495) and amplihack-rs #983 correctly deferred to separate PRs.
No changes requested. Ready to merge once required approvals are in.
rysweet
left a comment
There was a problem hiding this comment.
🔒 Security Review — Step 17c
Scope: #4578 engineer-worktree presence guard (TOCTOU fix). 9 files, +887/−33. Reviewed against the actual diff (src/engineer_worktree/mod.rs, subordinate.rs, typed_goal_session.rs, cycle.rs, claim.rs read path).
Verdict: ✅ Approve — no exploitable security vulnerabilities found. The change is an additive, fail-closed robustness fix within a single trust domain (the supervisor daemon). No changes requested.
Security checklist
| Item | Result |
|---|---|
| New vulnerabilities introduced | ✅ None |
| Injection (shell/SQL/command/log) | ✅ None — no command construction; tracing::warn! uses structured fields, values (goal_id, path) are daemon-internal |
| Sensitive data handling | ✅ Clean — claim sentinel holds only PID + starttime; logs emit path/goal_id, no secrets. claim_key semantics unchanged |
| AuthN / AuthZ | ✅ N/A — no auth surface touched; presence guard is a safety check, not a security boundary |
| Path traversal | ✅ None — is_present() → read_engineer_claim_full(&self.path) joins a constant (ENGINEER_CLAIM_FILE) onto a daemon-owned PathBuf; no untrusted input reaches the path |
| Memory safety | ✅ unsafe: 0 in diff. Parse is bounded and fail-closed (.ok()? on i32/u64) — malformed/oversized sentinels return None, no panic |
| Dependencies | ✅ No new crates, no network, no deserialization of untrusted data |
Non-blocking observations (defense-in-depth notes, no action required)
-
Symlink-following read (informational).
read_engineer_claim_fullusesfs::read_to_string, which follows symlinks. A local writer inside the worktree dir could plant a.simard-engineer-claimsymlink sois_present()returnstrue. Not a real vuln: the worktree lives under the supervisor-owned state root (same trust domain as the daemon), the probe is read-only (it never writes or deletes based on the resolved target), and liveness is still owned separately byfind_live_engineer_for_goal. Anlstat/symlink_metadataprobe would buy nothing here — correctly rejected by the author. -
Residual TOCTOU window (by design).
is_present()→ use is inherently racy; a reaper can still remove the checkout after the probe. This is a safety/availability property, not a security one — it fails closed to re-provision, so there is no privilege escalation or stale-privilege reuse. Correct design. -
Availability: FS syscall under lock (minor). In
typed_goal_session.rstheis_present()blocking FS read now runs while holdinglock_state, andcycle.rs/subordinate.rsadd a per-cycle read to hot paths. On a wedged filesystem (e.g., stalled NFS) this could stall the OODA cycle / hold the state lock. Low likelihood on local worktrees; acceptable for this fix. Flagged only for future hardening (e.g., timeout/bounded probe), not for this PR.
Conclusion: The fail-closed → re-provision design is the security-correct choice — it eliminates the crashloop without opening any injection, traversal, escalation, or data-exposure vector. No secrets, no unsafe, no untrusted input on the new code paths. Approved from a security standpoint.
🧭 Philosophy Guardian Review — Step 17dPR #4612 — #4578 engineer-worktree presence guard (TOCTOU fix). 9 files, +887/−33 (core code +~130 lines; remainder tests + docs). Reviewed against the actual diff, not the transcript. Compliance checklist
Observations (non-blocking, no action required)
Conclusion: This is a model example of ruthless simplicity — the smallest change that fully closes the TOCTOU gap, honest about its residual window, no over-engineering. Approved from a philosophy standpoint. Ready to proceed. |
…4578) Address Step 16/17 review feedback (S3): the assigned-engineer short-circuit reported a synthetic `engineer-{goal_id}` session_id while the disk-discovery reuse branch reported the worktree checkout basename. Normalize the assigned branch onto the same derivation (worktree directory basename with an `existing-engineer` fallback) so both reuse paths emit consistent EngineerRun evidence, improving observability and log correlation. Check+mutate semantics are unchanged: the `is_present()` probe still runs under the same `lock_state` guard via `.filter()`, and an absent checkout still falls through to clear the stale assignment and re-provision. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
📊 Coverage Summary
Coverage data from CI run. Test files matching |
Summary
Concise workflow-generated PR for documentation.
Issue
Closes #4575
Changed files
Diff stat
Behavior
Implemented behavior through these branch commits:
7685b42 fix(engineer-worktree): guard reuse against reaped worktrees (#4578)
Validation
ℹ NODE_OPTIONS=--max-old-space-size=32768 (saved preference). To change: /home/azureuser/.amplihack/config
I'll start by understanding the current state: branch, changed files, and toolchain.
● Get branch, status, and diff stat (shell)
│ cd /home/azureuser/src/Simard-deploy-4049/worktrees/feat/issue-4575-nodeoptions-max-old-space-si
│ ze32768-saved-preferen && git branch --show-current && echo "---REMOTE---" && git remote -v |
│ head -2 && echo "---STATUS---" && git status --short && echo "---DIFF STAT vs main---" && git
│ --no-pager diff --stat main...HEAD 2>/dev/null | tail -30
└ 37 lines…
This is a Rust (Cargo) project. Let me see the full diff stat and recent commits to understand the actual change.
Risk
No high-risk subsystem pattern detected from changed paths.
Checklist
This PR was created as a draft for review before merging.
Step 16b: Outside-In Testing Results
Tested the presence-guard fix (#4578) from the consumer boundary as a user of the worktree/goal-session lifecycle.
Detected toolchains: Rust CLI (
Cargo.tomlat root,cargo 1.95.0) — primary; Node.js CLI wrapper (package.json,bin.js) — unchanged by this PR. All changed files are Rust (src/engineer_worktree/,src/ooda_actions/advance_goal/,src/ooda_loop/cycle.rs) plus docs.Chosen strategy: Per qa-team repo-type detection, Rust CLI repos use native
cargo test(not gadugi-agentic-test). Exercised the newEngineerWorktree::is_present()presence seam and its stored-map/discovery consumers through the documented release test gate:cargo test --release --lib -- --test-threads=8 presence_guard. Also rancargo fmt --all -- --check.Scenarios
is_present_true_after_allocate,stored_map_worktree_reports_present_while_live,discovery_finds_live_worktree_while_present)cargo test --release --lib -- presence_guardis_present_false_when_dir_reaped_out_of_band,discovery_returns_none_after_worktree_reaped,stored_map_worktree_reports_absent_after_reap,is_present_false_when_claim_sentinel_removed,is_present_false_after_cleanup,is_present_is_side_effect_free_and_idempotent,attached_worktree_lives_under_managed_root)cargo test --release --lib -- presence_guardremove_dir_all,is_present()→ false and discovery →None; no phantom live workerAggregate:
test result: ok. 10 passed; 0 failed; 0 ignored; 9286 filtered out; finished in 0.32s.cargo fmt --all -- --checkexit 0.Fix count: 0 — all outside-in scenarios passed on the first run; no diagnose/fix/push iterations were required.
This PR changes the assigned-engineer reuse outcome in
advance_goal's live typed spawn. Previously, an already-assigned goal whose worktree had been reaped out of band returned a permanentErr(missing-workspace fault), which crash-looped the next cycle. It now:Succeededreusing the existing engineer (unchanged happy path).engineer_worktree.reaped_before_reusewarning, clear the staleassigned_to+ map entry, and fall through to a clean re-provision instead of failing permanently.This is intentional and is the mechanism that breaks the #4578 crashloop. Downstream reviewers/mergers should be aware that a previously terminal
Errpath is now a recoverableSucceeded/re-provision.Follow-up commit (Step 18b — review feedback S3)
Normalized the reuse-branch evidence
session_idfor observability: the assigned short-circuit now derivessession_idfrom the worktree checkout basename (with anexisting-engineerfallback), matching the disk-discovery reuse branch instead of the previous syntheticengineer-{goal_id}. Check+mutate semantics are unchanged (probe still runs under the samelock_stateguard). Commit305df99fe.