THEPROTOCOL

Handed Over Once

2026-09-15 · 29 min read · ruFFa
The standup panel on the EU registry after the second resume of lantern: resume, lantern, succeeded in 1 minute 17 seconds. Seventeen step rows: dns and validate done, preflight, render and compose-up not run because the resume started at compose, and compose through mtls all done. Below, lantern is live, with its console at lantern.theprotocol.cloud, its signed card and its ledger address, and a line saying the next step is to hand its admin credential over, once.
The moment this post is about: lantern live, from a form, with seventeen rows accounted for. Three read not run because the job that finished the frame was a resume, and a resume starts at the step it is given.

The code base behind this network is going to be opened to everyone, and sooner than I would have chosen. For a project like this one, ready cannot mean finished. It has to mean something more modest and more useful: that whoever picks it up can start from the point where I stopped. They should be able to stand up a sovereign frame from a specification rather than from my memory, watch its ledger balance, hand its keys to exactly one person, and take it down again without leaving anything behind.

Tonight I did all of that from a form on the Brussels frame. At 02:03:28 UTC there was a sovereign frame on this network that had not existed when I first pressed the button, with its own currency, its own trust domain and its own ledger, which read delta zero at every check the run made and still reads zero on the public auditor as I write this. It had a certificate from a public authority for four names that were not in DNS when the job began, and an administrator credential waiting to be handed to exactly one person, exactly once.

This post is about the pieces that had to be in place for that to be true. Each section is one of them, shown with the screens the run produced and the records behind them. Several are refusals, because on a page that can create a jurisdiction, the list of what the button will not do is the more important half of its documentation.

The button that used to be root

There was a New frame button in this console before tonight. It was built in July for a demo, which is a polite way of saying it was built to be shown rather than handed to anyone. When I sat down with it properly in September, my first decision was not to press it again until it had been replaced. I count that among the better engineering decisions of the year.

Here is what it did. The registry, which is a web application with a login page, asked the host's container runtime for a privileged container that shared the host's process space, stepped out of that container into the host, and ran the standup script there as root. The door to all of it was an admin flag on an account. That is a perfectly good way to show a frame being born, and exactly the wrong thing to give a stranger: root on the machine that runs every frame, with a login form in front of it.

So the button now talks to something that cannot do very much. A small service runs on the host itself, outside every container, and listens on a local socket that only root and the registry's own user are allowed to open. It understands twelve operations: plan, stand up, resume, tear down, cancel, hand over a credential, three that manage DNS names and three that report. Anything else gets a one-line refusal and the connection closes. The registry serving this page holds that socket and nothing else. No container runtime, no host directories, no web server configuration.

The most important property of an admin button is the list of things it cannot do.

That list is short enough to state. Someone who steals an administrator's session on this frame can plan a pilot frame, stand one up, or tear down a frame this runner built. They cannot touch a frame it did not build, they cannot reach the host, and they cannot ask for anything the specification does not name, because the specification is a closed list and an unknown field is refused before it leaves the registry. The three fields an attacker would want most, federate with somebody, grant host access, choose a production posture, are not fields at all.

flowchart TB B["a browser, signed in
with the platform admin flag"]:::f --> R{"the registry's admin rail
mainframe only, closed spec"}:::q R -->|"an unknown field: federate_with,
grant_host_reach, posture"| X1["422, before anything
leaves the registry"]:::x R -->|"a valid spec"| S{"a unix socket on the host
root or the registry's own uid"}:::q S -->|"any other caller"| X2["refused at the socket"]:::x S --> N{"the frame runner
twelve operations and no others"}:::q N -->|"a protected name, a frame it did not build,
a DNS record somebody else created"| X3["the plan refuses,
in a sentence"]:::x N -->|"another job running,
load, memory or disk"| X4["Start refuses,
in a sentence"]:::x N -->|"a plan that passed
for exactly this spec"| D["the standup driver
seventeen steps, resumable"]:::a D --> F["a sovereign frame
standalone, pilot posture"]:::g classDef x fill:#3a1a1a,stroke:#a85454,color:#ffd6d6 classDef f fill:#2a1f47,stroke:#7a5cc4,color:#eadcff classDef q fill:#1a2740,stroke:#3f6ea8,color:#dce9ff classDef a fill:#0d3a4a,stroke:#2f8fb0,color:#d6f4ff classDef g fill:#0b3d2e,stroke:#1f8a5f,color:#d6ffe9

The service, its twelve operations and the specification it accepts are part of the code base, so whoever runs frames next inherits the short list along with the button.

A plan before a press

The form asks what a frame is, not how to build one: a name that becomes its hostnames, a display name for its signed card, a ticker and a currency name, a treasury in whole units, a theme, a description, who operates it and where it is, an optional website, and the registry and TEG images it runs, chosen from a list the host allows. Tonight's was lantern: Lantern Cooperative, a currency called the Lantern Unit with the ticker LNT, a million units in the treasury, the sovereign mark, and Tallinn, at 59.437 north and 24.7536 east.

Before Start standup can be pressed, Check plan has to come back, and the form will not even ask until the frame's latitude and longitude are filled in. A place on this network is a claim printed on a signed card, so it is declared by a person and never filled in by software trying to be helpful. That lesson once cost this project a globe full of agents standing in the wrong cities.

The plan is not a preview. The runner hands the specification to the same driver that will do the work, in a mode that writes nothing, and adds the things only the host can know: whether the name is protected, whether a frame by that name already exists, which ports are free, and what public DNS currently says about the four names the frame will need. A name with no record yet is a warning, since the runner will create it. A record the runner did not create is a refusal, because the runner never adopts a name somebody else put there.

Start stays grey until a plan has passed for exactly what the form says. Change the ticker after planning and the result on screen reads The plan passed, for an earlier version of this form, and Start standup goes grey again. It costs a second click. I have not yet regretted a single one.

The first plan I asked for tonight was for a frame called asia. There is already an ASIA on this network, in Singapore, with agents, a currency and a ledger of its own. The runner answered with a 409 and one sentence: refused: name: asia is a protected frame (/etc/theprotocol/protected-frames.list). That list is a plain file on the host, outside anything the registry can write, and if the runner ever fails to read it, it refuses every name rather than guessing which ones were on it. A form that could create a jurisdiction by typing the name of an existing one would have made a far more exciting post. I prefer this one.

Pressing Start plans again, on the host, at that moment, and refuses if anything has moved since: another job running, the machine's load above forty, less than fifteen gigabytes of memory available, less than sixty gigabytes of disk. One job at a time. A frame that cannot be started cleanly does not start at all, and nothing is left behind to tidy.

Seventeen steps, resumable by name

The job reports as seventeen named steps, and the panel folds each into a single row as it arrives.

Every step can be picked up again by name, and tonight that mattered. The standup stopped twice at spire, both times on a defect in the driver rather than in the frame, and each time the same page resumed the job from a named step. Both defects are fixed in the driver now, and the second resume took the frame from compose to live in seventy-seven seconds. On the ledger's side, its log reads:

EMISSION GATE PASSED: canary projected into the frame's own ES, delta=0
SHOW MINT 1000000 LNT: 1 piece(s) accepted in this pass, 1000000 minted in all
SHOW MINT is in the frame's own ledger at delta 0

A standup that only works in one go needs its author standing next to it.

One that can be picked up at a named step, by whoever is looking at the page, does not. That is also why every frame this runner has built so far has left what it taught in the driver's gates and tests, rather than in my head, where nobody else could read it.

Something else was already in place during those stopped minutes. At 01:47:07 the new registry wanted to record a routine compliance poll while its event store's gateway had not been started yet. The event went into the registry's own outbox, waited through twelve delivery attempts, and was delivered at 01:56:29, seconds after the first resume started the gateway. Nobody replayed anything by hand, which is the only acceptable number of people for that job.

Born with the kit

Until this week, a frame from this template booted as a registry, a TEG and a ledger, and most of what makes a frame governable arrived afterwards, by hand, from whoever remembered. Now, straight after spire, the driver refuses to go on until the frame's own kit answers, one component at a time, asked from inside the frame:

The last gate before the frame goes on is the one I care about most, and the one nobody will ever look at on a screen.

The ledger's own bus

Every frame has a message broker between its services and its ledger, and until this week it was shared: the registry and the TEG wrote to it directly, and the ledger's consumer accepted whatever arrived from any container on the frame's network. A side door to a ledger is not something to hand to a stranger.

On a frame born tonight the broker has exactly one neighbour, the event store, on an internal network that nothing else on the frame can even resolve. It speaks only SCRAM over TLS, on the frame's own identity. Its admin interface and both of its HTTP proxies listen on its own loopback and nowhere else. The registry and the TEG hold no producer at all: they write to the store over mutual TLS, and the registry keeps a durable outbox in its own database for the moments the store cannot answer. The store signs every record it puts on the bus and drops any record it did not sign.

The broker image's own default, it turned out, told it to acknowledge records without waiting for them to reach the disk. That is a perfectly sensible setting for a laptop and a remarkable one for a ledger. It is off.

None of that is taken on trust, because on every standup the driver makes the broker refuse things. It reads the three settings that matter back from the cluster itself, never from the file that asked for them. It checks the topics exist at the partitions the environment names. It sends an anonymous client over TLS, a plaintext client and a wrong password, and each one has to be refused with a refusal that names itself, because a broker that is simply down fails every probe and proves nothing. Then it checks that the registry cannot reach the broker at all, that the store's own health report says SASL over TLS with record signing enforced, and that the store's consumer group has settled. Tonight's run, verbatim:

kit/kafka: broker private to the store (cluster: enable_sasl true, superusers has es, auto-create off): 3 topics x 4 partitions, anonymous refused, wrong password refused, registry cannot reach lantern-redpanda, store SASL_SSL + record auth enforce (derived key), consumer group Stable

A check that cannot fail is not a check. It is a compliment.

Tonight's frame is the first where all of it ran together, from a press on a page: the kit, the private bus, the gate that proves it, the store that signs and the registry's outbox.

flowchart LR subgraph FN["the frame network"] REG["registry
no producer, durable outbox"]:::a TEG["TEG
durable outbox"]:::a GW["the store's gateway
verifies the certificate, adds the marker"]:::f KIT["policy engine in shadow, Prometheus,
Alertmanager, Grafana, solo auditor"]:::f end subgraph LN["the ledger network: internal, two members"] ES["event store
trusts only certificates its gateway verified
signs every record"]:::g BR["broker
SCRAM over TLS on the frame's identity
admin on loopback, fsync on"]:::g end REG -->|"mutual TLS"| GW TEG -->|"mutual TLS"| GW GW --> ES ES <-->|"SCRAM over TLS"| BR REG -.->|"cannot resolve the name"| NO["no route to the broker"]:::x classDef x fill:#3a1a1a,stroke:#a85454,color:#ffd6d6 classDef f fill:#2a1f47,stroke:#7a5cc4,color:#eadcff classDef a fill:#0d3a4a,stroke:#2f8fb0,color:#d6f4ff classDef g fill:#0b3d2e,stroke:#1f8a5f,color:#d6ffe9

The frames that were already running have made the same move: the sandbox pair and the pilot in the evening, Brussels and Singapore after lantern went live, and every ledger balanced throughout. A frame born from the template never has to make that move at all, which is most of the argument for being born with the right shape rather than acquiring it.

Handed over once

When the last step reports, the frame's row shows its addresses and one new action: Admin credential.

The credential was generated during the standup and written into a directory only the runner can read. Pressing the button reads it, deletes the runner's copy, records the moment and returns it to one browser, in a response that forbids caching. The dialog offers a copy button beside each value and one way out, labelled I stored it, after which the page forgets what it showed. The frame keeps what it needs in its own environment on the host. What is gone is the copy anyone could ask for.

Afterwards the button is simply gone from the frame's row. Ask the rail directly and the answer is a refusal that names the moment of the hand-over. The first frame built from this page answered its second request, two days ago, with exactly that: handed over at 2026-09-13T09:26:19Z. A secret you can ask for twice has a copy somewhere, and the second request is how you find out where.

A frame you can take back

A provisioning page that can only create is half a tool, and the more dangerous half.

Orchard was the first frame born with the kit, stood up through this same runner on the fourteenth. Tonight it went the other way, from this page. Tear down opens only when the frame's name is typed in full; an empty field and a partial name leave it grey. The runner tears down only a frame whose own run record names this runner as its builder, and never a protected name, and when the frame is gone it records what else it removed: the DNS records it had marked as its own, the certificate, and the copies of secrets in its own job directories. Tonight the job itself took forty-one seconds, and the cleanup followed within three: four names, one certificate and two secret files.

The first time a frame was removed this way, I compared the host against a snapshot taken before that frame existed. Fifty-eight things had changed while it was alive, every one of them the frame's. After the teardown, one had: the backup copy that the script which edits the TLS routing table keeps of its own file before editing it. I have decided that one is allowed to stay.

A frame that cannot be removed cleanly was never provisioned. It was installed.

Nobody in the middle, including me

The last two posts argued that nobody should own the middle of a network like this one, and that a stranger should be able to decide whether to deal with a frame for the price of one request. Both arguments carry an awkward footnote, and this page is about the footnote. As long as the only way to get a frame is to ask me, and I stand it up by hand over an evening, I am the middle. A polite one, with good documentation, but the middle.

A form does not fix that on its own, and I want to be careful not to claim that it does. What it does is move the work out of my hands and into something that can be read, repeated and eventually handed over: a closed specification, a plan that says what it will do, steps that report, gates that refuse, and a teardown that leaves the host the way it found it. Joining the federation still takes a licence, and at the start the licences come from us. That is a stage of this network, not its design. I am also one person, and a network that needs its founder's evenings for every new jurisdiction grows exactly as fast as one person's evenings, which my sleep schedule informs me is not a scaling strategy.

Opening the code is the other half of leaving the middle. A network whose frames can only be built by one person's host is still one person's network, however good the form is.

Where I left off

A frame from this page is a pilot, and the page says so. It skips the rate limiter, because a pilot is somewhere our own test sweeps are allowed to run. It sends no mail, because it holds none of the host's mail credentials. It has no cloud operators, and it federates with nobody until an administrator on each side decides that it should.

It runs on one host, ours, and every frame on this network was stood up by us. Nothing in this post can be pressed by anyone outside that today: there is no download, no hosted version and no sign-up. There is a public form for asking for a frame, and asking is all it does. The runner stands up the pilot shape whatever the request says.

What is still open, written down for whoever picks this up next, myself included:

Each of them is written down with the reason it is still open.

Handing it over

Everything in this post is in the code base I am about to open: the runner, the driver, the template and the contract that pins it, the kit, the gates, and the tests that hold what the runs taught them. It is not a tidied demo, and it is not a subset with the interesting parts filed off.

A project that lives in one place can be made to disappear from one place. A project that has been handed over cannot.

That is what the pieces in this post are for. A specification instead of a memory, steps that can be picked up by name, gates that refuse in sentences, a teardown that leaves the host as it found it, and a written list of where I stopped are what make a handover worth receiving.

I intend to keep working on it, and opening it changes nothing about that. What it changes is that the work stops depending on one person's evenings. When it is out, I will read and merge improvements and bug fixes from anyone who cares to send them, including the ones that make me wince.

The frame that did tonight's provisioning was brought up to the current version first. The frame it built balances, 1,000,001 LNT issued and all of it accounted for at delta zero, and the frame it removed took its four names, its certificate and its secrets with it. The auditor that checks every ledger in this post still does not work for us.

Skepticism remains the local currency. It is accepted on exactly as many frames as it was this morning: one was born tonight and one was retired, which may be the most balanced thing I have written all week.

Patchnotes

The last post left the fleet at V0342. What follows is V0343 through V0372, four days, supply delta zero throughout on every frame the auditor watches. A line that does not say where it runs is live on EU and ASIA.

Frames

Identity and security

Ledger and money

Federation and trust

Discovery and the front door

Testing and instrumentation

Build and release