Plugin tools
A plugin contributes tools an agent can call in two lanes. Most reach the world over HTTP and run in a QuickJS sandbox with zero ambient authority: these are function tools, an extension of Subspace’s custom functions. A small minority need a protocol that cannot ride a fetch, like MTProto or ZeroMQ, and run as full-trust native tool modules. Both join the same tool registry as first-party tools, so gates, claims, approvals, and audit apply identically. The default answer is always the sandbox; the native lane is the deliberate escape hatch.
Function tools
Section titled “Function tools”A function tool is user or plugin TypeScript that runs in a fresh QuickJS-WASM sandbox and
joins the tool registry as a callable fn.<name>. The source is
an ordinary code/ page (see custom functions); the manifest
tools: [{ name, page }] binds a tool name to that page. Because the sandbox starts with
zero ambient authority, no filesystem, no process, no network, everything a tool can do
is exposed explicitly through the host API, and the plugin’s capabilities.host allowlist
decides which host methods it may call.
Capabilities are pinned into a source-pinned snapshot at install time. The snapshot
records capabilities.plugin provenance alongside the host allowlist, so a tool inherits
exactly its plugin’s declared reach, and a bare code/ function you write by hand keeps
the default minimal grant no matter what any manifest says.
Brokered fetch
Section titled “Brokered fetch”The one way a sandboxed tool reaches the network is http.fetch, and it is deliberately
narrow:
-
Domain allowlist, https only
A request must target a domain named in
capabilities.fetchover https. Anything else is refused before a socket opens. -
Credentials brokered host-side
capabilities.credentials: [{ name, into: <header template> }]names a secret and the header it lands in. The host resolves the named secret through the capability broker (grant-checked and audited) and injects it into the outgoing request host-side. The token never enters QuickJS, so sandboxed code cannot read, log, or exfiltrate it. -
Bounded and closed
Every response is size- and time-capped (
fetchMaxBytes/fetchTimeoutMs, per capability, with safe defaults), and redirects stay closed so a 302 cannot walk a request off the allowlist.
// code/lookup-book: a function tool bound by tools: [{ name: "lookupBook", page: "code/lookup-book" }]export default async function lookupBook(isbn: string) { // https + api.openlibrary.org must be in capabilities.fetch; // the Authorization header is filled from the brokered "openlibrary_key" secret host-side. const res = await http.fetch(`https://api.openlibrary.org/isbn/${isbn}.json`); const book = await res.json(); return { type: "stat", value: book.title, label: "Title" };}The header template is where the credential lands: a spec like
{ name: "openlibrary_key", into: { header: "Authorization", format: "Bearer {{secret}}" } }
tells the broker to set Authorization: Bearer <resolved-secret> on the request. The
plugin’s code only ever sees a URL; the secret is assembled outside the sandbox.
Native tool modules
Section titled “Native tool modules”Some providers cannot be brokered over a fetch: a Telegram client speaks MTProto, a market
feed speaks ZeroMQ, a service needs a raw websocket held open. nativeTools is the
full-trust lane for exactly these, and nothing else.
"nativeTools": [{ "module": "native/telegram.js", "names": ["tg.sendMessage", "tg.readChat"] }]module names a compiled ESM module inside the immutable version directory whose tools
(or default) export maps each tool name to
{ description, inputJsonSchema?, requires?, needsApproval?, execute(args, ctx) }.
Install: full-trust review, SDK-pinned
Native modules ride the same sdk: 1 gate as workflow bundles, and the referenced
module must exist inside the plugin directory. The install review card
renders every declared name as native tool <name> (<module>) in the full-trust
lane, under the version directory’s source-tree hash, so you approve specific bytes.
Runtime: only declared names register
On boot and on every plugins event, the registry syncs on both planes. Only the
declared names register; undeclared exports in the module are ignored. Native
tools compose under built-ins, so a plugin can never shadow a first-party tool name.
Gates, claims, and reach
Native tools ride the ordinary seams. A definition’s tools: allowlist decides which
ones an agent may call; needsApproval defaults to true (a plugin may lift only its
own gate, never claim alwaysGate authority); a requires: list of broker capabilities
gates the module’s reach; and every execution takes a <tool>:<callId> effect claim,
exactly like a first-party tool.
Same registry, same rules
Section titled “Same registry, same rules”Whichever lane a tool comes from, it is indistinguishable to an agent at the point of use:
it appears in the tool registry, an agent definition
opts in with its tools: allowlist, calls flow through the same
approval and gating machinery, and every invocation is
claimed and audited. A contract provider maps its methods
onto these same tool names, so a resolved cross-plugin call inherits the identical
guarantees.