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:
- Server on a VPS:
install.sh. Fine. - Mint a token: SSH to the host,
demarkus-token generate. Friction. - Deliver the raw token to the user: chat/email paste of a long-lived secret. The weak link.
/soul-join mark://host --token <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 <code>. 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:
install.shmints the owner token at install time and prints it once (the plugin's local-soul flow already does exactly this pattern).- Owner joins from their own machine with the owner token.
- Owner runs
demarkus invite -paths "/team/*" -ops publish -ttl 15m mark://host, gets a short code. - 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-invitecommand 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,
inviteop 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 --invitein the plugin, plus CLI parity (demarkus join --inviteor 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
inviteat 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/<code>), 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.