cox tools

From docs/tools.md at 8dbc1da View on GitHub

Every tool implements one contract (Tool::spec / subject / call): untruncated output goes to the archive first, the model sees the capped visible form plus an expand pointer. Permission rules match on subject() (path, command line, URL, or namespaced MCP name); bash also hands over segments(), the simple commands of its line, so a Bash(prefix:*) rule must cover each one (see how-it-works.md, Example 3).

Core tools are always in context; deferred tools join through tool_search (D6d). agent is never available to itself.

ToolRiskDeferredSubjectNotes
readReadOnlynopathwhole / lines="a-b" / mode="outline" / symbol="Foo::bar" (one definition; .rs/.ts/.tsx/.py/.go); images returned as images, other binaries refused
grepReadOnlynopatternripgrep libs, respects .gitignore
globReadOnlynopatternmtime order, fuzzy with query
editWritenopathexact str_replace, ambiguity errors
apply_patchWrite (Destructive past 5 deleted files)nopatch summaryCodex V4A grammar
writeWritenopathnew files; rewrites over 200 lines refused
bashExec (Destructive as classified)nocommand linesandboxed, streamed, background: true archives
todoReadOnlyno—drives the TUI todo panel
expandReadOnlynoarchive idreads back archived output, capped
ask_userReadOnlyyesquestionblocks the turn; --answer headless; TUI: modal
tool_searchReadOnlynoqueryreveals up to 5 deferred schemas
web_fetchReadOnlyyesURLreadability fallback; domain rules apply
agentmax of its tools; Destructive with isolation: "worktree"yespresetexplore / shell presets, own budget; worktree isolation asks (denied in plan)
memory_saveWriteyesnameone fact file + index + FTS row
memory_searchReadOnlyyesqueryFTS first, then files; top 5 capped
memory_getReadOnlyyesnameone fact's body after the frontmatter; missing name is not an error
docs_resolveReadOnlyyescrate namecargo/<name>/<version> from Cargo.lock; no cargo subprocess
docs_queryReadOnlyyescrate namecached rustdoc, at most 5 hits of 400 characters; name=llms searches a workspace llms.txt
docs_fetchReadOnlyyesdocs.rs URLone GET of https://docs.rs/crate/<name>/<version>/json.zst; no API key
diagnosticsReadOnly (Exec for the call that starts a server)yespathone sandboxed LSP server per language; falls back to bash
mcp__<server>__<tool>from server annotations (default Write)yesnamespaced namefail-open servers

MCP elicitation

An MCP server may ask the person for input during a tool call (elicitation/create, or a 2026-07-28 input_required round). cox answers only where a person can:

  • TUI and --plain: the client declares elicitation.form. Each field of the requested form is one question in the question modal, labelled mcp:<server> asks: (in --plain, question from mcp:<server>:), with enum labels, yes/no or free text; a bad answer is asked again with the reason. A review step names the fields to send and offers send, edit (ask again; Enter keeps the last answer) or decline. Questions are events and land in the rollout, so neither the review nor an edit round repeats what was typed. Esc (in --plain, an empty line) cancels the whole request. While a question is open the call's mcp.timeout_s does not count.
  • URL mode (TUI and --plain, elicitation.url): one question shows the message, the URL as it will be opened (normalized, so a non-ASCII host appears as punycode), its host, and a warning for a punycode host or a plain http URL; it offers open or decline. The browser opens only on open, and the server gets accept; the flow's outcome itself stays between the browser and the server. A URL that is not http(s), names no host or would not survive the opener unchanged is declined without asking.
  • cox run -p declares no elicitation capability and never asks; a server that elicits anyway gets decline. --answer does not apply.
  • cox acp connects no MCP servers today, so there is nothing to answer.

Answers go to the server only: never into the transcript, the rollout or the model's context. The server's text is shown sanitized, like any tool output.

Edits are diff-shaped (edit, apply_patch); write is for new files. Every path from the model passes path::confine; every shell command runs under the platform sandbox unless the session chose danger-full-access.

read returns a PNG, JPEG, GIF or WebP file (sniffed by its magic bytes, not its extension) as an image: a one-line media type, size text plus the image itself, which lines and mode do not apply to. One image is capped at 3,750,000 bytes (5,000,000 once base64-encoded, the smallest per-image limit a supported provider documents); a larger one is refused as too_large. These are the four formats Anthropic and OpenAI both accept.

diagnostics (P41) returns one file's diagnostics from a language server as path:line:col: severity: message [source code] lines, most severe first, then a summary such as 3 errors, 1 warning. path is confined like any other; wait_ms bounds the wait, capped by lsp.timeout_s. The first call for a language starts its server, which runs the project's build scripts and proc macros, so that call is Exec and the permission engine asks; once the server runs, calls are ReadOnly. Servers are spawned under the same sandbox wrap as stdio MCP servers (bare under danger-full-access; refused on a Landlock-only host or one with no sandbox backend) with the child env allowlist, one per language per session, and killed when the session ends. A server that dies is restarted once per call. Pushed diagnostics count as complete after lsp.quiet_ms with no newer push and no $/progress work open; a server that advertises diagnosticProvider is asked instead. A deadline returns what arrived with a note, not an error.

With lsp.after_edit = true (off by default, T59.3), edit and write end their result with the diagnostics the change introduced: only when the file's server already runs (an edit never starts one), keyed by start line, code and message against the server's last report for the file, errors first, at most ten lines, waiting at most lsp.after_edit_ms. A dead or slow server adds nothing, and neither does a report the server had not settled by the deadline; the server's text passes through sanitize first. ACP's client-backed edit and write are left alone: they write the editor's buffer, not the file the server reads.

definition and references (T59.6, deferred, ReadOnly) take path, a 1-based line and a 1-based col counted in characters, and answer with path:line:col: source line rows, sorted and deduplicated, the first 100 listed and the rest archived. They ask a server that already runs for the file (started by diagnostics); they never start one. With none running, or one that fails, they grep the identifier under the position (for definition, only the lines that declare it) and end with a (no server...) note, since a name match also finds unrelated symbols. A path the server names outside the workspace roots is printed as given and never opened; the whole answer passes through sanitize.

rename_symbol (T59.7, deferred, Write, exclusive) takes path, line, col (as above) and new_name, and renames the symbol through the running server's prepareRename and rename. Like the navigation tools it never starts a server; unlike them it has no grep fallback and refuses instead, since a rename by name match rewrites unrelated symbols. Before asking, cox re-syncs every file it opened whose disk text changed, so the server edits the text on disk. The server's WorkspaceEdit (documentChanges or changes) is refused whole if it creates, renames or deletes a file, names a file outside the writable roots (confine), carries a document version other than the one cox synced, or holds an edit outside its file or overlapping another. Otherwise every file's new text is computed first, each pre-image is archived as edit archives it, every new text is staged in a sibling temp file, and only then are the files replaced; a failed replace puts back the ones already replaced. Permission rules treat rename_symbol as edit (Edit(secrets/**) covers it). Before the call runs, the engine judges the symbol's file: a deny or plan mode refuses there, but an ask waits, since the other files are known only after the server answers. Once they are, the tool hands every file to the session (Relay::approve_writes) and the engine judges each as an edit of that path: a deny on any one refuses the whole rename with nothing written and names the file; any ask becomes one approval listing every file, answered once for the set; all allowed writes as above. A file that changes on disk while that approval waits refuses the rename. A surface with no session behind the call (cox mcp) cannot have the files judged, so the rename is refused there. The result lists the files, and the loop snapshots the workspace around the call. With lsp.after_edit, the result ends with the diagnostics the rename introduced in each file, as for edit.

The default servers ([lsp.servers], see config.md):

NameProgramExtensions
rustrust-analyzerrs
typescripttypescript-language-server --stdiots tsx js jsx
pythonpyright-langserver --stdiopy
gogoplsgo

Only the user config chooses them: a project .cox/config.toml that sets lsp.servers is ignored with a warning, because a repository must not pick a program cox runs. With no server for a file's extension, its program missing from PATH, or no sandbox to run it in, the tool answers with an error that tells the model to run the project's checker with bash instead (cargo check, tsc --noEmit, ...). cox doctor lists each server's program as found or missing.