AIP-59: MOBILE PAIRING — browser clients over an E2E rendezvous
How a plain browser (a phone scanning a QR code) pairs with an agentproto daemon and then reaches its HTTP surface end-to-end encrypted through an untrusted rendezvous broker. Fixes the offer URL carried in a URL fragment, the pair/v2 handshake over WebCrypto, the route/auth token split that keeps every secret off the broker, credential storage, the service-worker proxy model with bounded, chunked, cancellable frames, authenticated revocation, and the threat model.
| Field | Value |
|---|---|
| AIP | 59 (provisional — editors assign the final number) |
| Title | MOBILE PAIRING — browser clients over an E2E rendezvous |
| Author | Jeremy André <[email protected]> |
| Status | Draft |
| Type | Core |
| Requires | AIP-1 |
| Created | 2026-09-26 |
| Package | @agentproto/pair-client (client), @agentproto/secrets (handshake, derivations, offer codec) |
Abstract
An AIP-59 host that only dials outbound can be reached by a paired
client through a rendezvous broker that splices two WebSockets and relays
ciphertext. The agentproto daemon is one such host, not the only one. This AIP specifies that relationship for a browser client: a
phone scans a QR code, the pair page runs the pair/v2 handshake with
WebCrypto, stores a credential, and a service worker then proxies the
daemon's own UI and API through the end-to-end encrypted tunnel, including
streamed responses. The broker never sees plaintext or any secret that
authenticates.
Motivation
The existing remote paths into a daemon (a public quick tunnel, a reverse tunnel through a host) terminate TLS at an intermediary, which then sees everything; the only protection is a bearer token. Quick tunnels also break server-sent events, so live updates degrade to polling.
A native CLI can already pair over a rendezvous. A phone has no CLI, only a camera and a browser. Without a spec, each browser client would have to guess how to carry the offer, where to keep the long-term secret, how to fit an HTTP stream into a size-limited byte pipe, and how to tell "unpaired" from "offline". Each wrong guess leaks a secret or loops forever.
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in AIP-1 (RFC 2119).
Roles. The daemon is the AIP-59 host: any process that implements the
host side of this AIP. The agentproto daemon is one host; a non-daemon
application MAY be another. Everywhere this AIP says "daemon", it means the
host. The daemon holds a static identity: an X25519 key pair and an
Ed25519 key pair. Its fingerprint is the first 32 lowercase hex characters
(128 bits) of SHA-256(X25519 public key, SPKI DER), matching
^[0-9a-f]{32}$. The client is a browser. The
broker is a rendezvous WebSocket server that pairs two sockets presenting
the same route (?side=daemon|client&t=<route>) and relays messages
unmodified. The pair page is the web page that opens the offer and holds
the credential. All HKDF is HKDF-SHA256, and all strings are UTF-8.
1. Offer URL
1.1. The daemon mints an offer with these parameters:
| Param | Value |
|---|---|
v | offer version, 2 |
rv | broker URL, ws: or wss: |
id | daemon fingerprint, 32 lowercase hex (^[0-9a-f]{32}$) |
pk | daemon X25519 public key, SPKI DER, base64url without padding |
sk | daemon Ed25519 public key, SPKI DER, base64url without padding |
s | one-time offer secret, base64url; the daemon MUST generate at least 128 bits from a CSPRNG |
exp | expiry, unix seconds |
1.2. The canonical form is agentproto://pair?<params>. The web form,
which a phone camera can open, is the same parameter string placed verbatim
in the fragment of an https URL:
https://<pair-page-host>/pair#v=2&rv=…&id=…&pk=…&sk=…&s=…&exp=…The daemon MUST let its operator configure the pair-page URL as a template in
which {fp} is replaced by the daemon fingerprint (for example
https://{fp}.agentproto.cloud/pair). This lets users self-host the static
pair page. Since the offer carries the fingerprint, the link is fully known
when the offer is minted.
1.3. The offer MUST travel in the fragment, never in the path or query of the
web form. Browsers send neither the fragment nor the Referer header to the
pair page's server. The pair page MUST read the fragment once and MUST then
remove it from the address bar and from session history (for example with
history.replaceState) before it does any network I/O.
1.4. A client MUST reject the offer, before it opens any socket, when:
- a parameter is missing or empty, or is not valid for its type;
rvis notws:orwss:;idis not equal tofingerprint(pk);exphas passed.
A v other than 2 MUST be rejected. v=1 MUST be rejected with the code
pairing_protocol_outdated (§7).
1.5. The daemon MUST treat the offer as single-use and SHOULD give it a short TTL; the reference daemon uses 10 minutes. A hello with the wrong offer auth (§3) MUST NOT spend the offer.
2. Route and auth tokens
2.1. Every pairing secret yields two independent HKDF outputs: a route and an auth token. Both are encoded as base64url without padding.
| Token | IKM | salt | info | Length |
|---|---|---|---|---|
| offer route | s | agentproto/pair-offer | agentproto/rv-route | 16 B |
| offer auth | s | agentproto/pair-offer | agentproto/rv-auth | 32 B |
| epoch route | pair root | agentproto/rv-route-salt | agentproto/rv-route ‖ u64be(epoch) | 16 B |
| epoch auth | pair root | agentproto/rv-auth-salt | agentproto/rv-auth ‖ u64be(epoch) | 32 B |
For the offer tokens, the IKM is the UTF-8 bytes of the s string as it
appears in the URL. For the epoch tokens, the IKM is the raw 32-byte pair
root (§3.4). The epoch is the UTC day number, floor(unix_ms / 86 400 000).
2.2. Only route tokens reach the broker, as the t parameter of the
upgrade URL. The offer secret s, the pair root and every auth token MUST
NOT appear in any URL, header or unencrypted message. They MUST travel only
inside the sealed hello (§3.1).
2.3. The daemon MUST verify the auth token in constant time. It MUST refuse a hello whose auth token is wrong, and MUST NOT treat knowledge of a route as proof of anything.
2.4. On reconnect, the daemon SHOULD park on the routes of both the current and the previous epoch. The client SHOULD try the current epoch first, then the previous one.
3. Handshake (pair/v2)
The browser handshake MUST be byte-identical to the native one, so that a daemon cannot tell the two clients apart. Browser clients MUST implement it with WebCrypto (X25519, Ed25519, HKDF-SHA256, AES-256-GCM) or with a constant-time library that produces the same bytes.
3.1. Hello (client → daemon). The client generates an ephemeral X25519
key e. It seals {clientPub: e_pub, clientName, auth} to the daemon's
static X25519 key with an ECIES-style seal (X25519, HKDF-SHA256 with info
agentproto/secrets/seal v1, AES-256-GCM), which gives ct0. It then sends
{v: 2, ePub: e_pub, ct0}. The auth field is the offer auth on first
contact, or the epoch auth on reconnect.
3.2. Reply (daemon → client). The daemon opens ct0, checks that
clientPub equals ePub, and verifies auth (§2.3). It generates an
ephemeral key d_e and replies with {v: 2, dePub: d_e_pub, sig}, where
sig = Ed25519(daemon_ed25519, transcript) and
transcript = SHA-256(e_pub ‖ utf8(ct0) ‖ d_e_pub).
3.3. Keys. Both sides compute
HKDF(IKM = ECDH(e, d_e) ‖ ECDH(e, daemon_x25519), salt = transcript, info = "agentproto/pair/v2", L = 64). The first 32 bytes are the
client-to-daemon key, and the last 32 are the daemon-to-client key. The
client MUST verify sig against the sk pinned from the offer or the
credential, and MUST abort on failure. Any failure MUST close the channel
without continuing on attacker-chosen material.
3.4. Pair root. After a first-contact handshake, both sides derive
pairRoot = HKDF(IKM = sort(k_c2d, k_d2c), salt = transcript, info = "agentproto/pair-root", L = 32). The two keys are sorted byte-wise
and concatenated, so both peers get the same value.
3.5. Channel. Every tunnel frame after the handshake is sent
AES-256-GCM-encrypted as an e2e envelope {t: "e2e", n, d}. n is a
64-bit counter per direction, starting at 0. The nonce is 4 zero bytes
followed by u64be(n), and the AAD is u64be(n). A receiver MUST require
exactly the next n, and MUST close the channel on replay, reorder, tag
failure, or any frame that is not an e2e envelope. A sender MUST stop
before n reaches 2³² and rekey by reconnecting.
3.6. Confirmation. Before the first network I/O, the pair page MUST
display the offer's fingerprint. After the handshake, it MUST display the
daemon's name (from the tunnel hello frame) next to that fingerprint. It
MUST then obtain an explicit user confirmation before it persists any
credential. If the user declines, the page MUST discard the pair root. The
client MUST abort if the authenticated peer fingerprint differs from id.
4. Credential storage
4.1. A credential contains the fingerprint, the pinned pk and sk, the
rv, the pair root, the clientName, and timestamps. It MUST be keyed by
the daemon fingerprint, one credential per daemon per origin.
4.2. Credentials MUST be stored in IndexedDB or an equivalent origin-scoped
store that is readable from both the page and its service worker.
Credentials MUST NOT be written to localStorage, to sessionStorage or to
cookies, and MUST NOT be sent to any server.
4.3. The pair root SHOULD be imported as a non-extractable WebCrypto HKDF
CryptoKey before it is stored, so that script can derive the epoch tokens
but cannot read the secret back. A client MUST NOT keep an extractable copy
after import.
4.4. The credential is bound to the origin that stores it. A different origin (including a self-hosted pair page) has its own, separate credentials. Any script running on that origin can use every credential stored there, including daemon UI loaded under a worker's scope (§5.8).
5. Service-worker proxy
5.1. Scope. The pair page MUST register one service worker per pairing,
scoped to /d/<fingerprint>/. The worker MUST answer only requests under its
scope. It forwards the path that follows the scope prefix (with its query) to
the daemon.
5.2. Origin of UI code. Any daemon-served UI (for example the Control Center) MUST be loaded through the tunnel from the daemon, under the worker's scope. The pair page's server MUST NOT host or serve daemon UI code. The pair page ships only the pairing flow, the worker and the status pages. The daemon UI still runs on the origin of the worker's scope, with that origin's full privileges (§5.8).
5.3. One connection per credential. The worker MUST hold at most one
tunnel client per credential, shared by all fetch events, and MUST multiplex
requests over it by reqId. Documents under the scope MUST NOT open their own
channel. The broker allows one live socket per route, and the daemon parks a
bounded number of channels per pairing.
5.4. Requests. A request is sent as http_request. A response arrives
either as one http_response, or as http_response_head followed by
http_response_chunk frames, the last of which is marked end. The worker
MUST return a Response whose body is a ReadableStream fed chunk by chunk.
It MUST NOT buffer a streamed response (text/event-stream, NDJSON) to its
end. A daemon SHOULD send long-lived streams such as SSE as head and chunks,
and SHOULD emit periodic keep-alive data on them.
5.5. Frame bound. The broker MAY close any WebSocket message above its
limit; the reference broker's default is 1 MiB. Each sender MUST keep every
envelope under that limit. The raw bytes in one frame's body or data MUST
NOT exceed 256 KiB, and larger bodies MUST be split:
- responses as
http_response_headandhttp_response_chunk; - request bodies as
http_requestwithbodyChunked: true, thenhttp_request_chunkframes ending withend: true; - WebSocket messages as
ws_messagefragments withmore: true.
A client MUST send a request body in chunks only to a daemon whose hello
advertises capabilities.httpRequestChunks. It MUST fragment WebSocket
messages only when ws_open.fragments or capabilities.wsFragments has
been negotiated.
5.6. Cancellation. When a fetch is aborted (its AbortSignal, a
cancelled body stream, a closed page), the worker MUST send
http_cancel{reqId}. The daemon MUST then abort the upstream request and
stop sending frames for that reqId. Any frames that are still in flight
MUST be dropped by the client.
5.7. Liveness. The worker SHOULD reconnect with jittered, capped exponential backoff. When the channel drops, pending requests MUST fail as network errors, and new requests SHOULD wait a bounded time for a connection. When a browser stops an idle worker, the next fetch event MUST build a new client from the stored credential.
5.8. Origin isolation. Script in a daemon's UI runs on the origin of its
worker's scope. On a single origin (for example
https://cli.agentproto.sh/d/<id>/…), that origin also holds the pair page
and every other pairing's credential. Script in any paired daemon's UI (an
XSS in rendered agent output, or a malicious daemon) can then open IndexedDB
and use the non-extractable pair roots of other pairings to connect to
those daemons. It cannot export the keys, but it does not need to.
Per-daemon origin (SHOULD). Implementations SHOULD serve each daemon from its own origin, whose first DNS label is the daemon fingerprint:
https://<daemon-fingerprint>.agentproto.cloud/pair#<offer>The origin is chosen per daemon, not per pairing. The offer already carries the fingerprint, so the daemon can build the link when it mints the offer (§1.2). A browser holds at most one credential per daemon (§4.1), so on that device one daemon's origin holds exactly one credential. In this mode:
- the origin's first label MUST match
^[0-9a-f]{32}$. A deployment MUST NOT serve the pair page on the apex or on any other host; - the pair page MUST refuse an offer whose
iddoes not equal its origin's first label, before any network I/O; - the origin's credential store MUST hold only that daemon's credential. The page and its worker MUST NOT store or use a credential for any other fingerprint;
- the pair-page domain SHOULD be a dedicated registrable domain (an eTLD+1
such as
agentproto.cloud) that hosts nothing else. It then shares no cookie scope and no site with other services.
Daemon UI then never shares an origin with another daemon's credential. An XSS in one daemon's UI reaches only that daemon, as it would without pairing.
Shared origin (fallback). A deployment MAY serve every daemon from one
origin under /d/<fingerprint>/. A shared-origin deployment MUST document
the cross-pairing exposure to its users.
6. Revocation
6.1. When a pairing is revoked, the daemon MUST stop serving it at once. It
MUST send one E2E error frame with code: "pairing_revoked" on any live
channel for that pairing, and then close.
6.2. The daemon SHOULD keep a tombstone for a grace window (the reference default is 14 days). During that window it keeps parking on the pairing's epoch routes. The tombstone:
- MUST NOT keep the pair root;
- MUST keep only the per-epoch
{route, auth}pairs covering the grace window, derived when the pairing is revoked; - MUST verify the sealed auth token (§2.3) against that epoch's stored auth
before it answers. Only then does it complete the daemon-signed handshake
and send
error{code: "pairing_revoked"}as the first and only frame. It never sendshelloand serves nothing; - on an auth failure, MUST behave exactly as for any bad auth (§2.3): refuse
the hello and send no
pairing_revoked.
So a party that knows only the route (the broker) cannot get a signed
pairing_revoked.
6.3. A client MUST treat pairing_revoked as terminal only if it arrives as
an authenticated frame inside the E2E channel, after a handshake that
verified against the pinned sk. The client MUST then stop all reconnect
attempts, with no retry loop. It MUST delete or mark the credential, and
MUST tell the user that the device was unpaired and must be paired again
from a new offer. Broker closes, timeouts and failed handshakes MUST be
treated as "offline" (retry with backoff), never as revoked.
6.4. When the grace window ends, a revoked client can no longer tell the two cases apart and sees the daemon as offline.
7. Versioning and legacy
7.1. The offer version (v in the URL), the handshake version (v in the
hello and reply) and the HKDF info label agentproto/pair/v2 MUST all
change together whenever the handshake or the token schedule changes.
7.2. pair/v1 used one token as both route and proof, which let a broker
authenticate as a client. It MUST NOT be accepted:
- A client given a
v=1offer MUST fail withpairing_protocol_outdatedand tell the user to mint a new offer. - A daemon MUST NOT serve a
pair/v1pairing. When a v1 client reconnects (the epoch route is unchanged from v1), the daemon MAY complete the v1 handshake only to send a re-pair notice inside the encrypted channel, and MUST then close. It MUST NOT check a token, and MUST NOT serve anything. - Stored v1 credentials MUST be treated as legacy: never dialed, never served, never upgraded in place. The only remedy is to pair again.
Threat model
| Adversary | Capability | Outcome |
|---|---|---|
| Broker operator (or anyone logging its upgrade URLs) | reads, drops, delays, reorders and replays messages; learns every route; can dial any route and seal a hello to pk | Learns route tokens, both peers' IP addresses, message sizes and timing, and when a pairing is in use. Cannot read, inject or alter frames (§3.5), cannot pair or reconnect (it holds no auth token, §2), and can neither forge nor elicit pairing_revoked (§6.2, §6.3). Can deny service. Epoch routes rotate daily, which limits linking a pairing's sessions across days. |
| Network attacker (between a peer and the broker) | the same as the broker, over TLS-stripped or ws: links | The same as the broker. wss: SHOULD be used. |
| Offer-URL thief, before expiry and first use | pairs as a new client | Mitigated by the short TTL, the single use, the fingerprint and name shown to the operator, and pair revoke. After the legitimate client has paired, the offer is spent. |
| Swapped QR (evil daemon) | substitutes its own pk/sk/id | The user confirms a fingerprint they have never seen; the keys are pinned after the first pair. id = fingerprint(pk) catches a partial swap. |
| Malicious or compromised pair-page origin | serves script to the pair page and the service worker | Trusted: it can read the fragment, the credential and all plaintext, and can act as the client. The origin is a trust anchor equal to the device. Self-hosting the pair page removes the third party (§4.4). |
| XSS on the pair origin | runs script in the origin | The same as a malicious origin for as long as the XSS lasts. A non-extractable pair root (§4.3) prevents exfiltrating the secret, but not using it while the XSS lasts. |
| Script in a paired daemon's UI (an XSS in rendered agent output, or a malicious daemon) | runs script on the origin of its worker's scope | On a shared-origin deployment, this origin also holds the pair page and every pairing's credential. The script can use other pairings' non-extractable pair roots to connect to other paired daemons: a UI bug no longer reaches only the daemon that served it. Mitigated by per-daemon origins (§5.8, SHOULD), which confine it to the daemon that served it. A shared-origin deployment MUST document the exposure. |
| Stolen or unlocked device | uses the stored credential | Acts as the client until pair revoke. Revocation then reaches the device authenticated (§6). |
Out of scope: post-compromise security, multi-device credential sync, and broker federation.
Rationale
- Fragment, not query. A query reaches the pair page's server and its logs. A fragment never does. §1.3 removes it from history so that tab restore or a shared screenshot cannot reveal it.
- Route/auth split. A single token used both as the meeting point and as the proof (pair/v1) is a bearer credential that the broker sees in the clear. HKDF is one-way, so a published route reveals nothing about its auth token.
- Service worker on the pair origin, UI from the daemon. The daemon UI's
REST,
/mcpand SSE calls are same-origin relative fetches. A service worker lets the UI run unchanged, and it always matches the daemon's version. Serving that UI from the pair site would make the pair site a code-supply point for every daemon. The cost is that the UI runs on the credential's origin, hence §5.8's per-daemon origins. - One connection per credential. The broker allows one live socket per
route (
token in use), and the daemon parks a fixed number of channels per pairing. Per-tab channels would fight over them. - Authenticated revocation. Without it, a revoked client cannot distinguish "unpaired" from "offline" and retries forever. Accepting an unauthenticated signal would let the broker unpair anyone.
- Alternatives rejected. A public tunnel (with TLS terminated at the intermediary, and no SSE on quick tunnels). WebRTC data channels (these need TURN, which is another relay, and a larger stack). Storing credentials in cookies (these are sent to a server).
Implementation notes
These notes are informative. They describe optional host features and do not change what a conforming host or client MUST do.
- Local-device credential (optional host feature). A host MAY let a client
on the same machine, which has no camera and no browser, authenticate with a
device bearer minted by the host instead of scanning an offer. The host
mints the bearer on request, records the device next to its pairings, and
lists it like any pairing. The bearer is opaque to the client. The host
MUST verify it in constant time, MUST NOT log it, and MUST store only what
it needs to re-derive it, not the bearer itself. Revoking the device MUST
make the very next verification fail, exactly as revoking a pairing stops it
being served (§6.1). A local-device bearer is accepted only on the host's
loopback interface. It never travels through a broker and is not a
substitute for the
pair/v2handshake on any remote path. - The reference host library is
@agentproto/pairing-host. It stores local devices in an optionallocalDevicesfield of itspairings.json, so files written before the feature load unchanged.
Backwards Compatibility
This AIP has no replaces. It introduces pair/v2, which is deliberately
incompatible with pair/v1 (§7): v1 offers and credentials are refused and
must be re-paired. The tunnel frame additions (http_cancel,
http_request_chunk, bodyChunked, ws_message.more) are gated by
capabilities that the daemon advertises in hello. A peer that lacks them
receives whole frames and keeps working within the frame bound.
Security Considerations
- The pair page origin is a trust anchor (see the threat model). It SHOULD serve a strict Content-Security-Policy, SHOULD NOT load third-party script, and SHOULD be served over HTTPS with HSTS. Operators who do not trust the default pair page SHOULD self-host it.
- Cross-pairing exposure. Under the service-worker model, the daemon UI runs on the same origin as its worker's credential store. On a single origin, script in any paired daemon's UI can use every other pairing's credential to reach those daemons. Before, an XSS in the daemon UI reached only the daemon that served it. Implementations SHOULD give each daemon its own origin on a dedicated registrable domain. That origin MUST refuse offers for other fingerprints and MUST hold only its own daemon's credential. A shared-origin deployment MUST document the exposure (§5.8).
- The pair page MUST NOT log, persist or post the fragment, and MUST clear it before it does any network I/O (§1.3).
- The fingerprint is 128 bits because it selects the browser origin (§5.8). At 64 bits, a well-resourced attacker could grind a daemon key whose fingerprint collides with a victim's, and so get onto that daemon's origin.
- Every token comparison on the daemon MUST be constant-time.
- A client MUST NOT reuse an
(n, key)pair. Keys are per-connection, so every reconnect runs a fresh handshake. - Size limits are a correctness issue as well as a denial-of-service surface. A sender that ignores §5.5 gets its channel closed by the broker, and every multiplexed request fails.
- A tombstone (§6.2) holds per-epoch auth tokens for the grace window. They let a thief answer only as that revoked pairing, and only until the window ends. They cannot derive later epochs or serve anything, and the tombstone is dropped once the window ends.
pairFromOfferfinishes the handshake, and so creates the daemon-side pairing record, before the user confirms. If the user declines at §3.6, the daemon still holds a record, which the operator must revoke.
Open issues
- Service-worker lifetime. Browsers may stop a service worker while it
serves a long SSE response (Safari limits a worker's lifetime), which
drops the stream and the shared tunnel. Clients recover by reconnecting
(§5.7), but events emitted during the gap are lost unless the daemon's
stream supports resumption (
Last-Event-ID). - No post-compromise security. There is no ratchet. A stolen pair root lets its holder derive future epoch tokens until revocation, and a stolen session key exposes that connection.
- Declined confirmation leaves a daemon record (see Security
Considerations). A future revision may add an in-channel
pair_confirmstep before the daemon persists the pairing. - Traffic analysis. Frame sizes and timing are visible to the broker. Padding is not specified.
- Reference deployment. The reference deployment is moving to
per-daemon origins on
agentproto.cloud(§5.8). Until it ships, cli.agentproto.sh remains the documented shared-origin fallback. The decided design:- A Cloudflare Worker (route
*.agentproto.cloud/*, proxied wildcard DNS) serves one static bundle, identical on every<fp>.agentproto.cloud. - The Worker MUST answer 404 on the apex and on any first label that does
not match
^[0-9a-f]{32}$. - Its responses MUST carry a strict Content-Security-Policy and
Cross-Origin-Opener-Policy: same-origin, and MUST load no third-party script. - These headers cover only the static bundle. Daemon UI proxied by the service worker carries the daemon's own response headers.
- A Cloudflare Worker (route
Reference Implementation
@agentproto/secrets(packages/secrets/src/pairing/):handshake.ts(pair/v2),derive.ts(the route/auth split and the pair root), andoffer-url.ts(the offer codec). See agentproto/ts#1448.@agentproto/pair-client:inspectOffer,pairFromOffer,connect→TunnelClient.fetch, andcreateIndexedDbCredentialStore. See agentproto/ts#1447.@agentproto/acp(packages/acp/src/tunnel/):wrapE2Eand the tunnel frame codec, withMAX_FRAME_PAYLOAD_BYTES= 256 KiB.@agentproto/rendezvous: the broker, with a defaultmaxMessageBytesof 1 MiB.- The pair page and service worker live in agentproto/cli-site (
/pair,/d/<id>/).
AIP-58: RUN — agentrun/v1 (run resource, state machine, workspace, event log, journal)
Names "the run" as a first-class resource shared by every AIP that executes something — a workflow step, an app run, a routine fire, a bare tool call. Fixes the run's fields, a state machine that always reaches a terminal state or a durable suspend, a deterministic outcome rule (an explicit request-input signal, never a text heuristic, is what suspends a step), a dedicated per-run workspace with an explicit publish step (never an implicit sync to a path two runs could share), an append-only event log as the one true status interface, a per-step journal enabling crash-resume and replay, and the transport-agnostic operations (run.create/get/list/events/cancel/resume/replay/requestInput/publish) a host exposes over MCP and HTTP.
AIP-60: SENTINEL: watch and notify (subjects, events, providers, and the inbox delivery contract)
Names the sentinel, a persistent watch over a subject (`github:owner/repo#42`, `github:owner/repo`, a `*`-prefixed prefix match) that delivers matching events into a session's AIP-46 inbox until a stop condition holds, as a first-class daemon primitive. Fixes the SentinelSpec (`match`, `until`, `target`, `provider`, `group`, `label`), the CloudEvents 1.0 SentinelEvent envelope with its subject hierarchy and `terminal` flag, the provider interface (capabilities, create/attach/cancel/status, poll/ack, parseInbound, readiness) and the three shipped providers (`local-gh`, `webhook`, `agentpush`) with their capability declarations and auto-selection order, the poll runtime (15s/60s adaptive cadence, persisted per-sentinel dedup, at-least-once delivery, dead-session resume and parking), the push ingress route `POST /inbound/sentinel-<hookKey>`, the delivery urgencies (`fyi`, `next-turn`, `steer`, `interrupt`), the MCP tool surface (`sentinel_watch`, `sentinel_list`, `sentinel_unwatch`, `sentinel_poll_now`, `list_sentinel_adapters`, `setup_sentinel_provider`), the daemon-HTTP `/sentinels` routes, the `agentproto sentinel` CLI, and PR auto-link.