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.A credential narrowed with
domainsmay also opt into user-authored endpoint hosts withbindingDomains: [<metadata dot path>]. The host harvests those HTTPS names from readable binding pages at snapshot time. This extends only credential injection; the endpoint must still be independently granted bycapabilities.fetchBindings. -
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"], "dependencies": { "telegram": "^2.26.22" }}]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?, readOnly?, execute(args, ctx) }.
dependencies declares bare npm imports used by that module. Install resolves each package
against the host runtime, verifies the requested semver range, and links only those declared
package roots into the immutable plugin directory. The installer never fetches undeclared native
code: the app lockfile/image remains the integrity and upgrade authority. An unavailable or
out-of-range package rejects install before the version directory is copied.
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.
Modules may export activate(host) and dispose(host). The host object supplies the live,
process-local registration seams for task resolvers, write guards, and narrow standing
approvals. activate runs once when the module becomes enabled; disable or upgrade awaits
dispose before evicting it, which is also where long-lived sockets such as MTProto clients
should close. Register and unregister through the supplied host so development and compiled
runtimes share the same authoritative registries.
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 mutating execution takes a <tool>:<runId>:<callId> effect claim,
exactly like a first-party tool. A genuinely side-effect-free live read may declare
readOnly: true; the host then executes it on every call instead of replaying a memo that
could be stale. The full-trust review is responsible for verifying that declaration.
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
audited; mutating calls are claimed. A contract provider maps its methods
onto these same tool names, so a resolved cross-plugin call inherits the identical
guarantees.