Part V — Linked-device connections (the sidecar surface)
20. The model
Section titled “20. The model”Some providers authenticate a device session, not a request: there is no API key to paste and no OAuth redirect to run — a long-lived, stateful client holds session key material and speaks a proprietary transport (WhatsApp’s linked-device protocol is the reference case). Snug’s answer keeps every existing wall intact:
- A helper — a local, user-installed companion process — owns the device session: its key material, its transport, its sync state. The helper is LLM-free: transport and custody, never a second brain. Every analysis or compose turn runs in the governed host.
- The app reaches the helper only through the connected-fetch executor of §14, as an
ordinary
linked_deviceconnection — same grant, same confirm gate, same governance.
20.1 A helper is a capability, never a host
Section titled “20.1 A helper is a capability, never a host”The frozen ceiling is host-granular and has no port dimension, so 127.0.0.1 in a
ceiling would grant every loopback port on the machine — and a loopback host:port pair is
not even storable under the host grammar. A conforming host therefore MUST NOT admit a
loopback or localhost address into any ceiling for a helper. Instead:
- The helper listens on a unix-domain socket with
0600permissions (or an equivalent OS-scoped IPC endpoint). With no TCP endpoint there is no port to squat and no network path to filter: the filesystem decides who may connect. - The connection declares a symbolic host — a name under the RFC 6761-reserved
.localhostTLD (pattern<provider>.sidecar.localhost) that can never resolve publicly and is never dialled. The name exists so the connection has a stable identity the ceiling can hold: hosts are the ceiling’s unit, and a capability needs a host-shaped identity to be containable. The executor matches on it to route to the helper transport, and it stands down gate 5 (SSRF) only — every other gate applies, including gate 6’s confirm on every send (§14, gate 6a). - The symbolic host is single-homed in the protocol contract — a second spelling once sent an app’s reads to a real DNS resolver, which is the failure this rule exists to prevent.
20.2 Credential custody: the split
Section titled “20.2 Credential custody: the split”Two secrets exist and have opposite exposure rules:
- Provider session key material (the keys to the user’s account) lives ONLY in the helper’s own store and is serialized on no route — not scrubbed, not redacted: never in a response in the first place. Compromising everything Snug stores yields a key to a helper, not to the account.
- The helper access token (the key to this helper) is minted exactly once at
pairing — ≥256 bits CSPRNG; a second mint is refused — crosses the wire exactly once on
the releasing pairing poll, and lands in
snug_secretsas the connection’s declared credential field, injected thereafter by the executor like any credential.
Pairing routes are guarded by a spawn nonce: a per-launch secret the host passes to the helper at spawn, required on every pairing/verify route. The pairing status route releases the access token, so it must not be reachable by an arbitrary local caller — “local” is not “trusted”.
20.3 The helper contract
Section titled “20.3 The helper contract”- A closed, enumerated route table, method-pinned, is the entire reachable surface. The app-reachable subset is derived by filter from the one table (wizard-only prefixes: the pairing routes and the session routes — status and the destructive forget/unlink), never a second hand-written list — two lists drift invisibly until an app reaches a route nobody intended.
- Every route requires a credential — the access token for app routes, the spawn nonce for wizard routes. No open route exists.
- Path admission checks the decoded form: percent-decode first (malformed encoding
refuses), then refuse any
./..segment, then match method + pattern anchored. Placeholder segments match exactly one non-empty segment. - Caps refuse, never truncate: request and response bytes are bounded (reference: 1 MiB, enforced while reading), and an over-cap resource is refused with a structured answer — a truncated payload is a corrupt payload.
- Where a native shell mediates the socket (desktop hosts), the shell’s route admission MUST be held equivalent to the protocol table by test.
20.4 Lifecycle
Section titled “20.4 Lifecycle”- One writer per session store. Exactly one helper instance may hold a device session:
the host reaps a stale helper before spawning (order is load-bearing — reaping after
leaves two live helpers fighting over one session), and the helper watches its own
parent and exits through its clean shutdown path when orphaned (signal: a changed
parent pid, not
ppid == 1— subreapers exist). - Termination is graceful-first (SIGTERM, bounded wait, then SIGKILL): the helper’s clean shutdown flushes durable state a hard kill would drop.
- A stale-process verdict must identify the process (command line naming this host’s helper entry), never trust a recorded pid alone — pids are recycled, and a bare number would kill a stranger.
- Durable helper state is quarantined on corruption, never treated as fresh truth; and deleting or forgetting the connection MUST sweep the helper’s session store and derived caches with it.
20.5 Live updates
Section titled “20.5 Live updates”Rule R7 (§5) was written for this surface: the host MAY run a pump that long-polls the
helper’s event route through the governed executor on the app’s behalf and forwards
hints ({jid, kind, ts}-shaped references, every field rebuilt by the host) over
snug:host-event. The app answers a hint with its own governed reads. Media bytes stop at
the app frame: memory-only, never written to the app’s database, never into an LLM-bound
payload.
The two-population rule. Read-on-behalf eligibility (the pump) keys on an approved grant whose ceiling holds the symbolic host. The pseudonymisation population (§20.6) keys on the connection fact in any status — approved, declared-by-import, or revoked with data left behind. These are different predicates by design and MUST NOT be conflated.
20.6 The pseudonymisation backstop
Section titled “20.6 The pseudonymisation backstop”A linked-device connection is the one place Snug routinely handles other people’s data: thread participants never consented, are not Snug users, and cannot opt out. Under BYOK their message content reaches the user’s configured model provider — that is the feature — but their identities need not. A conforming host that mediates linked-device data to a model MUST enforce an egress backstop, host-side, that does not rely on app-layer redaction:
- Harvest at the governed seat. Third-party identities (display names, provider ids) are extracted from directory-shaped helper responses at the one seat every governed read crosses. Extraction is the scrub: only the identity fields enter the directory; message text and unknown fields never do — a directory that ingested content would itself become the leak.
- Redact on every LLM-bound surface of any app holding a sidecar connection fact, in
any status: the app-message envelope (all string values and object keys; response-schema
values case-sensitively with keys and
requireduntouched; envelope ids verbatim — a disclosed residual channel) inside every leaf transport, and provider-lane results classified by the canonical connection-URL grammar with the executor’s own normalization (hand-rolled classifiers have already been beaten by case and whitespace). Provider-id patterns and dialable digit runs (≥7 digits) are redacted as primitives. - Fail closed. An unreadable directory refuses the send; a scrub failure on the provider lane surfaces as a tool error, never the raw body; a malformed envelope is scrubbed as a raw string with an unescape-normalized shadow pass.
- Lifecycle. The directory is third-party PII in its own right: it is wiped — in the
same transaction — when the last approved sidecar connection is revoked or its app
deleted; it deliberately survives import (the demoted-to-
declaredrows travel with the replayable app data the scrub exists for); and the in-memory session copy resets on import/restore/revoke/delete, so one user file’s contacts are never written into another’s.
Honest statement of class (normative to repeat wherever this is claimed): the backstop is anti-default and anti-naive, not anti-adversarial. Disclosed residuals: an app that obfuscates (homoglyphs, base64, numbers smuggled as JSON numerics) defeats substring redaction; identities never surfaced through the helper seam are invisible to the directory; rows an app persisted in its own tables can reach a model through the data lane; and message content itself reaches the provider by design. Third-party consent remains a real residual, disclosed to the user before linking.
20.7 Disclosure obligations
Section titled “20.7 Disclosure obligations”Before a linked-device connection is created, a conforming host discloses: any provider-ToS risk (unofficial automation can violate terms and cost the account — pacing is harm reduction, never a guarantee and never detection evasion), the third-party-consent residual of §20.6, and — wherever the connection is disclosed — any standing approval (§17) currently armed on it.
20.8 Reference binding (non-normative)
Section titled “20.8 Reference binding (non-normative)”The reference implementation binds this surface to WhatsApp: a Node helper owning a
linked-device session; symbolic host whatsapp.sidecar.localhost; a 12-route table
(pair/start, pair/qr, pair/status, session/status, session/forget wizard-only; chats,
per-chat history/messages GET+POST, events, media, picture app-reachable —
POST /session/forget is the deep-delete unlink: nonce-guarded provider logout plus
auth-store erasure behind a persist tombstone, added 2026-08-21 ahead of the next
consolidated push); a device-link pairing family entry in the registry; and the macOS
desktop shell mediating the socket with a Rust-side admission table held equivalent by a
source-parsing test. None of those specifics are normative; §20.1–§20.7 are.