Skip to main content

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

FamilyRuns whereExamples
Local toolsInside 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 toolsIn 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:

  1. check passes — availability + permission gate. The git tool, for example, denies push / commit / merge / branch-creation unless the agent's permissions grant them.
  2. It is in the agent's allow-list — the agent's tools field. 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:

  1. Hardline blocklist (no override) — catastrophic operations: rm -rf /, fork bombs, mkfs, raw device writes, curl | sh, power-state changes.
  2. 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.
  3. 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:

  1. The catalogue + a call/finish protocol is appended to the SOUL preamble.
  2. The model replies with either a ```tool block (a {"tool","args"} call) or a final ```json result.
  3. A tool call is dispatched through the registry; the result is appended to a running transcript and the model is prompted again.
  4. 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-config pointing at nexus-mcp and runs with --permission-mode bypassPermissions (the pod is the boundary).
  • For Codex, it writes a $CODEX_HOME/config.toml with an [mcp_servers.nexus] entry and approval_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 initialize handshake, 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/call is 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-config mcpServers map, 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 a planned subtask 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 a ready_for_agent task with no dependency and an optional role hint, 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/updated SKILL.md into 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.viewprogressive 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.