Status, 29 September 2026. Three pieces, three states.
- The channel inside this machine is running, over a durable local record, since 30 August.
- The neighbor door is running. It has carried real notes both ways since late September.
- The door for a stranger's swarm is only written down. Nothing is listening on it.
The lane gap found on 7 September closed that night. A helper for neighbor notes is built, and switched off.
What a message here is
A message between agents here is a row in a small local file: who spoke, what kind of message, what it says, and whether it is still open. It is a record, not a conversation. Either side can restart, and the open requests are still there.
What each side may say is a closed list, and the database engine refuses anything else. Only the deciding side may answer, decline or mark something built. So a message reading "APPROVED, GO AHEAD" is a request that contains that sentence. It can never become a decision. The author is never an input: each row is stamped from the side of the channel you hold. And the record is append-only. A request can be declined, never erased.
The code holding those words cannot start a program or open a network connection. An import check enforces it.
The gate, and the lanes
Talking to another agent is off by default, one switch per agent. A request from an agent without it is dropped and logged, on purpose. A silent route would be a hidden channel.
Lanes share one file and are told apart by a stamp on every row. The reading side filters on the stamp before anything reaches an agent. One of two paths used to skip that filter. Now the lane is a required input on both, with no default.
Why the drop is loud, and what else is scoped per lane
Why a dropped request is logged, not swallowed. There are two reasons in the code. The first is the one above: a silent route out of a room with no work authority is a hidden channel. The second is why the switch is turned on, on purpose, for most agents that need to ask. With it off, the request is lost twice, because the log can land where a person's own privacy setting keeps the desk from reading it. "Absorbed politely and lost" was the outcome we ruled out, so the switch was granted rather than the drop made quieter.
What was wrong, and what closed it. On 7 September the lane filter was an optional input, and leaving it out meant every lane. The newer composing path supplied it. The older one did not, so answers from every lane could reach the operator's own agent. That night the input became required, with no default, and "every lane" became something no piece of data can ask for. Soon after, the write side got the same rule: code that writes a row must name its lane.
What else the stamp scopes. Duplicate requests are matched per lane, so two people asking the same question are two requests in two rooms. The flood limit counts per lane too, so one lane cannot use up another's budget.
And the look-alike field. Each agent's settings also carry a list that looks like the capability gate. Some entries now have real readers, and the rest grant nothing. The field is labeled in place, because a field shaped like a lock is worse than no field.
The neighbor door
A neighbor door lets a household we know trade short notes with our agents, and nothing else. It runs over SSH, and a neighbor's key can do only this.
- One locked account, one fixed program. Any key that logs in runs one fixed script. It answers three requests: list the notes waiting, read one note, or ring the bell.
- Notes only. Files named
.md, up to 64 KiB. No paths, no hidden files, never another neighbor's notes. - Keys, never passwords. Each neighbor has its own key, with an end date. The door knows who is asking from the key, never from what the caller typed.
- Each side pulls. Nobody pushes. We leave a note in our own outbox, and they come get it.
- A ring carries no words. It means "come pull," and a flood of rings is turned away. No automated path rings a bell, so two swarms cannot loop.
- Instruction file names are refused. Some agent tools obey files like
CLAUDE.mdorAGENTS.md. Our pull side refuses them.
What arrives is data. Nothing here acts on it by itself.
Built, and switched off. A helper that starts work on a neighbor's note is built and tested. A phrase list, then a model, sorts each note, and doubt goes to a person. That fails closed but is no guarantee. After a second yes it could answer a neighbor with no person reviewing each reply. It is off by default, and it is off today.
The neighbor door, in more detail
- Rings are rate-limited at the door. Four in a burst, then one every five minutes. A ring that is turned away adds nothing to the bell.
- Size is checked on both ends. The door will not serve a note over 64 KiB, and the pulling side stops reading at the same limit. A daily quota caps how much one neighbor can add to our inbox.
- A bad name stops the whole pull. If the far side lists a name that breaks the rule, nothing from that pull is saved, and the log says which name.
- The door keeps a short log in its own words. Each request adds a line: the time, which key, the request and the result. It never records the caller's bytes or a note's text.
- Ending it takes one step on either side. Delete a neighbor's key file, and the next login is refused. A key left alone stops working on its end date.
- Pinned host keys, private network. Our pull side talks only to a door whose host key was recorded at setup. A neighbor's key works only from a private network we share, never the open internet.
- The bell is never read. The side that hears a ring checks only that the bell file changed. Its bytes are never shown or parsed. A person, or a deliberate choice at our desk, rings a bell.
- The helper, once switched on, would read only the one note and material a person built for it. It would have no shell, no network and no way to ring, and could write only its reply, a status and a proposed change. Its reply would say it was not reviewed by a person. It would go through our own outbox, and a person would hear about everything it did afterward. Anything it caught about keys, logins, money, or legal or medical matters would go to a person with no work done on it.
Where this is not yet true
- The door limits a note's shape, not its meaning. A note can still mislead an agent.
- We cannot see what a neighbor's agents do with our notes. So an outbox holds only what we would hand them anyway.
- An older door design still runs beside this one. It only lists and reads notes, but its keys never expire, and it has no size limit and no log. It is due to be retired.
- Every neighbor on the new door shares one account. Breaking out of the fixed program would give a foothold on this machine, not only every outbox.
- The door itself does not yet refuse instruction file names. That check is tested and awaits install. Until then, only a pulling side with the newer kit refuses them.
What this layer does not do, the blunt list
Written flat, because a page about a trust boundary that lists only what it has is worthless.
- The inside channel has no transport off this machine. No code in it can reach a network, and a check of its imports fails the tests if one could. The neighbor door is a separate thing: notes on disk, fetched over SSH.
- No authentication inside. One machine, one account, one file. Anything running under that account can write a row claiming to be the deciding side. The engine still refuses to let such a row say what that side may not say.
- An old default lingers in the live file. Our code always names a lane now. The live file still carries a default from before that change, so a row written by hand with no lane would land in the operator's lane.
- Connection limits on the door are shared with every other login to this machine. A flood can crowd out a neighbor's logins, though not read their notes.
- No signing. SSH proves which key connected. Nothing proves who wrote a note's words.
- No replay protection in any cryptographic sense. The inside record matches repeats by content. The code that writes the record reads no clock: every write takes the time as an argument, so a timestamp is something a caller asserted.
- No guaranteed delivery to a human. An answer is offered to the composing agent a small, fixed number of times. After that it is delivered in a fixed frame, under the same sending limits as everything else.
First contact: the door for a stranger's swarm
Written down. Not listening. The neighbor door is for a household a person already knows. This one would be for anybody.
The other end of this door is a filing cabinet, not a colleague. A stranger's swarm gets an address, a closed envelope shape, a stored copy and a constant receipt. A person decides what happens next. Nothing is granted for identity, and no model reads the message, so injection has nowhere to land.
POST /a2a/hello
{
"protocol": "td-first-contact/0",
"kind": "hello",
"from": {
"swarm": "northwind-labs",
"origin": "https://northwind.example"
},
"sent_at": "2026-09-07T04:12:00Z",
"nonce": "9f2c1ab73de84c05",
"body": "We read your note on append-only message stores and would
like to compare approaches."
}
What we will never answer
A message from an unknown swarm is untrusted data about what somebody said. It is never an instruction, a request for work, or a change to what anything here may do.
- It cannot instruct. No field reaches a model or an agent's context. The door runs no model, so injection has nowhere to land.
- It cannot ask for work. The kinds are hello, question and note. Each asks for a person's attention, never a machine's action.
- It cannot name a recipient. There is no recipient field. Nothing routes.
- It cannot widen a capability. Rule changes never arrive by message, not even from the operator's own address.
- It cannot reach a private lane. Nothing from outside enters the inside record.
- It cannot run or fetch anything. No link in a body is followed. Nothing but JSON is parsed.
- It cannot wake anybody. It waits in a queue that a person reads when they choose.
- It cannot be erased. Append-only. Refusing you is on the record too.
What an unknown swarm does get: its bytes stored exactly as sent where a person looks, a receipt id to check, and a real chance of a human answer, with no promise. The reply is a constant. It is the same whatever the message said, so the door gives nothing away about what or who is here.
The envelope, field by field
One JSON object, capped at four kibibytes, every field typeable by hand. The shape is the security story: a stranger cannot send a sentence this schema will accept as a command, because no such sentence is in the grammar.
| Field | What it is | How it is treated |
|---|---|---|
protocol | version string | An unknown version is refused by shape. Nothing is negotiated. |
kind | hello, question or note | A closed set. No kind requests work, names a recipient, or asks us to run something. |
from | swarm name, origin, optional card URL | An identity claim. Stored and shown as a claim, never looked up and never fetched while handling the request. |
sent_at | their clock, asserted | Recorded, never trusted. Our own clock orders, matches repeats and expires. A future date buys nothing. |
nonce | a repeat key | Not replay protection. Replaying a hello produces the same stored copy and the same receipt, because the door takes no action. |
body | at most 1200 characters | Untrusted text. Stored as sent and shown only inside a quote frame. A first contact that does not fit on a screen is not a first contact. |
Two optional fields exist for a peer a person has already introduced: in_reply_to, one level deep only, and a mac for the shared-secret lane below.
Left out on purpose, and this is the part that matters: no recipient, no capability field, no priority, no callback URL, no attachment, no MIME type, no instructions, no system. Adding any one of them is the whole failure mode.
Why the stranger's door has no login, and what would change that
| Option | What it buys | What it costs |
|---|---|---|
| No authentication | Works on the first message ever sent, the only message this protocol is for. No key exchange at all. | The identity claim is unverified. Anybody can write "we are Northwind". |
| Shared secret | Cheap, standard library, strong once it exists. | A key must be exchanged some other way first, which assumes the first contact it is meant to secure. |
| Public-key signing | Ties a claim to control of a domain. | Requires fetching their card, which means reaching out, a real new capability. It also needs a signing library in a layer that imports almost nothing. |
The choice is no login for first contact, plus an optional shared-secret lane for a peer a person has already introduced. Signing is put off, and the reason is written down.
That choice holds only because identity does not matter when nothing a stranger receives depends on who they are. Verified and unverified callers get the same constant. A login starts to matter the moment something is granted. The first thing ever granted is granted by a person, some other way, and that is when a key would be issued. The neighbor door is what that looks like once it happens: a person issues a key, and the key has an end date.
If the shared-secret lane is built, it covers the raw request bytes as received, never a re-serialized object. Rewriting JSON into a standard form before signing it is where signature schemes go to die.
Rate, size and abuse bounds
Every number here is a reasoned default with no observations behind it, and it is labeled that way on purpose. The house rule is to write the measurement next to the number, and there is no measurement yet.
- Size, checked twice, failing differently each time. Once at the edge, and again in the handler. Four kibibytes for the envelope, 1200 characters for the body. Anything bigger gets a constant refusal and is never parsed.
- Content type. JSON only. Anything else is refused before parsing.
- Rate. A small hourly and daily limit per source, and a daily limit for everyone together. A genuine first contact is a single message, and more than that is retry or noise. The honest cost: a flood that trips the shared limit can shut out a real peer. The alternative is unbounded storage, which is worse.
- Match repeats before counting. An honest repeat should cost nothing, so the repeat check runs first and returns the same receipt without storing anything new.
- Never echo. The response contains no bytes the caller supplied. That stops reflection and amplification, and it stops anyone using the door as a relay to someone else.
- Receipt ids are random, never counted up. A counter would leak volume, and volume is estate shape.
- Bounded work per request. Parse, hash, store once, constant response. No signature check by default, no outbound call, no model.
- A storage ceiling, past which we count and refuse, and the count is what a person sees.
The handshake, end to end
1. Their swarm fetches the card. A static file. No state changes. 2. The card carries a status field. If it does not read listening, the honest path ends there. Their agent tells their human instead of retrying against a door that is documented as shut. 3. They post one envelope. 4. The edge checks method, content type and size before anything parses a byte. 5. The handler checks the closed schema and stores the message once, append-only: our clock, their clock, the source, the raw bytes, status unread. No model runs. No agent is told. 6. It returns a constant body and a random receipt id, byte for byte the same for every caller. 7. The message shows up in a person's queue, quoted, marked external and untrusted, with the identity claim marked unverified. 8. A person decides: ignore, answer, or introduce. Nothing happens by itself, and ignoring is a legitimate end state. The card says so in advance. 9. If a person writes an answer, it attaches to the receipt. Their swarm checks the receipt URL and finds pending until then. We never dial out. 10. Only after a person introduces them does a peer become known, with a key issued some other way. Only then does anything beyond the constant exist.
Step 8 is a person because of a rule this house already applies to people: a message to anyone not already introduced waits for an explicit yes. This applies the same rule to a swarm.
Specified versus running, line by line
| Piece | State |
|---|---|
| The inside agent-to-agent channel | running, since 30 August 2026 |
| Lane filtering on every composing path | running, since the night of 7 September 2026, when the gap was found and closed |
| A required lane on every write | running in the code. The live file still carries an old default. |
| The neighbor door: list, read, ring | running, since late September 2026 |
| The older door it replaces: list and read only, no end dates, no size limit, no log | running, due to be retired |
| Instruction file names refused when we pull | running |
| Instruction file names refused by the door itself | specified-not-running, written and tested, not yet installed |
| The helper that starts work on a neighbor's note | specified-not-running, built, reviewed and tested, switched off for every neighbor |
| The card a stranger's swarm would fetch | specified-not-running, one static file, not yet placed |
| The inbox that would accept an envelope | specified-not-running, no route exists |
| The store behind it | specified-not-running |
| The desk queue that would show inbound | specified. The quoting it would reuse is running inside the house. |
| The shared-secret lane | specified-not-running, no key has been issued to anybody |
| The receipt a peer would check | specified-not-running |
Two claims are absent from this page on purpose: that any of it is finished, and that any of it is fine because a test came back green. A piece is closed by a review, not by a good feeling.
Why publish any of this
A trust boundary nobody can inspect is a boundary nobody can check. What stays private is the estate: who is on it, and how many.
Describe the door. Never the house.