The gateway and manifest pinning
One local MCP endpoint serves every plugged server and refuses any tool list that no longer matches its signed manifest.
How a session runs
The gateway proxies remote HTTP servers and local stdio servers alike. It reads the pinned manifest hash for each listing from the Servers contract, caches it for 60 seconds, and checks the live tool list against it before your agent sees anything.
The manifest hash
A manifest is the canonical tool list of a server: every tool's name, description and input schema, sorted by name, serialised with JCS (RFC 8785), then hashed with keccak256.
const tools = (await client.listTools()).tools
.map(t => ({ name: t.name, description: t.description ?? "", inputSchema: t.inputSchema ?? {} }))
.sort((a, b) => a.name < b.name ? -1 : 1);
const manifestHash = keccak256(toBytes(jcs(tools)));The publisher kit at /sdk/publish.mjs exports this exact function, so a publisher can check a hash before signing it.
What a refusal looks like
If the hashes differ, the gateway drops that server for the session and tells the agent it is unavailable. The changed descriptions never reach the model. The keeper sees the same drift on its next probe (every 60 seconds) and flags the server, which puts 30% of the stake behind it at risk.
A publisher that wants to change its tools signs the new manifest first with publish(serverId, newHash). That is never a breach.
Secrets
Tokens such as GITHUB_TOKEN or SUPABASE_ACCESS_TOKEN stay in ~/.daemon/secrets (mode 600) and are sent only to the publisher's own endpoint. Daemon's servers never receive them.
The listing screen
Before the curator pins a manifest, it runs a published screen and refuses manifests that contain hidden Unicode, instructions to call other tools or read local files, or text aimed at the model inside a schema. A probe cannot judge intent, so the screen runs once at listing and drift detection covers every change after it.