magus v0.4.3 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
8 min read

Tokens and grants

Every server route except the health probes and the console's app shell needs a bearer token. This page is the model behind that: what a token may do, how a route says what it needs, and why no token can mint a wider one.

Grants

A grant is one level per surface. Levels are ordered: none < read < write.

Surface Levels Reaches
tokens none, write token management (the TokenService)
mcp none, write the /mcp endpoint
console none, read, write the console's read routes, and its writes

tokens=read and mcp=read mean nothing, so a grant naming either is refused. A grant renders as mcp=write or console=read; the one below holds everything.

Preset Grant Held by
operator tokens=write,mcp=write,console=write the operator token
connector mcp=write an MCP client
console console=write a browser tab, a console link
viewer console=read a second screen that only looks
share console=read a share link, on its own listener

Needs

Every Connect procedure and every /api/ route declares the level it needs on one surface, and the server's bearer guard compares that with the presented token's grant. It is the only place magus decides whether a token may use a route. The server refuses to start if a procedure has no need, or a need is none or names a level its surface lacks, and a server whose workspace failed to load holds every route to the same needs.

Route Needs
/mcp mcp=write
TokenService, every procedure tokens=write
JobService RunJob, every MemoryService procedure console=write
/api/v1/diff and its sub-routes, /api/v1/plan, /api/v1/attention, POST /api/v1/share console=write
every other procedure: Activity, Graph, Insight, Status, Tool, Notes, Metrics, Viewer, JobService ListJobs console=read
/api/v1/events, /api/v1/insight, /api/v1/graph console=read

Memory reads need console=write because the notes are the operator's own and reading them is audited like an edit. The diff, plan and attention routes need it because they serve unreviewed source, every target's name, or an action.

A request with no token gets 401 MGS9011. A token that is wrong, expired, revoked, or of a class this listener does not accept gets 401 MGS9001, which never says which. A valid token whose grant is below the route's need gets 403 MGS9015, naming the need.

Each MCP tool call is held to mcp=write again, on either transport, and a call below it answers MGS9015 as a tool error. Over HTTP that is the credential the guard admitted to /mcp. magus mcp serves over stdio with no token: the caller is the process the host launched as you, so every call carries the stdio credential, which holds mcp=write and nothing past it.

A need is checked for the whole life of a request, not only at its start. While a stream is open the guard checks its token again every few seconds, and a token that was revoked or expired ends the stream. A share link that closes (revoked, superseded, expired) cancels every request on it at once and cuts any that do not stop within a few seconds.

Classes

A token's class is its prefix, so the server knows which store can hold it before it hashes anything:

Prefix Class Lives Expires
mgo_ operator $XDG_STATE_HOME/magus/mcp_token, 0600 never; rotate it with magus config token generate --force
mgs_ stored $XDG_STATE_HOME/magus/tokens.d/<name>.json, only its SHA-256 90 days by default, at most 366
mgl_ share the server's memory 15 minutes by default, at most 24 hours
mgx_ exchange tokens.d, only its SHA-256 one minute, and spent on first use

Every class has one layout: the prefix, 43 base62 characters of randomness, and a 6-character CRC32 of those, so a typo fails before any lookup. A secret scanner finds all four with mg[oslx]_[0-9A-Za-z]{49}.

The loopback server checks an mgo_ token against the operator file alone and an mgs_ token against the store alone, and refuses mgl_ and mgx_ outright: an exchange code is never a bearer anywhere. A share link's listener accepts its own mgl_ token and nothing else. The operator token is refused from any peer that is not loopback, judged by the TCP peer address, so even a server bound past loopback with mcp.insecure_bind serves only stored tokens to the network.

One credential has no token and no prefix: socket-peer, the caller on the server's unix socket. The kernel names the uid of the process on the other end of each connection, and the server admits only its own, with mcp=write and console=write. It never holds tokens=write: a build step runs as the same user and can reach the socket, and a token it minted would outlive the run. Any other peer gets 403 MGS9022. The trail records its calls with class socket-peer, a grant, and no id or name.

The store holds every record to the rules a mint follows, at load: a record that holds tokens=write, expires more than 366 days after its creation, was created in the future, or names another file, is skipped with MGS9019. It verifies nothing and never disables the records beside it, and magus doctor fails naming it.

Minting

A token is never granted more than its minter holds. That is one check, auth.Store.Mint, and it applies at every door:

  • The CLI mints as the operator, since the shell is the user: magus config mcp connector create mints mcp=write, and magus config console token create [--viewer] mints console=write or console=read. No stored token holds tokens=write, not even one the operator mints (MGS9021).
  • The TokenService mints with the grant of the token that called it, and mints console grants only: a browser has no business minting an MCP token. A revoke there is held to the caller's grant the same way.
  • POST /api/v1/share mints a share link only for a caller holding at least console=read.

Every stored token expires. --expires never is refused, and so is a lifetime past 366 days, or a share link past 24 hours: MGS9018, and nothing is shortened to fit. magus doctor names any token that expires within 14 days, and an expired token's file is deleted the next time the store is listed or minted into.

Both token commands list the whole store, with each token's class and grant, and revoke by an exact id (eight hex digits) or an exact name, never a prefix. No name may look like an id, so one never reads as the other.

A link magus prints for you to open in a browser carries a one-time code, never a token:

open "http://127.0.0.1:7391/console/#code=$(magus config console token create --code --expires 12h)"

The console trades the code at POST /api/v1/token/exchange for a console token living 12 hours, and scrubs it from the address bar. A code lives one minute and works once, so the opener's argv, a shell history or a screen share holds nothing worth taking. The browser then holds a token that cannot reach token management, so a script injected into the console cannot mint anything. A console handed the operator token itself trades it for a console token at once, and says so loudly when it cannot.

Agents

Every credential verb is denied to an agent session (rule credential-verb): the console and connector token create and revoke commands, magus graph export --open --follow (its link carries a code), and magus config token print, generate and revoke, however the binary is spelled. The operator file and tokens.d are denied too (rule token-state), to an editor write and to any shell line that names them. An agent holds the token it was given.

Trust model

The guard is a seatbelt, not a boundary against a process running as you. That process can read the operator file, write tokens.d, or run the CLI outside any harness, and nothing magus does changes that; there is no keychain. What magus does is enforce every layer it can prove: a record in tokens.d cannot grant more than a mint could, a token never mints one wider than itself, an open stream ends when its token does, the operator token never leaves loopback, and every mint is recorded. On Linux, landlock keeps a target from reading the operator file; elsewhere nothing does, and magus doctor says so.

What the trail records

Every record made under a server request carries the credential that request presented: its class, its id (the first 8 hex of its SHA-256), the name it was minted under, and its grant at the time. Never the secret. A magus mcp tool call carries class stdio with its grant, and no id or name. The id is the identity: revoke laptop and mint a new laptop, and records made under each name a different id. An activity filter matches the class, the id or the name.

Every mint is recorded, whichever door made it: the CLI (cli.mint, cli.generate, link.code), a share (share.mint), a link code redeemed (link.redeem), and the TokenService (CreateToken). Each names the minted token's class, id, name and grant, and the credential that minted it: token 3fa9c1d2 (laptop) console=write until 2026-12-22. A CLI mint outside any workspace has no trail to land in, and says so.

Upgrading from an older magus

There is no migration. An operator file that is not mgo_ is refused with MGS9016; re-issue it with magus config token generate --force. Any file left in the retired connectors.d (or connectors.json) makes the token store fail with MGS9017, naming each file; remove them and mint replacements. The operator token keeps working meanwhile, since it never opens the store.

tokensgrantsauthbeareroperatorconnectorconsolesharemcpsecurity
Last updated (a43ac9db)
Earlier changes on this page (4)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). See operations.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. See spells.

Service

A long-running or shared process magus manages across runs, distinct from a one-shot target. See services.

Server

The background process a person starts with magus server start. It serves MCP, the console, background jobs and the warm knowledge graph, and adopts nested magus calls into one pool. See server.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Health

The at-a-glance server state derived from the pool: healthy when the pool is reporting, degraded when it reports an error, down when there is no pool. The dashboard color-codes each state. See server.

Insight

The reports magus derives over the graph and history (hotspots, affinity, ownership, trend, volatility, unreferenced). See insight.

Session

An agent host's conversation, by the id the host delivers to its hooks. magus never mints one: a record with no session is unattributed, and the OS user it carries says whose account ran it.

Job

The unit of delegated work, and one row of the job store: what an orchestrating agent handed out, with its goal, the checkpoint it was cut against, the paths it may write or must not touch, and the one check it runs. A job's holder is either a session, for work an orchestrator handed out, or the server, for its own maintenance. The store records; the agent guard is what reads those facts back when grading a write. See doctrine.

A job is not a run. magus run build web is a run, and no job exists for it. A job causes runs: its check executes as one, and a server job records the invocation of its last one. Jobs are listed with magus ls jobs and in the console's Jobs view; runs are listed in the Runs view.

Run

One target executing under one magus invocation, such as magus run test web or magus affected ci. A run keeps its captured output behind an output reference. Every magus run is a run whether or not any job asked for it; see Job for how the two relate.

Conventions

Placeholders

Angle brackets mark a value you replace with your own - never type the brackets:

magus run <target>
magus completion <shell>    # e.g. bash, zsh, fish

<target>, <path>, <shell>, <name> and the like are stand-ins, not literal text.