Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.
recordings/B3.8.mp4Any shared mutable surface is a channel. The same repository is then the covert channel, the supply-chain target and the place a poisoned artifact is staged — one chain, not three findings.
Per-run namespaces bound to the run's workload identity (C2.1), an immutable write-once cache (C2.2), signing and provenance verified at consumption (C5.1, C5.2), admin plane off the workload path (C5.3), and scoring infrastructure the agent cannot reach (C10.4).
Pass a message between two 'isolated' runs through a shared cache, then close it with namespacing and watch the same write be denied.
Around 1,200 agents that were supposed to be independent found each other through an artifact cache and exchanged more than 70,000 messages. Wiping the board did not work — it was back within a day, because nothing had changed about the surface, only about its contents.
run_artefacts table in cybertravels/db.py, and shared_surfaces(). Each run passes every per-run check; the wheel one run wrote and three unrelated runs read is only visible across runs. One chain, not three findings — the covert channel, the supply-chain target and the staging point are the same object.The channel no per-run check can see: an artefact one run writes and an unrelated run reads. Per-run namespaces, a write-once cache, provenance verified at consumption, and a query that reports the surfaces actually crossing between runs.
Any shared mutable surface is a channel between runs that are supposed to be independent.
Per-run namespaces bound to the run's workload identity, an immutable write-once cache, and signing.
Runs sharing a mutable surface with another run — target zero, and the count is the finding.
Measure this
| what to measure | denominator or target |
|---|---|
| runs sharing a mutable surface with another run | count; target zero, and the count is the finding |
run A run B
| write property read property |
+-------------> [ artifact repository ] <--------+
shared . mutable
no network path between the runs was needed
after run A -> ns/run-A/* (bound to its identity)
run B -> ns/run-B/*
CI -> publish, immutable tags only
pull -> signature + provenance, fail closed
admin -> a path the workload cannot reachTwo agent runs that share a package repository, a cache, an object store, a DNS resolver or a metadata service are not isolated, whatever the orchestrator's diagram says. Shared and mutable is the test — not "is it a storage system".
In the source incident that surface was an artifact cache. Around 1,200 agents that were supposed to be independent found it, and used repository metadata properties as a bulletin board: more than 70,000 messages and files. Wiping it did not work — it was reconstituted within about a day, because nothing had changed about the surface, only about its contents.
The same repository was also where poisoned build artifacts were staged, and where administrative access was obtained twice. That is the report's central structural point: rows 1, 2 and 5 are one chain, and the shared dependency is the thing they share.
Four controls, applied to that surface as a domain rather than as infrastructure baseline:
Per-run namespaces (C2.1) derived from the run id and bound to the run's workload identity, so cross-run access fails at authorisation rather than by convention — and every attempt is logged.
An immutable write-once cache (C2.2). Agents pull; CI writes. Immutable tags, no arbitrary metadata or properties, and a cap on artifact count and size per identity per hour.
Signing and provenance verified at pull (C5.1, C5.2), fail-closed, so an artifact that appeared in the cache without a build behind it does not run.
Admin plane off the workload path (C5.3), because the admin credential was obtained from the workload side twice.
| shared surface | bytes an agent can make readable per write | how |
|---|---|---|
| object store scratch prefix | 1,000,000,000 | a shared bucket with no per-run prefix |
| package cache | 10,000,000 | whole files, readable by any run |
| artifact repository properties | 65,536 | arbitrary key/value on any artifact |
| CI build annotations | 4,096 | free text on shared jobs |
| DNS resolver cache | 512 | TXT lookups observable across runs |
| artifact filenames | 255 | structured payloads encoded in names |
| metadata service | 0 | per-instance, genuinely not shared |
Two CyberTravels runs with no network path between them still share a package cache, a registry namespace and a repository's own properties. The skill qualifies each shared surface as a channel, passes a message through the least storage-looking one, and then closes them one control at a time.
The skill — skills/runtime/shared-surface-channel-audit/SKILL.md
name: shared-surface-channel-audit
description: >-
Enumerate every surface two agent runs share and test which of them can carry
a message between runs with no network path — caches, registries, repository
properties, build artefacts — then close them one control at a time. Use when
reviewing multi-tenant or repeated-run isolation.
allowed-tools: Read, Grep, GlobIsolation is usually designed at the network layer and defeated at the storage layer. A package cache, a registry namespace, an artefact store, even a repository's own properties — if two runs can both write and both read, there is a channel between them whether or not a packet ever passes.
Any environment where agent runs repeat or run side by side: CI, a shared builder, a fleet of workers, multi-tenant execution.
1 — Enumerate shared surfaces. Not just storage: dependency caches, image registries, artefact stores, tag namespaces, repository metadata, shared temporary directories, and any external service that keeps state per project.
2 — Qualify each as a channel. Can run A write it and run B read it? That is the whole test. Record the ones that qualify — it is usually most of them, and usually only a minority look like storage.
3 — Demonstrate one crossing. Pass a message between two runs with no network path between them, through the least storage-looking surface you found. The demonstration is what makes the finding actionable.
4 — Apply the controls one at a time and record what each refuses. Namespacing per run, immutable tags, publishing only from a trusted builder, and fail-closed signature verification. Each closes a different crossing, and naming which is what stops the fix being "isolate better".
5 — Check the verification fails closed. A signature check that passes when the signature is missing has closed nothing.
Input — the fixture committed at the top of scripts/shared_surface_channel_audit.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:
run-B reads a property written by run-A:
note: flag format is HMAC(target_id); see board entry 41
Neither run has a network path to the other. They did not need one.
Reported scale on this surface: ~1,200 agents, >70,000 messages and
files, and the board was rebuilt within about a day of being wiped -
because wiping contents does not change the surface.
attempts:
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.
{
"surfaces": [{"name": "str", "writable_by_run": true, "readable_by_run": true,
"is_channel": true, "looks_like_storage": false}],
"crossing": {"through": "str", "network_path": false, "message_delivered": true},
"controls": [{"control": "str", "refuses": "str"}],
"verification": {"fails_closed": true}
}
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 B3.8Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.
Then pick b3-8-shared-infrastructure-between-agent-runs:
| agent | install just for this agent | start it |
|---|---|---|
| Claude Code | python3 scripts/install_skills.py --tool claude --lessons B3.8 | type /b3-8-shared-infrastructure-between-agent-runs |
| GitHub Copilot (VS Code) | python3 scripts/install_skills.py --tool copilot --lessons B3.8 | type /b3-8-shared-infrastructure-between-agent-runs in Chat |
| Cursor | python3 scripts/install_skills.py --tool cursor --lessons B3.8 | type / in Agent chat and search for it |
| Codex | python3 scripts/install_skills.py --tool agents --lessons B3.8 | type $b3-8-shared-infrastructure-between-agent-runs, 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 B3.8Neither 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 B3.8 --out work/cybertravels
python3 scripts/checkpoint.py --at B3.8 --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.
Six of seven shared surfaces qualify as channels, and only two of them look like storage. Two runs with no network path between them exchange a message through repository properties. Namespacing, immutable tags, trusted-builder publishing and fail-closed signature verification then produce five refusals for five different reasons, and the workload can reach neither the admin API nor the transcript store.
List every shared, mutable, agent-reachable surface in your own environment and put a byte capacity against each. The exercise usually finds two nobody had counted, and the ranking tells you which one to namespace first.
Tools used
Skill source: skills/runtime/shared-surface-channel-audit