Skip to content
This repository was archived by the owner on Oct 24, 2026. It is now read-only.

fix(mcp): a well-formed upstream result is never delivery_uncertain - #853

Merged
PetrefiedThunder merged 5 commits into
mainfrom
claude/objective-knuth-d05d5a
Oct 24, 2026
Merged

PetrefiedThunder merged 5 commits into
mainfrom
claude/objective-knuth-d05d5a

Conversation

@PetrefiedThunder

@PetrefiedThunder PetrefiedThunder commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

A completed upstream MCP call could be signed delivery_uncertain. That outcome keeps the debit, is never redispatched, and puts a receipt in the audit trail that misstates a call that finished. A hostile upstream could force it, and each forced case counted toward the circuit breaker.

Cause: in mcp 1.30.0, ClientSession.call_tool validates structuredContent against the upstream's live outputSchema after the response arrives. The adapter opens a fresh session per call, so the SDK first sends a second tools/list. That work runs past the dispatch boundary. Schema drift, a failing post-call tools/list, a schema demanding structured content, or a hostile external $ref raised an exception that discarded the received result. The generic handler then saw dispatch_started=True and classified the call delivery_uncertain.

Fix (app/services/upstream_mcp.py, UpstreamMcpAdapter._bypass_sdk_result_validation): seed the SDK's per-session schema cache with None for the configured tool before the dispatch checkpoint. The SDK then sends no post-dispatch tools/list and runs no schema validation. A received result is judged only by the adapter's own shape, size and secret checks.

Chosen over reclassifying the SDK failure as response_rejected because the SDK raises a bare RuntimeError from inside call_tool and discards the received CallToolResult. There is nothing to hash or return, and a failed post-call tools/list cannot be told from a failed tools/call by exception type.

What is in this PR

Three commits, oldest first:

  1. e862e6f0 and 907659a0 are cherry-picks (-x) of the two commits on docs/jev-battery-gate-fixes: the MCP security posture doc, exit-and-export doc, tool-poisoning tests, gateway overhead measurement, and the mcp floor. That branch is based on c0eed0c4 (2026-09-30), which is 1,411 commits behind main, so a fix written against it would not apply to shipping code. docs/jev-battery-gate-fixes itself is untouched and will conflict with main in docs/README.md and requirements.txt whenever it is rebased.
  2. 07425e95 is the fix, its tests, and the doc updates (docs/mcp-security-posture.md sections 1, 2 and 3.2, docs/failure-semantics.md, docs/gateway-overhead.md, CHANGELOG.md).

Behavior change

  • A well-formed upstream result is now a charged success regardless of the upstream's live outputSchema. Before, it could end delivery_uncertain (504 / -32005).
  • The gateway no longer notices a mismatch between a result and the upstream's live schema. It never checked a pinned schema either. Drift detection is listed as still open in the posture doc.
  • GHSA-rwrf ($ref fetch during result validation) is structurally closed on the call path, because validation no longer runs.

Dependency and compatibility notes

  • pyproject.toml and requirements.txt raise the mcp floor to mcp>=1.30.0,<2. The fix reads a private SDK attribute (ClientSession._tool_output_schemas), which is why the upper bound matters.
  • If a future mcp release drops that attribute, the call fails closed before the dispatch claim (upstream_pre_dispatch_failed, refunded, retry-safe) and logs upstream_mcp_sdk_result_validation_not_disabled. It does not fall back to unvalidated dispatch.

Tests

  • tests/test_mcp_upstream_tool_poisoning.py: the strict xfail test_live_output_schema_drift_must_not_turn_a_completed_call_uncertain is now a plain passing test. New negative-path tests cover a drifted schema, a failing post-call tools/list, a schema demanding structured content, a hostile external $ref (outbound connections made to fail), and unchanged normal success. Boundary tests cover an error result and no result. Also covered: fail-closed on a missing SDK cache, and the production _session over HTTP.
  • tests/test_mcp_upstream_governed.py: end-to-end receipt test (one debit, one dispatch, a success receipt) through the governed router with a drifted live schema.

Run locally on this branch from a committed clean tree (.venv/bin/pytest, marker-gated):

  • Targeted: tests/test_mcp_upstream_governed.py, tests/test_upstream_mcp.py, tests/test_mcp_dispatch_reconciliation.py and the poisoning file: 148 passed.
  • tests/ -m "not proof": 5201 passed, 1 failed. The failure is tests/test_secret_scan_range.py::test_pinned_verified_base_is_on_this_history. It is unrelated to this change: the test pins commit dd160511, which is not an ancestor of origin/main. This PR does not touch that test or its pin.
  • Red check: with the one _bypass_sdk_result_validation call neutralized, 8 of the 14 poisoning tests fail and the governed end-to-end test returns -32005 delivery_uncertain.
  • ruff, scripts/check_doc_references.py, trufflehog (3 commits, 0 findings) and gitleaks protect --staged are clean.

Not covered

  • Not tested against a real third-party MCP server. The tests use the real SDK client against in-process and loopback HTTP SDK servers.
  • A neighbouring defect is recorded but not fixed here: a response whose envelope the SDK cannot parse (content a string, isError not a bool) still ends delivery_uncertain through a real SDK session. It is fixed in the follow-up PR, which is stacked on this branch.

Remaining risk

The adapter now depends on a private SDK attribute. The <2 bound and the fail-closed check contain that, but an mcp release that renames the attribute would turn every upstream call into a refunded pre-dispatch failure until the adapter is updated.

🤖 Generated with Claude Code


Note

High Risk
Changes charged terminal outcomes and POST /mcp client-visible error shapes, and depends on a private MCP SDK attribute; billing, replay, and circuit-breaker behavior are affected on the governed upstream path.

Overview
Fixes upstream MCP outcome classification so completed calls are not signed delivery_uncertain when the MCP SDK rejects a result after dispatch (live outputSchema drift, post-call tools/list, hostile $ref). The adapter seeds the SDK session’s _tool_output_schemas cache before the dispatch checkpoint (_bypass_sdk_result_validation), skipping post-dispatch validation and the extra tools/list; well-formed received results are charged success even when they disagree with the live schema. Missing cache fails closed pre-dispatch.

Separately, envelopes the SDK cannot parse into CallToolResult are recorded at the call_tool await as response_rejected (upstream_response_invalid), fixing misclassification when anyio wraps errors in ExceptionGroup. Malformed envelopes no longer feed the circuit breaker.

On POST /mcp (when enabled), response_rejected is surfaced like delivery_uncertain: isError tool results with do-not-resend text via _unknown_outcome_tool_result, because -32006 also covers refunded retry-safe errors.

Also raises mcp>=1.30.0, adds poisoning/governed-router tests, gateway overhead measurement script and research artifacts, and large doc/runbook updates (security posture, failure semantics, supported endpoints).

Reviewed by Cursor Bugbot for commit 8a55029. Bugbot is set up for automated code reviews on this repo. Configure here.

PetrefiedThunder and others added 3 commits October 22, 2026 17:05
…tery gates C2, H5, K2)

Self-scoring against the JEV agent-middleware interrogation battery capped
the product at 54/100 on gate C2: no written position on the April 2026
MCP SDK STDIO disclosure. Two more gates lost points for a missing
tool-poisoning scenario (H5) and no written export story (K2).

- docs/mcp-security-posture.md: the MCP attack classes and where each is
  handled, with file-level evidence; the two anti-patterns (confused
  deputy, token passthrough) stated plainly, including the one-service-
  identity gap to the upstream; the April 2026 disclosure as described by
  both CSA notes and the Python SDK advisory list, and why this repo was
  not exposed (no STDIO client anywhere in the tree); the SDK advisories
  that do touch the transports in use; the mcp version pins, including
  the uv.lock specifier drift against pyproject.
- docs/exit-and-export.md: every artifact a customer keeps, its route,
  format, and whether the verifier is independent or first-party; says
  outright that no traces exist and audit-chain verification is
  first-party.
- tests/test_mcp_upstream_tool_poisoning.py: a duplicated tool name is
  refused before any dispatch; a definition rewritten after boot never
  reaches the agent (pinning, not drift detection); the registered
  catalog carries the gateway description, not the upstream's. The
  hostile-description and bearer-echo cases were already covered in
  tests/test_upstream_mcp.py.
- Linked both pages from docs/README.md and SECURITY_LIMITATIONS.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit b8f8806c1a4e2ad9d48f95ad522243752d9054b3)
…ement; raise mcp floor

An extract, refute, critique pass over the three new pages and the new test
found 27 refuted claims and about 60 critic findings. All wrong and
misleading ones are fixed; the pages are rewritten where the fix was larger
than a sentence.

Findings that change what a reviewer should believe:
- The real MCP SDK lists tools after a call is dispatched and validates the
  result against the upstream's live outputSchema. Verified with a probe: a
  live schema the result does not satisfy makes the adapter sign
  delivery_uncertain for a call that completed. This withdrew the earlier
  claim that GHSA-rwrf was structurally mitigated; only the 1.30.0 floor
  protects. Pinned by a strict expected-failure test.
- POST /mcp mints its own single-call permit and generates an idempotency key
  when none is sent, so permit constraints and keyed replay protection do not
  apply there. README.md and SECURITY_LIMITATIONS.md call it the supported
  path; docs/deploy-railway.md says never to enable it. Flagged, not resolved.
- The input schema is published verbatim, so it is a residual poisoning
  channel. Recorded by a test instead of described as stripped.

Changes:
- docs/mcp-security-posture.md: rewritten with endpoint table, per-endpoint
  caveats, advisory ranges from the advisory pages, external sources and
  retrieval dates, and the pin situation (uv.lock is gitignored, so it is no
  longer cited).
- docs/exit-and-export.md: rewritten with credentials and rate limits,
  quiesce and deletion caveats, verifier availability (the only published SDK
  release predates the verifier), and the audit chain's real export limits.
- docs/gateway-overhead.md and scripts/measure_gateway_overhead.py: published
  handler time (about 30 ms first call, about 4.5 ms replay, stub upstream,
  SQLite, loaded M4 Pro) with provenance, load, and the figures it does not
  reconcile with. The script now asserts JSON-RPC success and distinct
  receipts, prices the tool under the hourly velocity guard, and records git
  state, load, and package versions.
- tests/test_mcp_upstream_tool_poisoning.py: assertions that cannot fail are
  replaced, tests now go through the registry, and the residual-schema and
  live-schema-drift cases are added.
- requirements.txt, pyproject.toml: mcp floor 1.29.0 to 1.30.0 (fixes five
  SDK advisories; the Docker image installs the floor unpinned).

Full non-proof suite: 2961 passed, 46 skipped, 1 xfailed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 7356a9017c4e8397282588c79f342fe9c771c726)
ClientSession.call_tool (mcp 1.30.0) validates a result against the
upstream's live outputSchema after the response arrives, sending a second
tools/list first because the adapter opens a fresh session per call. That
work runs past the dispatch boundary, so schema drift, a failing post-call
tools/list, or a schema demanding structured content raised an exception
that discarded the received result. The adapter then signed
delivery_uncertain for a call that had completed: debit kept, never
redispatched, receipt misstating the outcome, and a hostile upstream could
force it (and count it toward the circuit breaker).

Seed the SDK's per-session schema cache with None for the configured tool
before the dispatch checkpoint, so the SDK sends no post-dispatch tools/list
and runs no schema validation; a received result is judged only by the
adapter's own shape, size, and secret checks. If an mcp release drops the
cache attribute, the call fails closed before the dispatch claim.

Chosen over reclassifying SDK failures as response_rejected because the SDK
raises a bare RuntimeError from inside call_tool and discards the received
CallToolResult, so there is nothing to hash or return and a failed post-call
tools/list cannot be told from a failed tools/call by exception type.

Tests: the strict xfail is now a plain pass; adds drifted schema, failing
post-call tools/list, no structured content, hostile external $ref (outbound
connections made to fail), unchanged normal success, error result and
no-result boundary tests, fail-closed on a missing SDK cache, the production
_session over HTTP, and a receipt-level test through the governed router.
Docs: mcp-security-posture sections 1, 2 and 3.2, failure-semantics,
gateway-overhead, CHANGELOG. Records, but does not fix, a neighbouring defect:
a malformed response envelope still ends as delivery_uncertain through a real
SDK session.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 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-10-24T05:25:16.900291Z 07425e9 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

Here are some automated review suggestions for this pull request.

Reviewed commit: 07425e95a1

ℹ️ 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 docs/exit-and-export.md Outdated
PetrefiedThunder and others added 2 commits October 23, 2026 22:29
…ces are produced

Review finding on #853. docs/exit-and-export.md said no OpenTelemetry
instrumentation and no OTLP export exist, in the Traces row of section 1 and
again in section 3. app/core/tracing.py configures an OTLPSpanExporter and
app/main.py installs the tracing middleware (install_tracing), enabled by
OTEL_TRACING_ENABLED, default off, with the otel extra installed. A customer
following the exit procedure could overlook spans retained in a collector the
operator runs.

Both places now say optional tracing exists and is off by default, name the
six spans (the allowed set at app/core/tracing.py:26-31) and the attribute
allowlist (tool name, outcome state, wallet id hash, attempt id), and say
there is no export route and spans live in the operator's collector. Text is
identical to the same two hunks already in the stacked follow-up (#854), so
the two PRs do not conflict on these lines.

Text only; no behavior change.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… guides on rejected responses; correct posture claims (#854)

* fix(mcp): an unparseable upstream result envelope is response_rejected, not delivery_uncertain

A tools/call response the upstream sent but whose envelope the MCP SDK cannot
parse (content a string, isError not a bool, ...) ended as delivery_uncertain
(504 / -32005, counted by the circuit breaker) through a real SDK session,
though _call_tool_once intends response_rejected. The SDK's anyio task group
rewraps anything raised inside the session block in an ExceptionGroup, so the
adapter's ValidationError branch never matched and the generic handler
classified the call by dispatch_started alone. Tests that fake the session do
not enter the task group, which is why they passed.

_call_tool_once now records the rejection at the call_tool await, where the
SDK raises ValidationError only after the upstream's JSON-RPC result has
arrived, before the session unwinds. The call ends as response_rejected with
code upstream_response_invalid (502 / -32006): debit kept, never redispatched,
replay returns the same receipt. Precedence is unchanged: claim contention,
guard rejection, a known received result, this rejection, then
delivery_uncertain only when no response was obtained. A call with no response
(timeout, dropped connection, JSON-RPC error answering tools/call) and a
malformed initialize answer keep their outcomes.

The ValidationError branch also lacked the "a result is already in hand" guard
its two sibling handlers have, so a ValidationError or JSONDecodeError raised
only while the session closes could turn a received result into
response_rejected or replace a known rejection's code. It has the guard now.

Behavior change: a malformed envelope no longer counts toward the optional
circuit breaker, like the other response_rejected causes.

Tests drive a real SDK session behind the real adapter (in memory and over the
production Streamable HTTP client), including a fixture guard that the
envelope reaches the client and fails there, the no-response boundary, a
secret-echo check, and a governed-router receipt test (one debit, one
dispatch, a response_rejected receipt, no redispatch on replay, breaker state).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(mcp): /mcp tells the model not to resend a rejected response; reject only unparseable results

Two review findings on 7dbcd6d.

1. /mcp (app/routers/mcp_standard.py) turned only delivery_uncertain into a
   tool result carrying the do-not-resend text. response_rejected, which is
   also charged (the call was dispatched and the debit stands), became a bare
   -32006, the code that refunded, retry-safe failures
   (upstream_returned_error, upstream_pre_dispatch_failed) share. A model that
   sees only the message could resend a call whose charge stands and whose tool
   probably ran. response_rejected now returns the same guided result:
   isError true, text saying the upstream answered but the answer was unusable
   and the effect is unknown, the signed receipt, and
   _meta["io.agentmiddleware/outcome"] with status "unknown" and
   reason "response_rejected". The gateway outcome recorded for insights stays
   "failed", as it was on the JSON-RPC channel. Refunded failures stay bare
   errors; /mcp/messages and REST are unchanged.

2. The inner except ValidationError in _call_tool_once also caught a pydantic
   error raised while the SDK builds the request, before anything is sent
   (arguments with a non-string key: the claim is committed, no tools/call
   reaches the upstream, and the call was labeled response_rejected, which
   asserts the upstream answered). It now counts only a ValidationError for
   CallToolResult; any other falls through to the generic handler as before
   (delivery_uncertain, conservative since the checkpoint was claimed).

Tests: a /mcp end-to-end test with a real SDK upstream answering an
unparseable envelope (readable tool result, receipt, one debit, no redispatch
on replay), the existing test that upstream_returned_error stays a bare error,
and a real-SDK test that an unsendable request is not a rejected response. The
fake-session test now raises a genuine CallToolResult validation error.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(mcp): record the endpoint decision, re-pin stale citations, re-measure gateway overhead

Follow-up to 0698168.

Endpoint decision. POST /mcp/messages and the REST invoke route are the live
governed path in the supported pilot. POST /mcp ships off
(ENABLE_STANDARD_MCP_ENDPOINT defaults to false) and the production runbook
keeps it off, because it mints a short-lived single-tool permit and generates
an idempotency key on the caller's behalf. README.md and SECURITY_LIMITATIONS.md
no longer call it the supported or preferred path today. The posture doc
records the decision instead of describing a contradiction, and the overhead
page no longer calls /mcp/messages "not the supported endpoint".
docs/deploy-railway.md and docs/mcp-registry-submission.md were already
correct and are untouched.

Citations. The file:line citations in docs/mcp-security-posture.md and
docs/gateway-overhead.md were written against a base more than a thousand
commits older. Of 17, 15 had moved, one SDK range was a line short and
mcp.py:15 was still right. Each is re-pinned and the claim beside it re-read.
Corrected: idempotency_key_required applies to every governed call (including
any call in strict trust mode), not only permit-bound ones; a signed denied
receipt is issued only when the permit names the caller's wallet; the SDK
validation range is 412-448; exit-and-export.md said no OpenTelemetry
instrumentation exists, but optional tracing (OTEL_TRACING_ENABLED, default
off) does; the poisoning-test docstring named one call site of
register_configured_upstream_mcp where scripts/failure_lab.py is a second.

Response classification docs. failure-semantics and the posture page now state
the caveat that a body that is not a JSON-RPC message at all (non-JSON text,
jsonrpc 1.0, neither result nor error, unknown request id) still ends as
delivery_uncertain after the call timeout, each observed against a scripted
transport; that with the reconcile worker enabled response_rejected is
terminal and never probed or refunded; and that /mcp now returns
response_rejected as a readable tool result. The CHANGELOG no longer says
precedence is unchanged for the outer ValidationError branch, which gained the
guard, and describes the transport behaviour accurately. TROUBLESHOOTING gains
a response_rejected section and authority-required-flow names the renamed
helper.

Gateway overhead re-measured twice on this base (commit 0698168, git_dirty
false, load average recorded): first call p50 29.5-30.9 ms, replay p50
5.4-5.6 ms (earlier runs 29.5-31.9 and 4.4-4.7). Results in
docs/research/gateway-overhead-2026-10-23/. The public site's figure is not
touched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(mcp): one parse-failure rule for both handlers; correct posture claims; finish the endpoint wording

Second review of 0698168 and 6638679.

Code. The outer ValidationError / JSONDecodeError branch of _call_tool_once
still answered response_rejected for any such error after dispatch, so a
session that does not wrap exceptions would again label the request-build
error response_rejected. Both handlers now use one rule,
_is_unparseable_response: after dispatch a CallToolResult validation error or a
JSON decode error is response_rejected, and any other validation error is
delivery_uncertain.

Tests. A non-wrapping session double covers both directions. The /mcp
guided-result test now covers an oversized result and a bearer echo as well as
an unparseable envelope, and asserts nothing the upstream wrote is relayed. A
spy test pins the gateway outcome recorded on /mcp (failed for a rejected
response, unknown for an undelivered one); a mutation that always marked
"unknown" used to pass every test. A new test pins that POST /mcp honours a
permit the caller names, enforcing forbidden_fields with a signed denied
receipt, and mints an unconstrained permit only when the reference is omitted.
The keyless test asserts the generated key is readable in the receipt as the
forwarded key, and the comment in mcp_standard.py that said it is never
returned is corrected.

Docs, claims that did not hold. The posture page said operator permit
constraints apply only on /mcp/messages and REST (not so when the caller names a
permit), that there is no KMS and no row-level security (both exist as opt-ins,
default off), and listed governed_call causes incompletely. It now also notes
that the refunded upstream_returned_error relays the upstream's result payload
in error.data on all three surfaces. exit-and-export.md no longer says traces
are not produced and says the only mounted DELETE route that deletes anything
is for API keys. gateway-overhead.md quotes only the first-call p99 and max and
records the kernel release rather than a macOS version the result files do not
contain. The poisoning-test docstring lists all three call sites.

Endpoint wording, leftovers. The REST invoke route's OpenAPI description no
longer tells every integration to call POST /mcp; docs/openapi.json is
regenerated with scripts/export_openapi.py and differs by that one line. The
Cursor plugin README and skill say the standard endpoint ships off. README
names the REST invoke route as supported; authority-required-flow and two test
docstrings no longer present /mcp as the surface integrations are told to use.
The plugin's mcp.json, which targets /mcp on the first-party host, is
deliberately unchanged: that is a product decision.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(mcp): correct the idempotency-key and delete claims; say /mcp/messages is the live path

Third review of 67d56b3. Text and served text only; no behavior change.

Refuted claims. The posture page and CHANGELOG said an action-bound call
without an idempotency key is refused with idempotency_key_required on
/mcp/messages and REST. It is not: mcp.py replaces the key with a
server-derived act1- key (action_permits.py) before that check, and a call that
names no permit is refused earlier as permit_required. The refusal is for a
call that names a permit and sends no key. exit-and-export.md and the CHANGELOG
said the API-key DELETE route is the one that deletes something; it only
revokes (status, revoked_at, revoke_reason) and keeps the row, so in the
default configuration no mounted DELETE route deletes anything.

Served text. The /mcp/messages OpenAPI description called it a legacy endpoint
"kept for existing clients and the local proof scripts" and named only /mcp,
which ships off, as an alternative. It now says it is the live governed path in
the supported pilot and why it is still marked deprecated (a project transport
that does not implement the standard MCP initialization lifecycle, to be
replaced by /mcp once an operator enables it). The REST invoke description and
site/docs-src/http-api.md get the same fix. The description keeps its line
count so the cited mcp.py lines do not move. docs/openapi.json is regenerated
under requirements.lock and differs by those two descriptions. The
machine-read discovery labels (legacy_project_transport,
legacy_project_json_rpc) are deliberately unchanged.

Smaller fixes. The _call_tool_once docstring now says only a CallToolResult
parse failure is rejected, while a body that is not a JSON-RPC message is
logged by the SDK transport, never raised, and ends delivery_uncertain at the
timeout. On REST the relayed upstream text is detail.upstream_result in the
502 body. The CHANGELOG lists the other causes of response_rejected, which
/mcp matches on by reason. Two comments that contradicted corrected claims are
fixed (mcpContext.permit_id is read; the keyless key is readable in the
receipt). failure-semantics cites a test that exercises /mcp/messages and says
the REST status is untested. The Cursor plugin READMEs name REST invoke as a
live path and carry the ships-off caveat; mcp.json is untouched.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* docs(mcp): scope the act1- key and idempotency_key_required claims precisely

Fourth review of 93aee1a. Text only; no .py file differs from that commit.

The posture page said the server derives an act1- key "from the permit and
payload". The digest (action_permits.py) covers only the action permit's
identity: contract version, permit id, subject wallet, public tool id and
upstream binding hash. The permit's action_payload_hash goes into the request
payload, not the key. It now says so.

The same row said a governed call that names no permit is refused earlier as
permit_required. Two checks answer first: an action-bound tool called without
an action permit is action_permit_required (mcp.py:1672-1676), and an unknown
tool is "Tool not found" (mcp.py:1740-1750). Both are named, with the lines.

An older CHANGELOG entry and failure-semantics.md still said
idempotency_key_required applies to every governed call, including any call in
strict trust mode. Both now scope it to a call that names a permit and sends no
key, matching the posture row. exit-and-export.md called the API-key route "the
one mounted DELETE route" in a sentence that also names two DELETE stubs; it
now says the API-key route only revokes and the stubs refuse, so no default
mounted DELETE route deletes anything.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@PetrefiedThunder
PetrefiedThunder enabled auto-merge (squash) October 24, 2026 05:34
@PetrefiedThunder
PetrefiedThunder merged commit d7bc941 into main Oct 24, 2026
29 checks passed
@PetrefiedThunder
PetrefiedThunder deleted the claude/objective-knuth-d05d5a branch October 24, 2026 05:53
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant