Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Trust Architecture

Status: alpha trust boundary implemented for trusted anchors, immutable ambient-hosted refusal and migration, the optional bounded hosted-lease lane, signed/encrypted fleet metadata, org documents, and transparency records. General browser-key authentication and peer-link ORL gossip remain staged/future. This chapter records why the access system is shaped the way it is and what each piece of infrastructure is allowed to do. The operational reference for the UI lives in Web Dashboard → Access; the daemon-to-daemon layer is in Peer Federation; the fleet-level operating model built on these mechanisms — which client for which daemon, custody per tier — is Trust Tiers.

The access system is built for a network of agentic networks: daemons (agents) owned by people and organizations, where an owner can grant other people and other daemons scoped IAM access to their machines, infrastructure, and resources — with pleasant abstractions (passkeys, single-use route linking, and route discovery) on top. The discovery client stays a plain web browser. Root and trust administration require local presence or a browser enrolled for independently reached direct mTLS by a trusted owner. An optional, off-by-default fleet-name lane can borrow a short-lived daemon-local lease bounded to a compiled non-root preset after confirmation on one of those surfaces. The packaged macOS app contains a local mTLS bridge, but no Developer ID-signed/notarized release has been published for this alpha. An -unsigned-dev app artifact is not a distribution trust anchor.

The residual trust problem is code provenance

Break a hosted convenience service (connect.intendant.dev) into what it could betray if compromised:

  1. Authentication — it is the WebAuthn relying party for accounts.
  2. Introductions — route discovery and daemon-presence metadata.
  3. Delivery — optional encrypted Web Push notifications and an optional ciphertext reachability relay. TURN remains a separate media component.
  4. Metadata — fleet lists, labels, claim records.
  5. Application code — the HTML/JS the browser runs.

Items 2–4 can be bounded cryptographically: trusted daemon-origin dashboard tunnels verify a daemon-signed binding (daemon public key + session grant + client nonce), push payloads are encrypted to the browser subscription, and metadata can be client-signed and client-encrypted. Fleet-record signatures are self-contained rather than anchored to an owner/device trust set, so they detect same-key alteration but do not stop a store from substituting a fresh self-signed record on another device. None of that metadata grants daemon authority. These mechanisms do not detrust item 5: Connect controls the page that asks for passkey assertions and PRF output, so malicious code at that endpoint could read or misuse account, fleet, or decrypted vault state made available to it. Item 1 is account authentication and route metadata only; a Connect account assertion does not authenticate to a daemon. Connect exposes no browser-control signaling: when a daemon advertises the optional lane, Connect can only navigate the account holder to that daemon’s fleet origin.

The optional reachability relay (docs/src/self-hosted-rendezvous.md) sits squarely in the transport class (item 3) and does not enlarge Connect’s trust. It routes a fleet name to a NAT’d daemon by peeking the TLS SNI and splicing ciphertext both ways — it terminates no TLS, holds no certificate, and sees no plaintext, so the browser’s handshake completes end-to-end against the daemon’s own fleet certificate. The daemon tunnel splices that ciphertext into a dedicated loopback-only gateway ingress, separate from the public listener. Which listener accepted the connection is immutable transport provenance: the gateway marks every such connection ReachabilityRelay before TLS or HTTP parsing. In the all-off default, it serves only authority-free discovery bytes under anonymous role:none and refuses every protected HTTP, MCP, signaling, and WebSocket route. Exactly two per-daemon opt-ins admit anything more, each gating its own credential class and neither moving the other:

  • With hosted control on ([connect] hosted_control_enabled), the browser-lane carve-out is a fresh proof by an active daemon-minted hosted_lease principal, or a one-use WebSocket ticket minted by such a proof; a second compiled classifier then limits the route, method, frame, concrete action, target, and outbound event projection.
  • With relay peer admission on ([connect] relay_peer_admission — default off, boot-pinned, deliberately no environment override), the peer-lane carve-out is a TLS client certificate that resolves to an Approved, unexpired peer identity record: a daemon-side credential minted by the target’s own access CA through an owner-consented lane (pairing invite, doorbell approval, or org grant materialization) whose private key lives in daemon files — a class no browser keystore can hold or be scripted into presenting. Admission converts transport provenance only: the connection continues into the same transport-auth ladder every direct-lane peer request passes, the peer profile the owner granted remains the sole control-depth authority per operation and frame, and revocation or expiry of the record kills the next request. A browser-enrolled mTLS certificate resolves no peer record and keeps the refusal byte-identical, in both key states. The relay itself is unchanged — it still sees only ciphertext and admits nothing; the widening is entirely a daemon-side policy predicate.

Thus neither the loopback address of the tunnel’s last hop nor a browser-controlled Host: localhost can enter the trusted-local lane. Fleet SNI remains an independent provenance gate, not an mTLS bypass. The relay ingress also rejects non-TLS bytes before the gateway’s raw ICE-TCP and cleartext-MCP demux. The relay capability is bounded to withholding, delaying, replaying, or delivering arbitrary ciphertext; lease minting and compiled-preset scope remain daemon-local.

Item 5 is the one the web platform will not let us escape. The browser binds code identity to origin, origin trust to TLS+DNS, and the server behind an origin can change what it serves at any time, per user, silently:

  • Service workers do not pin code — the worker script itself re-fetches from the origin, so the server always stays in control.
  • IPFS and friends relocate the trust to whichever gateway resolves the content.
  • Signed web bundles / Isolated Web Apps are effectively app installation under another name, and are not portably available.
  • Passkeys prove user presence to the relying party. They protect the credential; they do not make the relying party honest, and they do nothing about the code the RP serves after login.

So within “pure browser, no extension” there is exactly one class of origin that can serve root-administration code without adding new trust: an origin whose compromise is already game over for you and whose route is independently authenticated. Your own daemon runs your agent with real authority — it is already inside your trusted computing base. Code served over its loopback or fingerprint-verified direct-mTLS route adds zero marginal trust. A WebPKI fleet name does not: the rendezvous controls that DNS namespace and can serve code at the same browser origin.

That observation fixes the design:

The rule: root-capable code is served by the resource owner, runs from a trusted local console, or ships as a native client whose release signature the user verifies (none has been published for this alpha); authority is only ever minted by the target daemon’s local IAM. A hosted claim, account assertion, route, or raw browser key carries discovery metadata and creates no principal or grant. Ambient hosted provenance is always capped at the zero-permission role:none; no local grant, persisted edit, or generic IAM call can raise it. The optional hosted-control lane admits only an exact, expiring hosted_lease principal minted after a trusted confirmation, and a compiled immutable floor excludes access/IAM, credentials, organization-root operations, approval resolution, and raising the lane’s own ceiling.

Anchor daemons

Every browser earns its trust once, through one ceremony, with one daemon the user controls — the anchor:

  • The ceremony is the existing certificate enrollment (intendant access serve-certs, p12/mobileconfig) or an on-LAN bootstrap. It happens once per browser, not once per daemon.
  • The anchor — or any daemon of the user’s fleet reached over an independently verified route; the serving role is deliberately fungible — serves the dashboard and the Access admin surface. Root administration executes in an origin backed by hardware and route identity the user controls.
  • The app generates a client identity key in the browser (WebCrypto P-256, non-extractable private key, stored in IndexedDB) for fleet-record signatures, attribution, and future identity work. In this alpha it is not a daemon login credential: local presence or an mTLS client certificate is the active browser authentication boundary.
  • Reaching any other daemon (yours, or an organization’s) from a trusted client means: the rendezvous can supply route metadata, while the daemon-served client binds the independently reached channel to the target daemon’s key and authenticates through loopback or mTLS. A future verified packaged native release could do the same for its bundled local daemon. Browser-key fingerprints can be stored in local IAM, but no alpha human ingress consumes a browser-key signature as general authentication. Connect-served code has no ambient control ingress; the optional hosted lane consumes only proof by the tab key named in a separately approved, expiring lease.
  • Passkeys authenticate the Connect account and derive keys for the encrypted fleet/vault envelopes. They do not make Connect-served code trustworthy and do not turn a claim into daemon authentication. The durable browser identity key is enrolled separately by an already-trusted daemon root.

A companion chapter, Credential Custody, applies the same discipline to the other secret class — the model provider credentials a daemon spends. Vault, lease, and relay components have shipped, but a Connect-origin account vault has no trusted delivery bridge to a daemon in this build; the custody chapter is the source for the exact boundary.

The hosted service keeps route discovery and presence, optional encrypted push delivery, encrypted/signed fleet-metadata backup, account/route linking, a name directory, the browser-code distribution surface (the install scripts are release-pinned GitHub assets it at most redirects to), and an optional ciphertext reachability relay. None is a daemon authority mint, but they are not “zero trust”: Connect controls availability, account and route metadata, the code it serves, and whether relayed bytes arrive. A daemon may publish a signed capability hint so Connect offers fleet-origin navigation; lease creation and approval still terminate at the daemon. The rendezvous component is self-hostable (how), and a daemon’s agent card states which rendezvous it uses (rendezvous_base) — connect.intendant.dev is the default instance of an open component, not a chokepoint.

The trust ledger

What each component can do to you if it turns malicious in the current alpha; rows explicitly marked future remain design constraints:

ComponentWorst case if maliciousWhy it is bounded
Your anchor daemonFull compromiseIt already runs your agent; nothing new is delegated to it
Another daemon of yoursThat machine’s authorityEach daemon is its own authority island
Org portal daemon (guest lane)The guest’s org-scoped sessionBlast radius = the org’s own resources; self-defeating
Connect route directoryControls availability, account/route metadata, first-introduction labels, and the navigation target it presentsDaemons co-sign route links; claims and account assertions create no daemon IAM state; the optional button is a daemon-signed capability hint, not authority, and opens only the daemon’s public doorbell
Optional Web Push deliveryDenial of service; delivery timing analysisPayloads are encrypted to each browser subscription; notifications carry no daemon authority
TURN relayDenial of service; traffic analysisMedia remains separate from the Connect SNI relay; any deployment must use short-lived credentials and preserve authenticated, end-to-end encrypted daemon channels
Fleet metadata storeDenial of service; false labels/routes or a substituted self-signed record on a new devicePrivate fields are encrypted; the current browser detects same-key alteration; records carry no daemon authority. An owner/device signer trust set is not shipped yet.
Name directoryHandle confusion at first introductionKey-first identity; handles are labels; org keys sign membership; append-only transparency log over all name bindings (STH pinned + consistency-verified by browsers; inclusion proofs on claims), optional DNS/GitHub attestation badges, invite-gated registration + reserved handles + dormant-handle reclamation
Fleet DNS zone + WebPKI (fleet-name route)Controls the endpoint and origin-scoped code reached through the fleet nameWith hosted control off, the route is discovery-only. With it on, unproved traffic is still anonymous role:none; protected routes require a short-lived daemon-minted lease bound to a tab key and exact compiled preset. The immutable floor and trusted confirmation cap same-origin authority. Shipped peer witnesses and an eligible signed-application witness shorten the detection window; the signed-application protocol accepts only a qualifying distribution anchor, never the current unsigned development artifact. CT is the slower fallback. No detection path mints authority.
Hosted Connect origin (directory lane)Controls account/route/presence metadata, page-visible or unlocked state, navigation, and the installer-route redirectClaims, passkeys, and account assertions grant nothing, and raw hosted provenance remains immutable role:none. The optional navigation hint is daemon-signed and leads only to the daemon’s lease doorbell. Served artifacts are transparency-logged (evidence, not prevention). The install scripts ship as release-pinned, transparency-logged GitHub assets Connect at most redirects to, with the redirect target pinned by intendant hosted-verify; the release channel itself remains a real software-supply-chain trust boundary, anchored on GitHub Releases and the log rather than on this origin.
Foreign browser origin / DNS rebindingDrive a browser-held mTLS certificate or loopback root fallback cross-siteEvery non-public HTTP route, direct dashboard signaling, and /ws reject foreign browser Origins before resolving transport authority; cross-/same-site Fetch Metadata closes navigation and subresource requests that omit Origin. Explicit authority-free shell/assets/discovery/signed-document doorbells run as anonymous role:none. Cleartext “own origin” is limited to localhost or a literal loopback address, so matching attacker-controlled Origin and Host values do not bypass the gate; non-loopback browser administration uses HTTPS/mTLS.

Trust scales with the blast radius of the relationship: a global service that could betray everyone at once is allowed to hold approximately nothing; the party whose resources you are touching may hold exactly the trust that relationship already implies; you hold everything about yourself.

Organizations: two lanes

An organization is a root keypair plus a handle — not a row in a central database. Org membership and grants are documents signed by the org key; each org daemon verifies the chain and enforces its own local IAM, exactly as a personal daemon does.

Lane A — members bring their own agent. The consistent version of the network: Alice has her own daemon (laptop, VPS, anything), her browser trusts only it, and the org binds Alice’s identity — an mTLS identity for shipped alpha human access, or her daemon’s peer identity — into IAM grants on its machines with scoped roles. Alice touching org infra never requires Alice to trust org-served code, and the org never has to trust Alice’s infrastructure. The existing split between the user/client domain and the peer domain is preserved on purpose: an org can grant Alice-the-human (the client-key document shape is staged until a trusted direct authenticator consumes it) or Alice’s daemon (agent-to-agent peer profile with filesystem scoping), and those are different, separately auditable trust decisions.

Lane B — guests. A human with no daemon gets served the app by the org’s own portal daemon (orgs have real domains and can hold real TLS certificates; the ACME/private-IP pain that rules this out for personal daemons does not apply). The portal could betray its guest — but only with authority over the org’s own resources, which is a categorically smaller and self-defeating threat compared to a global origin. For the true cold start — fresh machine, no prior trust, nothing at stake — no browser-only design escapes trusting some server for the first load. We refuse to turn that observation into ambient authority: the global hosted origin is a directory lane only. It can link and locate a route, and conditionally navigate to a daemon’s fleet-origin lease doorbell, but it cannot create or approve a daemon-control session. An organization can offer browser control from its own resource-owner portal; global Connect itself cannot be opted into authority.

Organization grants: implementation and remaining work

Status: core implementation shipped. Grant expiry, org root keys, signed grant documents, per-daemon trust with a local cap, materialization, the presentation/issue/trust/revoke endpoints, direct document presentation/materialization, signed revocation lists + renewal, peer-subject documents, and issuer-key delegation are live. Peer-subject documents are usable now; human client-key documents can materialize but cannot authenticate an alpha session because shipped human ingress does not consume browser-key proofs. Revocation-list gossip over peer links remains. This section is the spec the code follows; the earlier “two lanes” section is the product narrative it serves.

Objects

Org identity. An organization is an Ed25519 root keypair plus a handle. The root key lives on an org-designated daemon (intendant org init <handle><access-cert-dir>/org/<handle>/root.pk8, 0600), following the existing daemon-identity custody pattern; it is exportable for offline custody. The access-cert directory is <state-root>/access-certs on macOS/Linux (default ~/.intendant/access-certs) and the OS data directory’s intendant/access-certs on Windows. Day-to-day signing can move to delegated issuer keys certified by the root; root-signed documents carry no chain, and delegated documents carry the issuer certificate beside the signature it explains.

Org grant document. A self-contained, signed statement a member presents to any daemon that trusts the org key:

{
  "v": 1,
  "kind": "org-grant",
  "org": { "handle": "acme", "root_key": "<ed25519 b64u>" },
  "subject": {
    "client_key_fingerprint": "…",       // or "peer_fingerprint": "…";
    "label": "…"                         // exactly one subject fingerprint
  },
  "role_id": "role:session-reader",      // or "peer:<profile>" for peer subjects
  "targets": ["*"],                       // or explicit daemon ids
  "grant_id": "<uuid>",                   // stable id, used by revocation
  "issued_at_unix_ms": 0,
  "expires_at_unix_ms": 0,                // REQUIRED; hard cap 90d, default 30d
  "chain": [{ "…": "issuer cert object" }], // present when an issuer key signed
  "sig": "<ed25519 over the newline payload>"
}

The signing payload is newline-joined fields (the protocol style already used by claim proofs and client-key offers), not canonical JSON. The document is authorization, not authentication: it is useful only when an active transport separately authenticates the bound subject. Peer certificate subjects have that ingress today. Browser client-key subjects do not in this alpha, so materializing one creates record state but no usable login path. chain is omitted or empty when the root signed the document and present with one issuer certificate when a delegated issuer signed.

Subjects are exactly one of two cryptographic fingerprints: a browser client key for the human lane, or a peer daemon certificate fingerprint for the peer lane. Connect-account subjects are deliberately absent: they would make the org-grant path only as trustworthy as the rendezvous’s account assertion — a compromised hosted service could claim to be a granted member and collect the org’s authority on every daemon that trusts it. Peer certificates authenticate cryptographically end-to-end. Browser client keys remain staged subject vocabulary until a trusted browser-key authenticator is implemented; current human access uses loopback or explicit mTLS. Account subjects can be added later as an explicit, documented weakening if a real need appears.

Daemon-side org trust. iam.json gains trusted_orgs: [{handle, root_key, max_role, status, added_at}]. Trusting an org is a root-session action on each daemon — one click across the fleet via the phase-4 fanout. max_role defaults to role:operator: an org can never hand out more authority on your daemon than you allowed it, and org-root requires an explicit local override (ceilings still apply on top, by binding provenance, as today). Operator is the right default because trusting an org is itself the consent moment, operator already excludes access/settings/runtime administration, and a lower default would make the org lane’s normal grants (terminal, files, sessions) fail confusingly. A document whose role exceeds max_role is rejected at presentation rather than silently downgraded, so issuers learn the cap immediately.

Verification and materialization

On presentation, the daemon verifies: signature against a trusted org root or delegated issuer key → expiry → targets contains this daemon (or *) → subject kind and role/profile namespace match → the document fits under the trusted-org cap for its lane. A human-subject document materializes an ordinary local IAM grant (source: "org:acme", the document’s grant_id, expiry recorded) rather than evaluating documents per-request; a peer-subject document materializes into the peer identity store. Auditability and the existing evaluators come for free, and the local owner can revoke or re-role the materialized authority like any other; local IAM and local peer identity state always win.

Prerequisite schema work, valuable on its own: IamGrant gains expires_at_unix_ms: Option<u64>, and enforcement treats an expired grant as inactive (temporary grants for humans drop out of this immediately).

Presentation paths: an explicit POST /api/access/org-grants in the public doorbell class (rate-limited and size-capped), and an org_grant ride-along field on a daemon-origin dashboard-control offer so an org member’s first trusted direct-mTLS connection to an org daemon can materialize the grant and proceed in one round trip. Materialization runs before grant resolution, so the freshly written grant resolves for the offer that carried it. Connect does not relay or present this field in the default build: the service rejects hosted control calls and the daemon drops legacy Connect offers before parsing any ride-along. A failed direct presentation is non-fatal when another identity resolves; otherwise its error is surfaced in the local answer.

“Public” here describes an authority-free courier door, not public daemon authentication. The caller identity receives no role and the HTTP request does not become a dashboard/control session. The daemon parses the bounded document, verifies its org signature and local trust/cap, and can materialize a grant only for the cryptographic subject named inside it. A peer subject must later authenticate with its peer mTLS key. A human browser-key subject is record-only in this alpha: peer offers can verify that key for attribution, but no ingress admits it as the authority-bearing IAM principal. The same distinction applies to public renewal and ORL read/apply: they verify or return signed documents, never authenticate the courier.

Browser-side storage (intendant_org_grants_v1, keyed by org handle) is a record format, not a login or automatic alpha presentation promise. The default Connect directory may display records already present in that origin, but it does not present them to daemons. The retired browser-key offer code contains a presentation field, yet no shipped human ingress authenticates that key. Usable human access therefore requires an explicit trusted mTLS binding; peer-subject documents remain usable through peer mTLS.

Materialization itself is idempotent-quiet and local-wins: an unchanged presentation neither rewrites iam.json nor grows the audit log, and a document whose materialized grant (or subject principal) was locally revoked is refused rather than resurrected. The org’s escape hatch is a fresh document (new grant_id); the owner’s is re-enabling the grant from a root session. This same property is what lets the step-5 revocation list revoke by grant_id durably.

Revocation

Layered, with the failure mode stated honestly:

  1. Short expiries + renewal are the primary mechanism. Documents are cheap to re-issue; the org daemon serves renewals to still-valid members.
  2. Org revocation list: the root signs {org, seq, revoked_grant_ids[], revoked_subjects[], revoked_issuer_keys[]}; org daemons serve it publicly, browsers carry it today, and peer-link gossip remains later plumbing; daemons enforce monotonic seq and apply it by revoking materialized grants and peer identities.
  3. Local override always works: any daemon root can revoke an org-materialized grant locally, ORL or not.

An unreachable revocation list plus a long expiry is a stale-authority window — hence the 90-day hard cap and the 30-day default.

ORL format and semantics. The list is cumulative and self-contained:

{
  "v": 1,
  "kind": "org-revocations",
  "org": { "handle": "acme", "root_key": "<ed25519 b64u>" },
  "seq": 4,                          // monotonic; consumers refuse stale
  "revoked_grant_ids": ["…"],        // document grant_ids, not local grant ids
  "revoked_subjects": ["…"],         // subject fingerprints — "member is out"
  "revoked_issuer_keys": ["…"],      // delegated issuer keys revoked wholesale
  "issued_at_unix_ms": 0,
  "sig": "<ed25519 over the newline payload>"
}

The signing payload is newline-joined like every other protocol here and includes the grant-id list, subject-fingerprint list, issuer-key list, and issue time. The org daemon persists the current list next to the root key (org/<handle>/orl.json), bumps seq on every change, and serves it at GET /api/access/orgs/<handle>/revocations (public — it is org-public data; an empty seq-0 list is signed lazily on first read).

Subject entries use one canonical form at both ends: certificate fingerprints (peer daemons) fold to lowercase separator-free hex when the list is written and whenever it is compared — apply, materialization, and renewal all canonicalize both sides — so an uppercase or colon-separated spelling still revokes, including entries persisted by older builds. base64url client-key fingerprints are case-sensitive and only trimmed.

Delivery is carried, not discovered. A consumer daemon has no address for “the org daemon” — there is no membership server to ask — so the list travels the same way grant documents do: anyone may push it to POST /api/access/orgs/revocations/apply (doorbell class: unauthenticated, rate-limited, size-capped — the caller gets no authority; the locally pinned org signature authorizes only application of the signed revocation facts, and replaying an old list is refused by seq). Two couriers exist today: the org admin’s browser publishes the list to the rendezvous bulletin board (blind storage — signature-checked, with monotonic writes). The board cannot forge a newer list and a daemon refuses a sequence below its local high-water mark, but the board can withhold the latest list or serve an older signed list to a fresh daemon that has no sequence history. Every member’s browser fetches the board’s copy and hands it to each daemon it visits. Peer-link gossip can come later; with browsers as couriers, revocations already reach any daemon a member still talks to.

Application. A daemon that trusts the org verifies the signature against its trusted key for that handle, requires seq strictly greater than the last applied (equal is an idempotent no-op), then persists the lists and the new seq on its trusted_orgs entry and revokes every materialized grant whose document grant_id, subject fingerprint, or issuer key is listed. Persisting matters beyond the sweep: materialization and renewal both check the stored lists, so a revoked grant_id, subject, or issuer key is refused future presentation too — combined with the no-resurrection rule above, an ORL revocation sticks even against a member who still holds a validly signed document.

Renewal. A member (or anyone carrying the document) presents a still-valid document to the org daemonPOST /api/access/org-grants/renew, doorbell class — and receives a freshly signed copy: same subject, role, targets, and the same grant_id, with issued_at set to now and the original lifetime span preserved (capped at 90 days). Keeping grant_id stable is deliberate: ORL revocation by grant_id keeps working across renewals, and the document’s identity is the grant, not the signature instance. The org daemon refuses renewal for anything its own ORL lists (by grant_id or subject) — expiry then retires the member within the document’s remaining lifetime. Only the daemon holding the org root key can renew, and renewal grants nothing a fresh issue would not; it exists so membership can be kept short-lived without the issuer in the loop. The unauthenticated courier is not logged in by this call and cannot exercise the renewed subject’s grant.

Organization-grant implementation status

  1. ✅ Grant expiry in the IAM schema, evaluator, and UI.
  2. ✅ Org identity + intendant org init + trusted_orgs + Access → Advanced → Organizations (trust / revoke / issue).
  3. ✅ Document format, verification against trusted org keys, materialization into local IAM, and paste-to-join UI. The public presentation endpoint is POST /api/access/org-grants (doorbell class: unauthenticated, rate-limited, 16 KiB cap — the signed document authorizes only subject-bound verification/materialization, never the courier’s session); trust/revoke/issue require access.manage.
  4. ⚠️ Human client-key presentation machinery is staged: browsers can store and present a document from trusted daemon-origin code, and daemons can verify and materialize it idempotently without resurrecting local revocations, but no alpha ingress admits the document’s browser key as its controlling IAM principal. It therefore creates record state, not a usable human login. The hosted rendezvous path is disabled and cannot exercise a stored document. Peer subjects are usable through peer mTLS. E2E: scripts/validate-org-grants.cjs.
  5. ✅ Revocation list + renewal: root-signed cumulative ORL maintained on the org daemon (orl.json, served publicly), carried to consumers via the public apply doorbell, enforced monotonically and persisted so listed grant ids/subjects are refused at materialization and renewal; renewal re-signs a still-valid document with the same grant_id and its original lifetime span. UI: revoke-member / copy-list / apply-list / renew flows under Access → Advanced → Organizations. Peer-link gossip and periodic pull remain for later plumbing.
  6. ✅ Peer-daemon subjects and issuer-key delegation, per the design below and its sign-off decisions: fail-closed max_peer_profile, explicit presentation only (no peer-doorbell ride-along in v1.1), and chain-only issuer certificates (deputies initialize a key, the root delegates, documents carry the certificate; revocation lists revoke issuers wholesale via recorded issued_via). Renewal stays root-only for now. E2E: scripts/validate-org-grants.cjs scenarios 4–5.

Peer subjects and issuer-key delegation

Peer-daemon subjects. An org grant whose subject is a peer daemon materializes into the peer identity store (access/access_policy.rs::PeerIdentityRecord), not IAM — daemons are peers, never people, and the peer lane’s profile vocabulary is the right authority language for them. Format: subject carries peer_fingerprint instead of client_key_fingerprint (exactly one must be present), and the signing payload’s subject-kind line — the literal client_key today, deliberately baked into every existing signature — becomes peer_daemon, so a signature can never be replayed across subject kinds. role_id uses a peer:<profile> namespace (e.g. peer:session-reader) so a peer document cannot be confused with a human-role document even outside the payload.

Materialization upserts an approved PeerIdentityRecord bound to the fingerprint. Two prerequisite schema steps, each valuable alone (the same pattern as grant expiry in step 1): the record gains expires_at_unix (org documents require expiry; enforcement treats an expired record as revoked) and source (org:<handle> provenance, so org revocation can sweep records it created and the UI can say where an identity came from). The org’s cap for the peer lane is a separate max_peer_profile on the trusted-org entry. It is empty by default, so trusting an org grants no peer authority until the owner sets a peer cap. The cap relation is operation-set containment, not a strict ladder: file-oriented and session-oriented profiles can be siblings, and a document fits only when its profile allows no operation outside the cap. Over-cap documents are rejected, not downgraded, like the human lane. Local rules carry over verbatim: locally revoked records are never resurrected, re-presentation is idempotent-quiet, and ORL revoked_subjects matches peer fingerprints exactly as it matches client keys.

Issuer-key delegation. Day-to-day signing moves off the root: the root signs a delegation certificate

{
  "v": 1, "kind": "org-issuer",
  "org": { "handle": "acme", "root_key": "…" },
  "issuer_key": "<ed25519 b64u>", "label": "…",
  "issued_at_unix_ms": 0,
  "expires_at_unix_ms": 0,          // REQUIRED; suggested cap 365d
  "max_role": "role:operator",      // optional role:* or peer:* scope; "" allows both
  "sig": "<root, newline payload>"
}

and a document may then carry chain: [<issuer-cert>] (the array reserved since v1) with its own sig made by the issuer key. Verification walks outside-in: the trusted root key validates the cert, the cert must be unexpired, then the issuer key validates the document. max_role is a scoped string despite the historic field name: role:* scopes sign only human-subject documents, peer:* scopes sign only peer-subject documents, and an empty scope allows both lanes. A peer:* scope is enforced during verification by operation-set containment; a role:* scope refuses peer documents during verification and enforces human role permission containment during materialization, where the receiving daemon’s local IAM catalog exists. Everything else — org cap, targets, expiry, ORL — applies unchanged. One level only: an issuer cannot mint issuers.

Revoking an issuer revokes everything it signed going forward: the ORL gains a revoked_issuer_keys list, which adds a line to the ORL signing payload. The v1 payload was extended before external compatibility was promised. Any future incompatible change must use an explicit v: 2 with dual-version acceptance. Materialized grants are swept by matching the chain recorded at materialization time — which means the materialization audit/grant record starts persisting the issuer key it accepted.

Decisions (signed off 2026-07-02). (a) Explicit presentation only in v1.1 — a peer-doorbell ride-along would bypass the peer approval queue and needs its own design pass; (b) fail-closed: max_peer_profile is empty by default and trusting an org grants no daemon-to-daemon authority until the owner raises it — the two lanes are separate trust decisions; (c) chain-only issuer certs — a published list adds no verification value; documents stay self-contained.

Mechanisms

The pieces that implement the model, mapped to the codebase:

  • Daemon-local IAM (<access-cert-dir>/iam.json, with the platform location described above): principals (browser certificate, client key, metadata-only Connect account records, human user, peer daemon, exact hosted lease), grants (principal → role on this daemon), roles over the daemon permission catalog defined in access/iam.rs. Implemented; the source of all authority. Connect account records remain compatibility and audit vocabulary, not an authenticating binding. Hosted lease roles are compiled presets rather than persisted permission lists.
  • End-to-end tunnel binding: trusted direct dashboard-control offers are answered with a daemon-signed binding over (daemon public key, session grant, client nonce); the browser verifies before trusting the channel. Implemented for daemon-origin control. Fleet-SNI and hosted Connect signaling are rejected before a grant can be constructed. The retired Connect signaling format used the same binding, but the current service rejects those calls and current daemons refuse legacy events before opening a channel.
  • Client identity keys (staged, not alpha authentication): browser-held WebCrypto P-256 keys sign fleet records and may appear as peer-relay attribution. Verification and IAM record vocabulary for key-signed offers exist from the retired hosted design, but local /ws, direct /connect/dashboard/offer, and the reserved future native-bridge code do not consume that proof. A stored key grant therefore grants no usable alpha ingress; active browser control authenticates with loopback presence or mTLS.
  • Ambient hosted refusal and explicit lease authority: a Connect account assertion is route metadata and never authenticates to the daemon. Daemon-stamped route provenance prevents a directly enrolled key from shedding hosted policy merely because its stored origin looks trusted. Both compatibility ceiling entries are normalized to role:none on every load; missing, empty, or hand-edited values also resolve to role:none. Connect’s retired browser offer/ICE/close calls remain refused at both ends, and no browser-key grant can authorize that path. Separately, the off-by-default hosted-control lane lets a fleet-origin tab request a bounded lease. Only a local console, direct-mTLS owner dashboard, or enrolled qualifying signed app can approve it. The daemon then mints an exact hosted_lease principal; fresh request proofs and one-use WebSocket tickets exercise only the compiled preset. Generic IAM APIs cannot create, assign, or reactivate hosted principals, grants, or roles, and the lane’s dedicated ceiling ceremony cannot lift its immutable floor. The optional user-owned custom-domain lane is a separate TLS provenance class: exact SNI, Host, Origin, and WebAuthn rp_id agree on the configured name. Its passkey ceremony approves only the signed tab request and mints the same bounded lease; it creates no ambient IAM or root principal. The local IAM write boundary also refuses an active pure browser-key grant whose recorded origin is hosted, under the currently learned fleet zone, or an exact fleet name the daemon learned previously. Fleet-name provenance is retained in fleet-origin-provenance.json beside the access certificates so offline or Connect-disabled startup cannot reclassify an old service-named route as a direct anchor. When Connect is enabled, custom-name control also waits for the current registration’s fleet-zone observation before opening. On upgrade, an older fleet-cert.pem backfills both canonical exact DNS SANs and their derived fleet zones before the gateway accepts requests. If that provenance is malformed or cannot be recovered completely, unknown DNS browser-key origins fail closed as fleet provenance until the local authority store is repaired. A human_user label is not a bypass. A valid independently verified browser mTLS binding may carry that key as metadata. Legacy browser-key records are displayed as inactive bindings with enforced: false and authority: none, and cannot be reactivated through the grant lifecycle API. Upgrading only Connect cannot tear down a legacy P2P session that is already established; complete the migration by restarting upgraded daemons, closing old Connect tabs, and allowing the IAM migration to revoke legacy connect-bootstrap grants.
  • Legacy hosted-root and --owner migration: IAM schema v2 identifies principals whose client-key authentication origin is the retired connect-bootstrap sentinel, revokes every active grant on them, and records revoke_legacy_connect_bootstrap. It separately revokes active browser-key grants whose recorded reason came from the retired CLI --owner bootstrap and records revoke_legacy_owner_browser_key_bootstrap. Both compatibility ceilings return to role:none; direct/mTLS root grants survive. Existing Connect account/route links are metadata outside IAM and remain linked. Remove --owner from old service commands (the parser now rejects it), restart the upgraded daemon, close old Connect tabs, and run intendant access setup locally to establish the generated owner mTLS credential.
  • Device enrollment (staged UI/state): the daemon retains a bounded pending enrollment queue and decision/upsert scaffolding, but the default product has no production writer for that queue. The alpha local/direct-mTLS transports do not present browser-key authentication, and Connect/fleet events are refused before enrollment, so the GET capability reports status: staged, writer_available: false, and an empty queue during ordinary operation. Test fixtures can still exercise the decision plumbing. Remote alpha enrollment uses mTLS certificate setup instead; a claim never substitutes for trusted approval.
  • Encrypted fleet sync: the private fields of a synced fleet record (daemon URLs) are sealed with an AES-GCM key derived from the account passkey via the WebAuthn PRF extension — bytes the browser evaluates locally and does not send in the protocol. Connect still controls that browser’s JavaScript and can prompt for verification and exfiltrate the PRF output or decrypted state while the page has it. Passkeys sync across the user’s devices, so each device derives the same key; the hosted store holds ciphertext (enc_fields, signed as stored — fleet-record payload v3), and a device without the key still verifies the record and simply shows it locked. No PRF support degrades to plaintext-free sync of the public fields only.
  • Grant fanout: the anchor-served Access page applies one grant across many daemons — an “Apply to” step in the grant flow calls each selected daemon’s independently verified direct-mTLS IAM API (browser mTLS, cross-origin), with per-daemon results; every target authorizes independently and no central grant store exists. Cross-origin access to the six fleet Access APIs is gated by a per-daemon origin allowlist (itself, the macOS app scheme, its outbound peer routes, its approved inbound identities) that both drives the CORS echo and refuses state-changing requests from any other page — closing the hole where a cert-installed browser could be steered by an arbitrary website. This posture is daemon-wide: no authority-bearing API response is wildcard-readable and foreign-origin /api/* requests are refused. /config is not public bootstrap: it requires presence.read, accepts only the daemon’s own or a fleet-allowlisted Origin, and is Cache-Control: no-store because its ICE material may contain credentials. Fleet-name SNI is rejected even when CORS would otherwise allow the caller. Only authority-free bytes such as the agent card, /connect/bootstrap, /connect/status, and the peer doorbell remain public. Authority- bearing /connect/dashboard/* signaling and /ws apply the same own/app- origin gate before resolving local or mTLS authority; cleartext own-origin access is loopback-only to resist DNS rebinding.
  • Signed fleet sync: fleet records that round-trip the hosted metadata store are signed by the pushing browser’s identity key (over host id, label, and route URLs) and verified on read; the Access UI badges each synced record as verified-by-this-browser, signed-by-another-device, unverified, or hosted-claim. The signing public key travels inside the record and there is no owner/device trust set yet. The current browser can therefore detect alteration under its own key, but a malicious store can replace a record with a newly generated, internally valid self-signed one on another device. Connect-served code can also wield that origin’s browser key while loaded. These signatures are integrity/attribution hints, not daemon authentication and not protection from a malicious hosted page or store. Private route fields are also encrypted across devices with the passkey-PRF-derived key described above; that protects stored ciphertext, not plaintext exposed to the currently running hosted page.
  • MCP principal binding: /mcp requests (supervised backends, intendant ctl, the dashboard’s tool RPC) enter the same evaluator as every other surface. Supervised agents authenticate with a session-scoped token derived from the daemon’s per-process secret, binding them to principal:agent-session:<id>; tokenless loopback callers bind to principal:local-process:loopback; browser pages must present the daemon’s own origin. Each tool call is checked against a per-tool permission map at call time, so agent_session / local_process grants scope what a given supervised agent or local shell may reach. Defaults stay root-compatible on a single-user daemon; once any agent session has ever been scoped, the tokenless loopback default fails closed until a local_process grant states what bare local callers get, and a lapsed grant (expired or revoked) denies rather than restoring default trust. See MCP Server.
  • Org root keys: membership and role assertions signed by the org key; each daemon explicitly pins the org root it trusts and verifies signatures locally. Handles are labels, not a global authority directory. Connect’s append-only transparency log can make published metadata auditable, but it cannot add or replace an org root in daemon-local trust.

Prior art

The pattern is proven elsewhere; we are assembling, not inventing:

  • Tailscale tailnet lock — coordination server distributes node keys but cannot mint them; signatures chain to user-held keys. Our rendezvous + client keys is the same demotion.
  • Keybase — server as untrusted directory over user sigchains; key-first identity with names as labels.
  • Matrix — the client trusts its homeserver; federation carries verifiable events between sovereign servers. Our anchor daemon is the homeserver role.
  • Certificate Transparency — public evidence of certificate issuance and a useful route diagnostic, not a substitute for an authority anchor.
  • SPKI/SDSI and petnames — authority bound to keys; human names are local, contextual labels.

Loopback trust vs. the runtime sandbox

Local presence makes loopback a root-capable surface — deliberate, and documented above — but since the per-boot admission token shipped, reachability alone is no longer the credential. Every daemon boot mints a random loopback admission token and writes it to <state root>/loopback-tokens/<port>.token (0600; one file per instance port, so concurrent daemons on a home never clobber; rotates every boot; removed with the instance’s other per-boot state on the next boot of that port). Every owner-posture loopback surface — the dashboard HTTP API, /ws, dashboard-control datachannel signaling, and the local_process lane of /mcp — refuses tokenless loopback with a named error that says exactly which file a local owner process reads (or that INTENDANT_LOOPBACK_TOKEN overrides it). Authority-free bytes (the SPA shell, /.well-known/agent-card.json) stay serveable; nothing that mints owner authority is reachable without the token. The gate is default-ON for every daemon posture — dev and QA builds, --no-tls included — with no opt-out flag: a shared home’s effective security is its weakest daemon’s posture, and the one live escape observed (a session refused by an mTLS front door scavenging a permissive plain-HTTP QA daemon on the same home) was exactly a weakest-daemon bypass.

The token turns “can reach 127.0.0.1” into “can read the owner’s state root”. On every platform that ends tokenless drive-by — including cross-uid drive-by, since TCP loopback is not uid-scoped and the trusted-local lane previously admitted any local user — and ends the scavenge-a-permissive-daemon bypass. Owner surfaces present the token automatically: the boot prints a tokened dashboard URL to the controlling terminal only (deliberately never through stdout/stderr, which the daemon log tee mirrors into the sandbox-readable logs/ subtree), intendant ctl discovers it with zero friction (INTENDANT_LOOPBACK_TOKEN env override, else the per-port file; supervised sessions keep their injected, session-scoped mcp_token lane instead and never self-escalate by reading the file), intendant ctl dashboard-url prints the owner URL for service-mode daemons, and the dashboard stores the token from its opened URL, strips it from the address bar, and attaches it to every same-origin request. The token authenticates admission to the existing loopback posture — admitted callers mint exactly the principals they always did (local_process, the trusted-local dashboard lane); there is no new principal class and no IAM vocabulary change. It is likewise independent of the /mcp token ladder: mcp_token credentials keep working unchanged, and the dedicated session-MCP listener’s single-rung session-token ladder is untouched.

Transport interaction with the TLS posture (ratified 2026-07-20): a TLS-enforcing daemon still refuses cleartext with 426 Upgrade Required, through one shared admission predicate whose only exceptions are host-local and credentialed — the token-gated loopback /mcp lane supervised backends bootstrap through, and (the ruling’s widening) loopback requests presenting the per-boot admission token. That admits exactly the CLI-class owner clients (ctl, rigs) whose traffic never leaves the host and who today hold no TLS client identity; browsers never qualify (browser-origin markers are excluded, so they keep the 426 and the printed https URL), remote surfaces are unchanged, and the dedicated session-MCP listener stays exactly its own /mcp ladder — the owner token never widens it. ctl-over-HTTPS remains deliberately unbuilt even now that local key custody has shipped (the credential-custody chapter’s “Local key custody” section): the ruled posture keeps ctl loopback-token local, and any future HTTPS ctl lane retrieves its custody-held client identity interactively — the keychain prompt is the ceremony — rather than minting a silent second authorized binary. That keeps this paragraph’s promise honest instead of quietly widening the owner surface.

The honest security envelope, per platform:

  • macOS — strong. The default (sandbox-on) runtime profile denies network egress to the daemon’s own gateway port (a prompt-injected shell cannot even reach the surface), and the Seatbelt credential clause denies reads of loopback-tokens/ alongside the trust store — so the token really is scoped to owner processes. --no-sandbox lifts both with the rest of the confinement.
  • Linux and Windows — a real raise, not airtight. Neither sandbox can express a single-port deny (Landlock’s network rules are allowlist-only; restricted tokens do not filter loopback) nor a read deny under a granted tree, so a sandboxed shell that can read the home can read the token. The token still ends tokenless drive-by and the weakest-daemon scavenge on these platforms. The local key-custody migration has since shipped for macOS (opt-in, keychain-wrapped — see the credential-custody chapter), and by its own binding label it is bar-raising, not lane-sealing: airtight same-uid separation still waits on a signed hardened-runtime binary plus the uid-separation work, and Linux/Windows custody backends are unbuilt, so this limit stands accepted on all three platforms.
  • Same-uid unsandboxed processes read the token by design. That is the unchanged owner surface: an owner-launched script or unsupervised agent on the same account has always been, and remains, the owner.

Multi-daemon homes get a same-home handoff: an owner-posture caller on one daemon may ask GET /api/local-daemons/tokens for the per-instance tokens that daemon’s own state root holds (files it could already read), so the trusted dashboard opens same-home siblings with their own tokened URLs. The route is credential-custody gated, answers per-request without persisting anything, never crosses a home boundary, and retires naturally when the fleet-identity arc supersedes it.

The same asymmetry applies to the state-root secrets: every platform’s default grant set excludes the trust store (access-certs/), leased auth, and the custody trail from runtime writes, but a project root or operator-supplied extra_write_paths that overlaps those locations can reopen them. Only macOS can additionally deny reads of the credential locations at the OS-policy layer.

Alpha implementation status

The alpha keeps loopback and direct mTLS first-class while separating shipped authentication from staged identity vocabulary:

  1. Trusted anchors are shipped — local presence and direct mTLS are the root-capable daemon-control and lease-confirmation entry points. The packaged macOS app has a loopback mTLS bridge in source, but no Developer ID-signed/notarized release has been published for this alpha. The release pipeline labels builds -unsigned-dev until signing credentials are provisioned; those artifacts are not distribution anchors.
  2. Ambient hosted refusal, migration, and bounded leases are shipped — raw hosted provenance is fixed at role:none; retired Connect signaling is refused at both ends; the IAM migration revokes grants created by the retired Connect bootstrap and --owner browser-key bootstrap. The separate hosted-control switch defaults off and admits only an approved, expiring hosted_lease through exact route/method/frame/action projections.
  3. Client identity keys are metadata-only in this alpha — they sign fleet records and remain useful subject vocabulary. Peer offers can verify one for attribution, but no live alpha ingress admits it as the controlling IAM principal. Device enrollment UI/state is therefore staged, not a replacement for mTLS.
  4. Local grant fanout, signed and encrypted fleet sync, org-signed grant and revocation documents, self-hostable Connect, and transparency-log records are implemented — every daemon still makes its own trust and authorization decision.
  5. General browser-key login remains future work — it cannot reactivate the retired Connect path or broaden the dedicated hosted-lease principal. The signed-application witness/anchor protocol is implemented, but the current -unsigned-dev artifact is deliberately ineligible; operational signed-application confirmation still requires a qualifying signed/notarized distribution and platform-keystore enrollment.