A token acts as you, but need not be all of you - #161
Open
WaylandYang wants to merge 1 commit into
Open
Conversation
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
sources.ingest_tokenThe 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:
audit_events.actor_idholds 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
An intersection. A token can only narrow, never widen: a viewer's token with
writeticked 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_tokenis notNot an oversight. The plaintext decision for
ingest_tokenreasons 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 throughquery_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_atwritten mid-session has to take effect on the next call.Left open
Whether
query_dataandremembership 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