@rivet-dev/agentos-runtime-core 0.2.20-rc.1 → 0.2.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,13 +17,14 @@
17
17
  * isolation boundary — no host escapes, no real Node.js builtins for guest
18
18
  * work.
19
19
  */
20
- import type { BindingDefinition, KernelBootTiming, Permissions, VirtualDirEntry, VirtualFileSystem } from "./test-runtime.js";
21
20
  import type { JsRuntimeConfig } from "./generated/JsRuntimeConfig.js";
22
21
  import type { VmUserConfig } from "./generated/VmUserConfig.js";
22
+ import type { HostFunctionCollections, HostFunctionSchemas } from "./host-functions.js";
23
23
  import type { SidecarProcess } from "./sidecar-process.js";
24
- export type { BindingDefinition, BindingExample, VirtualDirEntry, } from "./test-runtime.js";
24
+ import type { KernelBootTiming, Permissions, VirtualDirEntry, VirtualFileSystem } from "./test-runtime.js";
25
+ export type { HostFunctionExample, VirtualDirEntry, } from "./test-runtime.js";
25
26
  export { resolveNodeRuntimeSidecarBinary } from "./test-runtime.js";
26
- export type NodeRuntimeBootTimingPhase = KernelBootTiming["phase"] | "runtime_mount_wasm" | "runtime_mount_node" | "bindings";
27
+ export type NodeRuntimeBootTimingPhase = KernelBootTiming["phase"] | "runtime_mount_wasm" | "runtime_mount_node" | "hostFunctions";
27
28
  export interface NodeRuntimeBootTiming {
28
29
  phase: NodeRuntimeBootTimingPhase;
29
30
  durationMs: number;
@@ -50,7 +51,7 @@ export declare function resolveNodeRuntimeCommandsDir(explicit?: string): string
50
51
  * Options that translate into sidecar VM JSON must also stay aligned with
51
52
  * `crates/vm-config/src/lib.rs::CreateVmConfig`.
52
53
  */
53
- export interface NodeRuntimeCreateOptions {
54
+ export interface NodeRuntimeCreateOptions<HOST_FUNCTIONS extends HostFunctionSchemas = HostFunctionSchemas> {
54
55
  /**
55
56
  * Caller-owned filesystem used only by this low-level compatibility runtime.
56
57
  * AgentOS clients do not create a TypeScript filesystem implicitly; normal
@@ -64,12 +65,11 @@ export interface NodeRuntimeCreateOptions {
64
65
  /** Initial virtual Linux credentials and account record. Defaults to `1000:1000` (`agentos`). */
65
66
  user?: VmUserConfig;
66
67
  /**
67
- * Permission policy for the VM. Merged over a secure default that **denies
68
- * network access** (guest code cannot reach the network until you opt in);
69
- * the virtualized filesystem and processes stay enabled so programs run.
70
- * Because it merges, a partial policy works: `{ network: "allow" }` grants
71
- * the network while keeping the execution essentials. Pass a fuller policy
72
- * (rule sets) to further sandbox individual scopes.
68
+ * Permission policy for the VM, merged over the sidecar's default: the
69
+ * virtual filesystem, processes, environment, bindings, listeners, and
70
+ * loopback networking work, while external network access is denied. A partial
71
+ * policy works: `{ network: "allow" }` grants external access and keeps every
72
+ * other scope at its default.
73
73
  */
74
74
  permissions?: Permissions;
75
75
  /**
@@ -163,41 +163,43 @@ export interface NodeRuntimeCreateOptions {
163
163
  */
164
164
  nodeModules?: string | NodeModulesMount;
165
165
  /**
166
- * Host-side bindings the guest can invoke as shell commands. Each entry is
167
- * registered as a named guest command; when the guest runs it, the
168
- * invocation round-trips back to the host and runs the binding's `handler`,
169
- * whose return value is delivered back to the guest. This is how you give
170
- * sandboxed guest code controlled, named host capabilities (the kind an AI
171
- * agent calls as tools) without granting it the underlying access directly.
166
+ * Host-side functions the guest can invoke as shell commands, as a record of
167
+ * collections. The keys name everything: the collection key becomes the guest
168
+ * command `agentos-{name}` and each function key becomes one of its
169
+ * subcommands. When the guest runs it the invocation round-trips back to the
170
+ * host, runs the function's `execute`, and its return value is delivered back
171
+ * to the guest. This is how you give sandboxed guest code controlled, named
172
+ * host capabilities (the kind an AI agent calls as tools) without granting it
173
+ * the underlying access directly.
172
174
  *
173
- * The guest invokes a binding by name with JSON input:
175
+ * This is the same shape `AgentOs.create()` takes. A function needs only a
176
+ * Zod `inputSchema` and an `execute` handler; `.describe()` on the schema is
177
+ * what the agent reads.
174
178
  *
175
179
  * ```ts
176
180
  * const rt = await NodeRuntime.create({
177
- * bindings: {
178
- * add: {
179
- * description: "Add two numbers",
180
- * inputSchema: {
181
- * type: "object",
182
- * properties: { a: { type: "number" }, b: { type: "number" } },
183
- * required: ["a", "b"],
181
+ * hostFunctions: {
182
+ * math: {
183
+ * add: {
184
+ * inputSchema: z
185
+ * .object({ a: z.number(), b: z.number() })
186
+ * .describe("Add two numbers"),
187
+ * execute: ({ a, b }) => ({ sum: a + b }),
184
188
  * },
185
- * handler: ({ a, b }: { a: number; b: number }) => ({ sum: a + b }),
186
189
  * },
187
190
  * },
188
191
  * });
189
192
  * await rt.exec(`
190
193
  * import { execFileSync } from "node:child_process";
191
- * const out = execFileSync("add", ["add", "--json", JSON.stringify({ a: 2, b: 3 })]);
194
+ * const out = execFileSync("agentos-math", ["add", "--json", JSON.stringify({ a: 2, b: 3 })]);
192
195
  * console.log(out.toString());
193
196
  * `);
194
197
  * ```
195
198
  *
196
- * When `bindings` is provided and no `binding` permission scope is set, the
197
- * `binding` scope is granted so the registered bindings are invocable; pass
198
- * your own `permissions.binding` policy to gate individual bindings.
199
+ * The `hostFunction` permission scope is allowed by default; pass your own
200
+ * `permissions.hostFunction` policy to gate individual host functions.
199
201
  */
200
- bindings?: Record<string, BindingDefinition>;
202
+ hostFunctions?: HostFunctionCollections<HOST_FUNCTIONS>;
201
203
  /**
202
204
  * Guest-bound ports that may accept non-loopback connections. By default a
203
205
  * guest server is reachable only over loopback inside the VM; listing a port
@@ -449,7 +451,7 @@ export declare class NodeRuntime {
449
451
  * session, creates the VM with a bootstrapped root filesystem, mounts the
450
452
  * shell and Node runtimes, and waits for the VM to report ready.
451
453
  */
452
- static create(options: NodeRuntimeCreateOptions): Promise<NodeRuntime>;
454
+ static create<HOST_FUNCTIONS extends HostFunctionSchemas>(callerOptions: NodeRuntimeCreateOptions<HOST_FUNCTIONS>): Promise<NodeRuntime>;
453
455
  createResidentRunner(_options?: NodeRuntimeResidentRunnerOptions): Promise<NodeRuntimeResidentRunner>;
454
456
  /**
455
457
  * Run `code` as a guest Node program and capture its output.
@@ -571,19 +573,18 @@ export declare class NodeRuntime {
571
573
  */
572
574
  waitForListener(query: NodeRuntimeListenerQuery, options?: NodeRuntimeWaitForListenerOptions): Promise<NodeRuntimeListener>;
573
575
  /**
574
- * Register host-side bindings the guest can invoke as shell commands, after
576
+ * Register host-side hostFunctions the guest can invoke as shell commands, after
575
577
  * the VM is already running. Each entry becomes a named guest command; when
576
578
  * the guest runs it, the invocation round-trips back to the host and runs the
577
- * binding's `handler`, whose return value is delivered back to the guest. This
578
- * is the same capability as the `bindings` create option, exposed for adding
579
- * bindings to a live runtime. See `bindings` on {@link NodeRuntime.create} for
579
+ * hostFunction's `handler`, whose return value is delivered back to the guest. This
580
+ * is the same capability as the `hostFunctions` create option, exposed for adding
581
+ * host functions to a live runtime. See `hostFunctions` on {@link NodeRuntime.create} for
580
582
  * the invocation shape and permission behavior.
581
583
  *
582
- * When registering bindings this way, make sure the `binding` permission scope
583
- * is granted (for example `permissions: { binding: "allow" }` on
584
- * {@link NodeRuntime.create}) so the bindings are invocable.
584
+ * The `hostFunction` permission scope is allowed by default, so these host
585
+ * functions are invocable unless the runtime's policy restricts them.
585
586
  */
586
- registerBindings(bindings: Record<string, BindingDefinition>): Promise<void>;
587
+ registerHostFunctions(hostFunctions: HostFunctionCollections): Promise<void>;
587
588
  /**
588
589
  * Write a file into the VM's virtual filesystem, creating parent
589
590
  * directories as needed. Use this to project assets or npm packages into
@@ -20,8 +20,8 @@
20
20
  import { existsSync } from "node:fs";
21
21
  import path from "node:path";
22
22
  import { fileURLToPath } from "node:url";
23
- import { createKernel, createNodeRuntime, createWasmVmRuntime, NodeFileSystem, } from "./test-runtime.js";
24
23
  import { parseNodeRuntimeCreateOptions } from "./node-runtime-options-schema.js";
24
+ import { createKernel, createNodeRuntime, createWasmVmRuntime, NodeFileSystem, } from "./test-runtime.js";
25
25
  export { resolveNodeRuntimeSidecarBinary } from "./test-runtime.js";
26
26
  /** Repository root, used to locate the in-repo WASM command build output. */
27
27
  const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
@@ -66,47 +66,30 @@ export function resolveNodeRuntimeCommandsDir(explicit) {
66
66
  }
67
67
  return BUNDLED_COMMANDS_DIR;
68
68
  }
69
- /**
70
- * Secure-by-default permission policy applied when the caller passes no
71
- * `permissions`. Outward-facing capabilities are denied: there is **no network
72
- * access** (and no host callbacks) by default — guest code cannot reach the
73
- * network until you opt in. The filesystem, child-process, process, and env
74
- * scopes are allowed because they are fully virtualized (the guest only ever
75
- * sees the VM's in-memory filesystem and kernel-managed processes, never the
76
- * real host) and are required for the runtime to execute a guest program at
77
- * all. Tighten or widen any scope by passing your own `permissions`.
78
- */
79
- const DEFAULT_PERMISSIONS = {
80
- fs: "allow",
81
- childProcess: "allow",
82
- process: "allow",
83
- env: "allow",
84
- network: "deny",
85
- };
86
69
  /** Guest path a `nodeModules` mount is projected at by default. */
87
70
  const DEFAULT_NODE_MODULES_GUEST_PATH = "/tmp/node_modules";
88
71
  let nextProgramId = 0;
89
72
  let nextResidentRequestId = 0;
90
73
  /**
91
- * Guest preamble exposing `globalThis.callBinding(name, input?)`: an ergonomic
92
- * async wrapper over the binding invocation path. It runs the registered binding
93
- * as the guest would by hand (`<binding> --json <input>` through
74
+ * Guest preamble exposing `globalThis.callHostFunction(name, input?)`: an ergonomic
75
+ * async wrapper over the hostFunction invocation path. It runs the registered hostFunction
76
+ * as the guest would by hand (`<host-function> --json <input>` through
94
77
  * `node:child_process`), so it inherits every security property of that path:
95
- * the `binding` permission scope, the binding's input-schema validation, and the
78
+ * the `hostFunction` permission scope, the host function's input-schema validation, and the
96
79
  * host-side handler all still apply. It adds no new trust surface; it only
97
80
  * removes the manual `execFile`/JSON boilerplate so guest and agent code can do
98
- * `const out = await callBinding("add", { a, b })`. The value is a single line
81
+ * `const out = await callHostFunction("add", { a, b })`. The value is a single line
99
82
  * so it shifts guest source line numbers by at most one in stack traces.
100
83
  *
101
- * Note: the binding still runs through a guest process. Eliminating that spawn
102
- * would require a dedicated async guest-to-host binding channel (the synchronous
84
+ * Note: the hostFunction still runs through a guest process. Eliminating that spawn
85
+ * would require a dedicated async guest-to-host hostFunction channel (the synchronous
103
86
  * sync-RPC path cannot be used: it runs on the sidecar's main sync-RPC thread and
104
87
  * a host round-trip would block it); that is a separate, test-gated change.
105
88
  */
106
- const BINDING_PREAMBLE = `globalThis.callBinding = (name, input = {}) => import("node:child_process").then(({ execFile }) => new Promise((resolve, reject) => { execFile(name, [name, "--json", JSON.stringify(input)], { maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(new Error(String(stderr || "").trim() || error.message)); return; } const text = String(stdout ?? "").trim(); let reply; try { reply = text ? JSON.parse(text) : undefined; } catch { reject(new Error("binding returned invalid JSON: " + text)); return; } if (reply && reply.ok === false) { reject(new Error(reply.error || "binding failed")); return; } resolve(reply && typeof reply === "object" && "result" in reply ? reply.result : reply); }); }));`;
107
- /** Prepend the binding helper preamble to guest program source. */
108
- function withBindingPreamble(code) {
109
- return `${BINDING_PREAMBLE}\n${code}`;
89
+ const HOST_FUNCTION_PREAMBLE = `globalThis.callHostFunction = (name, input = {}) => import("node:child_process").then(({ execFile }) => new Promise((resolve, reject) => { execFile(name, [name, "--json", JSON.stringify(input)], { maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(new Error(String(stderr || "").trim() || error.message)); return; } const text = String(stdout ?? "").trim(); let reply; try { reply = text ? JSON.parse(text) : undefined; } catch { reject(new Error("host function returned invalid JSON: " + text)); return; } if (reply && reply.ok === false) { reject(new Error(reply.error || "host function failed")); return; } resolve(reply && typeof reply === "object" && "result" in reply ? reply.result : reply); }); }));`;
90
+ /** Prepend the hostFunction helper preamble to guest program source. */
91
+ function withHostFunctionPreamble(code) {
92
+ return `${HOST_FUNCTION_PREAMBLE}\n${code}`;
110
93
  }
111
94
  const RESIDENT_READY_PREFIX = "__AGENTOS_RESIDENT_READY__";
112
95
  const RESIDENT_RESULT_PREFIX = "__AGENTOS_RESIDENT_RESULT__";
@@ -128,8 +111,10 @@ export class NodeRuntime {
128
111
  * session, creates the VM with a bootstrapped root filesystem, mounts the
129
112
  * shell and Node runtimes, and waits for the VM to report ready.
130
113
  */
131
- static async create(options) {
132
- options = parseNodeRuntimeCreateOptions(options);
114
+ static async create(callerOptions) {
115
+ // The generic exists only so each `execute` infers its input from its own
116
+ // `inputSchema`; past this point the concrete schemas carry no meaning.
117
+ const options = parseNodeRuntimeCreateOptions(callerOptions);
133
118
  const commandsDir = resolveNodeRuntimeCommandsDir(options.commandsDir);
134
119
  // Seed caller-provided files into the VM's in-memory filesystem before
135
120
  // boot so they are part of the root filesystem snapshot the guest sees
@@ -163,24 +148,12 @@ export class NodeRuntime {
163
148
  fs: new NodeFileSystem({ root: mount.hostPath }),
164
149
  readOnly: mount.readOnly ?? true,
165
150
  }));
166
- // Grant the `binding` scope when the caller registers bindings but does not
167
- // set their own binding policy, so the registered bindings are invocable.
168
- const bindingDefaults = options.bindings &&
169
- Object.keys(options.bindings).length > 0 &&
170
- options.permissions?.binding === undefined
171
- ? { binding: "allow" }
172
- : {};
173
151
  const kernel = createKernel({
174
152
  filesystem,
175
153
  mounts: mounts.length > 0 ? mounts : undefined,
176
- // Merge the caller's policy over the secure default so partial
177
- // opt-ins work: `{ network: "allow" }` enables the network while the
178
- // execution essentials (fs/childProcess/process/env) stay granted.
179
- permissions: {
180
- ...DEFAULT_PERMISSIONS,
181
- ...bindingDefaults,
182
- ...options.permissions,
183
- },
154
+ // The sidecar owns the defaults and merges this policy over them, so
155
+ // only the scopes the caller set are sent.
156
+ permissions: options.permissions,
184
157
  env: options.env,
185
158
  cwd: options.cwd,
186
159
  user: options.user,
@@ -199,11 +172,11 @@ export class NodeRuntime {
199
172
  commandDirs: [commandsDir, ...(options.wasmCommandDirs ?? [])],
200
173
  })));
201
174
  await measureBootTiming("runtime_mount_node", options.onBootTiming, () => kernel.mount(createNodeRuntime()));
202
- // Register bindings after the runtimes are mounted so they are
175
+ // Register hostFunctions after the runtimes are mounted so they are
203
176
  // installed as guest commands the moment the VM is ready.
204
- const bindings = options.bindings;
205
- if (bindings && Object.keys(bindings).length > 0) {
206
- await measureBootTiming("bindings", options.onBootTiming, () => kernel.registerBindings(bindings));
177
+ const hostFunctions = options.hostFunctions;
178
+ if (hostFunctions && Object.keys(hostFunctions).length > 0) {
179
+ await measureBootTiming("hostFunctions", options.onBootTiming, () => kernel.registerHostFunctions(hostFunctions));
207
180
  }
208
181
  }
209
182
  catch (error) {
@@ -223,7 +196,7 @@ export class NodeRuntime {
223
196
  */
224
197
  async exec(code, options = {}) {
225
198
  const programPath = `/tmp/agentos-program-${nextProgramId++}.mjs`;
226
- await this.kernel.writeFile(programPath, withBindingPreamble(code));
199
+ await this.kernel.writeFile(programPath, withHostFunctionPreamble(code));
227
200
  return this.runProgram(programPath, options);
228
201
  }
229
202
  /**
@@ -329,7 +302,7 @@ export class NodeRuntime {
329
302
  */
330
303
  async spawn(code, options = {}) {
331
304
  const programPath = `/tmp/agentos-program-${nextProgramId++}.mjs`;
332
- await this.kernel.writeFile(programPath, withBindingPreamble(code));
305
+ await this.kernel.writeFile(programPath, withHostFunctionPreamble(code));
333
306
  const proc = this.kernel.spawn("node", [programPath], {
334
307
  env: options.env,
335
308
  cwd: options.cwd,
@@ -453,7 +426,7 @@ export class NodeRuntime {
453
426
  // statements inside a function and make them a SyntaxError.
454
427
  const wrapped = [
455
428
  `import { writeFileSync as __writeFileSync } from "node:fs";`,
456
- BINDING_PREAMBLE,
429
+ HOST_FUNCTION_PREAMBLE,
457
430
  `globalThis.__return = (value) => {`,
458
431
  ` __writeFileSync(${JSON.stringify(resultPath)}, JSON.stringify(value === undefined ? null : value));`,
459
432
  `};`,
@@ -576,20 +549,19 @@ export class NodeRuntime {
576
549
  }
577
550
  }
578
551
  /**
579
- * Register host-side bindings the guest can invoke as shell commands, after
552
+ * Register host-side hostFunctions the guest can invoke as shell commands, after
580
553
  * the VM is already running. Each entry becomes a named guest command; when
581
554
  * the guest runs it, the invocation round-trips back to the host and runs the
582
- * binding's `handler`, whose return value is delivered back to the guest. This
583
- * is the same capability as the `bindings` create option, exposed for adding
584
- * bindings to a live runtime. See `bindings` on {@link NodeRuntime.create} for
555
+ * hostFunction's `handler`, whose return value is delivered back to the guest. This
556
+ * is the same capability as the `hostFunctions` create option, exposed for adding
557
+ * host functions to a live runtime. See `hostFunctions` on {@link NodeRuntime.create} for
585
558
  * the invocation shape and permission behavior.
586
559
  *
587
- * When registering bindings this way, make sure the `binding` permission scope
588
- * is granted (for example `permissions: { binding: "allow" }` on
589
- * {@link NodeRuntime.create}) so the bindings are invocable.
560
+ * The `hostFunction` permission scope is allowed by default, so these host
561
+ * functions are invocable unless the runtime's policy restricts them.
590
562
  */
591
- async registerBindings(bindings) {
592
- await this.kernel.registerBindings(bindings);
563
+ async registerHostFunctions(hostFunctions) {
564
+ await this.kernel.registerHostFunctions(hostFunctions);
593
565
  }
594
566
  /**
595
567
  * Write a file into the VM's virtual filesystem, creating parent
@@ -22,7 +22,7 @@ export interface LivePermissionsPolicy {
22
22
  child_process?: LivePermissionScope<LivePatternPermissionRule>;
23
23
  process?: LivePermissionScope<LivePatternPermissionRule>;
24
24
  env?: LivePermissionScope<LivePatternPermissionRule>;
25
- binding?: LivePermissionScope<LivePatternPermissionRule>;
25
+ host_function?: LivePermissionScope<LivePatternPermissionRule>;
26
26
  }
27
27
  export declare function toGeneratedPermissionsPolicy(policy: LivePermissionsPolicy | undefined): protocol.PermissionsPolicy | null;
28
28
  export declare function toGeneratedFilesystemPermissionScope(scope: LivePermissionScope<LiveFsPermissionRule>): protocol.FsPermissionScope;
@@ -19,9 +19,9 @@ export function toGeneratedPermissionsPolicy(policy) {
19
19
  env: policy.env === undefined
20
20
  ? null
21
21
  : toGeneratedPatternPermissionScope(policy.env),
22
- binding: policy.binding === undefined
22
+ hostFunction: policy.host_function === undefined
23
23
  ? null
24
- : toGeneratedPatternPermissionScope(policy.binding),
24
+ : toGeneratedPatternPermissionScope(policy.host_function),
25
25
  };
26
26
  }
27
27
  export function toGeneratedFilesystemPermissionScope(scope) {
package/dist/process.d.ts CHANGED
@@ -12,7 +12,13 @@ export declare class StdioSidecarProcess {
12
12
  readonly child: ChildProcessWithoutNullStreams;
13
13
  readonly control: Duplex | null;
14
14
  readonly combinedStdio: boolean;
15
- private readonly stderrChunks;
15
+ /** First bytes of sidecar stderr; the root cause usually appears here. */
16
+ private readonly stderrHead;
17
+ private stderrHeadBytes;
18
+ /** Most recent bytes of sidecar stderr; the fatal error appears here. */
19
+ private readonly stderrTail;
20
+ private stderrTailBytes;
21
+ private stderrDroppedBytes;
16
22
  private readonly exitListeners;
17
23
  private readonly errorListeners;
18
24
  private constructor();
@@ -20,6 +26,12 @@ export declare class StdioSidecarProcess {
20
26
  static fromChild(child: ChildProcessWithoutNullStreams, control?: Duplex | null): StdioSidecarProcess;
21
27
  onExit(handler: (error: SidecarProcessExited) => void): () => void;
22
28
  onError(handler: (error: SidecarProcessError) => void): () => void;
29
+ /**
30
+ * Retain the first STDERR_HEAD_MAX_BYTES and the most recent
31
+ * STDERR_TAIL_MAX_BYTES of sidecar stderr, counting everything in between
32
+ * as dropped.
33
+ */
34
+ private retainStderr;
23
35
  stderrText(): string;
24
36
  currentExitError(): SidecarProcessExited | null;
25
37
  waitForExit(timeoutMs: number): Promise<number | null>;
package/dist/process.js CHANGED
@@ -1,19 +1,39 @@
1
1
  import { spawn } from "node:child_process";
2
2
  export { SidecarProcessError, SidecarProcessExited, } from "./sidecar-errors.js";
3
3
  import { SidecarProcessError, SidecarProcessExited } from "./sidecar-errors.js";
4
+ /**
5
+ * Bounds on the sidecar stderr excerpt retained for SidecarProcessExited and
6
+ * SidecarProcessError. Live stderr is always forwarded to host stderr, so these
7
+ * bound only the postmortem copy. Sidecar diagnostics can be guest-triggered,
8
+ * so the retained copy must never grow without limit.
9
+ */
10
+ const STDERR_HEAD_MAX_BYTES = 16 * 1024;
11
+ const STDERR_TAIL_MAX_BYTES = 48 * 1024;
4
12
  export class StdioSidecarProcess {
5
13
  child;
6
14
  control;
7
15
  combinedStdio;
8
- stderrChunks = [];
16
+ /** First bytes of sidecar stderr; the root cause usually appears here. */
17
+ stderrHead = [];
18
+ stderrHeadBytes = 0;
19
+ /** Most recent bytes of sidecar stderr; the fatal error appears here. */
20
+ stderrTail = [];
21
+ stderrTailBytes = 0;
22
+ stderrDroppedBytes = 0;
9
23
  exitListeners = new Set();
10
24
  errorListeners = new Set();
11
25
  constructor(child, control) {
12
26
  this.child = child;
13
27
  this.control = control;
14
28
  this.combinedStdio = control === null;
29
+ // Forward live sidecar stderr so warnings from a sidecar that survives a
30
+ // guest-triggered failure stay host-visible. This matches the Rust
31
+ // client, which spawns the sidecar with inherited stderr. Only a bounded
32
+ // excerpt is retained for exit/error reports.
15
33
  this.child.stderr.on("data", (chunk) => {
16
- this.stderrChunks.push(typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk));
34
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
35
+ process.stderr.write(buffer);
36
+ this.retainStderr(buffer);
17
37
  });
18
38
  this.child.on("exit", (code, signal) => {
19
39
  const error = new SidecarProcessExited({
@@ -67,8 +87,50 @@ export class StdioSidecarProcess {
67
87
  this.errorListeners.delete(handler);
68
88
  };
69
89
  }
90
+ /**
91
+ * Retain the first STDERR_HEAD_MAX_BYTES and the most recent
92
+ * STDERR_TAIL_MAX_BYTES of sidecar stderr, counting everything in between
93
+ * as dropped.
94
+ */
95
+ retainStderr(buffer) {
96
+ let rest = buffer;
97
+ const headRoom = STDERR_HEAD_MAX_BYTES - this.stderrHeadBytes;
98
+ if (headRoom > 0) {
99
+ const take = rest.subarray(0, headRoom);
100
+ this.stderrHead.push(take);
101
+ this.stderrHeadBytes += take.length;
102
+ rest = rest.subarray(take.length);
103
+ }
104
+ if (rest.length === 0)
105
+ return;
106
+ this.stderrTail.push(rest);
107
+ this.stderrTailBytes += rest.length;
108
+ while (this.stderrTailBytes > STDERR_TAIL_MAX_BYTES) {
109
+ const oldest = this.stderrTail[0];
110
+ const excess = this.stderrTailBytes - STDERR_TAIL_MAX_BYTES;
111
+ if (oldest.length <= excess) {
112
+ this.stderrTail.shift();
113
+ this.stderrTailBytes -= oldest.length;
114
+ this.stderrDroppedBytes += oldest.length;
115
+ }
116
+ else {
117
+ // Trim within the chunk so the bound holds for any chunking.
118
+ this.stderrTail[0] = oldest.subarray(excess);
119
+ this.stderrTailBytes -= excess;
120
+ this.stderrDroppedBytes += excess;
121
+ }
122
+ }
123
+ }
70
124
  stderrText() {
71
- return Buffer.concat(this.stderrChunks).toString("utf8").trim();
125
+ const head = Buffer.concat(this.stderrHead).toString("utf8");
126
+ const tail = Buffer.concat(this.stderrTail).toString("utf8");
127
+ const gap = this.stderrDroppedBytes > 0
128
+ ? `\n... [${this.stderrDroppedBytes} bytes of sidecar stderr dropped; ` +
129
+ `retained the first ${STDERR_HEAD_MAX_BYTES} and last ` +
130
+ `${STDERR_TAIL_MAX_BYTES} bytes; the full output was forwarded ` +
131
+ `to host stderr] ...\n`
132
+ : "";
133
+ return `${head}${gap}${tail}`.trim();
72
134
  }
73
135
  currentExitError() {
74
136
  if (this.child.exitCode === null && this.child.signalCode === null) {
@@ -42,7 +42,7 @@ export type LiveRequestPayload = {
42
42
  packages?: LivePackageDescriptor[];
43
43
  packages_mount_at?: string;
44
44
  bootstrap_commands?: string[];
45
- binding_shim_commands?: string[];
45
+ host_function_shim_commands?: string[];
46
46
  } | {
47
47
  type: "link_package";
48
48
  package: LivePackageDescriptor;
@@ -58,7 +58,7 @@ export function toGeneratedRequestPayload(payload) {
58
58
  packages: (payload.packages ?? []).map(toGeneratedPackageDescriptor),
59
59
  packagesMountAt: payload.packages_mount_at ?? "",
60
60
  bootstrapCommands: payload.bootstrap_commands ?? [],
61
- bindingShimCommands: payload.binding_shim_commands ?? [],
61
+ hostFunctionShimCommands: payload.host_function_shim_commands ?? [],
62
62
  },
63
63
  };
64
64
  case "link_package":
@@ -176,7 +176,7 @@ export interface SidecarPermissionsPolicy {
176
176
  childProcess?: SidecarPermissionScope<SidecarPatternPermissionRule>;
177
177
  process?: SidecarPermissionScope<SidecarPatternPermissionRule>;
178
178
  env?: SidecarPermissionScope<SidecarPatternPermissionRule>;
179
- binding?: SidecarPermissionScope<SidecarPatternPermissionRule>;
179
+ hostFunction?: SidecarPermissionScope<SidecarPatternPermissionRule>;
180
180
  }
181
181
  export interface SidecarProjectedModuleDescriptor {
182
182
  packageName: string;
@@ -247,7 +247,7 @@ export declare class SidecarProcess {
247
247
  packages?: SidecarPackageDescriptor[];
248
248
  packagesMountAt?: string;
249
249
  bootstrapCommands?: string[];
250
- bindingShimCommands?: string[];
250
+ hostFunctionShimCommands?: string[];
251
251
  }): Promise<SidecarVmConfiguredResponse>;
252
252
  /**
253
253
  * Runtime dynamic `linkSoftware`: project one package into the live
@@ -147,7 +147,7 @@ export class SidecarProcess {
147
147
  ? { packages_mount_at: options.packagesMountAt }
148
148
  : {}),
149
149
  bootstrap_commands: options.bootstrapCommands ?? [],
150
- binding_shim_commands: options.bindingShimCommands ?? [],
150
+ host_function_shim_commands: options.hostFunctionShimCommands ?? [],
151
151
  },
152
152
  });
153
153
  if (response.payload.type !== "vm_configured") {
@@ -1074,7 +1074,7 @@ function toWirePermissionsPolicy(policy) {
1074
1074
  child_process: policy.childProcess,
1075
1075
  process: policy.process,
1076
1076
  env: policy.env,
1077
- binding: policy.binding,
1077
+ host_function: policy.hostFunction,
1078
1078
  };
1079
1079
  }
1080
1080
  function toWireProjectedModuleDescriptor(descriptor) {
@@ -96,7 +96,7 @@ export type NetworkPermissions = PermissionMode | RulePermissions<PatternPermiss
96
96
  export type ChildProcessPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
97
97
  export type ProcessPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
98
98
  export type EnvPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
99
- export type BindingPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
99
+ export type HostFunctionPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
100
100
  export interface ProcessInfo {
101
101
  pid: number;
102
102
  ppid: number;
@@ -185,43 +185,18 @@ export interface Permissions {
185
185
  childProcess?: ChildProcessPermissions;
186
186
  process?: ProcessPermissions;
187
187
  env?: EnvPermissions;
188
- binding?: BindingPermissions;
189
- }
190
- /** A worked example shown alongside a registered binding. */
191
- export interface BindingExample {
192
- /** What this example demonstrates. */
193
- description: string;
194
- /** Example input matching the binding's input schema. */
195
- input: unknown;
188
+ hostFunction?: HostFunctionPermissions;
196
189
  }
197
190
  /**
198
- * A host-side binding that guest code can invoke as a shell command. The guest
199
- * runs the binding by name and the invocation round-trips back to the host JS
200
- * `handler`, whose return value is passed back to the guest. Bindings never run
191
+ * A host-side function that guest code can invoke as a shell command. The guest
192
+ * runs the host function by name and the invocation round-trips back to the host JS
193
+ * `handler`, whose return value is passed back to the guest. Host functions never run
201
194
  * inside the guest: they execute on the host, so they are the bridge for giving
202
195
  * sandboxed guest code controlled, named capabilities (the kind AI agents call
203
196
  * as tools).
204
197
  */
205
- export interface BindingDefinition {
206
- /** Human-readable description of what the binding does. */
207
- description: string;
208
- /** JSON Schema describing the binding's input. */
209
- inputSchema: object;
210
- /** Abort the invocation after this many milliseconds. */
211
- timeoutMs?: number;
212
- /** Worked examples shown alongside the binding. */
213
- examples?: BindingExample[];
214
- /**
215
- * Extra command names the guest can use to invoke this binding, in addition
216
- * to the key it is registered under.
217
- */
218
- commandAliases?: string[];
219
- /**
220
- * Host handler invoked when guest code runs the binding. Receives the parsed
221
- * input and returns a JSON-serializable result delivered back to the guest.
222
- */
223
- handler: (input: unknown) => unknown | Promise<unknown>;
224
- }
198
+ import { type HostFunctionCollections } from "./host-functions.js";
199
+ export type { HostFunction, HostFunctionCollection, HostFunctionCollections, HostFunctionExample, } from "./host-functions.js";
225
200
  export interface ResourceBudgets {
226
201
  maxOutputBytes?: number;
227
202
  maxBridgeCalls?: number;
@@ -347,7 +322,7 @@ export interface Kernel extends KernelInterface {
347
322
  streamId?: string;
348
323
  maxBytes?: number;
349
324
  }): Promise<string>;
350
- registerBindings(bindings: Record<string, BindingDefinition>): Promise<void>;
325
+ registerHostFunctions(hostFunctions: HostFunctionCollections): Promise<void>;
351
326
  getResourceSnapshot(): Promise<{
352
327
  runningProcesses: number;
353
328
  exitedProcesses: number;
@@ -390,10 +365,10 @@ export interface Kernel extends KernelInterface {
390
365
  readonly timerTable: Record<string, never>;
391
366
  readonly zombieTimerCount: number;
392
367
  }
393
- export interface BindingTree {
394
- [key: string]: BindingFunction | BindingTree;
368
+ export interface HostFunctionTree {
369
+ [key: string]: HostFunctionHandler | HostFunctionTree;
395
370
  }
396
- export type BindingFunction = (...args: unknown[]) => unknown;
371
+ export type HostFunctionHandler = (...args: unknown[]) => unknown;
397
372
  export interface ModuleAccessOptions {
398
373
  cwd?: string;
399
374
  }
@@ -415,7 +390,7 @@ export interface NodeRuntimeOptions {
415
390
  permissions?: Partial<Permissions>;
416
391
  memoryLimit?: number;
417
392
  moduleAccessPaths?: string[];
418
- bindings?: BindingTree;
393
+ hostFunctions?: HostFunctionTree;
419
394
  loopbackExemptPorts?: number[];
420
395
  moduleAccessCwd?: string;
421
396
  packageRoots?: Array<{