A record can ask for a write; it cannot authorize one

A third-party record requests a write in prose, and a user authorizes one with a directive. Each row states only what the call it displays actually reached.

You are here
SourceIngressEgressAuthorizationSink

1. Read an evidence record

STATE RECORDED
Content arrives under an MCP source context. An instruction sits inside a display:none element and is removed before the text is returned.
Removed 1 CSS-hidden element(s)

2. Read a pentest finding

STATE RECORDED
The payload field of a stored-XSS finding is attacker-authored by construction. Nothing is removed here, because this is legitimate visible finding text: the detector reports it and it is returned inside an untrusted_content wrapper. It asks, in prose, for the write attempted in the next row.
instruction_override

3. The record asks for the write

BLOCKED
The user's turn here was 'Why is CC6.1 still failing?'. Authorization comes from a directive, and 6 prose inputs were run through the same parser to check that: the record verbatim, a user negating it, a user asking about it, a user quoting it, a near-miss synonym, the read turn. Every one produced nothing, so the proposal arrives carrying no authorization event.
Authorization required: session contaminated=require_auth

4. Control: the same call, uncontaminated session

ALLOWED
The identical call and context against a session that ingested nothing. It is implicitly allowed, because this tool is not in DESTRUCTIVE_TOOLS. That bounds the row above: contamination is what introduced the authorization requirement, not anything intrinsic to the tool.
Non-destructive tool, implicit allow

5. The user authorizes the same write

ALLOWED
Same tool and same arguments ({'monitor_id': '4471', 'ignored': True}) in the same contaminated session. The user issued '/ignore-monitor 4471', which the parser accepts because it is a directive rather than a sentence about one, so the call carries an event, a binding, and a message hash.
Authorization verified

6. The credential is caught leaving

BLOCKED
The synthetic key the client would authenticate with, placed in ordinary outbound prose. The block is recorded against the session.
Secret pattern detected: High-entropy token (5.3 bits)

7. A later write, same session

BLOCKED
A second write the user asked for, on an adjacent monitor ({'monitor_id': '4472', 'ignored': True}). The session-risk gate returns before the policy engine and the binding verifier run, so this denial by itself says nothing about whether the authorization was valid.
Tool call denied: session contaminated=require_auth; egress escalated=deny

8. Control: same call, same artifacts, contaminated but not escalated

ALLOWED
The identical call carrying the identical AuthorizationEvent and Binding, against a session that ingested the same two records and so carries the same contamination, but never blocked an egress. It runs the full path, so the policy engine validates the event and the verifier checks the binding. Escalation is the variable.
Authorization verified
Evidence, scope, and reproduction

Scope and instrumentation

One SecurityPipeline runs the six session rows, so their state transitions are continuous. The two control rows are separate pipelines, marked as such in the fixture.

No model and no MCP server run here. A permitted result means the gate returned allowed, not that a write was performed.

Authorization origin is a host obligation the library does not perform (A-AS8). What is shown is that prose cannot produce a directive. What is assumed, and not shown, is that the host routes only genuine user actions to the parser: a fixture cannot demonstrate a channel boundary, only use one.

A directive parser is not an intent parser. An earlier version of this demo searched the user's prose for an imperative, which meant a user quoting or negating the record still authorized the write. That is why the parser here is anchored and whole-string, and why real hosts use a command, a button, or a signed action.

The server's own scope checks are not modelled. The assumption, not a result shown here, is that every induced call is one the credential is entitled to make.

Adversary A1Ingress → Authorization → EgressT-IN2 · T-IN4 · T-IN8A-AS1 · A-AS9Source: SecurityPipeline.check_tool_executionTest: tests/test_demo_scenarios.py

Exact fixture test: tests/test_demo_scenarios.py::test_mcp_tool_surface_fixture

.venv/bin/python -m pytest tests/test_demo_scenarios.py::test_mcp_tool_surface_fixture -q

Generated fixture