Bindings
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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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 }; }, }), },});