Agentic-Dev

tool-exec

Governed tool-call / sandboxed-exec primitive, a default-deny command allowlist, Zod-strict argv validation, and execFile arg-arrays only, never a shell.

@caisson/tool-exec is the security floor Agentic-Dev's tool layer stands on: a governed gate between an agent's tool call and a real process spawn. A call names a registered logical command; anything unregistered is refused before anything spawns.

What it does

  • Default-deny command allowlist. createToolExec({ allowlist }): an empty or absent allowlist refuses every call, fail-closed. An unregistered name throws NotFoundError before a process is spawned.
  • execFile arg-arrays only. Never execSync/exec/shell: true, never a concatenated command string. Each registered command declares a real executable plus a Zod-.strict() schema its args must satisfy; args are validated with parseStrict and the validated result becomes the exact argv array passed to execFile: no agent-supplied value ever reaches a shell.
  • Structured argument provenance. Every call returns { command, args, exitCode, stdout, stderr, ok, reason?, at }, a plain-data audit record, output bounded to 64KB. A non-zero exit resolves in the record rather than throwing; only an unregistered name or a schema failure throws.
  • Everything injected. cwd, timeoutMs, the spawn seam (execFn), and the clock (now) are all config, no module-level secrets or constants for endpoints or executables.

Install

bun add @caisson/tool-exec

Quickstart

import { z } from "zod";
import { createToolExec } from "@caisson/tool-exec";

const toolExec = createToolExec({
  allowlist: [
    {
      name: "git-status",
      command: "/usr/bin/git",
      argsSchema: z.array(z.string()).max(1).default(["status"]),
    },
  ],
  cwd: "/repo",
  timeoutMs: 30_000,
});

const result = await toolExec.run("git-status", ["status"], "agent-turn-14");
// result.ok / result.exitCode / result.stdout / result.args (the resolved argv array)

A name not on the allowlist, or args that fail the schema, throw before any process spawns.

Configuration

ToolExecConfig:

  • allowlist: CommandSpec[]: required. Each entry is { name, command, argsSchema }.
  • cwd?: string: defaults to process.cwd().
  • timeoutMs?: number: defaults to 30_000.
  • execFn?: ExecFn: inject a spawn double for tests; defaults to a real execFile call.
  • now?: () => number: inject a clock for testable provenance timestamps; defaults to Date.now.

Composition

tool-exec depends on @caisson/kernel for NotFoundError and parseStrict: the same default-deny and schema-validation primitives the rest of the base substrate uses, so a tool call's failure modes look like every other kernel-governed boundary. It's the gate Agentic-Dev's agent kernel and agent-runner reach for whenever a running agent needs to touch a real executable.

Commercial, Agentic-Dev

@caisson/tool-exec ships under the Agentic-Dev bundle license.