Fix skill discovery, sandbox reads and abandoned approvals (reconcile-worktrees post-mortem) - #2620
Merged
Merged
Conversation
…econcile-worktrees post-mortem Three findings from the 2026-09-09 post-mortem of /reconcile-worktrees stalling inside Copse when driven by Codex over ACP. 1. Skill discovery ignored ~/.codex/skills. `.codex` joins the container list at both scopes, last in precedence; project-scope roots are now sorted by that precedence instead of readdir order so the documented order is true. `userSkillRoots()` and a test-only home override make the user scope testable without the developer's real skills leaking in. 2. run_shell could not read the invoked skill's directory or the primary checkout. Invoking a skill now grants that thread read-only sandbox access to the skill directory plus validated `paths` frontmatter entries (thread-read-roots.ts / skill-read-roots.ts); the seatbelt overlay adds them to allowRead only and the shell-scope classifier treats them as contained read roots, so read-only commands naming them auto-run in the sandbox. `sed` is admitted as structurally read-only in its provable filter shape. A linked-worktree thread can now read (never write) the shared primary checkout, with the shared .git, its own nested worktree and nested sibling worktrees carved out per child so Linux bind ordering cannot shadow write binds; the primary's HEAD, index and sibling admin dirs are readable so `git worktree list` and `git -C <primary>` work. 3. An abandoned bridged MCP call cancelled its pending approval. The bridge now aborts an abandoned call (socket close or notifications/cancelled) with an AbandonedCallAbort reason; the approval service parks the prompt instead of cancelling it, rejects the caller with ApprovalPendingError telling the agent to retry the same call, and replays the eventual verdict once to an identical request (same thread, execution root and prompt) within ten minutes. Turn end still dismisses parked prompts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Contributor
🖥️ PR preview
|
Contributor
Reference screenshots ready for reviewReview GitHub’s image diffs in screenshot PR #2622. Rendered for If this source branch moves, a later successful render closes the stale review PR and replaces this link. |
jonathanKingston
temporarily deployed
to
github-pages
September 9, 2026 04:00 — with
GitHub Actions
Inactive
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.
Summary
Fixes the three findings from the 2026-09-09 post-mortem of
/reconcile-worktreesstalling inside Copse (Codex over ACP, thread in a linked worktree, tools via the bridgedrun_shell).1. Skill discovery ignored
~/.codex/skills.codexis now a skill container at both scopes (~/.codex/skills,<workspace>/.codex/skills), last in precedence.SKILL_CONTAINER_DIRSdocuments the order.readdirorder, so first-writer-wins actually follows the documented order.userSkillRoots(home)is exported and asetUserSkillsHomeForTestoverride isolates suites from the developer's real skills (this machine's~/.codex/skills/reconcile-worktreeswas shadowing the bundled copy in an existing test, exactly as user-over-bundled intends).2.
run_shellcould not read the skill directory or the primary checkoutpathsfrontmatter entries. Newthread-read-roots.ts(session-only ledger) andskill-read-roots.ts(validation: relative only, no.., no credential files/dirs, no home/root, symlink escapes only for trusted sources). The seatbelt overlay adds them toallowReadonly; the shell-scope classifier now takes multiple contained read roots so read-only commands naming them auto-run inside the sandbox. The invoked-skill prompt says what is readable and whichpathsentries were refused.sedis admitted as structurally read-only in its provable filter shape (no-i/-f/unknown options, now/W/r/R/eletters in the script), so the post-mortem'ssed -n '1,320p' ~/.codex/skills/.../SKILL.mdno longer prompts..git, the thread's own nested worktree and nested sibling worktrees carved out, because a bareprimary/**would let Linux bind ordering shadow the.git/objectswrite binds. The primary'sHEAD,index,ORIG_HEAD/FETCH_HEADand every registered sibling's admin dir are readable, sogit worktree list --porcelainlists everything with real HEADs andgit -C <primary> …works. The security trade-off is written up inprimaryCheckoutReadPathsinconfig.ts.docs/cursor-plugins.mdgains a discovery-roots and frontmatter section (includingpaths);docs/supply-chain-security.mdanddocs/acp-agents.mdupdated.3. Abandoned MCP calls cancelled pending approvals
notifications/cancelledrouted through a bridge-wide in-flight map since stateless mode gives each POST its own server, or the SDK's per-request signal) with anAbandonedCallAbortreason.AbortSignal.anypropagates that reason, so the turn and session aborts keep cancelling as before.approval.tstreats that reason as a detach: the prompt stays open with a parked stand-in waiter, the caller is rejected withApprovalPendingError("still pending, retry the exact same call"), and the user's eventual verdict is stored for ten minutes keyed by thread, execution root and prompt. An identical request replays it once without prompting; a retry that arrives while the prompt is still open joins it and retires the stand-in, so one answer never authorises two runs. Turn end and transport teardown still dismiss parked prompts and store nothing.Review fixes
Tests
Finding 1:
skills-registry.test.ts(.codexproject and user discovery, precedence,userSkillRoots).Finding 2:
thread-read-roots.test.ts,skill-read-roots.test.ts(paths validation),skill-prompt.test.ts(grant + prompt text),security/shell-scope.test.ts(classifier waiver),shell-guard/src/shell-argv.test.ts(sed),project-sandbox/config.test.ts(overlay shape, carve-outs, thread roots),project-sandbox/worktree-integration.test.ts(real ASRT: primary readable, not writable, nested sibling denied,git -C primary,git worktree list).Finding 3:
approval.test.ts(park, replay, single use, join, TTL, scoping, turn end, headless) andacp-native-bridge.test.ts(socket drop then replay; MCP cancel returns the pending error to the agent).Review regressions: bounded filesystem enumeration; existing ACP probe/session worker bundle, stdout protocol, thread identity, and read-root tests.
Local validation: full
pnpm run checkpassed 8,883 tests andpnpm run buildpassed on the pushed head; actual macOS seatbelt worktree integration included.After Support directory-scoped AGENTS.md instructions #1976 and feat(security): run a thread unattended inside a hardened container #2348 merged separately, the clean local merge with current main (
d231c026b) passed the full check again: 9,109 tests, zero failures/skips, plus a successful build.The review fixes change internal enumeration and dependency wiring, with no renderer/DOM changes.
Not touched: the audit script under
~/.codex/skills(separate task).🤖 Generated with Claude Code