Skip to content

🤖 fix: keep MCP out of an unsanitized replacement checkout when sanitize fails - #4673

Merged
ThomasK33 merged 2 commits into
mainfrom
g2/pr-mcp-after-sanitize
Sep 26, 2026
Merged

ThomasK33 merged 2 commits into
mainfrom
g2/pr-mcp-after-sanitize

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Summary

This adds a real-stack test proving that a workflow replacement launch starts no MCP server and runs no prompt discovery until its checkout is sanitized. The success path already behaves correctly. The test also found an ordering bug when sanitize fails: init was marked complete before the unsanitized checkout was reclaimed, which let a waiting prompts.list start MCP in that checkout. This PR fixes it.

Fixes #4576.

Background

G2 (#4577, #4593) replaces a workflow child that ended without a report. The replacement launches through createMany(..., { retires }) and the reserved launch path. #4577 checked "sanitize before MCP" only at the TaskService/host seam. #4576 asked for proof against a real MCP server.

Findings

MCP and prompt discovery start lazily. They start in exactly two places:

  1. During turn assembly inside the send: getToolsForWorkspace, listServers, getPrompt.
  2. From the workspace.mcp.prompts.list RPC.

Workspace metadata listeners, AgentSession creation, and StreamManager do not start MCP.

The reserved path's send is awaited after sanitize. A client's prompts.list blocks in waitForInit until init completes, and on the success path that happens only after sanitize.

Bug on the failed-sanitize branch. It called logComplete(-1) before reclaimUnsanitizedTaskCheckout. That released waiting prompts.list calls while the unsanitized checkout and its published row still existed. In a diagnostic run that held the row removal, the waiting request started the real stub server in the unsanitized checkout (2 of 2 runs).

Implementation

  • reclaimUnsanitizedTaskCheckout now reports whether it reclaimed. Init ends in a finally after the reclaim attempt:
    • Reclaimed (row, checkout, and session dir removed): initStateManager.clearInMemoryState(taskId), the same call workspace removal makes. Waiters are released and fail because the workspace is gone.
      • Moving logComplete(-1) later instead would have persisted init-status.json and re-created the deleted session directory.
    • Reclaim failed or unconfirmed (row and checkout retained): logComplete(-1) runs after the attempt, so waiters do not hang.
  • The test harness's createMockInitStateManager gains clearInMemoryState.

Validation

taskService.replacementMcpOrdering.test.ts uses the real service container (TaskService, WorkspaceService, AIService, MCPServerManager) and a stdio recording MCP server (tests/fixtures/mcp/recording-server.ts) that is genuinely enabled for the project. Only the language model is stubbed.

  1. Success path.
    • Sanitize is held by a pass-through spy while a real claimed replacement launches.
    • While it is held, a prompts.list call is started and seen waiting in waitForInit. There are no manager calls, the stub stays silent, and endInit has not run.
    • After release, "sanitized" is the first recorded step. Both routes then reach the stub from the replacement's checkout (positive control).
  2. Failed sanitize.
    • The row removal is held. During the hold, init has not ended and there are no manager calls or stub starts.
    • After release, the request fails, the stub never starts in the reclaimed checkout, and the row, checkout, and session dir are gone.
    • The test fails 3 of 3 runs without the fix.

Negative controls:

  • Moving sanitize after the send fails test 1.
  • Moving init completion before sanitize fails test 1.

Local runs:

  • New file: 3 runs.
  • taskService suites: 988.
  • MCPServerManager, mcpIdentity.assembly, and the replacementRace/replacementRestart tests: 321.
  • make static-check: green.

Risks

Low. The change only affects the failed-sanitize branch of the reserved launch.

Still open and unchanged: when the reclaim itself fails, the unsanitized checkout stays published, and a later request can still start MCP there. That exposure existed before this PR. So did the case where checkout deletion fails after the unpublish. Both are tracked in #4674, which proposes fail-closed discovery and sends for a retained unsanitized checkout. The direct-create and deferred-checkout rollback paths still complete init after their rollback, which likely re-creates the removed session directory. That is not a sanitize-ordering issue, so it is left for a separate change.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high • Cost: $253.94

…iscovery before sanitize

End-to-end Gate 4 test for #4576 on the real ServiceContainer (TaskService,
WorkspaceService, AIService, MCPServerManager) with a real stdio MCP stub that
records its start and every request; only the language model is substituted.

The replacement's sanitize is held while a renderer-style prompt-catalog request
(workspace.mcp.prompts.list) parks on the launch's running init. Nothing reaches
the stub or the manager until sanitize returns; afterwards both the catalog
request and the launch's first send (getToolsForWorkspace) reach the stub from
the replacement's checkout (positive control).

Negative controls (not committed): moving sanitize after the send fails the
held-state ledger check (getToolsForWorkspace seen); moving init completion
before sanitize fails the ordering check (getPromptsForWorkspace before
sanitized).

Refs #4576

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high` • Cost: `$`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high costs= -->
… reclaim

On the reserved launch's failed-sanitize branch, init was completed before
reclaimUnsanitizedTaskCheckout unpublished the row and deleted the checkout.
Completing init releases requests parked in waitForInit, so a prompt-catalog
request (workspace.mcp.prompts.list) could start MCP servers inside the
unsanitized checkout while the row was still published.

Init now ends only after the reclaim attempt (try/finally). A reclaimed task
drops its init state like workspace removal does; completing it would
recreate the deleted session dir to persist init-status.json. A retained
checkout (row still published) completes init so waiters do not hang.

New deterministic test in taskService.replacementMcpOrdering.test.ts holds the
reclaim's unpublish and fails 3/3 before this change (init completed while the
reclaim was held).

Fixes #4576

---

_Generated with `xum` • Model: `anthropic:claude-opus-5-5` • Thinking: `high` • Cost: `$4.99`_

<!-- mux-attribution: model=anthropic:claude-opus-5-5 thinking=high costs=4.99 -->
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-26T11:40:00.052266Z 9330cd7 Manual request
🔒 Security Review ✅ Completed 2026-09-26T11:41:29.104015Z 9330cd7 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9330cd7c67

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/taskService.ts
Comment thread src/node/services/taskService.ts
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

Round 1's two findings (failed-reclaim paths) predate this PR and are deferred to #4674, with reasons given in each thread. This round covers the same head, 9330cd7.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 9330cd7c67

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review · Automatically triggered

Security review completed. No security issues were found in this pull request.

Reviewed commit: 9330cd7c67

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 added this pull request to the merge queue Sep 26, 2026
Merged via the queue into main with commit 8b9ba7c Sep 26, 2026
57 of 60 checks passed
@ThomasK33
ThomasK33 deleted the g2/pr-mcp-after-sanitize branch September 26, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🤖 tests: prove workflow replacement launches never start MCP or prompt discovery before sanitize

1 participant