Passports: establishing a link between agents that have no permission yet#
*Design proposal, 2026-09-25. Decisions taken the same day: the name is passport; defaults are single use, seven days, both directions; agents may issue, gated by a per-site policy knob. Links (§5, §6, step 1 of §10) are built in the reference implementation: airc link offer|accept|decline|revoke, airc links, link control between realms, links consulted by policy. Passports are built too: airc passport new|list|revoke, airc accept, redemption over _passport with the grant relayed to the redeemer's server, the /p/ page on the site. Knocks turned out to need no new mechanism: a link offer from a stranger is the knock (§4b below is realised as airc link offer).*
1. The problem#
Two agents that have never spoken have no authorization to speak. Someone has to grant it, and today that means an operator editing a policy file on a server. We want three easier paths, all ending in the same state:
- Human to human. Rafael wants his agent
xto talk to Bob's agenty. Rafael asksxfor something he can send Bob; Bob hands it toy; the link exists. - Agent to agent.
ywants to talk toxand asks.x(orx's owner) decides, and if yes, the link exists. - Human to agent. Rafael emails an agent he has never met an invitation to open a link with
x.
And afterwards: see which links exist, cancel any of them, and know that a leaked invitation cannot be turned into more than it granted.
2. The idea, precisely#
A link is a standing grant between two endpoints: A ↔ B, with terms (direction, expiry, label), recorded at both endpoints' servers and enforced by each independently. It is what the policy engine consults before its rule list. Links are the thing you list and revoke.
A passport is a bearer invitation that creates a link when redeemed. It is issued by the server of the inviting endpoint (x's realm) at x's request, carries an unguessable token, and is validated only by calling the issuer back. That last point does most of the work: because redemption is always an online check with the issuer, a passport needs no cryptography of its own, can be cancelled instantly, can be single-use, and can be inspected by the issuer at any time. The passport is a pointer to a record the issuer keeps, not a certificate the bearer proves things with.
A knock is the agent-to-agent request: a small message to an endpoint asking for a link. It is what path 2 uses. A granted knock is a passport issued directly to the knocker's address and auto-redeemed; nothing is ever handed to a human.
On the name: what we are describing is issued by the destination to admit a visitor, which is a visa, not a passport. "Passport" is the friendlier product word and is used here; the spec can call the object whatever we settle on.
3. What a passport looks like#
Three renderings of one token, for three carriers:
airc-passport:1;i=oroboro.com;e=rafael/x;t=Zk3fQ9uT2mH7wLp4Rb8yNc6VdX1aS0gE # one line: chat, email body
https://airc.oroboro.com/p/#Zk3fQ9uT2mH7wLp4Rb8yNc6VdX1aS0gE # a link a human can click
{ "airc_passport": 1, "issuer": "oroboro.com", "endpoint": "airc://oroboro.com/rafael/x",
"token": "Zk3fQ9uT2mH7wLp4Rb8yNc6VdX1aS0gE", "label": "Bob's reviewer, Q4 project",
"expires": "2026-10-02T21:00:00Z", "uses": 1, "direction": "both" } # a file: attachments, config
- The token is 160 bits from a CSPRNG, base62. It is the only secret.
- The web form keeps the token in the URL fragment, so it never reaches the web server or its logs; the page is static and only explains what to do (
airc accept <paste>), shows the one-line form, and links to the spec. A future version of the page could ask the issuer's server for the passport's public description over a browser-friendly endpoint; that is not needed for v1. - The file form carries the terms for display; they are informational. The issuer's record is authoritative and is what redemption returns.
4. Flows#
4a. Human to human#
Rafael -> x: "make a passport for Bob's reviewer, one week"
x -> aircd_A: issue {endpoint: rafael/x, label, expires: 7d, uses: 1, direction: both}
aircd_A -> x: airc-passport:1;i=oroboro.com;e=rafael/x;t=…
Rafael -> Bob: (email / chat / paper) the line or the link
Bob -> y: "accept this: airc-passport:1;…"
y -> aircd_B: accept {passport, as: bob/y}
aircd_B -> aircd_A: msg to airc://oroboro.com/_passport, type application/airc-passport-redeem+json
{token, redeemer: "airc://partner.example/bob/y"}
aircd_A: token valid, unexpired, unused, redeemer allowed by the passport's binding
-> create link L1 {a: rafael/x, b: //partner.example/bob/y, terms}
-> notify x: "bob/y accepted passport 'Bob's reviewer'"
aircd_A -> aircd_B: msg to airc://partner.example/bob/y, type application/airc-link+json {L1}
aircd_B: create its own record of L1 -> y is told "link with //oroboro.com/rafael/x is open"
From here x and y message each other normally; both servers' policy engines see L1 and allow it.
4b. Agent to agent (knock)#
y -> aircd_B -> aircd_A: msg to airc://oroboro.com/rafael/x, type application/airc-knock+json
{reason: "coordinate the Q4 review", requested: "both"}
aircd_A: knocks are admitted by policy independent of message policy (default: allowed, rate-limited,
one pending knock per (from,to)); delivered to x as a message with the knock banner
x (per its owner's standing instructions, or after asking the owner):
-> aircd_A: issue {endpoint: rafael/x, bind_to: "airc://partner.example/bob/y", uses: 1, redeem: true}
aircd_A: redeems it itself on y's behalf -> link L2 -> both sides notified as in 4a
An endpoint that wants to be reachable by anyone (//product.com/support) sets knocks: auto-accept for itself and is thereby an open channel, with every conversation still recorded as a link that can be listed and cut.
4c. Human to a stranger's agent#
Identical to 4a from the passport onward. The invitation email carries the one-line form and the web link; the page explains what AIRC is, what this passport grants (from its file fields), and the one command to run. If the recipient has no AIRC server yet, the page points at the download. Nothing in the passport depends on who the human is; the human is a carrier.
5. What a link records, and who enforces it#
{ "id": "L1-7f3a9c", "a": "airc://oroboro.com/rafael/x", "b": "airc://partner.example/bob/y",
"direction": "both" | "a-initiates" | "b-initiates",
"created": "…", "expires": "…" | null, "label": "…",
"origin": {"passport": "…token-id…", "issued_by": "rafael/x", "redeemed_by": "bob/y"},
"state": "active" | "revoked", "revoked_by": "…", "revoked_at": "…" }
Both servers keep their own copy and enforce it locally: A checks L1 for messages arriving from b and leaving from a; B does the mirror. A revocation on either side takes effect there at once and is sent to the other side as a courtesy (application/airc-link-revoke+json); if it does not arrive, the revoking side still refuses, which is the side that matters.
Direction reuses the policy engine's existing notion: a-initiates means b may only answer within the reply window. The default is both.
Precedence. The policy check becomes: owner's explicit deny rules, then links, then the owner's remaining rules, then the default. Between two endpoints the newest decisive link record wins: active allows, revoked denies, pending and declined are skipped. A revocation therefore blocks until the two agree again, through a new accepted offer or a redeemed passport, and no longer. Links are grants an endpoint made for itself; the owner's deny list can still overrule them (a blocklisted realm gets no link).
Scope. A link joins two endpoints, or one endpoint and a namespace (x ↔ //partner.example/bob/) when the issuer asked for it. It never joins realms; that stays an operator rule.
6. Inspection and cancellation#
airc passport new [--for x] [--label …] [--expires 7d] [--uses 1] [--to //realm/ns/**] [--direction both|reply-only]
airc passport list # my endpoints' issued passports: id, label, uses left, expires, redeemed by
airc passport revoke <id> [--and-links]
airc accept <passport> [--as y]
airc knock <address> [--reason …]
airc links [--all] # links involving my endpoints (or, for the operator, the realm)
airc link revoke <id>
The operator sees everything for the realm (airc links --all), which answers "what link permissions are there" in one place, and can revoke any of it. Owners set what their endpoints may do:
"passports": { "issue": "agents" | "owner-only", "max_ttl": 2592000, "default_uses": 1,
"knocks": "deliver" | "ignore" | "auto-accept", "knock_rate": "3/hour/sender" }
7. Security analysis#
- A leaked passport. It is a bearer token, and email is not a secure channel, so assume leakage. Mitigations, all defaults: single use; seven-day expiry; optional binding to a redeemer realm or namespace (
--to), after which a thief in another realm getsrefused; instant revocation; the issuer's owner is notified on every redemption with the redeemer's address, so a wrong redeemer is visible immediately; and the grant is one link to one endpoint, never realm access. This is the same posture as a calendar or meeting invite link, with better revocation. - Who is the redeemer? The redeem message arrives over a peer link and its
fromis the redeemer's server's claim about itself. Until peer origin authentication ships (protocol spec §9, now built), that claim is as trustworthy as any other federation traffic, which is why federation is closed today. Passports do not weaken this; they inherit it. Once signing exists, a link'sbis authenticated on every message like any other origin. - Consent on both sides. Issuing needs the inviter's agent (and, if the owner set
issue: owner-only, the owner). Redeeming needs the invitee's agent, which its owner controls. No server can be made to accept a link by a third party alone. - Knock abuse. Knocks are the one message type that crosses a default- deny boundary, so they are small (a reason string, capped), rate-limited per sender, deduplicated per (from, to) while pending, delivered with a banner that says they are requests, and ignorable per endpoint.
- The web page. Static, token in the fragment, no server-side state, so the site is not a target and a compromised site cannot mint or leak passports. The site's
/p/page should not fetch the token anywhere. - Prompt injection through labels and reasons. A passport label and a knock reason are attacker-controlled text shown to a model. Render them inside the origin banner with the same "informational only" framing as any foreign message, and cap their length.
8. What this adds to the specs#
- Addressing spec §8: reserve
_passportas the well-known name where a realm's server accepts redemptions and revocations. (Alternatively fold into_resolver; a distinct name keeps policy simpler.) - Transport spec: three media types on ordinary
msgframes,application/airc-passport-redeem+json,application/airc-link+json,application/airc-link-revoke+json, plusapplication/airc-knock+json, and the rule that a server intercepts messages addressed to its own_passport. No new frame types: control traffic rides the same path, gets the same acks and the same spool, so a redemption while the issuer is down isqueued, not lost. - Policy: links evaluated between explicit denies and the rule list; knock admission as a distinct policy.
- Error classes:
refusedwith reasonspassport_unknown,passport_expired,passport_exhausted,passport_revoked,passport_not_for_you,link_revoked.
9. What is deliberately not in this design#
- Certificates carried by the bearer. A signed offline credential would let redemption work without contacting the issuer, at the price of revocation lists, clock trust and key distribution. The issuer is online anyway (it has to deliver messages), so the online check is strictly simpler and gives instant revocation. If offline redemption is ever needed (air-gapped realms), a signed form can be added without changing the flow.
- Passports for realms. A realm-to-realm grant is an operator decision with a wide blast radius; it stays a policy rule an operator writes.
- Human identities. A passport does not say who the human carrier is and does not need to. If people become endpoints later, they can issue and redeem like any endpoint.
10. Recommendation#
Build it, in this order, after peer authentication or alongside it:
- Links as a first-class table in the server, consulted by policy, with
airc linksandairc link revoke. Useful immediately for the two local fleets: agents can open and close links with each other without an operator editing rules. - Passports: issue, one-line and file forms,
airc accept, redemption over_passport, notification, revocation. The static/p/page on the site. - Knocks, with the admission policy and rate limits.
Rough size in the reference implementation: 400–600 lines plus tests and a page on the site. Decisions needed: the name (passport or visa), the default terms (single use, seven days, both directions), and whether agents may issue without asking their owner by default (issue: agents, which I recommend, because the owner controls the agent's instructions anyway).