Tool System
Everything an agent can do goes through the tool system: a central
registry of functions with JSON-Schema specs, an executor that enforces
mode/confirmation/rate-limit gates, and two pre-registered tool sets —
6 built-in tools plus 46 ported tools. The system plugs into the harness
via harness.use(ToolSystem()) (model_harness/tools/system.py:46).
Architecture
ToolSpec(model_harness/tools/dtos.py:117) — schema DTO for a tool:name,description, JSON-Schemaparameters,requiredlist, and theread_onlyflag.to_openai_tool()exports the OpenAI function-calling shape ({"type": "function", ...}).Tool(dtos.py:145) — a registered tool: spec + implementation callable + metadata (version,tags,timeout_sec,rate_limit_rpm,requires_confirmation,is_async).ToolRegistry(model_harness/tools/registry.py:27) — central catalog. Tools are stored as{name: {version: Tool}};get(name)returns the latest semver. Registration is decorator-based (@registry.register(name=..., tags=..., read_only=..., ...)); parameter schemas are generated from type hints byschema_gen.generate_tool_spec. Supports tag-based filtering and bulk export viaopenai_tools().ToolExecutor(model_harness/tools/executor.py:73) — runs tools in a thread pool (sync) or awaits them (async) with a per-tool timeout, JSON-Schema argument validation, optional rate limiting, a best-effort memory cap viatracemalloc, and the mode / policy / confirmation gates described below.ToolSystem(system.py:35) — facade implementing theIToolSystemprotocol; composes registry, executor,ToolCallHandler(conversation injection), andToolVersionManager. The constructor registers builtins and ported tools (register_builtinsdefaults to True;register_portedfollows it). It also bridges MCP servers into the registry as proxied tools named"{server}__{tool}"(stdio only, fail-open —system.py:176).ToolExecutionResult(dtos.py:172) — per-call result:success,output,error,duration_ms,confirmed.
flowchart LR
Agent[BaseAgent] -->|execute_tool name args mode confirm| TS[ToolSystem]
TS --> Reg[ToolRegistry]
TS --> Ex[ToolExecutor]
Ex --> ModeGate[Mode gate]
ModeGate --> Policy[Policy hook]
Policy --> Confirm[Confirmation gate]
Confirm --> Rate[Rate limit]
Rate --> Run[Run with timeout and memory cap]
Run --> Result[ToolExecutionResult]
Reg -.registers.-> Builtins[6 builtin tools]
Reg -.registers.-> Ported[46 ported tools]
Reg -.bridges.-> MCP[MCP servers]
read_only vs requires_confirmation
Two independent flags control what a tool may do:
read_only=Trueon the spec means no side effects (dtos.py:123). Read-only agent modes (EXPLORE, PLAN) only allowread_onlytools.requires_confirmation=Trueon theToolmeans the executor pauses the call and returnserror="Confirmation required", confirmed=Falseunless invoked withconfirm=True(executor.py:171).
Enforcement by mode (model_harness/agents/modes.py):
| Mode | Policy |
|---|---|
EXPLORE |
read-only tools only, except scoped writes into docs/explore/ |
PLAN |
read-only tools only, except scoped writes into docs/plan/; ask_user is exposed |
WRITE_TEST |
all tools; write/execute tools still confirmation-gated unless auto-confirm is on |
WRITE_NO_TEST |
like WRITE_TEST, but the prompt tells the model not to run tests/validators/builds |
The executor is the enforcement point (executor.py:146): in a
read-only mode, a non-read-only tool is rejected with a structured
error — unless the mode policy declares scoped_write_dirs AND the
tool is in the vetted _SCOPED_WRITE_TOOLS set (write_file,
append_to_file, insert_lines, replace_in_file, move_file,
copy_file, apply_diff, edit_file_line, remove_line_range —
executor.py:54) AND every recognized path argument resolves inside a
scoped directory. Tools without a caller-supplied path
(execute_python, run_command, git mutations, snapshot_file,
generate_image) stay fully blocked. The gate fails closed on any
ambiguity (executor.py:337).
An optional policy_hook(tool, arguments) -> Optional[str] is consulted
between the mode gate and the confirmation gate; a non-empty return
vetoes the call, and hook exceptions fail open (executor.py:159).
On the agent side, BaseAgent (model_harness/agents/base.py) passes
confirm=auto_confirm_tools (default False) and mode=self.mode into
execute_tool (base.py:1270). When a call comes back
confirmed=False and a confirmation_handler is installed by the web
layer, the agent asks the user and re-executes with confirm=True on
approval (base.py:1283). The mode's system_prompt_addendum is
appended to the system message so the model knows its constraints.
Path-traversal safety
All filesystem tools resolve paths through
_resolve_path() (builtin.py:52; ported tools use the mirrored
helper in ported/common.py):
- The workspace root is: request-scoped override (guest sandbox) →
explicit
WORKSPACE_ROOT→Path.cwd()dynamically (builtin.py:34). - Relative paths resolve against the workspace root; absolute paths must already be inside it.
resolved.relative_to(workspace)failing raisesPathTraversalError(dtos.py:75, codePATH_TRAVERSAL).- The executor deliberately re-raises
PathTraversalErrorinstead of converting it to a tool error (executor.py:255) — a workspace escape is a security error that must propagate.
Built-in tools
Registered by register_builtin_tools() (builtin.py:200). All are
path-traversal safe.
read_file(filepath, max_lines)— read a UTF-8 text file;max_linestruncates and appends a "... (N more lines)" note.read_only(builtin.py:217).read_pdf(filepath, start_page=1, max_pages=40, max_chars=20000)— extract page-numbered text viapypdf(builtin.py:244). Long PDFs are read in multiple passes: output stops atmax_pages/max_charsand the truncation note names the exactstart_pagefor the next pass (start_page={end + 1}). Encrypted PDFs are tried with an empty password; scanned image-only PDFs return a plain "no extractable text, no OCR" message.read_only, 30 s timeout.write_file(filepath, content)— write UTF-8 text, creating parent directories. If the file already existed, the return string carries " (overwrote an existing file)" so the agent cannot silently clobber a file (builtin.py:340).requires_confirmation=True.list_files(directory, pattern, recursive)— glob listing with workspace-relative paths, sizes, and file/dir icons.read_only.web_search(query, top_k=5)— POSTs to the Tavily API (https://api.tavily.com/search) usingTAVILY_API_KEYfrom the environment; returns numbered title/URL/snippet results (snippets truncated to 400 chars,top_kcapped at 10). Without the key it returns an "unavailable" message instead of failing.read_only, 20 s timeout (builtin.py:384).execute_python(code, timeout_sec=15)— runs code in a restricted sandbox:execwith a globals dict whose__builtins__is a whitelist of safe builtins — no__import__, noopen, noeval/exec, no file I/O or network (SAFE_BUILTINS,builtin.py:119). Stdout is captured and returned. Ten known escape vectors are catalogued inSANDBOX_ESCAPE_VECTORS(builtin.py:90) for security tests.requires_confirmation=True, 30 s tool timeout. Note: this sandbox restricts builtins but is not a process/OS-level jail; the executor's timeout and memory cap still apply.
ask_user (schema only)
ask_user is not in the registry and never executes in the
sandbox. Only its ToolSpec exists (ASK_USER_TOOL_SPEC,
builtin.py:158), with question, optional choices, and multi
parameters. BaseAgent appends the schema whenever a
question_handler is installed (web-driven runs, all modes —
base.py:662) and intercepts calls to route them through the handler
(base.py:1255), which the web layer turns into an SSE question
event plus a wait for the user's answer. Without a handler the tool
reports itself unavailable.
Ported tools (46)
register_ported_tools() (ported/__init__.py:48) registers 46 tools
adapted from the Kucatoo source app — plain synchronous functions that
run in the executor's thread pool. Categories:
| Module | Count | Tools | Flags |
|---|---|---|---|
fileops.py |
10 | read_file_lines, find_files, file_tree, file_exists, move_file, copy_file, delete_file, append_to_file, insert_lines, replace_in_file | 4 read-only; 6 confirmation |
editing.py |
5 | apply_diff, edit_file_line, remove_line_range, compare_file_content, snapshot_file | compare is read-only; 4 confirmation |
gitops.py |
9 | git_status, git_diff, git_log, git_branch; git_add, git_commit, git_create_branch, git_snapshot, git_revert_snapshot | 4 read-only; 5 confirmation |
execution.py |
6 | run_command, run_script, run_python_script, run_tests; detect_shell, check_permissions | 2 read-only; 4 confirmation |
validation.py |
4 | validate_python_syntax, check_structure, validate_code_quality, run_linter | all read-only |
search.py |
5 | grep_search, find_definition, find_references, analyze_stacktrace, detect_orphaned_code | all read-only |
envdetect.py |
2 | detect_package_manager, detect_tech_stack | all read-only |
imagegen.py |
2 | generate_image, edit_image (DashScope qwen-image-*) | confirmation |
model_harness/lsp/tools.py |
3 | lsp_definition, lsp_references, lsp_diagnostics | all read-only; graceful "Error:" strings when no server binary is installed |
videogen (MiniMax video) lives in the ported package but is not an
agent tool — renders take minutes; it is the web Video page entry
point (ported/__init__.py:18).
Full tool table
The table below is generated by scripts/wiki_tables.py from the live
registry (builtins + ported) — do not hand-edit it.
<!-- AUTO:tools -->
| Tool | Read-only | Needs confirmation | Description |
|---|---|---|---|
analyze_stacktrace |
yes | no | Parse a stacktrace and extract code locations. Recognizes Python traceback frames ('File "...", line N, in ... |
append_to_file |
no | yes | Append content to the end of a file. Creates the file (and parent directories) if it does not exist. |
apply_diff |
no | yes | Apply a unified diff (standard '@@ -a,b +c,d @@' hunks with context/'-'/'+' lines; '---'/'+++' headers opti... |
check_permissions |
yes | no | Check whether a workspace path is readable/writable via os.access. If the path does not exist yet, the near... |
check_structure |
yes | no | Show an AST outline of a Python file: top-level and nested classes/functions with line numbers and indentat... |
compare_file_content |
yes | no | Compare proposed content with the file on disk. Reports 'identical' (no write needed) or 'differs' plus a s... |
copy_file |
no | yes | Copy a file within the workspace. Creates parent directories of the destination; refuses to overwrite an ex... |
delete_file |
no | yes | Delete a single file from the workspace. Refuses to delete directories. |
detect_orphaned_code |
yes | no | Detect suspicious trailing fragments in a Python file — incomplete def/class blocks at EOF, syntax errors n... |
detect_package_manager |
yes | no | Detect which package managers apply to the workspace: which manifest files are present (requirements.txt, p... |
detect_shell |
yes | no | Report the OS platform, default shell, which shells are on PATH, and the Python version. |
detect_tech_stack |
yes | no | Detect the likely languages, frameworks, ORM, and database of the workspace project. Combines manifest insp... |
edit_file_line |
no | yes | Replace exactly one 1-based line of a file with new content. Safer than full rewrites for small fixes. Show... |
edit_image |
no | yes | Edit an existing workspace image per a text instruction via DashScope (qwen-image-edit-plus) and save the r... |
execute_python |
no | yes | Execute Python code in a restricted sandbox. No imports, no file I/O, no network access. Safe builtins only. |
file_exists |
yes | no | Check whether a path exists in the workspace, and report whether it is a file or directory and its size in ... |
file_tree |
yes | no | Show an indented ASCII tree of directories and files under a directory, capped by max_depth. Skips noisy di... |
find_definition |
yes | no | Find where a symbol is defined in the workspace (lean local replacement for an LSP resolve-symbol call). Pa... |
find_files |
yes | no | Recursively find files matching a glob pattern under a directory, capped by max_depth. Skips noisy dirs (.g... |
find_references |
yes | no | Find references to a symbol in the workspace (lean local replacement for an LSP find-references call). Word... |
generate_image |
no | yes | Generate an image from a text prompt via DashScope (qwen-image-max) and save it under images/ in the worksp... |
git_add |
no | yes | Stage files for the next commit (git add). Paths must be inside the workspace. |
git_branch |
yes | no | List local branches (current one marked with *). Set show_all=True to include remote-tracking branches. Lis... |
git_commit |
no | yes | Commit staged changes with the given message. A fallback committer identity is supplied so repos without gi... |
git_create_branch |
no | yes | Create a new branch. With checkout=True (default) also switch to it (git checkout -b). |
git_diff |
yes | no | Show uncommitted changes (git diff). Set staged=True for staged changes, and optionally limit to a single f... |
git_log |
yes | no | Show recent commit history. oneline=True gives one line per commit; otherwise includes per-commit file stat... |
git_revert_snapshot |
no | yes | Hard-reset the workspace to a previous snapshot commit (git reset --hard <sha>). Uncommitted changes are di... |
git_snapshot |
no | yes | Take a snapshot of the whole workspace: initializes a git repo if needed, stages everything (git add -A) an... |
git_status |
yes | no | Show the working tree status of the workspace git repository, with the current branch. Use short=True for a... |
grep_search |
yes | no | Search file contents with a regular expression (like grep). Searches files under a workspace directory whos... |
insert_lines |
no | yes | Insert content into a file so its first line becomes the 1-based line_number. line_number may be one past t... |
list_files |
yes | no | List files in a directory matching a glob pattern. Use recursive=True to search subdirectories. |
lsp_definition |
yes | no | Go to the definition of an identifier via a language server (textDocument/definition). The identifier is lo... |
lsp_diagnostics |
yes | no | Type-check a file via its language server by opening the buffer and collecting textDocument/publishDiagnost... |
lsp_references |
yes | no | Find references to an identifier via a language server (textDocument/references). The identifier is located... |
move_file |
no | yes | Move or rename a file within the workspace. Refuses to overwrite an existing destination. |
read_file |
yes | no | Read the contents of a file from the workspace. If max_lines is set, returns only the first N lines. |
read_file_lines |
yes | no | Read a file with a 1-based inclusive line range. Each line is prefixed with its line number. Omit end_line ... |
read_pdf |
yes | no | Extract text from a PDF file in the workspace. Returns page-numbered text starting at start_page (1-based);... |
remove_line_range |
no | yes | Delete an inclusive 1-based line range from a file. Reports how many lines were removed and previews the fi... |
replace_in_file |
no | yes | Replace a literal string (NOT regex) in a file. Fails without writing unless the string occurs exactly expe... |
run_command |
no | yes | Run a shell command in the workspace. Output is captured and returned. Dangerous commands (disk wipes, fork... |
run_linter |
yes | no | Run a static analysis tool (pyflakes, flake8, pylint, or mypy) on a workspace file or directory via 'python... |
run_python_script |
no | yes | Run a Python (.py) file from the workspace with the current Python interpreter and return its output. |
run_script |
no | yes | Run a script file from the workspace. .sh runs via sh, .bat/.cmd via cmd.exe, .py via the current Python; o... |
run_tests |
no | yes | Run pytest on a workspace path (or the whole workspace). Use pattern to select tests by name (-k). Writes n... |
snapshot_file |
no | yes | Snapshot a file into the workspace's .versions/ directory with a timestamp suffix before making risky chang... |
task |
no | no | Delegate a scoped subgoal to a fresh child agent and get its final answer. Use for self-contained chunks of... |
use_skill |
no | no | Run a registered skill by id. Skills are reusable, versioned capabilities (code review, summarization, anal... |
validate_code_quality |
yes | no | Lightweight quality gate for a Python code string: compile check, mixed tabs/spaces indentation, lines over... |
validate_python_syntax |
yes | no | Validate Python syntax without executing the code. Accepts either a workspace file (filepath) or an inline ... |
web_search |
yes | no | Search the web for current information. Returns top_k results with title, url, and a content snippet. |
write_file |
no | yes | Write content to a file in the workspace. Creates parent directories if needed. |
| <!-- /AUTO:tools --> |