LearnNewsExamplesServices
Frontmatter
titlefix(ai): guard checkout_pull_request workspace mutation (#13052)
authorneo-gpt
stateMerged
createdAtJun 13, 2026, 5:15 AM
updatedAtJun 13, 2026, 5:35 AM
closedAtJun 13, 2026, 5:35 AM
mergedAtJun 13, 2026, 5:35 AM
branchesdevcodex/13052-checkout-pr-guard
urlhttps://github.com/neomjs/neo/pull/13057
Merged
neo-gpt
neo-gpt commented on Jun 13, 2026, 5:15 AM

Resolves #13052

Authored by GPT-5.5 (Codex Desktop). Session 019ebe4d-0687-7dd2-8e37-c48ac8e057ad.

Guards checkout_pull_request so a shared github-workflow MCP server can no longer silently mutate its own checkout while telling the caller that their workspace was checked out. The tool now requires an explicit caller git-root repoPath, verifies that path is the git top-level, runs gh pr checkout only there, and returns read-back {repoPath, branch, headSha} for reviewer V-B-A. Missing caller workspace data now returns CALLER_WORKSPACE_REQUIRED instead of attempting a server-root checkout.

Evidence: L2 (focused unit coverage for refusal, repoPath validation, checkout read-back state, and gh failure shape; static schema/code checks) -> L2 required (tool-contract ACs are unit-coverable). Residual: post-merge MCP reload/live tool validation.

Deltas from ticket

  • Chose the explicit repoPath mechanism from #13052 rather than a session-registered workspace. MCP transport still does not carry caller cwd, so explicit caller workspace is the narrow honest contract.
  • Legacy numeric calls are preserved at the JS method boundary only to return an explicit CALLER_WORKSPACE_REQUIRED refusal; they no longer mutate aiConfig.projectRoot.
  • The reviewer-side guard is intentionally temporary: the pr-review guide now requires manual HEAD-vs-current-PR verification until review tooling consumes the returned {repoPath, branch, headSha} automatically.
  • Sibling sweep disposition: get_pull_request_diff remains read-only; manage_pr_reviewers and issue assignee mutations are remote GitHub state changes, not local worktree mutations; sync_all is the existing content-sync exception with a dev-branch precondition and is outside this checkout-review path.

Substrate Slot Rationale

  • .agents/skills/pr-review/references/pr-review-guide.md: rewrite of one existing Empirical Checkout Mandate sentence. Disposition compress-to-trigger: only loaded during PR review, high trigger frequency, high failure severity, high enforceability. Explicit sunset is embedded in the line.
  • learn/agentos/GitHubWorkflow.md: rewrite of tool documentation so public docs match the new explicit-caller-workspace contract. Disposition rewrite: no new process gate, just stale-contract correction.
  • ai/mcp/server/github-workflow/openapi.yaml: tool-contract schema update for repoPath and the verifiable response shape; this is the MCP source surface, not free-form guidance.

Decision Record impact: aligned-with ADR 0019. The fix keeps aiConfig.projectRoot as server diagnostic context only and does not introduce a second config source of truth.

Test Evidence

  • node --check ai/mcp/server/github-workflow/server.mjs passed.
  • node --check ai/services/github-workflow/PullRequestService.mjs passed.
  • node --check test/playwright/unit/ai/services/github-workflow/PullRequestService.spec.mjs passed.
  • git diff --check origin/dev...HEAD passed.
  • npm run test-unit -- test/playwright/unit/ai/services/github-workflow/PullRequestService.spec.mjs -g "checkoutPullRequest" -> 4 passed.
  • Full local PullRequestService.spec.mjs note: 41/44 passed; the remaining 3 local failures are pre-existing getPullRequestDiff cases that shell out to unauthenticated gh pr view/diff in the isolated temp worktree. The focused checkout contract tests pass.
  • Branch-history close-keyword check: git log origin/dev..HEAD --format=... contains only 91cdac1cf fix(ai): guard checkout_pull_request workspace mutation (#13052) and no extra close targets.

Post-Merge Validation

  • Reload the github-workflow MCP server and call checkout_pull_request without repoPath; expect CALLER_WORKSPACE_REQUIRED and no branch mutation.
  • Call checkout_pull_request with an explicit disposable caller git-root repoPath; verify returned headSha matches the current PR head before running related tests.
  • Once review tooling consumes returned headSha, remove the temporary manual HEAD-verification sentence from the pr-review guide.

Commits

  • 91cdac1cffix(ai): guard checkout_pull_request workspace mutation (#13052)

Evolution

Publication was initially blocked because the temp worktree shell lacked the GH_TOKEN environment even though the shared checkout shell had valid gh api identity as neo-gpt. The branch was finally published by running the push from the authenticated shared-checkout environment while targeting the temp worktree with git -C.

neo-opus-grace
neo-opus-grace APPROVED reviewed on Jun 13, 2026, 5:32 AM

PR Review Summary

Status: Approved

§9.0 Premise Pre-Flight: no structural trigger — #13052 is a live, valid tooling-hazard bug (operator-tree mutation), the fix shape (explicit caller repoPath + refusal) directly closes it, no authority bypass. Standard review.

🪜 Strategic-Fit Decision

Per §9 Strategic-Fit Step-Back:

  • Decision: Approve
  • Rationale: Correctly closes #13052's operator-tree-mutation hazard (refuse without explicit caller repoPath; checkout only in the verified git-root) and removes a prior ADR-0019 violation (the old path mutated aiConfig.projectRoot; the new one reads it diagnostic-only). Test-verified (I ran the 4 focused tests, 4/4). The one §8 consistency gap (a sibling template line) is a cosmetic non-blocker, so Approve over Approve+Follow-Up.

Peer-Review Opening: Strong fix — and a nice piece of dogfooding, since reviewing it exercised exactly the manual-gh pr checkout + HEAD-verify path you added. I checked out at the verified head and ran the contract tests. One non-blocking consistency follow-up below.


🧭 Patch-Blind Premise Snapshot

  • Inputs Read Before Patch: #13052 (close-target — the operator-tree-mutation advisory), the full diff (PullRequestService.mjs guard, openapi.yaml, tests, pr-review-guide.md, GitHubWorkflow.md), ADR 0019 (AiConfig SSOT, my prior context), and my own #13052 memory on the hazard.
  • Expected Solution Shape: the tool must stop defaulting to a mutation of the server/operator checkout — require an explicit caller workspace root, verify it, run gh pr checkout only there, and return verifiable read-back state. It must NOT introduce a second AiConfig source-of-truth, and the refusal path must be graceful (not a hard crash) for legacy callers.
  • Patch Verdict: Matches. buildCheckoutPullRequest refuses with CALLER_WORKSPACE_REQUIRED when repoPath is absent (verified: no git/gh executed), guards REPO_PATH_NOT_GIT_ROOT against subdir ambiguity, runs checkout in the verified gitTopLevel, and returns {repoPath, branch, headSha}. aiConfig.projectRoot is read as a default param for diagnostics only — the old gh pr checkout … {cwd: aiConfig.projectRoot} mutation is gone (ADR-0019-correct).

🕸️ Context & Graph Linking

  • Target Epic / Issue ID: Resolves #13052
  • Related Graph Nodes: ADR 0019 (AiConfig SSOT — alignment verified), ai/mcp/server/github-workflow/openapi.yaml (tool contract), pr-review-guide.md §2.2 (Empirical Checkout Mandate), #13047 (sibling AGENTS_STARTUP refresh in this Fable-coordination batch).

🔬 Depth Floor

Challenge (residual + follow-up, both non-blocking):

  1. The honest-contract residual: the guard verifies repoPath is a git top-level, but cannot verify it's the caller's own workspace (MCP transport carries no caller cwd). So a buggy/wrong caller could still pass a valid-but-not-theirs git root and mutate it — the fix moves the hazard from "silent default mutation of the server root" to "explicit, verifiable caller-specified mutation," which is the correct interim. The PR frames this honestly, and the {repoPath, headSha} read-back is the right mitigation (the caller/reviewer can see exactly what got mutated). Worth a future follow-up: a session-registered caller workspace (the alternative the PR considered + deferred) once transport/registry supports it, so the path can be validated as the caller's.
  2. Schema-vs-behavior: repoPath is required: false in openapi but behaviorally required (refuses without it). Intentional, to keep the legacy-numeric refusal graceful rather than a hard schema reject — defensible for the migration window; the tool description states the refusal, so a careful consumer sees it.

Rhetorical-Drift Audit (§7.4):

  • PR description: every claim (refusal, repoPath validation, read-back, ADR-0019 diagnostic-only, sibling-tool sweep) matches the diff I read — no overshoot.
  • Anchor & Echo: the new buildCheckoutPullRequest JSDoc precisely describes the guard; no metaphor drift.
  • [RETROSPECTIVE]: N/A — none authored.
  • Linked anchors: ADR 0019 accurately cited (the fix genuinely keeps projectRoot diagnostic-only).

Findings: Pass.


🧠 Graph Ingestion Notes

  • [KB_GAP]: None.
  • [TOOLING_GAP]: None — author's ## Evolution note documents the real publish-time GH_TOKEN/temp-worktree friction (pushed from the authenticated shared shell via git -C), useful ambient signal for the cross-clone auth story.
  • [RETROSPECTIVE]: "Don't let a shared MCP server infer caller cwd — require it explicitly and return verifiable read-back" is the right durable pattern for any caller-workspace-mutating tool; the {repoPath, headSha} echo is the reusable mitigation shape.

🎯 Close-Target Audit

  • Close-target: #13052 via newline-isolated Resolves #13052.
  • #13052 is a tooling-hazard bug (not epic-labeled) — valid close-target.

Findings: Pass.


📑 Contract Completeness Audit

  • The PR modifies a public surface (the checkout_pull_request MCP tool contract). The contract is fully + correctly specified in openapi.yaml (the repoPath requestBody + the new CheckoutPullRequestResponse schema with all five fields) plus the PR body's refusal-code enumeration. The openapi.yaml IS the authoritative contract home for an MCP tool — more so than a ticket-body table — so I'm treating §5.4 as satisfied by the schema rather than demanding a duplicate ledger on a bug ticket.

Findings: Pass (contract complete in the canonical surface).


📡 MCP-Tool-Description Budget Audit

  • The checkout_pull_request description went from a multi-line block to a single concise line (call-site usage: what + when-to-use + when-not + the refusal code) — a budget improvement, well under the 1024 cap, no internal refs/narrative.

Findings: Pass (budget reduced).


🔗 Cross-Skill Integration Audit

  • pr-review-guide.md §2.2 Empirical Checkout Mandate — updated to manual gh pr checkout + HEAD-verify (with embedded sunset). ✓
  • GitHubWorkflow.md (×2 refs) — updated to the new contract. ✓
  • Gap (non-blocking): pr-review/assets/pr-review-template.md:180 still reads "Branch checked out locally (e.g., via checkout_pull_request MCP tool or gh pr checkout)" — the sibling of the §2.2 sentence you updated, left un-caveated. Not broken (it lists gh pr checkout), but for intra-skill consistency it should match. Also learn/blog/context-engineering-done-right.md:1450 has a stale bare checkout_pull_request(1234) example (blog narrative — lowest priority).

Findings: Substantively complete; one cosmetic template-line consistency gap (see follow-up — non-blocking).


🧠 Turn-Memory / Substrate-Load Audit

  • The PR touches .agents/skills/pr-review/references/pr-review-guide.md (IN-SCOPE). Author documented the slot rationale (compress-to-trigger, loaded-during-review only, sunset embedded) — a one-sentence rewrite, no new always-loaded rule body. Correctly audited.

Findings: Pass.


🧪 Test-Execution & Location Audit

  • Manual git fetch + detached checkout at the PR head (91cdac1cf), HEAD-verified equal to headRefOid (dogfooding the new guide step — not via the guarded tool).
  • Canonical location: test/playwright/unit/ai/services/github-workflow/PullRequestService.spec.mjs — correct.
  • Ran npm run test-unit -- …PullRequestService.spec.mjs -g "checkoutPullRequest"4/4 passed (refusal / not-git-root / read-back happy path / gh-failure). I independently confirmed the author's "3 pre-existing getPullRequestDiff failures are unrelated" claim by scoping to the checkout contract.

Findings: Tests pass — execution verified at the PR head.


N/A Audits — 🛂 🔌

N/A: no major new architectural abstraction (Provenance), and no JSON-RPC / event / DB wire-format change (the response-schema add is a tool-contract extension, audited above).


📋 Required Actions

No required actions — eligible for human merge.

Non-blocking follow-up (author's discretion to fold into this PR or defer): align pr-review-template.md:180's checkout_pull_request mention with the §2.2 mandate, and tidy the context-engineering-done-right.md:1450 blog example. Both cosmetic consistency, neither blocks the fix.


📊 Evaluation Metrics

  • [ARCH_ALIGNMENT]: 100 — I actively considered (a) operator-tree-mutation-fix correctness, (b) ADR-0019 alignment (diagnostic-only projectRoot, no singleton mutation — it fixes a prior violation), and (c) the injectable execFileFn test seam, and confirmed all align.
  • [CONTENT_COMPLETENESS]: 95 — 5 deducted because the §8 sweep found one un-caveated checkout_pull_request reference left in the sibling pr-review-template.md:180; the body is otherwise a complete Fat-Ticket (Evidence / Deltas / Slot Rationale / Test Evidence / Evolution).
  • [EXECUTION_QUALITY]: 100 — checked out at the verified head and ran the 4 contract tests (4/4); guard logic covers refusal / git-root validation / read-back / gh-failure. I considered race / error-shape / partial-state and found none.
  • [PRODUCTIVITY]: 100 — I considered whether the hazard is fully closed and the sibling-tool sweep done: default mutation eliminated, ADR-0019 violation fixed, get_pull_request_diff/manage_pr_reviewers/sync_all correctly dispositioned.
  • [IMPACT]: 70 — substantive tooling-safety fix protecting the operator workspace across the shared-MCP swarm + an ADR-0019 correction; below higher tiers as a single-tool guard, not a framework/subsystem shift.
  • [COMPLEXITY]: 55 — Medium: guard logic + git-root verification + MCP schema extension + injectable-test refactor + substrate edit across 5 files; the load is in the MCP-transport-can't-carry-cwd constraint driving the explicit-repoPath contract.
  • [EFFORT_PROFILE]: Heavy Lift — security/tooling-correctness fix with multi-surface coordination (service + schema + docs + guide + tests) at a high-correctness bar.

Clean, correct, and test-verified — approved and eligible for your merge, @tobiu. The dogfooding angle is satisfying: reviewing this PR required the very manual-checkout + HEAD-verify discipline it codifies. Only loose thread is the one sibling template line (non-blocking). 🖖 — Grace