# Plan: Remote token issuance (invite flow) Status: draft, recorded 2026-07-16 (Fritz + Claude). Details the roadmap's "Remote Token Issuance: PLANNED" section; the decision between the two options below should land as an ADR before implementation. ## One-line Let a server owner onboard a new person or device to a remote soul or standalone world without SSH and without hand-carrying a long-lived raw token. ## The problem The goal is a low-barrier remote knowledge base. The onboarding chain today: 1. Server on a VPS: `install.sh`. Fine. 2. Mint a token: SSH to the host, `demarkus-token generate`. Friction. 3. Deliver the raw token to the user: chat/email paste of a long-lived secret. The weak link. 4. `/soul-join mark://host --token `. Fine. `/soul-join` consumes a token but cannot mint one (the server's tokens.toml is not local to the plugin). The broker solves issuance for k8s universes (Phase 6.3: OIDC, mints scoped tokens, writes hashes to Secrets) but is far too heavy for a personal or small-team standalone server. The gap is the middle tier: one owner, a few collaborators or devices, no k8s, no OIDC. ## Shared shape: one-shot invites Both options use the same primitive. An invite is a short-lived, single-use code that redeems for a freshly minted scoped capability token: - Owner creates an invite with a label, path globs, ops, and a TTL (default minutes, not days). - New user redeems it once: `/soul-join mark://host --invite `. The join flow receives the raw token, writes it to the 0600 token file as today, and the invite burns. - Expired or already-redeemed codes fail closed. Every create and redeem is audit-logged with labels. - Nothing secret in transit outlives the TTL; the raw token is generated at redemption and travels once, over the QUIC session doing the redeem. The options differ only in where invite creation runs. ## Option A: invite minted on the host (offline, zero core change) `demarkus-token invite` is a new subcommand beside `generate`: writes a pending-invite row (hashed code, scope, expiry) to tokens.toml; the server's existing file watcher hot-reloads it. Redemption is the only new server surface: a redeem operation that exchanges a valid code for a new token, appends the token hash, and marks the invite spent. - Pro: invite creation stays offline, matching the current trust model exactly (only someone with shell access to tokens.toml can create credentials). - Pro: smallest possible server delta (redeem only). - Con: the owner still SSHes per invite. The handoff is fixed (short-lived code instead of long-lived secret) but the barrier is only half-removed. For the person running one server for themselves plus a phone and a laptop, step 2 friction remains. ## Option B: invite minted over the protocol (capability delegation) Tokens gain an `invite` op. A token holding it can create invites over the wire, from any joined client, constrained by attenuation: an invite may only grant a subset of what the inviting token holds (paths within its globs, ops within its set, never `invite` itself unless explicitly re-granted). The owner never SSHes after install: 1. `install.sh` mints the owner token at install time and prints it once (the plugin's local-soul flow already does exactly this pattern). 2. Owner joins from their own machine with the owner token. 3. Owner runs `demarkus invite -paths "/team/*" -ops publish -ttl 15m mark://host`, gets a short code. 4. New user redeems via `/soul-join --invite`. This makes issuance itself a capability, which is the natural extension of the existing model: delegation with attenuation is the classic capability pattern, no accounts and no identity provider. - Pro: actually delivers low barrier; the whole lifecycle runs over the protocol. - Pro: composes with the plugin cleanly (a `/soul-invite` command needs nothing but the joined MCP server). - Con: breaks the standing constraint that the core server never writes its own tokens file. The server learns invite-create and invite-redeem operations and appends to tokens.toml (or a sibling invites file). Contained (append-only pending rows, existing audit log, existing hot-reload) but it is a real expansion of the write surface on a component deliberately kept out of the issuance business. - Con: incompatible with the read-only chroot deployment by construction. Acceptable: that mode is explicitly single-publisher, and invites there fall back to Option A's offline path or plain `generate`. - Con: a stolen owner token now mints credentials, not just content. Mitigations: attenuation cap, TTL ceiling on invites, `invite` op never granted by default, audit rows for every create/redeem, revocation unchanged (drop the label, reload). ## What both options require regardless - Redeem surface on the server (the one unavoidable core change): a verb or well-known endpoint that runs pre-auth (the redeemer has no token yet), rate-limited like everything else, constant-time code compare, hashed codes at rest. - tokens.toml schema addition for pending invites (hashed code, scope, expiry, spent marker) that old servers ignore gracefully. - `/soul-join --invite` in the plugin, plus CLI parity (`demarkus join --invite` or equivalent). - Docs: the security page's threat model gains an invites section. ## Comparison | | A: host-minted | B: protocol-minted | |---|---|---| | Core server delta | redeem only | redeem + create + `invite` op | | Owner UX per invite | SSH | one command from anywhere | | Trust model change | none | issuance becomes a delegable capability | | Read-only chroot | works (offline create) | create unavailable, redeem N/A | | Fits "low barrier" goal | partially | fully | ## Recommendation B, scoped tightly: `invite` op, attenuation enforced, redeem pre-auth endpoint, append-only writes, audit everything. A is not a stepping stone to B (its offline-create path survives inside B unchanged, since `demarkus-token invite` on the host is just create with a local writer), so building A first costs nothing but delivers little. The deciding question for the ADR: is "the server never writes credentials" a load-bearing security property or an artifact of history? The read-only mode already answers how to keep the hard-line posture available: run that mode and issuance stays fully offline. ## Open questions - Redeem transport: new protocol verb (spec change, CC0 protocol surface) vs a well-known reserved path handled outside the verb table. A verb is honest; a magic path is expedient. - Should redeemed tokens be able to carry `invite` at all (transitive delegation), or is one level of delegation the ceiling? - Invite code shape: short human-typeable code vs full URL (`mark://host/.well-known/invite/`), QR for phones. - Broker relationship: does the broker's minting core eventually reuse this (invite as the broker's non-OIDC fallback), or do they stay parallel? - Rate limiting and lockout on redeem attempts (code brute force) beyond the existing per-IP limiter.