Tools · Execution¶
The workspace and shell tools. File operations route through the workspace executor; shell commands run inside the project container. See Tool Reference for how to read these signatures.
WorkspaceTools¶
Group workspace. All five tools are tier Standard — file I/O stays inside the workspace, carrying no network or escalation risk.
Every path argument is normalized before it reaches the container: surrounding whitespace is trimmed, a path written against the container's workspace root has that prefix removed, and a leading relative workspace/ segment is stripped too, so a file does not land one directory deeper than the plan asked for.
read_file, write_file, edit_file and revert_file also accept file_path as another name for path, and write_file accepts text for content. The Engine renames them when it parses the call, before any guard sees the arguments; when both names are present, path and content win.
read_file¶
read_file(path, start_line?, end_line?) → file contents
| Param | Type | Req | Description |
|---|---|---|---|
path |
string | yes | File path relative to workspace root |
start_line |
int | no | 1-based first line to read (default 1) |
end_line |
int | no | 1-based last line to read (default: the last line; past the end means the last line) |
Returns the raw file contents (or an Error… string). With a range it returns only those lines, each prefixed with its number (990| …) under a [path: lines A-B of N] header; lines are counted the way edit_file counts them, so a range read here is the range edit_file's start_line/end_line replace. A start past the end, or an end before the start, is an error that states the line count. A ranged read is never offloaded to .tool-output/; a whole-file read too large for the context still is, and its notice gives the file's line count and how to read a range of it. A path whose extension names an image, audio, video, document, spreadsheet, slide-deck or archive format is refused before it is read, and the refusal names list_files on its folder as the check that works; SVG, HTML and scripts are text and read normally. Description steers the agent away from calling it before edit_file/code_review, which read internally.
write_file¶
write_file(path, content) → status string
| Param | Type | Req | Description |
|---|---|---|---|
path |
string | yes | File path relative to workspace root |
content |
string | yes | Full file content to write |
Creates or overwrites. Captures a best-effort pre-write snapshot for revert_file.
Four guards sit in front of the write and can refuse it. They exist because a rewrite that damages a file is indistinguishable from a good one downstream — the step still reports that a productive tool fired:
- Shrink guard — refuses a write that would discard most of an existing file. Retention is
measured as the share of the existing content, by character, that survives; below
0.5the write is refused. Files under 400 bytes are not protected. -
Reference guard — refuses an HTML write that would leave a sibling script reaching for element ids the new markup no longer declares. Only regressions count: an id already missing before the write is not attributed to it, and only literal
getElementById('…')/querySelector('#…')lookups are checked, so a computed id is never reported. -
Binary-path guard — refuses text at a path whose extension names a binary format (images, archives, audio and video, fonts, compiled objects, SQLite files, and the 3D scenes and engine assets
.blend,.glb,.usdz,.3ds,.umapand.uasset). The refusal names the tool that does produce that file instead:comfyui_generateorcapture_referencefor an image,download_pdffor a PDF; for a 3D file it says that nothing in the workspace makes one and that a stand-in under its name is not the file..fbxstays writable, because the format has an ASCII form; the required-output gate reads its header instead. It is judged before the snapshot, because there is nothing to snapshot for a write that must not happen, and it applies toedit_fileon the same terms. -
Escaped-fence guard — refuses a Markdown write (
.md,.mdx,.markdown; thewrite_filecontent or theedit_filereplacement) in which a line opens with a backslash-escaped code fence,\`\`\`, which renders as literal backslashes instead of a code block. It refuses twice for the same path, naming the first such line; a third attempt has the escapes repaired and goes through. Logged as[ESCAPED-FENCE].
The shrink and reference guards refuse once, naming what would be lost and pointing at
edit_file; if the agent asks again for the same path the write is allowed, so a deliberate
rewrite is never blocked. Both fail closed — if the check itself errors, the write is refused with
the error, and that refusal does not count as the first ask. Refusals are logged as
[SHRINK-GUARD] and [REF-GUARD]. The binary-path guard has no second ask: a text tool never
produces those formats. The shrink guard also stands aside for a file the step owes at a
words range (a script such as show/episode-01-script.md words 400-520): every draft of such
a file is new prose, so retention would read as a rewrite on any cut, and the required-output
gate already governs its length. That exemption is per path, so a page owed beside the script
is still protected, and the skip is logged.
In a sequential team one more rule applies to write_file, edit_file and revert_file: a
stage may not change a file another stage owes (that stage's required_output) unless it owes the
file too. The refusal names the stage that owns the file and what this stage owes, and a fourth
attempt ends the turn; refusals are logged as [STAGE-LANE]. Files no stage owes are not
affected, and a checking stage keeps its own narrower rule (it writes only its report).
An agent that owes files also has a write phase: from eight rounds before the end of its
tool-round budget (round 5 at the earliest), or earlier when it tries to stop with files still owed, its research tools are
withdrawn and it is told which files it owes. From then on, a write_file or edit_file that
lands none of them while none is on disk yet comes back with "That is not the file you owe" and
the list still to write. Before the write phase a note or a helper file passes without comment. A repair turn whose owed files are all on disk already has no write phase and keeps its tools.
When the turn is about to end at its round cap, after eight read-only calls in a row, or on the same-tool streak or duplicate-call exit, while a file is still owed or the turn is a repair, it gets a final write phase instead of a closing reply: two more calls in which only its write tools run (write_file, edit_file, and the tool that makes an owed binary), told which paths it owes and, in a repair, why the turn was opened. Any other tool is refused. Checkers, re-plan turns and agents that owe nothing end as before.
edit_file¶
edit_file(path, search, replace, start_line?, end_line?) → status string
| Param | Type | Req | Description |
|---|---|---|---|
path |
string | yes | File path to edit |
search |
string | yes | Text to find. Exact match preferred; whitespace differences are tolerated. Empty to create a missing file or when using a line range |
replace |
string | yes | Text to replace with (the whole file content when creating or replacing the whole file) |
start_line |
int | no | 1-based first line of a range |
end_line |
int | no | 1-based last line of a range (omitted: the last line of the file when search is non-empty, so a search from start_line looks to the end; start_line when search is empty, where the range is the edit; past the end of the file means the last line). The agent-facing parameter description still reads "defaults to start_line" |
Matching is exact first, then whitespace-tolerant (line by line, ignoring indentation and spacing) when that matches exactly one place. With an empty search the tool creates the file if it does not exist; with a range it replaces those lines (empty search) or confines the search to them. A range from line 1 to the last line with an empty search is the sanctioned whole-file rewrite: an end_line past the end is clamped to the last line (the result says so), and because a line range states explicitly what is replaced, it is not put through the write_file shrink guard. A search that matches more than once is replaced everywhere, except when the inserted material would outweigh the text being edited (the replacement's length times the match count exceeds the file's, or the range's): that edit is refused, because a short search times a document-sized replacement is the document repeated, not an edit. The same refusal applies to a search that matches once when the replace opens with the file's own first line and runs to more than four times the lines of the search — a title line swapped for a whole regenerated document would leave the file holding the text twice, so the message names the whole-file form instead. A miss reports the nearest line so the agent can correct the search text without a re-read; when the failed search is at least half the file long, or the range is out of bounds, the message also spells out the whole-file form. The description tells the agent not to read_file first because the tool reads the file itself. The router accepts find as a legacy alias for search, and reads start_line/end_line given as numeric strings. Captures a pre-edit snapshot.
search_files¶
search_files(query?, path?, glob?, regex?, max_results?) → matches or file list
| Param | Type | Req | Default | Description |
|---|---|---|---|---|
query |
string | no | — | Text (or regex when regex=true) to find in file contents; omit to only list files |
path |
string | no | root | Directory to search, relative to the workspace root |
glob |
string | no | every file | File name pattern such as *.py or docs/**/*.md |
regex |
bool | no | false |
Treat query as a regular expression |
max_results |
int | no | 200 |
Maximum matching lines or files to return |
Read-only, output capped. With a query it returns matching lines as path:line: text grouped by file; without one it lists the files matching the glob. Runs find/grep inside the container, or walks the host filesystem when containers are off. Its description tells the agent to use it instead of run_shell with ls, find or grep.
list_files¶
list_files(path?, depth?) → directory tree
| Param | Type | Req | Default | Description |
|---|---|---|---|---|
path |
string | no | . (root) |
Directory path relative to workspace root |
depth |
int | no | 3 |
Max directory depth to list |
Returns a tree view. Description discourages calling it before write_file.
revert_file¶
revert_file(path) → result string
| Param | Type | Req | Description |
|---|---|---|---|
path |
string | yes | Same path passed to the original write_file/edit_file |
Restores the file to its pre-edit snapshot; deletes it if the file did not exist before the first write. Snapshots are taken on the first write to a path within a build run and are scoped to that run. Fails if no snapshot exists — including for a file that existed but could not be read when the write happened, which deliberately gets no snapshot rather than an error message stored as its content.
ShellTools¶
Group shell. Both tools are tier Elevated. run_shell additionally forces human approval (no supervisor fallback) when the command matches a destructive pattern — see tiers.
run_shell¶
run_shell(command, background?) → combined output
| Param | Type | Req | Default | Description |
|---|---|---|---|---|
command |
string | yes | — | Shell command to execute |
background |
bool | no | false |
Run in background and return immediately (for dev servers) |
Runs through the container executor, not the host shell. Returns combined stdout with stderr and exit code folded in (\nSTDERR:\n…, \nExit code: N). With background: true the command is wrapped as nohup <command> > /workspace/.server.log 2>&1 &, which can start only one plain executable, so a cd/export/source prefix, &&, ||, ;, a newline, a trailing & or a leading VAR= is refused without running, and the error names the working form. Wrap a compound command as sh -c '<script>'. Errors if there is no container context. Every invocation is recorded to the container_command_logs table.
read_logs¶
read_logs(source, target, lines?, filter?) → Markdown log block
| Param | Type | Req | Default | Description |
|---|---|---|---|---|
source |
string | yes | — | docker, systemd, or file |
target |
string | yes | — | Container name, systemd service name, or log file path |
lines |
int | no | 100 (clamped 1–2000) |
Number of lines to return |
filter |
string | no | — | Optional grep pattern to filter lines |
Returns a fenced log block (## Docker logs: / ## Systemd logs: / ## File log:) or an Error… string for an unknown source or missing target.
With source: "file" the target is a container path, and nothing on the Engine's host is opened. A path under /workspace, or a relative one, is read the way read_file reads it; any other absolute path (a background server's /tmp/server.log) is read inside the team container, and is refused when there is no container. A file too large to read, or one that cannot be read, returns an error pointing at run_shell with tail. The header says how much of the file it shows (last N of M lines).
Where to go next¶
- API · Containers & Snapshots — the HTTP exec/file surface these tools sit above.
- MCP Server Classes —
WorkspaceToolsandShellToolsdependencies. - §07 — MCP Architecture — why shell is Elevated.