Skip to content

refactor(mcp): move git tool handlers behind one tool context - #1142

Merged
ScriptedAlchemy merged 13 commits into
codex/tracedecay-total-redesign-plan-reopenedfrom
sol/mcp-handlers-1073
Sep 9, 2026
Merged

refactor(mcp): move git tool handlers behind one tool context#1142
ScriptedAlchemy merged 13 commits into
codex/tracedecay-total-redesign-plan-reopenedfrom
sol/mcp-handlers-1073

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Child of #707 for #1073 (composition root).

Moves the git MCP tool handlers out of the root crate into tracedecay-mcp behind a single McpToolContext that binds all admitted authorities in one step. Review of that binding surfaced three authority-proof defects, fixed here with RED/GREEN tests:

  • McpToolContext::bind now proves authority coherence for real (project, worktree, store lease) instead of accepting matching-looking inputs; the dead shipped McpToolContext::standalone is removed.
  • Session-store binding requires the StoreShardScopeV1::ProjectSessions family exactly and exhaustively. Previously StoreShardScopeV1::project_id() reports Project, ProjectSessions, and Code alike, so a lease on the admitted project's own project store or a code store passed as session authority. Refusal is mcp_tool_binding_store_not_session_shard; the old "not project scoped" variant is gone.
  • PR context cursor identity derives from the admitted scope and carries the shard scope whole, so one project's three shard families no longer share a cursor identity (previously identical digests), and a foreign store is denied rather than reported stale. Mutable branch scope_digest is no longer part of the identity.

RED evidence (guards reverted): only_the_project_sessions_family_carries_session_authorityCode shard returns Some(project.admitted) where None is required; a_cursor_cannot_travel_between_shard_families_of_one_project — session and project shards hash to the same digest. GREEN: 254/254.

Verification: tracedecay-mcp --lib 254 passed; root mcp:: 380 passed; clippy -D warnings on both crates --all-targets; fmt; commitlint. Merged #707 at d56a8a1d2.

Not in scope, flagged for its owner (fable/reset-idempotency-1130): production_codex_hook_ingest_survives_message_search_reopen fails in observation ingest because crates/tracedecay-rusqlite-runtime/src/repository/observation/authority.rs wraps a provenance collision into rusqlite::Error::InvalidParameterName, surfacing a real collision as retryable infrastructure failure.

The git tool family (affected, diff/commit/changelog/PR context,
branch list/search/diff, its shell reads and PR-context cursor) plus
the dependency-hint adapters and the retrieval-cursor decoder now live
in `tracedecay-mcp`. They no longer take a `TraceDecay` handle or a
per-handler pile of authority parameters: every daemon-owned input
crosses one boundary, `McpToolContext`, which carries the admitted
worktree route and active branch, the caller's deadline and
cancellation, the registered project session store that authenticates
PR-context cursors, and the code-index search/branch-diff executors
with the authorization proved for them.

An authority the daemon did not admit stays a typed `None` in the
context, so a standalone server reports each handler's own unavailable
state instead of minting a substitute. The composition root keeps only
context construction and dispatch, and the moved code carries no
compatibility reexport in either direction.

Promotes `gix` to a workspace dependency so both crates share the
pinned version and feature set.
McpToolContext accepted the project root, session store, code-index
executors and search authority as independent public arguments, so a
caller could bind a store from one project alongside executors admitted
for another and nothing would notice. Replace that surface with a single
McpToolBinding that carries the admitted project store and code index
together with their resolved scopes, and validate the whole set in
McpToolContext::bind: the root must be absolute, the store and index
scopes must agree with the request scope, and an admitted index must
carry both its authority and its executors. Mismatches return a typed
McpToolBindingError instead of silently dropping an authority.

Graph queries only materialize inside a handler, so verify_graph_scope
checks a query's checkouts against the admitted scope at use time, and
dependency hints now take the bound context rather than parallel
deadline and cancellation parameters.
The pr_context cursor hashed Path::to_string_lossy() for its root
identity, so two distinct roots whose non-UTF8 bytes lossily encode to
the same string produced interchangeable cursors, and decoding asserted
ValidatedAuthorization::Authorized locally instead of taking the
authorization the request was admitted under.

PrContextCursorBinding now carries the canonical resolved project,
repository and worktree identity alongside the root's native OS bytes
from tracedecay_runtime_core::os_str_bytes, and the cursor authority
comes from the bound tool context's authorized project session db. The
binding hashes into two digests so a foreign project, store or root is
refused as denied while a moved diff comparison is refused as invalid.
McpToolContext::bind trusted the labels its caller attached: a store lease
and code-index executors each arrived with a scope of their own, so any
public caller could present one project's authority beside another
project's scope, and authorized_project_session_db() minted Authorized for
whatever lease it held. A request that resolved no checkout also waved a
verified graph through, because absence of an admitted scope was read as
nothing to isolate against.

The binding now carries exactly one scope — the checkout project open
resolved for the serving route, published from the daemon through server
construction into tool dispatch. Scoped authorities are admitted under it
instead of relabelling it: AdmittedProjectStore and AdmittedCodeIndex have
private fields and fallible constructors, a store lease is checked against
the logical shard the registry opened it for, and a code-index admission
must carry both the read admission envelope it authenticates and at least
one executor. Authorization is the root's verdict, carried through
untouched, so an unauthorized store denies at the PR-context cursor
authority rather than reporting a missing capability. A graph query with no
admitted scope is refused, and the dead standalone constructor is gone.

PR-context cursor identity drops the reference-sensitive scope digest for
the three fields identifies_same_checkout compares, plus the signing
store's own registered shard, so pagination survives an ordinary branch
switch while a foreign checkout, root, or store stays denied.
The admitted store lease was checked with StoreShardScopeV1::project_id(),
which reports Project, ProjectSessions, and Code shards alike. A lease on
this very project's project store or one of its code stores therefore
passed as project-session authority: the project ids matched, and the
family never entered the comparison. Those are separate stores with their
own tables and retention.

The family is now matched exactly, and exhaustively, so a shard family
added later must be classified rather than inherit an answer. A lease that
is not the admitted project's session shard is refused as
mcp_tool_binding_store_not_session_shard, which also subsumes the profile
and remote-node shards that carry no project at all.

The PR-context cursor's bound store identity had the same flattening: it
reduced the signing shard to the project it mentioned, so one project's
session, project, and code shards produced a single store identity and
their cursors verified interchangeably. It now carries the shard's logical
scope whole through the canonical serialization, which keeps a foreign
store denied rather than merely stale.
@changeset-bot

changeset-bot Bot commented Sep 9, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 5809c14

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 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-09T08:53:56.974165Z 707d451 PR opened
ℹ️ 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

tokio::task::spawn_blocking(work)
.await

P2 Badge Cancel blocking Git workers after dispatch timeouts

When tracedecay_changelog or tracedecay_commit_context exceeds its carried or universal dispatch deadline, tokio::time::timeout drops this future, but dropping a spawn_blocking join handle does not stop a task that has already started. The tree diff, status scan, or rev-walk therefore continues detached; repeated timed-out requests against a large or pathological repository can accumulate filesystem work and exhaust the blocking pool even though every caller has already received a timeout. Route these operations through a cancellation-aware worker like the controlled PR-context path and cover the timeout/drop case.

AGENTS.md reference: AGENTS.md:L154-L156

ℹ️ 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".

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Performance Comparison codex/tracedecay-total-redesign-plan-reopenedsol/mcp-handlers-1073

Total Elapsed Time: 3.94s → 3.95s (+0.2%)
CPU Baseline: 85.29µs → 83.24µs (-2.4%)
Benchmark ID: index-bench-timing

timing - Execution duration of functions.

+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| Function                                 | Calls                      | Avg                            | P95                            | Total                          | % Total                      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| tracedecay-index-bench                   | 1 → 1 (+0.0%)              | 3.94s → 3.95s (+0.3%)          | 3.94s → 3.95s (+0.3%)          | 3.94s → 3.95s (+0.3%)          | 100.00% → 100.00% (+0.0%)    |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| query.artifact.append_pages              | 13 → 13 (+0.0%)            | 117.46ms → 119.10ms (+1.4%)    | 159.51ms → 162.79ms (+2.1%)    | 1.53s → 1.55s (+1.3%)          | 38.73% → 39.19% (+1.2%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| code_index.workers.install               | 83 → 83 (+0.0%)            | 13.63ms → 13.84ms (+1.5%)      | 47.09ms → 47.64ms (+1.2%)      | 1.13s → 1.15s (+1.8%)          | 28.70% → 29.08% (+1.3%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| domain.canonical.sha256                  | 110414 → 110414 (+0.0%)    | 8.54µs → 8.80µs (+3.0%)        | 11.07µs → 11.61µs (+4.9%)      | 943.45ms → 972.25ms (+3.1%)    | 23.93% → 24.61% (+2.8%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| code_index.build.and_publish             | 2 → 2 (+0.0%)              | 442.52ms → 454.54ms (+2.7%)    | 517.73ms → 527.17ms (+1.8%)    | 885.05ms → 909.09ms (+2.7%)    | 22.45% → 23.01% (+2.5%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| query.artifact.batch.sqlite              | 13 → 13 (+0.0%)            | 66.91ms → 68.56ms (+2.5%)      | 90.18ms → 91.62ms (+1.6%)      | 869.85ms → 891.24ms (+2.5%)    | 22.06% → 22.56% (+2.3%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| query.artifact.batch.postings            | 13 → 13 (+0.0%)            | 58.66ms → 60.36ms (+2.9%)      | 80.74ms → 81.92ms (+1.5%)      | 762.61ms → 784.67ms (+2.9%)    | 19.34% → 19.86% (+2.7%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| query.artifact.finalization.advance_wake | 14 → 14 (+0.0%)            | 48.08ms → 45.57ms (-5.2%)      | 277.61ms → 270.01ms (-2.7%)    | 673.10ms → 637.97ms (-5.2%)    | 17.07% → 16.15% (-5.4%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| code_index.chunk.index_file              | 276 → 276 (+0.0%)          | 2.15ms → 2.17ms (+0.9%)        | 3.05ms → 3.17ms (+3.9%)        | 592.13ms → 597.97ms (+1.0%)    | 15.02% → 15.14% (+0.8%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| code_index.extract.parser_artifact       | 276 → 276 (+0.0%)          | 2.09ms → 2.05ms (-1.9%)        | 3.18ms → 3.16ms (-0.6%)        | 577.95ms → 566.62ms (-2.0%)    | 14.66% → 14.34% (-2.2%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| query.artifact.prepare_pages             | 13 → 13 (+0.0%)            | 43.29ms → 42.76ms (-1.2%)      | 62.19ms → 61.64ms (-0.9%)      | 562.79ms → 555.85ms (-1.2%)    | 14.28% → 14.07% (-1.5%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| query.artifact.batch.parallel_prepare    | 13 → 13 (+0.0%)            | 33.62ms → 33.97ms (+1.0%)      | 48.20ms → 49.25ms (+2.2%)      | 437.05ms → 441.57ms (+1.0%)    | 11.09% → 11.18% (+0.8%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| query.artifact.batch.postings.ngram_rows | 13 → 13 (+0.0%)            | 32.70ms → 33.85ms (+3.5%)      | 45.58ms → 46.60ms (+2.2%)      | 425.10ms → 440.05ms (+3.5%)    | 10.78% → 11.14% (+3.3%)      |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| code_index.build.assemble                | 2 → 2 (+0.0%)              | 196.20ms → 202.02ms (+3.0%)    | 205.13ms → 210.24ms (+2.5%)    | 392.40ms → 404.04ms (+3.0%)    | 9.95% → 10.23% (+2.8%)       |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+
| code_index.chunk.build                   | 276 → 276 (+0.0%)          | 1.37ms → 1.37ms (+0.0%)        | 1.90ms → 1.99ms (+4.7%)        | 377.96ms → 379.48ms (+0.4%)    | 9.59% → 9.61% (+0.2%)        |
+------------------------------------------+----------------------------+--------------------------------+--------------------------------+--------------------------------+------------------------------+

Generated with hotpath-rs

@ScriptedAlchemy
ScriptedAlchemy merged commit 434b511 into codex/tracedecay-total-redesign-plan-reopened Sep 9, 2026
11 of 13 checks passed
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.

1 participant