MCP Tool Authority Replay
Synthetic MCP-like replay fixture for a tool-authority claim contract: replay result records, negative cases, scope limits; no live MCP/provider/account secret
The write-up
This component replays a recorded MCP tool trace and calls the tool use safe only when every action shows a narrow scope, prior approval, a rollback path, and untrusted output kept out of instructions. When an agent uses outside tools through a protocol like MCP, the sentence "the agent used the tool safely" is cheap to write. This component makes it expensive. It takes a recorded trace of tool use and checks that every action in it was fenced: bound to a narrow permission, approved before it changed anything, given a way to undo it, and stopped from letting untrusted tool output give the agent orders.
The check runs on a built-in example of labels and references only, scope names, argument hashes, and token ids, so tool-use safety is inspectable without touching a live account. The check runs against a small built-in example, not a live account. The example carries only labels and references: capability scope names, argument hashes, approval token ids, ledger and rollback references. No secrets, no account secrets, no tool contents. So the safety properties an agent's tool use is meant to have become inspectable without anything touching a real provider or account.
It reads a directory of input JSON and emits four metadata-only records carrying counts, status bits, and digests, but no payload, account secret, or tool body. The component reads a directory of input JSON and emits a set of metadata-only result records: a result, a board, a validation record, and a fixture sign-off record. Each records counts, status bits, and reference digests. None of them carries a payload, a account secret, or a tool body.
Purpose
What a loose tool trace hides
Describing a tool call is easy and backing it is hard, and loose prose hides the two failure modes that matter for tool-using agents. A tool call is easy to describe and hard to back. A trace can say a write happened and that it was fine. Loose prose like that hides the two failure modes that matter for tool-using agents.
Confused deputy and injected instructions
The two modes are the confused deputy, where a call borrows a scope wider than its task, and tool-output-as-instruction, where untrusted output is obeyed as a command. The first is the confused deputy: a call asks for a scope wider than its task needs, so a tool quietly borrows more authority than it was granted. The second is tool-output-as-instruction, the prompt-injection shape where text returned by an untrusted tool is obeyed as a command. Both pass a reader who only sees a summary. Both fail once you require the trace to show its working.
Each call is an evidence-citing transaction
Each tool call is treated as a transaction that must cite its evidence, and every reference is cross-checked against the accepted calls by call id rather than taken on trust. So the component treats each tool call as a small transaction that must cite its evidence. A call names a narrow capability scope and an argument hash. If it writes, it must also name an approval token, a side-effect ledger entry, and a rollback record. Those references are not taken on trust. The side-effect ledger and the cold replay records are cross-checked against the accepted call records by call id, so a rollback record no call refers to, or a write that skips approval, is caught rather than waved through. A reference string is not authority until something downstream resolves it.
How it works
Two entrypoints over one pipeline
run validates the first-wave fixture with its eight negative cases and run_tool_authority_bundle validates an exported bundle without them, and both call the shared _build_result pipeline. The two entrypoints are run and run_tool_authority_bundle. run validates the first-wave fixture with the eight negative cases included, then writes the four fixture records through _write_receipts. run_tool_authority_bundle validates an exported bundle without the negative cases and writes a single bundle result record. Both call _build_result, which is the pipeline.
Secret scan runs before plane validation
_build_result runs in a fixed order: load the input JSON, scan inputs and copied source for a leaked secret first, then validate each plane and pass the accepted records forward. _build_result runs in a fixed order. It loads the input JSON with _load_payloads, then runs load_forbidden_classes and scan_paths over the inputs and any copied source files to catch a leaked secret before anything else. It then validates each plane on its own and passes the accepted records forward:
| Function | What it checks |
|---|---|
validate_tool_policy | the declared policy id and allowed tool classes |
validate_tool_manifest | declared tool ids and their classes; produces the set of accepted tool ids |
validate_tool_calls | each call, using _validate_call_row; requires three calls with one approved write and one untrusted result |
validate_tool_results | each result binds to an accepted call id; one untrusted output is kept as data and ignored as instruction |
validate_side_effect_ledger | one approved write side effect whose approval, ledger, and rollback references match its call record |
validate_cold_replay | three passing replays whose evidence references bind to the manifest and to runtime records |
validate_projection_protocol | the protocol id and source-to-target projection references |
Where a call is admitted or blocked
_validate_call_row rejects undeclared tools, overbroad scopes, exported account secrets, unapproved writes, missing rollbacks, and in-record payloads, returning accepted_tool_call_metadata only when no reason fires. _validate_call_row is where a call is admitted or rejected. It rejects an undeclared tool id, an overbroad scope (*, account_full_access, and the like), an exported account secret, untrusted output flagged as instruction, live account access, final-answer-only grading, a write that lacks approval or a ledger reference, a missing rollback record, and any call whose payload sits in the record instead of staying metadata-only. It also checks that every field in REQUIRED_CALL_FIELDS is present. A clean call returns accepted_tool_call_metadata; any reason at all returns blocked.
Trace spans and the body floor
After the planes validate, three public trace spans are contributed and the body floor verifies each copied source digest against its live reference with body_in_receipt: false on every row. After the planes validate, build_public_mcp_tool_authority_trace contributes three public trace spans for the calls, and _source_module_manifest_result runs the body floor for the bundle path: it verifies the digest of each copied source file against its live source reference, checks required anchors, and requires body_in_receipt: false on every row. _body_import_verification binds that public refactor back to the source and target digests.
The final status is pass only when every plane passes, no expected negative case is missing, the secret scan has zero blocking hits, and the source-module floor is present when the bundle requires it. The final status is pass only when every plane passes, no expected negative case is missing, the secret scan has zero blocking hits, and the source-module floor is present when the bundle requires it. _write_receipts then emits the result, board, validation, and sign-off records, and result_card projects a compact card that carries counts and status bits while omitting the tool rows, call rows, findings, and source bodies listed in CARD_OMITTED_FULL_PAYLOAD_KEYS.
Diagram source & refs
flowchart TD input["input JSON directory"] scan["secret scan load_forbidden_classes + scan_paths"] planes["plane validators policy, manifest, calls, results, side effects, cold replay"] trace["three public trace spans"] floor["source-module body floor"] status["pass only if all clear"] records["metadata-only result records"] input --> scan scan --> planes planes --> trace planes --> floor trace --> status floor --> status status --> recordsNegative cases
Eight negatives as a falsification set
The first-wave fixture ships eight negative inputs, and a run passes only if it observes the error code for every one, making the fixture a falsification set. The first-wave fixture ships eight negative inputs, one per case in EXPECTED_NEGATIVE_CASES. A first-wave run passes only if it observes the error code for every one of them, so the fixture is a falsification set the validators have to trip on:
overbroad_scoperaisesMCP_TOOL_OVERBROAD_SCOPE.- hidden account secret export raises MCP TOOL HIDDEN account secret EXPORT.
tool_output_as_instructionraisesMCP_TOOL_OUTPUT_AS_INSTRUCTION.unapproved_side_effectraisesMCP_TOOL_UNAPPROVED_SIDE_EFFECT.live_account_accessraisesMCP_TOOL_LIVE_ACCOUNT_ACCESS.final_answer_only_gradingraisesMCP_TOOL_FINAL_ANSWER_ONLY_GRADING.missing_rollback_receiptraisesMCP_TOOL_MISSING_ROLLBACK_RECEIPT.unredacted_tool_payloadraisesMCP_TOOL_UNREDACTED_PAYLOAD_EXPORT.
An unobserved case blocks the run
A negative case that never trips is recorded under missing_negative_cases, and its absence blocks the run rather than letting it pass. If any expected case goes unobserved, _build_result records it under missing_negative_cases and the run is blocked.
Prior Art Grounding
Capability security made replayable
The design follows capability-security and least-privilege lineage from Saltzer and Schroeder through Hardy and the MCP authorization spec, then makes it replayable so scope and approval references must precede any write authority. The component follows capability security, least privilege, and current MCP authorization guidance. The classic lineage is Saltzer and Schroeder's Protection of Information in Computer Systems and Hardy's Confused Deputy: authority should be narrow, mediated, and bound to the action being requested. The MCP-specific lineage is the official MCP authorization and security best practices guidance on least-privilege scopes and token audience boundaries. The contribution here is making that shape replayable: a tool story has to expose scope, approval, side-effect, rollback, and instruction/data references before its write authority counts as evidence.
Validation Result record Path
Run the first-wave fixture into disposable records from the Microcosm root:
Run the exported bundle through the same component:
cd microcosm-substrate
PYTHONPATH=src ../repo-python -m microcosm_core.organs.mcp_tool_authority_replay run-tool-authority-bundle --input examples/mcp_tool_authority_replay/exported_mcp_tool_authority_bundle --out /tmp/microcosm_mcp_tool_authority_bundle
Then run the focused test and the corpus parity check:
cd microcosm-substrate
PYTHONPATH=src ../repo-python -m pytest -p no:cacheprovider tests/test_mcp_tool_authority_replay.py -q
PYTHONPATH=src ../repo-python scripts/build_doctrine_projection.py --check-paper-module-corpus
What a pass certifies
A green run certifies only internal consistency: every plane validated, all eight negatives observed, the secret scan clean, and the records metadata-only. A pass means every plane validated, all eight negative cases were observed, the secret scan found nothing, and the records stayed metadata-only.
Scope boundary
Scope limit
The strongest supported claim
The strongest claim is narrow: a synthetic MCP-like replay preserved its tool-authority boundaries over metadata records, with every reference and negative case lined up or the run blocked. The strongest claim is this: a synthetic MCP-like replay preserved its tool-authority boundaries over metadata records. Capability scopes, argument hashes, approval references, side-effect and rollback references, instruction/data splits, cold replay references, source digests, the eight negative cases, and metadata-only validation records all lined up, or the run blocked.
What stays false on a pass
Even on a pass the component opens no account, calls no provider, and handles no account secret, so it claims no live MCP safety and certifies no real integration secure. That is the scope limit and the proof boundary. The component never opens an MCP account, calls a provider, or handles a account secret, and it keeps every payload, tool result, and account secret out of what it writes. So it does not claim live MCP account safety, account secret-handling certification, live tool or provider behavior, benchmark security, source-file changes, complete security, publishing-scope decision, or launch-scope decision. It checks that a tool-authority story is internally consistent and metadata-only. It does not certify that any real tool integration is secure.
Context & evidence
In short Public Plectis projection of a tool-authority claim contract. Replays a synthetic MCP-like tool-authority scenario and validates replay result records, negative cases, and scope limits with metadata-only result records.
Scope limit Synthetic MCP-like replay fixture only; not a live MCP account test, external model access, account secret-handling certification, benchmark security result, or launch claim.
Covers MCP Tool-Authority Policy Replay
Source
Source Source module: src/microcosm_core/organs/mcp_tool_authority_replay.py · Design note · Source registry