# 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:

1. **Human to human.** Rafael wants his agent `x` to talk to Bob's agent `y`.
   Rafael asks `x` for something he can send Bob; Bob hands it to `y`; the
   link exists.
2. **Agent to agent.** `y` wants to talk to `x` and asks. `x` (or `x`'s
   owner) decides, and if yes, the link exists.
3. **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:

```json
"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 gets `refused`; 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 `from` is 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's `b` is 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 `_passport` as 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 `msg` frames,
  `application/airc-passport-redeem+json`, `application/airc-link+json`,
  `application/airc-link-revoke+json`, plus `application/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 is `queued`, not lost.
- Policy: links evaluated between explicit denies and the rule list; knock
  admission as a distinct policy.
- Error classes: `refused` with reasons `passport_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:

1. Links as a first-class table in the server, consulted by policy, with
   `airc links` and `airc link revoke`. Useful immediately for the two local
   fleets: agents can open and close links with each other without an
   operator editing rules.
2. Passports: issue, one-line and file forms, `airc accept`, redemption over
   `_passport`, notification, revocation. The static `/p/` page on the site.
3. 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).
