# Per-Customer Sellable User-Token Minting

Use this runbook only from the VPS `sellable-admin` profile. A throwaway
`preflight_smoke` runs through `action: preflight` with the already-resolved
Sellable `workspaceId`, autonomously before the human checkpoint. A real customer
mint, status, or revoke runs through the already-authorized onboarding
lifecycle. This document defines the credential mechanism and does not
authorize unrelated customer mutations.

## Credential and isolation contract

- Mint a user-level Sellable API token with `workspaceId` bound to the exact
  customer workspace and dedicated scopes `hermes-customer-token` and
  `hermes-workspace:<workspaceId>`.
- Do not add the legacy literal `mcp` scope: that scope intentionally activates
  a narrow route allowlist and would block the customer profile's approved
  campaign, sender, lead-list, and enrichment tools.
- Product middleware rejects a missing, duplicate, or mismatched Hermes
  workspace scope and rejects any `x-workspaceid` that differs from the token's
  server-side `workspaceId`, even when the user belongs to both workspaces.
- Defense-in-depth workspace isolation also requires the pair
  `SELLABLE_LOCK_WORKSPACE_ID=<customerWorkspaceId>` and
  `SELLABLE_REQUIRE_WORKSPACE_LOCK=1` in the customer profile's plain Sellable
  MCP environment.
- Reject a lifecycle plan that omits either lock or names a different workspace
  than the server-bound credential.

## FORBIDDEN settings route

Do not use `POST /api/v3/integrations/claude-code/token` for per-customer
minting. In the product source at
`src/app/api/v3/integrations/claude-code/token/route.ts:113-182`, that settings
route takes a user-scoped advisory lock, finds the user's prior settings-owned
tokens, and sets `revokedAt` before and after creation. Reusing it for customer
B revokes sibling tokens needed by customer A and other profiles.

## Approved non-revoking path

Use only the public Sellable Admin MCP lifecycle tool:

```text
admin_hermes_onboarding_lifecycle
```

Before authorization, call it once with `action: preflight` and the verified
business `workspaceId`; the broker creates,
proves, and revokes a unique smoke credential in one call. After composition
plan succeeds, call it with `action: token`, the opaque
`lifecycleId`, and `mode: mint`, `status`, or `revoke`. The broker calls `POST`,
`GET`, and `DELETE` on `/api/v3/integrations/hermes/customer-tokens` through a
response-redacting adapter. Its low-level child request identity and credential
authorization are internally derived and never entered by an operator. The
internal action list records both `token-mint` and `token-revoke` authority.

Gmail, Browserbase, CLI magic links, local browsers, SMS, and user auth tickets
are not dependencies for Sellable customer-token minting.

The management route has a separate capability boundary:

- Before granting capability, run a fleet-wide bearer fingerprint comparison
  between the configured Sellable Admin bearer and every customer runtime
  config. A match fails the grant preflight: keep the shared row active but
  unscoped, create a dedicated management bearer stored only in the
  `sellable-admin` profile, and verify every customer fingerprint differs.
- The configured Sellable Admin bearer must then be the exact active,
  Admin-only token carrying `admin:hermes-customer-token-manage`.
- The route re-hashes and re-reads that bearer and verifies the owning user's
  admin metadata. It never trusts forwarded identity or scope headers.
- Customer-profile tokens never receive the management capability, even though
  they authenticate the same `admin@dittto.ai` user.
- Mint creates one managed Hermes customer token bound to the exact customer
  `workspaceId`. It
  never updates, deletes, or revokes a sibling token.
- The first exact request returns plaintext once to the private adapter. An
  exact duplicate returns metadata only; identity drift fails closed.

The public identity is immutable:

- The lifecycle authorization freezes `customerSlug`, company, team, users,
  invite policy, and model.
- The opaque `lifecycleId` is the only operator-visible handle after approval.
- The saved composition plan freezes workspace, channel, run, profile, model,
  token destination, packages, surfaces, and response policy.
- The broker persists its child audit records in its owned Admin home and
  supplies them privately to the low-level adapter. Operators never copy or
  reconstruct them.

If a response is ambiguous or the one-time plaintext is no longer recoverable,
the adapter reconciles status and revokes only the exact managed token. Treat
`token_plaintext_unrecoverable` and typed `token_mint_ambiguous_*` outcomes as
fail-closed retry signals; never mint blindly or substitute the forbidden
settings route.

Plan 05-02 proves this mechanism live with a throwaway smoke before any Delta
mutation. Its receipt must show `userActionRequired=false`, exact-token
disposal, locked-workspace authentication, and a sibling profile that remains
authenticated.

## Secret handling

1. The broker verifies the only allowed destination:
   `/opt/data/profiles/sellable-admin/secrets/<slug>.sellable-token`. Do not
   accept or pass an output-path override.
2. The adapter requires the parent directory to be owned mode `0700`, writes a
   unique mode-`0600` file with exclusive create, fsyncs it, atomically renames
   it, and verifies ownership, mode, readback, and fingerprint.
3. Record only `tokenId` plus a 16-hex token fingerprint in public receipts and
   registry metadata. This is fingerprint-only recording.
4. The lifecycle passes the saved token-file reference to composition; the
   operator does not coordinate a secret handoff.
5. Never place the raw token in argv, chat, public receipts, registry,
   `config-state.json`, evidence, or planning docs.
6. On a local write failure after mint, require exact-token compensation before
   retrying. On revoke, remove the local file only after the server confirms
   that exact managed token is revoked.

## Non-revocation verification

This non-revocation verification is mandatory for every new customer token.
After minting customer B's token, run a direct authenticated plain-Sellable MCP
probe from customer A's already-existing profile. Require customer A's locked
workspace auth and workspace read to remain green. Then probe customer B with
its own mandatory lock and require it to resolve only customer B's workspace.
If customer A fails after the mint, classify it as a sibling-token revocation
failure, stop before composition, and retain only sanitized fingerprints and
receipt references.
