Keys for everything that calls
Keeps 4 promises
The principals are the account key; your devices, Admin or Member
(devices.md); and grantees, keys that hold grants but can never add,
revoke or grant, or answer an ask: a service or an extension on a node, an
MCP client, someone else's node. A grantee is never Added as a device,
so no grant it holds can make it one.
A node makes a key for each service and each extension instance it runs,
and for each MCP client added to it (mcp.md), and keeps them beside its
own. A service never sees its key: it asks its node to call as itself, and
the node signs. Inside the node, the local socket already says which
service is calling. To another node, the call carries a chain from the
calling node to the service's key, so the far holder checks the service,
not the node it runs on. A crashed or broken-into service can call as
itself only through its node, only within its grants, and can't take its
key away.
Resources and classes
Keeps 4 promises
A resource is a key expression, <node key>/<service>/<path>, with
@<version> where the service keeps versions, and a scope
is one with * (one level) and ** (any depth): ab12…/files/home/sam/notes/**.
A node's own state (its keys, its log, its settings) is not a resource of
any service, so no grant can name it.
Every service ships a manifest: each verb, its class, which argument
names the resource, and whether it can show a preview. The holder reads
the manifest, not the call, to find the class and the resource, so a
caller can't relabel a verb. A built-in service's manifest is built into
the node; an extension's is the one shown when it was installed
(sandbox/declared).
| class | for example | |
|---|---|---|
| read | list, stat, read a file | changes nothing |
| append | add a new file under a folder | adds, never changes |
| write | write, move, remove a file; merge a fork | changes what's there |
| run | open a terminal, start a process | as much as the shell it gets |
| reach | dial a host from a box | leaves the node; the sandbox's gate (sandbox.md) |
A path is checked twice. The holder checks it as written against the
scope, and refuses early. The service then gets the call with the
granted roots beside it and confines the path to them after following
symlinks, as files confines to its one root now. The holder knows
authority and not the filesystem; the service, the other way round, so
both checks stay.
run is stated for what it is. A terminal as your user can read your
keys, change anything and dial anywhere; no scope round it changes that.
Your own devices may have one. A grantee gets a terminal only inside a box
(sandbox.md), where its folders, its processes and its reach are what
the box was made with.
Grants with fields an older holder can't read come in a new kind of log entry, not as extra bytes on an old one: an unknown kind is refused, and a refusal is safe where a partial reading is not.
Ask is in the grant
Keeps 2 promises
A grant is {subject, scope, classes, ask, expiry} in the log (D-12,
D-30). ask says a person answers each call it covers. Changing your mind
is an ungrant and a new grant, made from the same settings page. A key
with no grant that covers a call gets no, so a client added for reading
can't inherit a yes given to another.
A session keeps the grants it is relying on, and re-checks each when it expires and whenever the log changes under it, so an ungrant that arrives mid-session refuses the next call, not the next session.
An ask, from call to answer
Keeps 5 promises
- The holder parks the call and makes an ask: an id, the caller's chain, the verb, the hash of its exact arguments and a preview. The service makes the preview (a write's diff, a merge's changes), since it knows content and not authority.
- The ask is sealed to each Admin device's mailbox at the relay (D-18) and a closed browser is woken by Web Push (D-19). The caller hears pending and the ask's id.
- The shell shows the ask in the top layer, where no pane can draw
(
contain/asks-outside): who is asking, by its chain; what, and the preview. - A person answers on any of your devices, which signs the answer with
its key, bound to the ask's hash:
- once: this call;
- for this task: a chain from the device to the caller, scoped to what the ask showed, ending with the task or after an hour;
- always: a grant in the log.
- The holder checks the answer against the log (an Admin device, not the caller, hash matching) and runs the call, or refuses it.
So an answer is a grant: one call long, a task long, or until taken back. Nothing new holds authority.
In person. A script in a tab can sign as the tab (core.md, Who sees
what), so an answer that widens what a key may do (always, a new grant,
pairing, a terminal outside a box) carries a passkey assertion over the
ask's hash, with user verification. An agent driving a browser can't make
one.
Fewer asks by shape. An ask every call is one nobody reads. Grants are
narrow enough that most calls are plain allow; an agent's edits land on a
fork and merging it is the one ask, with the whole diff
(ai/fork-approved). A checker may sort asks, deny one or raise it, but
never answers yes: only a person does.
Nodes and services calling each other
Keeps 2 promises
A machine joins as a Member, and a Member holds no grants until one is made, so a machine reaches another of yours only for what was granted, the same way any grantee does. An Admin device may do anything; that's what an Admin is, and why a machine isn't one.
A service calling another service on its node is a call from its key,
checked by the node like a call from outside. A built-in service is
granted what its job needs when it's installed, shown in settings; an
extension, its manifest at most (sandbox/declared).
Nodes that face outward
Keeps 3 promises
A grantee's manifest says who it serves: nobody else, the node's own OS
user (mcp.md), anyone signed in to it, or the internet. Nothing checks
that past the node; the grant page shows it, so a grant reads as what it
is. A node facing outward holds a copy of what it serves (files/hold-some)
rather than calling through to a machine that may be asleep, and where it
is hosted can read that copy. Its grants are allow or nothing: a stranger's
request must never become an ask on your phone. Publishing is moving the
ref it was granted, a write that can ask. A name it passes along for its
caller (an OAuth user) is audited as its claim, never as a key. A node may
instead give its callers keys of their own, by chains, when you want
grants to tell them apart, as MCP does for each agent.
The audit
Keeps 1 promise
The holder writes each call it answered (allowed, asked, answered or
refused) with the caller's chain, the verb and the resource. The audit
is a doc of the node's, synced sealed to your devices like the sandbox's
(sandbox/audit-reaches-you), so any device can read what any caller did anywhere.
What a caller reads, it keeps; the audit says what it saw, and a revoke
decides what happens next.
Where it breaks
- A holder that's behind answers by the log it has. Short chains,
log heads in every session and the mailbox drained first narrow that
gap (
devices.md, Revocation);log/freshnesscloses it at a price. - The holder is trusted for its own data. A broken-into machine can hand out what it holds; grants decide who else may ask it, not what it does with its own.
- Same-user processes. A program running as the node's user can read the node's key file. Nothing on that machine is safe from it, and nothing here pretends otherwise (D-27).