ARGOS MCP · Customer auth flow

One token, in two forms.

A bearer token gets generated exactly once. We store the hash of it in admin.bearers. We email the plaintext of the same token to the customer. They're the same token, in two forms — one persistent and one ephemeral. The confusion about "sending a different token" is really about which form goes where.

The mental model

Two forms of the same token, one direction between them.

The hash function is one-way. Given the plaintext, you can compute the hash in microseconds. Given only the hash, you cannot recover the plaintext. That asymmetry is why the customer can hold something we don't.

Plaintext form Ephemeral
argos_SAMPLE_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8
  • Shown to Adrian once, in a modal at generation time
  • Emailed to the customer
  • Lives only in the customer's MCP client config
  • We never write it to disk, log it, or store it in any DB
Hash form Persistent
a3f8c9e0d7bc4b2f8912e04d55c17ab26f0c4c3a1e2ba9d3f47cd0a86b512d95
  • Stored in admin.bearers.bearer_hash
  • Persists as long as the customer exists
  • Cannot be reversed to recover the plaintext
  • Used at request time to look up who's calling
Flow · Onboarding a new customer

Adding analyst@bigfund.com to SEDAR + Funding.

Three actors, seven load-bearing moments. The atomic thing to notice: at step 4 we insert the hash and at step 5 we display the plaintext — from the same generation, in the same request. After the modal closes, no one on our side has the plaintext.

Phase A · Adrian in the admin UI
  1. AdrianAdd customer form
    Enters analyst@bigfund.com, ticks [x] sedar and [x] funding, adds label "prod - laptop", clicks Generate & Save.
  2. UI backendClient-server round trip
    Generates a random plaintext token: plaintext argos_SAMPLE_a1b2c3d4e5… (43 chars, 256 bits of entropy).
  3. UI backendHash computation
    Computes sha256(plaintext)hash a3f8c9e0d7bc4b2f…
  4. UI backendadmin.bearers row
    Inserts the hash, not the plaintext.
    INSERT INTO admin.bearers (bearer_hash, email, verticals, label, created_by) VALUES ('a3f8c9e0d7bc4b2f…', 'analyst@bigfund.com', ARRAY['sedar','funding'], 'prod - laptop', 'adrian@kscope.io');
  5. UICopy-once modal
    Displays the plaintext in a "Copy now — you won't see it again" dialog.
  6. AdrianModal
    Copies the plaintext, closes the modal. From this moment forward the plaintext is unrecoverable from anywhere on our infrastructure.
Phase B · Handoff
  1. AdrianEmail
    Pastes the plaintext token and the vertical URLs (mcp-sedar.kscope.io/mcp/, mcp-funding.kscope.io/mcp/) into an email to the customer.
Phase C · Customer
  1. CustomerMCP client config
    Pastes the plaintext into their MCP client (Claude Code, claude.ai, Cursor…) as Authorization: Bearer argos_SAMPLE_a1b2c3d4…. Their client now holds the only copy of the plaintext form.
Flow · Every request

What happens when the customer's client calls the server.

The server never remembers plaintexts. It hashes the incoming token, looks up the row, and grants or denies. Sub-millisecond warm.

01 Customer's client sends Authorization: Bearer plaintext
02 Server computes sha256(incoming)hash
03 Looks up admin.bearers WHERE bearer_hash = ?
04 Checks revoked_at IS NULL AND the current vertical is in verticals[]
05 Serves the tool call. Audit log records the resolved email. 200 grant
Flow · Rotation and revocation

Customer loses the token, or a contract ends.

Rotation is per-customer. Nothing about revoking one customer's bearer affects any other customer.

01 Adrian clicks Revoke. UI sets revoked_at = now() on the row. bearer dead
02 Within 30 seconds the customer's next request returns 401. (30 s = cache TTL on our read path.)
03 Adrian generates a new bearer for the same customer via the same onboarding flow above. new bearer
04 Adrian emails the new plaintext to the customer. Customer swaps their config. Done.
Direct answers

The two questions that keep coming up.

Do we update the new table?
Yes

Every generation inserts one row into admin.bearers. Every revocation updates that row's revoked_at.

Do we email a different token than we stored?
No

Same token. We store its hash. We email its plaintext. Never the other way around, never a different value.

Pilot state vs steady state

Where we are on the way to per-customer bearers.

Adrian's admin UI is still in flight. Until it ships, we're on a manageable pilot pattern.

Right now (pilot)

Shared beta bearers.

  • One shared bearer per vertical, stashed in /etc/argos-mcp-<vertical>/env
  • Raul emails the same plaintext to any pilot customer for that vertical
  • Revoke = rotate the shared bearer, re-email everyone still active
  • Fine at 1–3 customers per vertical, gets ugly past that
After Adrian ships

Per-customer bearers.

  • Every customer gets their own bearer via the flow above
  • Revocation is per-customer; audit log resolves calls to the specific email
  • Per-vertical entitlements handled by verticals[], not by URL
  • The shared beta bearers get retired as each pilot customer swaps