Write a tool

Pin tool definitions

Verify tool snapshots and publish a measured function listing.

For: plugin authors.

Verify tool snapshots and publish a measured function listing.

Snapshots: a tool may not change what it says it does

The first node that runs a plugin version lists its tools and takes a snapshot of each function: its description, its input and output schemas, and its annotations, with a hash. For an update with evals, that is the quality gate's test node. The catalog, and the Tools and skills page, show the functions as their snapshot has them.

From then on, nodes call a function only while it lists itself the same way. A tool that starts to describe itself differently, such as a remote MCP server whose owner changed a description or a schema, is refused:

team.hello lists itself differently than when its plugin's tools were approved, so it is not called; check the
change and approve it with: oz0 plugin approve-tools team

The plugin's other functions keep working. oz0 plugin list and the plugin's page in the console show which functions changed and on which node, and oz0 catalog --json has the new listings. Approve a change once you have looked at it:

Terminal
oz0 plugin approve-tools team

Nodes call the functions again within seconds, without restarting. The audit log records the snapshot, each change and each approval (plugin.tools.snapshot, plugin.tools.changed and plugin.tools.approve). A new version of the plugin gets a snapshot of its own.

A node calls no plugin's function before it holds the snapshots: one that just joined, or started again, waits for the edge's plugin set. A node that starts again takes the snapshots of the plugin versions it already runs from that set before it installs anything, so they are called while another plugin's update installs; the version being replaced, and one no longer assigned, are not called meanwhile (its plugin's version ... is installing on this node).

An update that fails leaves the running version under the snapshot it was approved with; if the node started again while that update was pending, the version stays refused until the update is fixed or the version is assigned again.

What the snapshot shows

Text people do not see cannot hide in a listing. Before a node hashes a function's listing, and before a model reads it, every invisible code point in it (Unicode TAG characters, zero-width characters and joiners, bidi controls, variation selectors, other format and control characters, private-use code points) is written as a visible escape such as \u{E0041}. The model reads the same text people approve, the catalog and the console show the escapes, and a server that adds hidden text to a description changes its listing, which waits for approval.

A listing without such characters hashes as it did before. Approval cards in the console, chat threads, the CLI and MCP clients show the arguments of a call the same way.

A node also leaves out a function whose name could pass for another's, and says so in oz0 plugin list:

  • a name outside MCP's tool-name characters (letters, digits, _, - and ., at most 128), such as one that starts with a byte order mark;
  • a name its server lists twice;
  • two functions a model would see under one name (a.b__c and a__b.c both reach models as a__b__c).

The tool ID oz is the platform's own (oz.delegate, oz.ask_user, ...): no plugin's tool or harness may take it, and a node refuses one from an older bundle. A call of a function its server did not list is refused too.

Remote endpoints are pinned

A remote tool (url:) is pinned at its origin, scheme://host[:port], the first time a version of its plugin lists it. A later version whose URL names another host, such as an MCP Registry server that moved its endpoint under the same name, gets its other functions at once, and that tool's functions wait for approval:

Tools of weather changed on node-1: wx.today. Nodes do not call them until you approve: oz0 plugin approve-tools weather
  endpoint moved: wx: https://mcp.example.com -> https://mcp.other.example

A remote tool whose host is a connection's param is not pinned this way: its host is the one the connection was connected for (see Remote servers on connections).

oz0 plugin approve-tools pins the new origin for that version. Each version keeps its own pins: when the version that moved is a candidate (a canary or a rollout), approving it leaves the installed version calling the origin it was approved at, and once the candidate is installed, its origin is where the next version starts. Another path on the same origin is no move, and the MCP client follows a redirect only within the origin, so a server cannot move its calls by redirecting them.

The snapshot catches change, not a tool that misdescribes itself from the start: read a tool's listing before you install it.

The measured listing

plugin inspect, plugin lint, the studio and the hub run no plugin code, so on their own they cannot say what your servers list. plugin test can: it starts them anyway, and writes what each one listed into tools.listing.json at the plugin's root, exactly as a node records it for the snapshot (each function's listing and its hash). Commit the file. plugin pack --list-tools writes it without running evals:

Terminal
oz0 plugin test ./acme-github          # also writes tools.listing.json
oz0 plugin pack ./acme-github --list-tools
Output
Wrote tools.listing.json: 12 functions as the test node listed them; commit it, so inspect and the hub can show them

The file also records a digest of the plugin's other files, leaving out the ones the tools write from them (the cost-amplification probe's tools.amplification.json and the SBOM sbom.cdx.json). When they change, plugin lint, plugin pack and plugin inspect say the listing is stale; run plugin test again.

The first node's snapshot stays the authority. When it differs from the version's measured listing, nothing is blocked (the snapshot already decides what nodes call), but plugin list and the Plugins page name the functions that differ, and the audit log records plugin.tools.listing_mismatch: either the listing is stale, or the server describes itself differently on a node than on your machine.

Copyright © 2026