Skip to main content
Smithers provides five built-in tools that are compatible with the AI SDK tool interface. These tools give AI agents the ability to read, write, search, and modify files, as well as execute shell commands — all within a security sandbox.

Import

import { tools, read, write, edit, grep, bash } from "smithers-orchestrator/tools";
The tools object is a convenience bundle containing all five tools keyed by name:
const { read, write, edit, grep, bash } = tools;

Sandboxing

All tools are sandboxed to rootDir, which defaults to the workflow directory. Every file path provided to a tool is resolved relative to this root, and any attempt to escape it (including via symlinks) is rejected with an error. The sandbox enforces:
  • Path resolution. Relative paths are resolved against rootDir. Absolute paths are validated to ensure they fall within the root.
  • Symlink traversal. Symlinks that resolve outside the sandbox root are rejected.
  • Output size limits. All tool outputs are truncated to maxOutputBytes (default: 200KB / 200,000 bytes). Files and command outputs exceeding this limit are either truncated or rejected.
  • Timeouts. Shell commands (bash, grep) have a default timeout of 60 seconds. Processes that exceed the timeout are killed with SIGKILL.
  • Network blocking. The bash tool blocks network-accessing executables by default. See the bash tool section for details.

Tool Call Logging

Every tool invocation is logged to the _smithers_tool_calls database table with the following fields:
FieldDescription
runIdThe current workflow run ID
nodeIdThe task node that invoked the tool
iterationThe Ralph loop iteration
attemptThe retry attempt number
seqSequential tool call counter within the task
toolNameName of the tool (read, write, edit, grep, bash)
inputJsonSerialized input arguments
outputJsonSerialized output (truncated if over maxOutputBytes)
startedAtMsTimestamp when the tool call began
finishedAtMsTimestamp when the tool call completed
status"success" or "error"
errorJsonSerialized error details (if status is "error")

read

Read a file from the filesystem. Input schema:
{
  path: string   // File path, relative to rootDir or absolute
}
Returns: The file contents as a UTF-8 string. Behavior:
  • The file is resolved against the sandbox root and validated against symlink escapes.
  • If the file size exceeds maxOutputBytes (default 200KB), the tool throws "File too large".
  • Output is truncated to maxOutputBytes if needed.
Example:
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { read, grep } from "smithers-orchestrator/tools";

const codeAgent = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  tools: { read, grep },
});
<Task id="review" output="review" agent={codeAgent}>
  Read the file src/auth.ts and identify any security vulnerabilities.
</Task>

write

Write content to a file, creating parent directories as needed. Input schema:
{
  path: string      // File path, relative to rootDir or absolute
  content: string   // Content to write
}
Returns: "ok" Behavior:
  • Parent directories are created recursively if they do not exist.
  • If the content byte size exceeds maxOutputBytes (default 200KB), the tool throws "Content too large".
  • The content hash (SHA-256) and byte size are logged, but the full content is not stored in the tool call log to conserve space.
Example:
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { write, read } from "smithers-orchestrator/tools";

const writerAgent = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  tools: { write, read },
});

edit

Apply a unified diff patch to an existing file. Input schema:
{
  path: string    // File path to patch
  patch: string   // Unified diff format patch
}
Returns: "ok" Behavior:
  • The file must already exist. The current contents are read, the patch is applied using the diff library’s applyPatch function, and the result is written back.
  • If the patch byte size exceeds maxOutputBytes, the tool throws "Patch too large".
  • If the file size exceeds maxOutputBytes, the tool throws "File too large".
  • If the patch cannot be applied (e.g., context lines do not match), the tool throws "Failed to apply patch".
  • The patch hash (SHA-256) and byte size are logged.
Example unified diff format:
--- a/src/auth.ts
+++ b/src/auth.ts
@@ -10,3 +10,4 @@
   const token = jwt.sign(payload, secret);
+  logger.info("Token issued", { userId: payload.sub });
   return token;

grep

Search for a regex pattern in files using ripgrep (rg). Input schema:
{
  pattern: string    // Regex pattern to search for
  path?: string      // Directory or file to search (default: rootDir)
}
Returns: Matching lines with file paths and line numbers, as produced by rg -n. Behavior:
  • The search path is resolved and sandboxed to rootDir.
  • Uses ripgrep (rg) under the hood with the -n flag for line numbers.
  • Output is truncated to maxOutputBytes (default 200KB).
  • If the command times out (default 60s), the process is killed and an error is thrown.
  • If rg exits with code 2 (error), the stderr output is thrown as an error.
  • Exit code 1 (no matches) returns an empty string, not an error.
Prerequisite: ripgrep must be installed and available in the system PATH. Example output:
src/auth.ts:15:  if (token.expired()) {
src/auth.ts:42:  validateToken(token);
tests/auth.test.ts:8:  const token = createTestToken();

bash

Execute a shell command. Input schema:
{
  cmd: string                     // Executable name or command
  args?: string[]                 // Command arguments
  opts?: { cwd?: string }         // Working directory (sandboxed to rootDir)
}
Returns: Combined stdout and stderr as a string. Behavior:
  • The working directory defaults to rootDir and is sandboxed. If opts.cwd is provided, it is resolved and validated against the sandbox.
  • Stdout and stderr are captured separately and combined in the output.
  • Output is truncated to maxOutputBytes (default 200KB).
  • Default timeout is 60 seconds. Processes exceeding the timeout are killed with SIGKILL via process group.
  • Non-zero exit codes throw an error with the exit code or signal name.

Network Blocking

By default, the bash tool blocks commands that access the network. This is controlled by the allowNetwork option in RunOptions, the CLI (--allow-network), or the server configuration. When allowNetwork is false (the default), the following are blocked: Blocked executables:
CategoryCommands
HTTP clientscurl, wget
Remote shellsssh, scp, sftp, ftp, telnet
Network toolsnc / netcat, ping, traceroute, dig, nslookup, nmap, openssl
Package managersnpm, bun, pip, pip3, pnpm, yarn, apt, apt-get, brew, cargo, go, gem
Version control (remote ops)git push, git pull, git fetch, git clone, git remote
Otherhg, svn, powershell, pwsh
URL detection: Any argument containing ://, www., http://, https://, ws://, wss://, git@, or ssh:// is blocked. IP detection: Arguments matching an IP address pattern (e.g., 192.168.1.1:8080) are blocked. Proxy detection: Arguments containing --proxy, http_proxy, or https_proxy are blocked. Local git commands (e.g., git status, git diff, git log) are allowed even when network access is disabled. Example:
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { bash } from "smithers-orchestrator/tools";

const devAgent = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  tools: { bash },
});
<Task id="lint" output="lint" agent={devAgent}>
  Run the linter on src/ and report any issues.
</Task>

Using Tools with Agents

The standard pattern is to pass tools to an AI SDK agent, then assign that agent to a <Task>:
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { read, write, edit, grep, bash } from "smithers-orchestrator/tools";

const codeAgent = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  tools: { read, write, edit, grep, bash },
  instructions: "You are a senior engineer. Use the available tools to complete tasks.",
});

export default smithers((ctx) => (
  <Workflow name="refactor">
    <Task id="refactor" output="result" agent={codeAgent}>
      {`Refactor the function in ${ctx.input.file} to improve readability.`}
    </Task>
  </Workflow>
));
You can also pass the entire tools bundle:
import { ToolLoopAgent as Agent } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { tools } from "smithers-orchestrator/tools";

const fullAgent = new Agent({
  model: anthropic("claude-sonnet-4-20250514"),
  tools,
});

Configuration

Tool behavior is configured through RunOptions when calling runWorkflow or through the server/CLI:
OptionDefaultDescription
rootDirWorkflow directorySandbox root for all file operations
allowNetworkfalseAllow network-accessing executables in bash
maxOutputBytes200000 (200KB)Maximum output size for all tools
toolTimeoutMs60000 (60s)Timeout for bash and grep commands
const result = await runWorkflow(workflow, {
  input: { file: "src/auth.ts" },
  rootDir: "/home/project",
  allowNetwork: false,
  maxOutputBytes: 500_000,
  toolTimeoutMs: 120_000,
});