This repository was archived by the owner on Oct 24, 2026. It is now read-only.
Repository navigation
fix(mcp): a well-formed upstream result is never delivery_uncertain - #853
Merged
Merged
Conversation
…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>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 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".
…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>
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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
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_toolvalidatesstructuredContentagainst the upstream's liveoutputSchemaafter the response arrives. The adapter opens a fresh session per call, so the SDK first sends a secondtools/list. That work runs past the dispatch boundary. Schema drift, a failing post-calltools/list, a schema demanding structured content, or a hostile external$refraised an exception that discarded the received result. The generic handler then sawdispatch_started=Trueand classified the calldelivery_uncertain.Fix (
app/services/upstream_mcp.py,UpstreamMcpAdapter._bypass_sdk_result_validation): seed the SDK's per-session schema cache withNonefor the configured tool before the dispatch checkpoint. The SDK then sends no post-dispatchtools/listand 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_rejectedbecause the SDK raises a bareRuntimeErrorfrom insidecall_tooland discards the receivedCallToolResult. There is nothing to hash or return, and a failed post-calltools/listcannot be told from a failedtools/callby exception type.What is in this PR
Three commits, oldest first:
e862e6f0and907659a0are cherry-picks (-x) of the two commits ondocs/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 onc0eed0c4(2026-09-30), which is 1,411 commits behindmain, so a fix written against it would not apply to shipping code.docs/jev-battery-gate-fixesitself is untouched and will conflict withmainindocs/README.mdandrequirements.txtwhenever it is rebased.07425e95is the fix, its tests, and the doc updates (docs/mcp-security-posture.mdsections 1, 2 and 3.2,docs/failure-semantics.md,docs/gateway-overhead.md,CHANGELOG.md).Behavior change
successregardless of the upstream's liveoutputSchema. Before, it could enddelivery_uncertain(504 / -32005).$reffetch during result validation) is structurally closed on the call path, because validation no longer runs.Dependency and compatibility notes
pyproject.tomlandrequirements.txtraise the mcp floor tomcp>=1.30.0,<2. The fix reads a private SDK attribute (ClientSession._tool_output_schemas), which is why the upper bound matters.upstream_pre_dispatch_failed, refunded, retry-safe) and logsupstream_mcp_sdk_result_validation_not_disabled. It does not fall back to unvalidated dispatch.Tests
tests/test_mcp_upstream_tool_poisoning.py: the strictxfailtest_live_output_schema_drift_must_not_turn_a_completed_call_uncertainis now a plain passing test. New negative-path tests cover a drifted schema, a failing post-calltools/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_sessionover HTTP.tests/test_mcp_upstream_governed.py: end-to-end receipt test (one debit, one dispatch, asuccessreceipt) through the governed router with a drifted live schema.Run locally on this branch from a committed clean tree (
.venv/bin/pytest, marker-gated):tests/test_mcp_upstream_governed.py,tests/test_upstream_mcp.py,tests/test_mcp_dispatch_reconciliation.pyand the poisoning file: 148 passed.tests/ -m "not proof": 5201 passed, 1 failed. The failure istests/test_secret_scan_range.py::test_pinned_verified_base_is_on_this_history. It is unrelated to this change: the test pins commitdd160511, which is not an ancestor oforigin/main. This PR does not touch that test or its pin._bypass_sdk_result_validationcall 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) andgitleaks protect --stagedare clean.Not covered
contenta string,isErrornot a bool) still endsdelivery_uncertainthrough 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
<2bound 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_uncertainwhen the MCP SDK rejects a result after dispatch (liveoutputSchemadrift, post-calltools/list, hostile$ref). The adapter seeds the SDK session’s_tool_output_schemascache before the dispatch checkpoint (_bypass_sdk_result_validation), skipping post-dispatch validation and the extratools/list; well-formed received results are chargedsuccesseven when they disagree with the live schema. Missing cache fails closed pre-dispatch.Separately, envelopes the SDK cannot parse into
CallToolResultare recorded at thecall_toolawait asresponse_rejected(upstream_response_invalid), fixing misclassification when anyio wraps errors inExceptionGroup. Malformed envelopes no longer feed the circuit breaker.On
POST /mcp(when enabled),response_rejectedis surfaced likedelivery_uncertain:isErrortool results with do-not-resend text via_unknown_outcome_tool_result, because-32006also 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.