Skip to content

A token acts as you, but need not be all of you - #161

Open
WaylandYang wants to merge 1 commit into
devfrom
feat/mcp
Open

A token acts as you, but need not be all of you#161
WaylandYang wants to merge 1 commit into
devfrom
feat/mcp

Conversation

@WaylandYang

Copy link
Copy Markdown
Contributor

Decision record only — no code. Exposing Utopia's seven tools over MCP raises one question before transport, schemas, or anything else: what identity does a client connect as?

Why neither existing credential fits

Follows Stored Expiry Revocable
JWT a person stateless 7 days no
sources.ingest_token a source plaintext none rotate only

The JWT was designed for a browser session: short-lived, re-signed on every login, stateless because it never needed a table. An MCP client is long-lived, machine-driven, and configured in a file on someone else's laptop. Seven-day expiry means reconfiguring weekly; "not revocable" means a lost laptop is a wait, not an action.

A road not taken, recorded

The first proposal was a per-KB machine token. Rejected for two reasons, both written into the record:

  1. It adds a third authorization model. Workspace membership and KB roles already exist; a token with its own permissions means three tables to consult before answering "what can this agent see" — and every layer that can be misconfigured fails in the direction of granting too much.
  2. Attribution becomes fiction. audit_events.actor_id holds a real person today. A machine token would put a synthetic id there, creating a class of ledger rows that nobody did — in a ledger whose whole purpose is recording who accepted what, when.

The decision

effective permission = the person's role  ∩  this token's scope

An intersection. A token can only narrow, never widen: a viewer's token with write ticked is still read-only. Scope is a ceiling, not a grant.

Identity follows the person because every existing guard keeps working unchanged, attribution stays real, deactivating someone kills their tokens, and one key reaches every base they can already reach.

Scope still narrows separately because of a problem MCP has and in-app chat does not: the confused deputy. The client is someone else's agent with someone else's system prompt, and what it reads is the knowledge base's documents — untrusted content. A document saying "run this SQL" or "remember that X" may simply be obeyed, with the person's full privileges. In-app chat shares the exposure, but there Utopia owns the prompt and the loop.

And that person's full privileges are large: read-only SQL against every production database mounted in every base they belong to, plus writes into an append-only ledger — on a string sitting in plaintext in a config file.

Why this one is hashed when ingest_token is not

Not an oversight. The plaintext decision for ingest_token reasons that a compromised database has already leaked the documents, so hashing buys nothing. That holds because an ingest token only pushes documents in. A personal token reaches out through query_data, to a warehouse on another machine holding another set of data. Different blast radius, different storage.

One implementation rule, carried over

Validate scope at every tool entry, not once at handshake. This is the lesson from 0014_data_source_grants, quoted from its test: filtering the list only blocks what is visible, while the mount endpoint is called by id. The MCP shape of that mistake is trusting a connection for its whole lifetime; revoked_at written mid-session has to take effect on the next call.

Left open

Whether query_data and remember ship in the first version (leaning no — four read-only tools first), stdio versus streamable HTTP, and whether tokens should eventually scope by workspace rather than by base, which would start to resemble the data source grants table.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant