The Bash tool
# grade this stage (from your course directory)
npx theinference test --stage bash --watchThis is the tool that turns your agent from a file editor into something that can act: run the test suite, install a dependency, git diff, list a directory the way a human would. bash hands the model a shell — and a shell is the most dangerous tool in the box, because it can do anything the process can.
Three non-negotiables follow from that. The command runs with its cwd pinned to the sandbox (never the host's working directory). Its stdout, stderr, and exit code are captured and fed back as one observation. And it is killed at a hard wall-clock timeout — because at grade time a hanging command would hang the entire stage, and a graded stage can never hang.
bash("node test.js", 30000)
└─ spawn bash -c "node test.js" cwd = $TASK_DIR timeout = 30000ms
├─ exits in time → { content: stdout+stderr, is_error: status!==0, exit_code: status }
└─ still running → SIGKILL → { content: "timed out…", is_error: true, exit_code: 124 }
What to build
Implement Tools.bash
Implement bash(command, timeoutMs) on the Tools class. Run the command through a shell with cwd pinned to the sandbox root (this.taskDir), capture stdout and stderr concatenated into content, and return the process exit code. Set is_error when the exit code is non-zero. If the command doesn't finish within timeoutMs, kill it and return a timeout result (exit_code: 124) instead of waiting forever. Default timeoutMs to 30000.
/** `bash` — run with cwd pinned to the sandbox, captured output, hard timeout. */
bash(command: string, timeoutMs = 30000): ToolOutput & { exit_code: number } {
// spawn a shell with cwd = this.taskDir, capture stdout+stderr,
// kill at timeoutMs, and return { content, is_error, exit_code }
}
If bash can block, your whole stage can block, and a grader that hangs is a grader that fails everyone. Enforce the deadline yourself: with Node's spawnSync pass { timeout: timeoutMs } and check r.error.code === 'ETIMEDOUT'; if you drive it asynchronously instead, Promise.race the exec against a timer and SIGKILL the child on expiry. Either way, a runaway command resolves to a timeout result — it never lingers.
The command must run with its working directory set to the sandbox (this.taskDir) — relative paths like node test.js resolve there, not in whatever directory the agent process happens to live in. The full path-canonicalization guard arrives in Chapter 07; here the cwd pin is the floor under it.
program.ts bash seeds a sandbox, runs your bash(command) against the case fixture, and prints { content, exit_code }. The grader compares that JSON exactly — so cat hi.txt against a seeded hi.txt: "hello" must yield {"content":"hello","exit_code":0}. Capture and exit-code reporting are checked here; a companion process test confirms a hang is actually killed.