Commit Graph

119 Commits

Author SHA1 Message Date
NCBM dfaaad563c tools/documents: add document tools over capped tool results
A tool result stays whole in its own tool message; a request-prep marker
names a document_id (the message's tool_call_id). read_document,
search_document, document_info, and list_documents resolve that id back to
the message body, so reading and searching a capped result needs no re-run.

The store binds on the host instance and reads the agent's live history,
which carries whole results and re-reads whatever a resume reloaded.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-10-29 02:36:36 +08:00
NCBM 44b6ab90ed test: cover the scratch hint in the escape refusal
One test for the write path that now names `new_temporary_workspace`, one
parametrized over read and exec, which must not.
2026-10-26 00:52:38 +08:00
NCBM 6e69e5046c test: cover the modifier combinations
Pins the parameter arithmetic on a few shapes (shift, alt, ctrl, shift+ctrl,
ctrl+alt), the Alt-without-a-CSI-form case, Ctrl+Space as NUL, and the three
combinations that stay errors (`<s-a>`, `<c-tab>`, `<c-s-tab>`).
2026-10-24 02:08:57 +08:00
NCBM dbb68e7273 test: cover the key markers and the keys line
Pins the markers that used to be unreachable (s-tab, home, bs, pgup, f5, a
repeat, a literal `<`), the two shapes of a wrong marker (unknown name, bad
repeat) coming back as an error from the tool instead of being typed, and the
pretty line naming the keys it sent.
2026-10-24 02:03:40 +08:00
NCBM c1b40a7bf4 test: cover options read with getopt
`--` ends a wrapper's options, jammed values (`-p2222`) and launcher flags
(`uv run --no-project pytest`) resolve, and an unknown option stops the unwrap
with the wrapper as the command — where the review asks and the denylist still
sees `sudo`.
2026-10-23 10:47:45 +08:00
NCBM 3cb4d490ed core/tools: announce a remote shell instead of judging it
`ssh prod` runs no command this scan can resolve, and what is typed or piped
into the shell it opens never reaches any gate — `write_pty` and
`run_argv(stdin=…)` are both unchecked input. The tool description said nothing
about it, so the call looked like any other.

The hop is now reported even when no command follows
(`RemoteTarget.shell`), and the review announces it: `remote shell on 'prod'
(ssh with no command; its input is not checked)`. It is a soft confirm like the
other reviews (YOLO may skip it, an escalation behind a hop still may not) — the
client itself stays outside the denylist, since there is no program to name.
2026-10-23 10:43:05 +08:00
NCBM 8c289db545 test: cover the far end of ssh
What the scan resolves (destination, options, jammed `-p2222`, mosh's `--`, a
hop behind `sudo`), what it leaves alone (a client with no command), and both
gates: `check_command_allowed` now refuses `ssh prod rm -rf /` and prompts for
the `sudo` behind the hop, while harmless remote commands stay allowed; the
review prints a remote `bash -c` code block and always asks under
`BatchMode=yes`.
2026-10-23 10:37:14 +08:00
NCBM 37e9395ac5 test: cover the image gate
The gate has four entry points and each now has a test: `view_image` and
`fetch_local` refuse with the message naming the config key, the registry drops
a plugin tool's picture and says why, and the workspace helpers keep `None` (no
host verdict) permissive. On the CLI side the verdict is asserted to follow the
selected model, and to reach the tools once an instance is bound.
2026-10-21 21:39:33 +08:00
NCBM 2369592473 core/tools+cli/display: name the trash in the delete confirm
The box asked "delete path 'x'", which reads as final even now that it is not.
It now says "move path 'x' to the system trash" — and "delete path 'x'
permanently" when the path really is destroyed: an explicit `permanent=true`, a
host that turned `[trash] enabled` off, or a scratch path under the OS temporary
directory. The classifier resolves the path to answer that, and falls back to
the trash wording when path policy will refuse the call anyway.

The pretty line follows: a delete that landed in the trash prints `(to trash)`
instead of `(done)`, so a session's transcript still shows which deletes could
be undone.
2026-10-21 20:23:54 +08:00
NCBM 17813281d8 core/cli: hand the trash switch to the tools
`[trash] enabled` now reaches `delete_path` through the bound instance's
workspace policy, the same channel `[agent] path_denylist` already travels: set
when the session state is built and re-read by `reload_config_from_disk`, so a
config reload in the middle of a chat changes the next delete too.

The policy flag is what `delete_path` consults, so a host that never sets it
gets the safe default (deletes go to the trash) instead of a silent no-op.
2026-10-21 20:22:50 +08:00
NCBM 7b7c90b8ee core/tools: send a deleted path to the system trash
`delete_path` is the only destructive file entry point the agent has, and it
carries `ToolTag.YOLO` — under `--yes` / `/yolo` it runs with no confirmation at
all. A delete is now a move into the desktop's own trash (freedesktop trash on
Linux, Finder trash on macOS, Recycle Bin on Windows) through `send2trash`, so
the human can put the file back with the file manager they already have. The new
`permanent=true` argument is the old behaviour, chosen on purpose.

A trashing that fails is reported, never downgraded to a deletion: the human was
told the path is recoverable, so a volume with no writable trash directory
leaves the file where it is and the error names `permanent=true` as the way to
delete it anyway. A broken install (the library is a required dependency) is
reported the same way.

Scratch paths under the OS temporary directory are still deleted for good —
nobody restores them, and a trash on the `/tmp` volume would only collect them.
The rule now lives in `tools/trash.py` (`under_system_temp`) and the temporary
workspace cleanup uses it too, instead of keeping a second copy of the check.
2026-10-21 20:18:35 +08:00
NCBM 8ec63aa6a2 core/agent: let YOLO mode skip YOLO-tagged soft confirms
`--yes` / `/yolo on|off|once` clears the prompt of a `YOLO`-tagged tool: interpreter runs (`run_argv`, `run_argv_batch`, `open_pty`), the file mutations (overwrite / copy / move / delete) and `fetch_local`'s cleartext and mutating requests. A tool without the tag still asks, and hard path/command denylists stay untouched.

`classify_danger` now answers with a `DangerReason` — the reason text plus `yolo_skip` — so one call can refuse that skip: a command that runs through `sudo` / `su` / `doas` / `pkexec` (as the program or a wrapper) keeps its confirm whatever YOLO mode says. It never adds a reason of its own, so the program the denylist gate already asks about is not asked twice.

`prepare_batch_confirm` leaves the covered calls out, so a skipped call neither prompts on its own nor takes a slot in the round's aggregated prompt, and one escalating step keeps the whole batch in front of the human.
2026-10-21 19:10:46 +08:00
NCBM e9ee71e853 core/tools: show images instead of describing them
New `view_image` reads a local picture (JPEG, PNG, GIF, WebP, sniffed from its
bytes) and hands it to the model as an image part, with an optional `detail`
level — `low` measured 3.4x cheaper on a 1200x900 picture. `read_file` now
points at it instead of returning decode noise.

`fetch_local` returns an image response as the picture itself, and MCP image
content blocks become image parts instead of `[image: ...]` placeholders, so a
screenshot server finally reaches the model.
2026-10-21 18:43:29 +08:00
NCBM cfe36b024b core/tools+cli: split fetch into remote fetch and fetch_local 2026-10-17 17:06:43 +08:00
NCBM cb19652b2a core/tools+config: search through the DeepSeek Search API 2026-10-17 16:52:31 +08:00
NCBM 85b0318b77 core/agent+tools: apply a batch's same-file writes in call order 2026-10-16 19:37:29 +08:00
NCBM 1c84d63f37 core/tools+config: name the access mode in the workspace escape error 2026-10-16 19:29:11 +08:00
NCBM 2eb99a079d core/agent+tools+cli: number todos by listing order, not the internal id 2026-10-16 18:52:36 +08:00
NCBM ac33e92d33 core/tools+config: refuse write_file overwrite unless overwrite=true 2026-10-16 18:44:14 +08:00
NCBM 6c4c8b9328 core/tools: edit_lineno rejections name the read_file call 2026-10-09 19:46:16 +08:00
NCBM 2bb6e06171 core/tools: search a list of sources, DeepSeek's index then Bing
``search_sources`` (a provider list, ``["deepseek", "bing"]`` by default,
``[]`` to not offer the tool at all) names what the local ``web_search`` tool
searches with, in fallback order: ``search_in_order`` asks each source in turn
and keeps the first answer, so a source that errors — a rate limit, a rejected
key — or finds nothing hands over to the next instead of failing the tool. The
handler reports the failures only when every source came up empty.

The Bing leg answers with its RSS results (``?format=rss``), which need no key
and carry a description — the snippet DeepSeek's own index does not hand out —
and it goes through the ``fetch`` client, so the SSRF and host-grant policy
applies to it too (a host that binds tool instance state, as the CLI does).
2026-10-07 00:09:46 +08:00
NCBM a0d39bba6d core/tools: search through a local tool where a hosted one is ignored
Only DeepSeek's Anthropic-compatible surface runs a hosted ``web_search``: the
Responses and chat-completions surfaces answer "I can't browse the web" with
``{"type": "web_search"}`` and with the server-tool shape alike, so a DeepSeek
provider that talks them had no search at all.

``config.routing.local_web_search`` decides per provider: a DeepSeek provider
whose ``provider_tools`` ask for ``web_search`` and whose convention is not
``anthropic`` gets the local tool (``tools/net/search.py``), which asks the
search-capable endpoint behind the scenes and hands the model one ``title —
url`` line per hit — read a page with ``fetch``, the bodies stay server-side.
DeepSeek's preset asks for no hosted tool by default, so this stays opt-in.
2026-10-06 23:45:35 +08:00
NCBM 135c28a622 core/tools: review a wrapper option that carries a command line
`env -S "rm -rf /"` ran a command nothing looked at: the option belongs to the
wrapper, so the scan skipped it and the denylist found no program to block while
the soft confirm had no interpreter to review. Collect those values — `env
-S`/`--split-string` and `su -c`/`--command`/`--session-command`, jammed
`--opt=value` and `-Svalue` forms included — and confirm the call the way an
interpreter's `-c` one-liner is confirmed, printing the text under its own
`env -S` label. Recursing into the text for a hard denylist is left out for now:
the review asks the human, who sees the command.
2026-10-06 21:54:18 +08:00
NCBM 2b3b2073a0 core/tools: only value-taking options may swallow the program
The wrapper option table listed flags as if they consumed the next token, so
the program behind them was skipped: `exec -c git -c init.defaultBranch=main
init` resolved to `init` and the policy denied it as a denylisted basename,
while `exec -c rm -rf /`, `command -p rm -rf /` and `command -v rm` hid `rm`
from the denylist completely and `exec -c python -c …` lost the interpreter
review. Drop the entries that take no value — `exec -c`/`-l`, the whole
`command` entry, `pkexec --disable-internal-agent`, `su -p`, and `xargs -e`
(its argument is optional, and the jammed form needs no entry) — so the
resolved program is the one that really runs.
2026-10-06 21:50:21 +08:00
NCBM c7680491c9 core/tools: denylist power-management subcommands and privileged programs 2026-10-06 19:55:14 +08:00
NCBM 7862a555bc core/tools: review interpreters and detached runners past wrappers 2026-10-06 19:44:29 +08:00
NCBM 9501efb3d7 core/tools: block a denylisted program behind env wrappers and run launchers 2026-10-06 19:42:06 +08:00
NCBM 6453a8ac89 core/tools: resolve the program behind env wrappers and run launchers 2026-10-06 19:41:17 +08:00
NCBM 25b8ef3e58 core/tools: resume fetch bodies from memory instead of refetching 2026-10-06 19:14:38 +08:00
NCBM de03e5932b core/tools: rename grep_files to regex_files (regex pattern) 2026-09-22 23:22:13 +08:00
NCBM 20fc493c47 tools/skills: rebase search hits from either path form
A skill root can live inside the workspace (the project-scope .claude/skills),
and grep reports those hits relative to the workspace root while outside ones
come back absolute. The attribution only stripped an absolute skill prefix, so
an in-workspace root produced alpha/alpha/SKILL.md - and, worse, a path that no
longer started with the skill name when the root itself was the workspace.
Normalizing the hit to an absolute path before relativizing to the skill covers
both, and the hit line is now matched with a pattern instead of a colon split so
a Windows drive letter cannot split it wrongly.

Found by running the REPL against a workspace-local skill root; the tool tests
only covered roots outside the workspace, so they get that case now.
2026-09-22 22:59:57 +08:00
NCBM c7441d4a43 tools/skills: create and modify skills behind a grant
skill_create writes a SKILL.md (name, description, body) plus whatever bundled
files the model lists, and skill_edit changes one file of an existing skill
either by literal replacement, by appending, or by rewriting it. Both resolve
every path inside the skill directory first, so a bad argument is answered
before anything is written and before the human is asked anything; a rewritten
SKILL.md is re-parsed so a skill that lost its description (or its frontmatter)
says so instead of failing quietly. The store is reloaded after a write, which
makes a new skill visible to the very next skill_list.

Writing is a host policy, not a directory-access grant: the denylist wins
first, then a session grant, then [skills].allow_write, then the host's confirm
hook — asked once per skill directory, so one answer covers its files. A host
without a hook denies, which keeps the library default safe; the confirm can
also answer 'session' to cover the rest of the chat.
2026-09-22 22:46:21 +08:00
NCBM 1d05da62c2 tools/skills: list, read, and search skills
skill_list shows what was discovered (name, source root, directory, description)
together with the copies a name collision shadowed and the notes from repaired
frontmatter, so a third-party skill never disappears quietly. skill_read
addresses a skill by name and defaults to its SKILL.md, reusing read_file for
ranges, line numbers, and the continuation token; a bundled file is resolved
inside the skill directory only, symlinks included. skill_search greps every
visible skill (or one) and rebases each hit on its skill, which makes a match
attributable without absolute paths.

Reads need no grant: the host registers the roots as static read-only roots on
the workspace policy, so read_file/grep_files see skill directories too while
write access stays denied — skill writes will go through their own confirm.
Grep used to drop every file outside the primary workspace root, which also hid
hits under a granted directory; those now come back as absolute paths.
2026-09-22 22:42:45 +08:00
NCBM 27bd7b46f7 prompting: let Ctrl+C cancel poll-based prompt reads
The wait tool's prompt runs off-loop on a worker thread, but its read polls
select/msvcrt, so it can act on a cancel: the SIGINT target now sets a flag the
reader checks between polls and raises PromptCancelledError with, which the tool
reports as "cancelled by user" while the turn continues. run_prompt_async /
run_cancellable_prompt_async hand the matching hook to the CLI guard; a stale
cancel never leaks into the next prompt read.
2026-09-22 19:28:03 +08:00
NCBM f94d9a6f49 agent/tools: report missing required arguments before running 2026-09-19 09:50:13 +08:00
NCBM cb35e3b105 tools/chat: shape-check the wait duration and reason 2026-09-19 09:44:43 +08:00
NCBM 86d90bfeb1 tools/chat: return shape errors for mis-typed ask-family arguments 2026-09-19 09:37:25 +08:00
NCBM 22f52a0bd2 config: [agent] allow_paths static pre-allow 2026-09-09 22:19:23 +08:00
NCBM 49d1e08ebe tools: request_directory_access and grant stores 2026-09-09 22:16:22 +08:00
NCBM ba021cb138 tools/workspace: mode-aware path grants and escape hint 2026-09-09 22:11:50 +08:00
NCBM d603615d6a tools: register MCP server tools in the catalog (mcp source, purge) 2026-09-08 12:06:34 +08:00
NCBM 7c377a1a53 core/tools: two-line wait prompt with reason input and colors 2026-08-17 17:42:32 +08:00
NCBM 5436457ac0 core/tools: allow edit_lineno append to an empty file
Appending via edit_lineno required the last line to be read, which made
writing to an empty file impossible (line 0 does not exist). An empty
file has nothing to clobber, so the append needs no prior read.
2026-08-15 17:41:06 +08:00
NCBM 2cf66edb60 core/tools: edit results preview the old content
Wrong-target edits (the test_run_aside pass clobber, the edit_replace
mangling) were silent: the success message reported counts only, so the
model had no signal it replaced the wrong thing until later.

edit_lineno now reports the first removed line ("first: 'pass'") and
edit_replace reports the matched pattern ('old' -> 'new'), giving the
model an immediate self-correction signal.
2026-08-15 17:40:26 +08:00
NCBM 38671482f2 core/tools: edit_lineno unread error names the read window
The boundary overguard (read offset=50 shows lines 51+, then editing
line 50 fails) forced blind re-reads. The error now reports the read
window ("this turn read lines 51-86") and advises reading with a margin
around the target range; read_file's description says the same.
2026-08-15 17:38:58 +08:00
NCBM c9815e32bb core/tools: mtime freshness replaces per-write lineno invalidation
edit_lineno now rejects edits when the file changed since the numbered
read (mtime recorded at read time), so stale line numbers are caught
even when the mutation came from run_argv / an external process — the
class the explicit invalidations could never cover.

This lets write_file / edit_replace / edit_lineno drop their explicit
invalidate calls (the write itself changes the mtime, and the error is
now the accurate "changed since it was read" instead of a misleading
"not read"). copy/move keep explicit invalidation because shutil
preserves the source mtime, which the freshness check cannot see.
2026-08-15 17:38:10 +08:00
NCBM d914590677 core/tools: numbered read continuation via get_truncated
Two fixes for the truncated numbered-read flow:

- read_file(with_lineno) tokens were rebased onto numbered-body chars
  (each line carries an N| prefix), so get_truncated over-advanced into
  raw text and silently skipped lines. Tokens now advance by raw chars
  of the complete lines shown and carry a numbered flag.
- get_truncated resumes numbered tokens as numbered lines (via
  read_file with_lineno), marking them readable for edit_lineno, so
  read -> get_truncated -> edit_lineno works end-to-end without gaps.
2026-08-15 17:35:57 +08:00
NCBM 132ac520ec core/tools: mark only displayed lines read on truncated read_file
read_file(with_lineno=true) marked the whole requested slice before
applying the max_chars cap, so a truncated read silently unlocked every
line of the file for edit_lineno even though the model never saw them.
Mark only the complete numbered lines actually returned (excluding the
[Truncated] marker); the unseen tail stays unread.
2026-08-15 17:31:43 +08:00
NCBM 4fa05e2456 core/tools: tree format markdown|flat|decorated, markdown default
The model picks the representation per call via the format param (no
user config). markdown (default) is a nested bullet list: ASCII-only,
cheapest tokens, indentation-based hierarchy the model reads reliably.
flat lists one relative path per line (dirs with trailing /, empty dirs
included) with max_entries capping the total lines, so paths are directly
usable. decorated keeps the classic box-drawing tree for humans.
2026-08-15 17:06:12 +08:00
NCBM ddca6e23d3 core/tools: add READ_ONLY tool attribute
New ToolTag.READ_ONLY marks tools that never mutate host state, plus
ToolRegistry.clone(read_only_only=True) to select just those (for safe
side turns / planning). Builtin read-only set: read_file, get_truncated,
glob_paths, grep_files, listdir, tree, todo_list, wait, ask_user_*,
vcs_* and fetch.

fetch is tagged READ_ONLY but can mutate via method; a new
read_only_context ContextVar makes it accept GET only while set.
2026-08-15 16:46:27 +08:00