Bindings
Give sandboxed guest code a narrow, curated set of host-backed capabilities.
Bindings are host-side functions the guest invokes by name:
- Where the handler runs: on the host, never inside the guest sandbox.
- Return value: round-trips back to the guest as JSON.
- Why use them: hand untrusted guest code a narrow, curated capability surface (the kind an AI agent calls as tools) without granting it the underlying access.
Registering tools at boot
Pass tools to NodeRuntime.create(). Each key becomes a named command the guest can run.
import { NodeRuntime } from "secure-exec";
const rt = await NodeRuntime.create({
tools: {
"get-weather": {
description: "Look up the current temperature for a city",
inputSchema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
// Runs on the HOST. The return value is delivered back to the guest.
handler: ({ city }: { city: string }) => {
const table: Record<string, { temp_f: number }> = {
"San Francisco": { temp_f: 61 },
Tokyo: { temp_f: 75 },
};
return table[city] ?? { temp_f: null };
},
},
},
});
Registering tools on a live runtime
You can also add tools after the VM is running with rt.registerTools({...}). This is the same capability as the tools create option, exposed for a running runtime.
await rt.registerTools({
reverse: {
description: "Reverse a string",
inputSchema: {
type: "object",
properties: { text: { type: "string" } },
required: ["text"],
},
handler: ({ text }: { text: string }) => ({ result: [...text].reverse().join("") }),
},
});
When you register tools on a live runtime, make sure the tool permission scope is granted (see below) so the tools are invocable.
The tool definition
A HostToolDefinition has these fields:
description(required): human-readable summary of what the tool does.inputSchema(required): JSON Schema describing the input.handler(input)(required): the function that runs on the host, returning a JSON-serializable value.timeoutMs(optional): abort the host handler after this many milliseconds.examples(optional): worked examples (each{ description, input }) shown alongside the tool.commandAliases(optional): extra command names the guest may use, beyond the registered key.
const rt = await NodeRuntime.create({
tools: {
lookupWeather: {
description: "Look up the current weather for a city",
inputSchema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
timeoutMs: 5000,
examples: [{ description: "Weather in Tokyo", input: { city: "Tokyo" } }],
commandAliases: ["weather"],
handler: async ({ city }: { city: string }) => {
// Real host access lives here, behind the tool boundary. The guest
// never gets the network credential, only the curated result.
const res = await fetch(`https://example.com/weather?city=${city}`);
return await res.json();
},
},
},
});
The full type shape is in the TypeScript SDK reference.
Invoking a tool from the guest
Guest code calls a tool with the callHostTool(name, input) global. It returns a promise that resolves with the host handler’s JSON result:
await rt.exec(`
const { temp_f } = await callHostTool("get-weather", { city: "Tokyo" });
console.log(temp_f); // 75
`);
callHostTool is available in every guest program run through exec, run, and spawn. A commandAlias (for example weather above) works in place of the registered key.
Under the hood, a registered tool is exposed two ways:
- As a
PATHcommand: resolved as/usr/bin/<name>inside the VM, so the same invocation can be driven directly throughnode:child_processif you prefer. - Via
callHostTool: a thin wrapper over exactly that command path, so both share the same permission and validation behavior.
await rt.exec(`
import { execFileSync } from "node:child_process";
const input = { city: "Tokyo" };
// argv[0] is the command name, then --json and the JSON-encoded input.
const out = execFileSync("get-weather", ["get-weather", "--json", JSON.stringify(input)]);
// The raw command writes a { ok, result } envelope; the handler's return
// value is under "result". callHostTool unwraps this for you.
const { ok, result } = JSON.parse(out.toString());
console.log(result.temp_f); // 75
`);
The tool permission scope
Tool invocation is gated by the tool permission scope.
- When you pass
toolstocreate()and set notoolpolicy, thetoolscope is auto-granted so your tools are invocable out of the box. - Otherwise grant it explicitly so tools can run:
const rt = await NodeRuntime.create({
tools: { /* ... */ },
permissions: { tool: "allow" },
});
Use a rule set instead of "allow" for per-tool gating, allowing some tool names while denying others. See Permissions for rule-set semantics.
Wiring a tool into an LLM agent
Bindings pair naturally with an LLM agent: expose a sandbox capability to the model as a tool and let the model drive it through its own tool-calling loop.
The example below uses the Vercel AI SDK to give the model a runJs tool whose execute runs guest code inside the runtime. The model-generated code is untrusted input, but it executes only inside the VM under the secure-default policy (network denied here), so the model can experiment freely without reaching the host.
import { NodeRuntime } from "secure-exec";
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";
const rt = await NodeRuntime.create({ permissions: { network: "deny" } });
await generateText({
model: openai("gpt-4o"),
prompt: "Compute the 10th Fibonacci number by writing JavaScript.",
tools: {
runJs: tool({
description: "Run JavaScript inside the sandbox and capture its output.",
inputSchema: z.object({ code: z.string() }),
execute: async ({ code }) => {
const result = await rt.exec(code);
return { stdout: result.stdout, stderr: result.stderr };
},
}),
},
});