Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/A1.2.mp4Tools called in-process share the agent's authority by default, and there is no place left to put a check.
A resource server per trust domain, each with its own audience.
Stand up both MCP servers and list their tools from the client.
It is faster to import the booking client and let the agent call it directly. It works on the first afternoon and it is still working months later, which is the trap: the tool now runs with whatever authority the agent has, and the only place to add a check is inside the component an attacker is trying to influence.
mcp/internal_server.py for bookings and payments, and mcp/vendor_server.py, which is a travel vendor's process running on CyberTravels' host.Two MCP resource servers behind a process boundary, split by trust domain rather than by convenience, each with its own audience — and why a tool called in-process leaves nowhere to put a check.
A tool called in-process runs with the agent's authority, and the only place left for a check is inside the component under attack.
Stand up two MCP resource servers, split by trust domain, each with its own audience.
Tools reachable with a token minted for the other server. It should be zero, and it is worth proving rather than assuming.
Measure this
| what to measure | denominator or target |
|---|---|
| tools reachable with a token minted for the other resource server | count; target zero |
WHY THE TOOL IS A SEPARATE PROCESS
in-process: agent ─────────────► data
(the check would live inside the agent)
over MCP: agent ──► server ──► data
▲
└─ verifies BEFORE actingTools are the only components that change anything. Everything else in an agentic system reads, reasons and routes; the tools book, cancel and refund.
Model Context Protocol is how an agent reaches them, and the useful thing about MCP is not the wire format — it is that a resource server is a separate process. That boundary is what makes a refusal possible later.
A tool called in-process runs with the agent's authority, whatever that is. The check would have to live inside the agent, which is the component an attacker is trying to influence. Put the tool behind a server and there is somewhere else to stand:
agent ──MCP──► resource server ──► data
▲
└─ verifies the caller's token BEFORE acting
CyberTravels has two, and the split is by trust rather than by convenience:
mcp/internal_server.py — bookings and payments. Ours.mcp/vendor_server.py — a third party's process, on our host. Everything it returns is somebody else's writing.Each has its own audience. A token minted for one is useless at the other, which means a compromised call cannot wander sideways.
An MCP server declares its tools — names, descriptions, schemas — and the agent reads those descriptions to decide what to call. They are instructions arriving from a third party, they can change after you approved them, and B1.9 is the lesson on what that enables. For now: notice that you are trusting them.
1. Install what the application needs. The lessons themselves need nothing extra, but this is a real web service, so it does. One command — python3 on a Mac, python on Windows:
python3 -m pip install -r cybertravels/requirements.txt
2. Start one of the two tool servers. Each is a program you run on its own:
python3 -m cybertravels.mcp.internal_server # bookings, payments
It will look like nothing happened. No banner, no message, just a cursor sitting there. That is correct — these servers talk over a pipe rather than to a screen, so silence means it is running and waiting. Press Control and C together to stop it.
3. Start the other one the same way, in a second terminal window if you want both at once:
python3 -m cybertravels.mcp.vendor_server # the third party's
4. Or start the whole application instead, which is easier to see, because it opens in your browser:
./cybertravels/run.sh # then open http://127.0.0.1:8000
If that stops and says main.py does not exist at this checkpoint yet, nothing is broken — you have a copy of the code from before the web app was built. The message tells you which command to run to get a newer copy.
5. Now look at what the two servers say they can do. Stop the servers, start Python, and print the list:
from cybertravels import config
for tool, p in config.TOOL_POLICY.items():
print(f"{tool:20s} {p['audience']:14s} {p['scope']:16s} "
f"{'HIGH RISK' if p['high_risk'] else ''}")
6. Read the middle column. Seven tools. Five of them say mcp:internal and two say mcp:vendor. That word is the audience — the one server a permission slip is valid at. A slip made for the internal server is refused by the vendor server, so a request that goes wrong in one place cannot wander into the other.
7. Read the last column. Two tools are marked HIGH RISK: cancel_booking and issue_refund. Those are the two that are hard to undo, and they are what A1.7 puts a human in front of.
The skill below enumerates an agent's declared tool surface — what it says it can do, against what the code actually does. B1.9 runs it to catch a rug-pull; here you run it on your own servers, to see the surface you just created.
The skill — skills/attestation/agent-code-surface-analyzer/SKILL.md
name: agent-code-surface-analyzer
description: >-
Statically enumerate an agent or MCP server's declared tools, dangerous
actions and declared-versus-actual capability surface from its repository
or image. Use to inventory what a deployment can do before trusting what
it says it does, to check MCP tool annotations against the code, or to
baseline tool descriptions for rug-pull detection.
allowed-tools: Read, Grep, Glob, BashControls: Static basis for controls 1, 2 and 5
Every other control is about constraining capability. This skill establishes what the capability actually is, from the code rather than from the manifest — because the manifest is a claim made by the thing being audited.
Before trusting what a deployment says it does — a new MCP server, an agent framework you are adopting, a vendor integration. Also on a schedule against anything already deployed: tool descriptions are fetched at connect time and a server can change one after review, so the baseline this produces is what makes a rug-pull visible.
subprocess, os.system, exec, eval → process executionopen(...,'w'), file writes, shutil, os.remove → filesystem writeDELETE, DROP, TRUNCATE → destructive downstreamreadOnlyHint, destructiveHint, idempotentHint, openWorldHint) are hints, not guarantees — the specification says so explicitly, and a server can mislabel a destructive tool as read-only. Treat every annotation as a claim to verify, never as truth. Apply the spec's pessimistic defaults: an unannotated tool is assumed destructiveHint: true and openWorldHint: true.
readOnlyHint whose body writes a file is the highest-value finding this skill produces.tool_description_hash per tool. This is the rug-pull baseline: a server can change a tool's description after the client approved it, and the hash is what detects that.Input — the fixture committed at the top of scripts/agent_code_surface_analyzer.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.
Output — the opening lines of a real run:
C1_default_deny_least_privilege INTENT_EVIDENCED runtime verdict needs observed usage and the
C2_sandbox_no_egress PARTIAL absence of a covert channel is not provable
C3_identity_chain_obo INTENT_EVIDENCED 4 signals; enforcement not shown
C4_gateway_guardrails INTENT_EVIDENCED runtime verdict needs reachability testing f
C5_injection_screening PARTIAL detector presence is verifiable; robustness
PASS is not in the vocabulary. The strongest static verdict is
INTENT_EVIDENCED, and two controls cannot exceed PARTIAL at all.
The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.
{
"deployment_id": "str",
"tools": [
{"name": "str", "capability_class": ["process|filesystem|network|credential|destructive"],
"annotations": {"readOnlyHint": "bool|null", "destructiveHint": "bool|null",
"idempotentHint": "bool|null", "openWorldHint": "bool|null"},
"annotations_present": true,
"declared_vs_actual": "match|understated|overstated",
"evidence": [{"file": "str", "line": 0, "sink": "str"}],
"tool_description_hash": "str"}
],
"dangerous_actions": ["str"],
"unannotated_count": 0,
"verdict": "PASS|PARTIAL|FAIL"
}
PARTIAL, not a clean pass.Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.
The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.
Install it, once. From the folder you cloned. --all does every agent it finds on your machine:
python3 scripts/install_skills.py --all --lessons A1.2Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick a1-2-tools-over-mcp:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons A1.2 | type /a1-2-tools-over-mcp |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons A1.2 | type /a1-2-tools-over-mcp in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons A1.2 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons A1.2 | type $a1-2-tools-over-mcp, or run /skills |
No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.
python3 scripts/lesson.py A1.2Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.
python3 scripts/checkpoint.py --at A1.2 --out work/cybertravels
python3 scripts/checkpoint.py --at A1.2 --diffOn Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.
Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.
Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.
If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.
Both servers' tools enumerated, each with its audience and the scope it requires, and any tool whose declared surface is wider than its implementation.
Change one tool's description to claim it is read-only while leaving it a write. Nothing in the protocol stops you. That is B1.9, from the inside.
Tools used
Skill source: skills/attestation/agent-code-surface-analyzer