Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Over MCP

An agent framework that speaks the Model Context Protocol rather than ikigai reaches the same ledger through ikigai mcp. It projects a kernel’s capability-scoped manifold as MCP tools: one tool per action the session may invoke, with the arguments that action declares. Nothing is written for the agent. The tool list is the ledger’s own Descriptions, filtered by a capability, so it cannot offer what the session may not do.

The previous chapter ended on what the socket cannot bound: every request on it is the owner’s, so a sealed session can still file an item whose author claims a passkey. An agent that must not be able to claim a person needs an identity of its own, on the door where the author rule runs. That is the QUIC door from A QUIC client, and this chapter puts the two together: the agent gets a client certificate and a grant on the server, and ikigai mcp reaches gonk through that door. Three things to see at the end, each on a transcript below:

  • the agent’s tool list is the ledger’s writer actions for one ledger, and nothing else;
  • a retried filing files once, because the append is keyed;
  • the server names the agent, in the item’s author and on every access line.

Every command here runs ikigai at the version this book pins, which speaks MCP over standard input and output.

The server: an identity for the agent

In a new empty directory, as in every chapter: if the gonk from Over the socket is still running, stop it first, since this one takes the same port. This chapter’s gonk starts only after the agent’s certificate is minted, because it reads the certificates it trusts at startup.

As in A QUIC client, the server mints the agent a certificate and enrolls it under a grant built from a role. The agent may write the default ledger:

$ mkdir -p .config/ikigai
$ ikigai-gonk client add agent --ledger default=write --port 1070 --config-home "$PWD/.config/ikigai"
client `agent`  …/.config/ikigai/gonk/quic/clients/agent
  fingerprint  …
  principal    urn:iki:host:gonk:client:…
  enrolled     grant `agent` (4 scopes) in …/.config/ikigai/gonk/grants.json
  restart ikigai-gonk: trusted certificates are read at startup
  this bundle holds the client's PRIVATE key and the server never reads it — move the directory to the client, then from the client:
    ikigai --connect quic://<gonk host>:1070 --cert-dir <the moved directory>

Four scopes: the ledger’s writer, read and write on default, and the store’s tokens for that ledger’s graph, which the ledger uses for its own writes. Not delete, not purge, and no other ledger. gonk enforces this list on every request from that certificate, whatever the client asks for. Start the server, which reads the certificates it trusts at startup:

$ ikigai-gonk --config-home "$PWD/.config/ikigai" --data-home "$PWD/.ikigai" --port 1070 --no-backup 2>&1 | tee gonk.log
ikigai-gonk 0.1.0 — holding the store at …/.ikigai/store
…
  quic    udp 0.0.0.0:1070 — 1 trusted certificate(s), 1 enrolled
…

The agent’s side: a bundle and a ceiling

The agent runs on “another machine”, the directory agent/, with its own config home. It gets the bundle (on a real second machine, moved there and deleted here):

$ mkdir -p agent/.config/ikigai
$ cp -R .config/ikigai/gonk/quic/clients/agent agent/gonk

and a grant of its own, in its own config home’s grants.json. This is the client-side ceiling: ikigai mcp --grant agent serves the manifold under exactly these scopes, so the tools of everything else the agent’s ikigai could reach (its files, its secrets, a model) are filtered out of the list by capability. show narrows what is listed further, to the ledger’s tools; it changes what the agent sees, not what it may call. The scopes are the server’s four, which ikigai-gonk grants default write prints:

{
  "agent": {
    "scopes": [
      "urn:cap:ledger:read:default",
      "urn:cap:store:read:graph:urn:iki:ledger:graph:default",
      "urn:cap:ledger:write:default",
      "urn:cap:store:write:graph:urn:iki:ledger:graph:default"
    ],
    "show": ["ledger-*"]
  }
}

There are two ceilings, then, and they are independent. The server’s grant is the one that holds against a compromised agent, because the agent’s machine cannot change it. The client’s is the one that keeps the tool list honest, so an agent is not offered a tool that would only be refused.

The tool list

An MCP client starts the server as a child process and talks JSON-RPC to it, one message per line. Here the messages are a file, so the transcript is repeatable: the handshake, then the request for the tool list.

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"book","version":"1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}

The command is the one an agent framework is configured with. --override mounts gonk’s QUIC door at urn:iki:ledger:, so every ledger name goes there and nowhere else; --cert-dir after it is that mount’s identity; --grant is the ceiling above. The page points XDG_CONFIG_HOME at the agent’s directory to give it a config home of its own (--config-home agent/.config/ikigai says the same since ikigai-cli 0.1.43). The tool names are picked out of the answer and sorted (byte order, so the list reads the same on every machine), because the full answer carries every argument’s description:

$ env XDG_CONFIG_HOME="$PWD/agent/.config" ikigai mcp --override urn:iki:ledger:=quic://127.0.0.1:1070 --cert-dir agent/gonk --grant agent < agent/list.jsonl | grep -o '"name":"[^"]*__[^"]*"' | LC_ALL=C sort
ikigai mcp: serving the manifold under 4 scope(s)
ikigai mcp: tool visibility — 1 shown, 0 hidden pattern(s)
ikigai mcp: mount   override urn:iki:ledger: -> quic://127.0.0.1:1070  [certs agent/gonk]
"name":"ledger-append__sink"
"name":"ledger-claim__delete"
"name":"ledger-claim__sink"
"name":"ledger-close__sink"
"name":"ledger-comment__sink"
"name":"ledger-count__source"
"name":"ledger-defer__delete"
"name":"ledger-defer__sink"
"name":"ledger-doctor__source"
"name":"ledger-item-about__delete"
"name":"ledger-item-about__sink"
"name":"ledger-item-closed__exists"
"name":"ledger-item-holder-is__exists"
"name":"ledger-item-state-is__exists"
"name":"ledger-item-state__sink"
"name":"ledger-item-state__source"
"name":"ledger-item__exists"
"name":"ledger-item__sink"
"name":"ledger-item__source"
"name":"ledger-items__source"
"name":"ledger-label__delete"
"name":"ledger-label__sink"
"name":"ledger-ledgers__source"
"name":"ledger-lifecycle__exists"
"name":"ledger-lifecycle__source"
"name":"ledger-link__delete"
"name":"ledger-link__sink"
"name":"ledger-next__source"
"name":"ledger-policy__exists"
"name":"ledger-policy__source"
"name":"ledger-reopen__sink"

Thirty-one tools, each an endpoint and a verb: ledger-append__sink is a sink to urn:iki:ledger:append, ledger-next__source a source of urn:iki:ledger:next. Eight of them arrived with ikigai-ledger 0.5.0 and are not used in this book yet: an item’s lifecycle state (ledger-item-state__*, a compare-and-set, so of two agents moving one item out of the same state exactly one wins), the assertions an agent can check before it acts (ledger-item-state-is__exists, -holder-is, -closed), the lifecycles themselves as resources, and the doctor, a read-only report of orphaned, abandoned and lease-expired work. Two more arrived with 0.6.2: ledger-item-about__sink and __delete, which add and remove what an item is about (a file, a PR, the session that delivered it) without touching its title or body. And one with 0.9.0: ledger-count__source, the number of items a filter admits, without the listing (the figure part 5 asks for). A __delete tool is here only where the ledger’s writer may delete (a label, a claim, a link, a deferral, an item’s about). ledger-item__delete is not: deleting an item takes the ledger’s delete token, and purging takes another, and this grant holds neither.

Every tool that names a ledger takes it as an argument, ledger, with the default default (the ledger whose name the IRI may omit, as in Whose ledger). An agent that leaves it out writes to default, which is the ledger it was granted. An agent granted a named ledger passes ledger every time, and two such agents are offered the same tools (Two teams, one gonk compares them).

Filing, once

The agent files a task with its own name for it, key=agent-task-1, the way a hook files a finding by the finding’s id. Then it files the same thing again, as an agent does when it is not sure the first call landed. Then it reads the item back by that key, and finally it tries three things outside its grant: an author naming a person’s passkey, the item’s delete tool, and another ledger:

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"book","version":"1"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ledger-append__sink","arguments":{"content":"Read the notes","key":"agent-task-1"}}}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ledger-append__sink","arguments":{"content":"Read the notes","key":"agent-task-1"}}}
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"ledger-item__source","arguments":{"id":"key:agent-task-1"}}}
{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"ledger-append__sink","arguments":{"content":"Not really Ada","author":"urn:iki:gonk:passkey:ada"}}}
{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"ledger-item__delete","arguments":{"id":"1"}}}
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"ledger-append__sink","arguments":{"ledger":"acme","content":"Not mine"}}}
$ env XDG_CONFIG_HOME="$PWD/agent/.config" ikigai mcp --override urn:iki:ledger:=quic://127.0.0.1:1070 --cert-dir agent/gonk --grant agent < agent/calls.jsonl 2>/dev/null
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"ikigai","version":"0.1.61"}}}
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"#1 urn:iki:ledger:default:item:…\n"}],"isError":false}}
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"#1 urn:iki:ledger:default:item:… existing open\n"}],"isError":false}}
{"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"   #1  open    p-  Read the notes\n  iri:      urn:iki:ledger:default:item:…\n  key:      agent-task-1\n  filed:    … by urn:iki:host:gonk:client:…\n  updated:  …\n"}],"isError":false}}
{"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"denied: `author` names a principal (urn:iki:gonk:passkey:ada), and only the door names one: a write may name its own principal or a plain-text author, never another identity. This request is from urn:iki:host:gonk:client:…."}],"isError":true}}
{"jsonrpc":"2.0","id":6,"result":{"content":[{"type":"text","text":"tool `ledger-item__delete` is not available under this capability"}],"isError":true}}
{"jsonrpc":"2.0","id":7,"result":{"content":[{"type":"text","text":"denied: this capability does not hold `urn:cap:ledger:write:acme`. A ledger grant names exactly one ledger — holding a grant over another ledger satisfies the declared family `urn:cap:ledger:write:*` but not this resource, which is the point of naming them"}],"isError":true}}

Read the answers in order:

  • 2 and 3: one item. The second append found the key taken and filed nothing; it answered with the item that holds it, marked existing. The check and the filing are one store update in the ledger, so this holds for concurrent retries as well as sequential ones. An agent that keys what it files can retry freely.
  • 4: the author is the agent. The item was filed with no author argument, and gonk filled it with the name of the identity that asked, urn:iki:host:gonk:client:<fingerprint>, from the certificate the connection was made with.
  • 5: and only the agent. On the QUIC door an author that names a principal must be the caller’s own, so the agent cannot file as Ada. This is the residual the socket chapter ended on, closed by giving the agent its own door.
  • 6: never sent. The item’s delete is not in this session’s manifold, so ikigai mcp refuses the call itself; gonk never sees it.
  • 7: sent, and refused by the ledger. The client’s ceiling holds the write family (for default), so the call passes the local check and crosses the wire; the ledger refuses it, because a ledger grant names one ledger. A tool that takes ledger as an argument can be asked about any ledger; only a granted one answers.

What the server saw

$ grep -E 'quic client| door=quic ' gonk.log
ikigai-gonk: quic client … → grant "agent"
ikigai-gonk: quic client … → grant "agent"
… gonk:Access urn:iki:ledger:default:append door=quic verb=sink outcome=ok bytes=… dur=… principal=urn:iki:host:gonk:client:… q=-
… gonk:Access urn:iki:ledger:default:append door=quic verb=sink outcome=ok bytes=… dur=… principal=urn:iki:host:gonk:client:… q=-
… gonk:Access urn:iki:ledger:default:item:key:agent-task-1 door=quic verb=source outcome=ok bytes=… dur=… principal=urn:iki:host:gonk:client:… q=-
… gonk:Access urn:iki:ledger:default:append door=quic verb=sink outcome=denied bytes=… dur=… principal=urn:iki:host:gonk:client:… q=-
… gonk:Access urn:iki:ledger:acme:append door=quic verb=sink outcome=denied bytes=… dur=… principal=urn:iki:host:gonk:client:… q=-

One connection line per ikigai mcp process, naming the grant. One access line per request that reached gonk, each naming the agent, refusals included. Two things in the names are worth reading. Every append is logged as urn:iki:ledger:default:append: the tool call carried no ledger, the argument’s default filled it, and the request went out under the ledger’s full name. And the delete is not in the log at all, because it never left the agent’s machine.

Wiring it into an agent

An MCP client is configured with a command to start, and this is the command. What the configuration file looks like is the framework’s own; the part that is ikigai’s is the line below, with the server’s address and the bundle where the agent’s machine keeps it:

$ ikigai mcp --override urn:iki:ledger:=quic://server.local:1062 --cert-dir ~/gonk-agent --grant agent

Mount flags go after mcp, and they replace the mount lines of the agent’s own config home for this process, so nothing else the agent’s machine has configured is composed in. A gonk on the default port opens its QUIC door on UDP 1062.

What you have now

how it reaches gonkunder what authoritywho the log says
a browserHTTP, loopbackanonymous, or a passkey’s grantanon, or the passkey
curl, a hookHTTP, loopbackanonymousanon
ikigai on this machinethe socketthe owner’s, narrowed with cap sealowner
an agent over MCPQUIC, with its own certificateits client’s grant on the server, and its own ceiling on the clientthe agent’s urn:iki:host:gonk:client:…

The agent’s tool calls were the ledger’s resources by their own names, the same append, item and next that the page’s kernel ran in the first chapter, with the same contracts. MCP added a way to list them; what made the list safe to hand an agent was the capability it was computed under, and what made the agent answerable was the door it came in by.