Tools
A tool is an executable capability an agent can call mid-run — read a file, run a shell command, search code, propose a memory. Tools are how an agent acts, as opposed to skills (procedures it reads) and memory (context it recalls).
Tools live in the nexus-tools crate. Each one self-describes (name +
description + JSON-Schema arguments), self-gates (check), and executes
(invoke). A registry offers the per-run catalogue and dispatches calls,
redacting secrets from every result.
Two families
| Family | Runs where | Examples |
|---|---|---|
| Local tools | Inside the run pod, against the checked-out workspace. No round-trip. | fs.read, fs.write, fs.edit, shell.exec, execute_code, grep.search, git |
| Platform tools | In Nexus Core — the only authority over platform state. The pod proxies the call over NATS request/reply. | memory.propose, memory.search, task.subtask, task.delegate, board.comment, skill.propose, skill.list, skill.view, run.search |
Local tools never leave the container, so they are fast and need no authorization round-trip. Platform tools mutate authoritative state (memory/board/tasks), so they are always executed by Core, which re-checks permissions against the stored agent record — it never trusts the pod.
Gating
A tool is offered to the model (and accepted on call) only when:
checkpasses — availability + permission gate. Thegittool, for example, deniespush/commit/merge/ branch-creation unless the agent's permissions grant them.- It is in the agent's allow-list — the agent's
toolsfield. An empty list means "offer every gated-available tool".
On top of gating, shell.exec and execute_code run their command/script
through a three-tier command firewall:
- Hardline blocklist (no override) — catastrophic operations:
rm -rf /, fork bombs,mkfs, raw device writes,curl | sh, power-state changes. - Pattern detection (
firewall::suspicious) — flags risky-but-not-fatal commands:sudo, package installs, credential reads (~/.ssh/id_*,~/.aws/credentials), host/cluster tooling (kubectl,docker,systemctl), recursive permission changes, and network fetches. - Auxiliary-LLM risk classifier (opt-in via
NEXUS_FIREWALL_SMART=1) — only consulted for flagged commands; a cheap model returns ALLOW or BLOCK with a reason. It fails open (allows) on any model error so a flaky aux model never wedges a run — the confined pod is still the boundary.
execute_code complements shell.exec: instead of a single command, the agent
supplies a multi-line program (bash, sh, python, or node) which is piped
to the interpreter and run in the workspace — useful for logic that is awkward as
a one-line shell command.
In-run tool RPC (nexus-tool)
For the lifetime of the agentic loop the runner hosts a per-run Unix-socket
tool server bound to the same registry and context the model uses. A script
launched by execute_code / shell.exec can therefore chain several tools in
one turn — local or platform — by calling the nexus-tool helper, without a
model round-trip per call:
nexus-tool fs.read '{"path":"Cargo.toml"}'
nexus-tool memory.search '{"query":"deployment steps"}'
echo '{"query":"auth"}' | nexus-tool run.search # args on stdin
The socket path is exported as NEXUS_TOOL_SOCK. Calls go through the same
permissions, firewall, env-scoping, and redaction as direct tool calls.
Secret redaction & credential scoping
Tool results flow back into the model's context, where a leaked secret would be
persisted and could be exfiltrated. The registry runs every result through a
redactor that masks the values of any env var whose name looks secret
(*KEY*, *TOKEN*, *SECRET*, *PASSWORD*) before returning it.
Child processes are scoped further: shell.exec / execute_code strip every
secret-shaped env var from the spawned process unless a skill loaded for the
run opted it back in. Skills declare required_environment_variables (and,
advisory, required_credential_files); the worker unions these into the run's
allow-list. So a skill that needs GITHUB_TOKEN gets exactly that — and nothing
else leaks into shelled-out commands.
The tool-calling loop (API backends)
rig-driven API backends (openai-api, anthropic-api) run a bounded
agentic loop in the runner:
- The catalogue + a call/finish protocol is appended to the SOUL preamble.
- The model replies with either a
```toolblock (a{"tool","args"}call) or a final```jsonresult. - A tool call is dispatched through the registry; the result is appended to a running transcript and the model is prompted again.
- The loop ends when the model emits a final result, or after a step cap (handed back for review).
CLI backends (claude-code-cli, codex-cli) ship their own tool loops, so they
do not use this loop — instead they reach Nexus's platform tools over MCP
(see below).
MCP for CLI backends
CLI backends already have their own file/shell tools, but not Nexus's
authoritative platform tools. The runner exposes those by launching
nexus-mcp — a small MCP stdio server —
as an MCP server the CLI connects to:
- For Claude Code, the runner writes an
--mcp-configpointing atnexus-mcpand runs with--permission-mode bypassPermissions(the pod is the boundary). - For Codex, it writes a
$CODEX_HOME/config.tomlwith an[mcp_servers.nexus]entry andapproval_policy = "never".
nexus-mcp reconstructs the run from NEXUS_RUN_CONFIG, connects to NATS, and
serves only the platform tools (memory.*, task.*, board.comment,
skill.*, run.search) over JSON-RPC — each call is proxied to Core exactly
like the API-backend path, so permissions are still re-checked authoritatively.
This is on by default but self-gating: it only engages when the nexus-mcp
binary is present and NATS is reachable, and can be disabled with
NEXUS_MCP_ENABLED=0.
External MCP servers (client)
Where the section above makes Nexus an MCP server, an agent can also consume
external MCP servers — Nexus is the MCP client. Each agent declares zero or
more servers (Admin UI → agent editor → MCP servers, persisted on the agent
document as mcp_servers):
mcp_servers:
- name: filesystem # namespaces the tools: filesystem.read_file, …
command: npx
args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
env: { }
tools: [] # optional allow-list; empty = expose all
disabled: false
These are stdio servers only (launched as subprocesses) — confined pods have
no general egress, so remote http/SSE servers are out of scope. The command
must exist in the agent image.
How they merge depends on the backend:
- API backends — the runner connects to each server, runs the MCP
initializehandshake,tools/list, and registers every advertised tool in the run's tool registry under a namespaced name (<server>.<tool>). They then participate in the same agentic loop as built-in tools;tools/callis proxied over the stdio connection. A server that fails to start is logged and skipped — a bad MCP config never aborts the run. - CLI backends — the same servers are written into the CLI's own MCP config
(alongside
nexus) so the CLI connects to them directly: into the Claude--mcp-configmcpServersmap, or as extra[mcp_servers.<name>]blocks in$CODEX_HOME/config.toml.
External MCP tools are not routed through Core — they execute inside the pod like other local tools, under the same firewall and redaction. Reserve them for read/inspection servers; authoritative writes still go through the platform tools above.
Platform tool RPC
When the model calls a platform tool, the runner publishes a ToolRequest
(run_id + the call) on the tools.invoke NATS subject and awaits the reply.
Nexus Core resolves the run → agent → task, executes the tool against MongoDB,
and replies with the result:
memory.propose— inserts an agent-proposed memory into the review queue (status: pending); it is not active until a human approves it. This is the safe, human-in-the-loop form of procedural learning.memory.search— full-text search over the agent's readable, active memories (global + its project + its own private scope). On-demand recall the agent can run before starting work.task.subtask— creates aplannedsubtask under the current goal that depends on the current task, so it is sequenced after it (split your own work into ordered steps).task.delegate— creates aready_for_agenttask with no dependency and an optionalrolehint, so the worker routes it to a teammate in parallel — the fan-out primitive for a team of agents.board.comment— posts a progress comment onto the task's board card, mirrored to Taiga when the task is linked (best-effort otherwise). Lets agents communicate status to humans and teammates.skill.propose— captures a reusable procedure the agent discovered as a new/updatedSKILL.mdinto the review queue (status: draft), scanned for injection/exfiltration first. Human-in-the-loop procedural learning: the skill is not loadable until approved. Re-proposing an existing slug creates the next version rather than overwriting the active one.skill.list/skill.view— progressive disclosure: list active skills (name + slug + description only), then pull the full markdown of just the ones needed. Keeps prompts lean instead of injecting every skill body.run.search— full-text search over the agent's own finished runs (HISTORY) so it can recall how it handled similar work before.
Because Core is the authority, an agent can never write authoritative state directly from a confined pod — only request it, subject to re-checked permissions.
Related
- Permissions — what gates the destructive tools
- Agent run — where the loop fits in a run
- Security — isolation, secrets, network policy
- Hermes parity — firewall/MCP/tool-RPC roadmap