Agent Toolkit (@nsys-cortex/tools)
The toolkit provides 27 sandboxed tools for AI agent execution, a configurable policy engine, and supporting infrastructure for memory persistence, multi-session coordination, and external MCP server integration.
Tool Inventory
| Tool | Category | Group | Read-Only | Effect class | Description |
|---|---|---|---|---|---|
read |
filesystem | fs | yes | read | Read file contents with line numbers, offset/limit (streamed — works on any file size), image support; a whole-file read is refused above 5 MiB (page it) |
write |
filesystem | fs | no | mutate-reversible | Create or overwrite files (auto-creates parent dirs) |
edit |
filesystem | fs | no | mutate-reversible | Surgical text replacement (must match exactly once) |
bash |
execution | runtime | no | mutate-reversible | Execute shell commands with timeout and output truncation |
grep |
search | search | yes | read | Search content via ripgrep (rg) with regex, globs, context lines |
find |
search | search | yes | read | Find files by glob pattern via fd/find |
ls |
search | search | yes | read | List directory contents |
apply_patch |
filesystem | fs | no | mutate-reversible | Apply unified diff patches across multiple files with rollback |
process_start |
execution | runtime | no | mutate-reversible | Start a background process (dev server, watcher, long-running task) |
process_list |
execution | runtime | yes | read | List running processes (procps or BusyBox ps; sorted by memory where supported) |
process_read |
execution | runtime | yes | read | Read buffered output of a background process |
process_kill |
execution | runtime | no | mutate-reversible | Kill a background process by PID |
snapshot |
neuron | neuron | yes | read | Flush pending writes (sync) — not a full VM checkpoint |
neuron_status |
neuron | neuron | yes | read | Health summary, every line labelled: agent uptime and RSS, the container's cgroup v2 limits when present, workspace capacity |
neuron_disk |
neuron | neuron | yes | read | Capacity of the workspace filesystem (shared with the host, no per-neuron quota), /tmp (tmpfs in docker mode), root writability, workspace contents |
neuron_memory |
neuron | neuron | yes | read | The container's cgroup v2 limits first (when present), then the host-wide RAM/CPU view labelled as such |
neuron_packages |
neuron | neuron | yes | read | Installed packages (npm local; +global in Firecracker mode) |
neuron_env |
neuron | neuron | yes | read | Environment variables visible inside the neuron |
memory_search |
memory | memory | yes | read | Search memory files by keyword with section-aware results |
memory_get |
memory | memory | yes | read | Read a memory file with optional line range |
memory_append |
memory | memory | no | mutate-reversible | Append content to a memory file |
memory_replace |
memory | memory | no | mutate-reversible | Replace existing content in a memory file (old_text required) |
sessions_list |
sessions | sessions | yes | read | List active sessions |
sessions_send |
sessions | sessions | no | mutate-reversible | Send message to another session |
sessions_spawn |
sessions | sessions | no | mutate-reversible | Spawn a sub-session |
sessions_history |
sessions | sessions | yes | read | Get message history from a session |
elevated |
execution | runtime | no | destructive | Execute on HOST (requires explicit enablement, not included by default) |
Effect classes (ADR-011). Every tool declares effectClass — read, mutate-reversible, mutate-irreversible or destructive, in rank order (EFFECT_CLASSES) — and, above read, an idempotency contract (provider-native, check-before-act or at-most-once); version is the field a downstream wrapper stamps (unset on the built-ins: the package version is theirs). CortexToolRegistry.registerTool and registerDeferredTool throw ToolRegistrationError on a tool without a known class, the agent loop stamps the class on every tool_call_start / tool_call_end / tool_error event, and GET /neurons/:agentId/tools lists { name, effectClass, idempotency?, version? }. Every built-in writer is mutate-reversible — its effect lands in the neuron's overlay; the shell and the process control are classed as a whole — with check-before-act on write, edit, apply_patch, memory_append, memory_replace and process_kill, at-most-once on bash, process_start, sessions_send and sessions_spawn; no built-in is mutate-irreversible; elevated is destructive; an MCP server's tool is mutate-irreversible / at-most-once and the mcp_find_tools meta-tool is read. The class is metadata for the loop, the policy and the surfaces; input validation and shell escaping (below) are unchanged. (#12)
Design Rationale
Tool Selection
The 27 tools were designed to give an AI agent full software engineering capability within a sandboxed environment. The core set (read, write, edit, bash, grep, find, ls) covers the fundamental operations any coding agent needs. Beyond that:
- apply_patch — Enables multi-file atomic changes via unified diffs, which is how agents naturally express large edits. Includes rollback on failure.
- process_ tools* — Background process management lets agents run dev servers, watch commands, and long-running tasks without blocking. Split into single-purpose
process_start/process_list/process_read/process_kill(PR 12) so JSON Schema enforces parameter requirements (e.g.pidon kill) instead of runtime guards.process_startruns the command under its ownbash -c(compound commands, loops and pipes are one job) and the output file/tmp/cortex-proc-<pid>.logis named after the pid it returns, whichprocess_readtails andprocess_killsignals. - memory tools — Persistent memory across sessions so agents can build up project knowledge, record decisions, and avoid repeating work.
memory_appendandmemory_replaceare single-purpose (formerly onememory_updatewith anactionparam). - session tools — Multi-agent coordination: a lead agent can spawn sub-agents for parallel work and communicate between them.
- neuron_ / snapshot* — Self-awareness: the agent can inspect its own environment (
neuron_status/disk/memory/packages/env, each with focused when-to-use guidance) and flush pending writes before risky operations. - elevated — Escape hatch for host-level operations when explicitly enabled. Not included in any preset by default.
NeuronProxy Abstraction
Tools never interact with the vsock transport or sandbox manager directly. The NeuronProxy sits between them, translating high-level operations (readFile, exec, writeFile) into sandbox commands. This means:
- Tools are testable in isolation (mock the proxy, not the transport)
- The transport layer can change (vsock, gRPC, HTTP) without touching any tool code
- Tools work identically in dev mode and Firecracker mode
Policy Engine Design
The four-layer policy engine (path, command, resource, tool access) uses a composite pattern where each layer is independent and composable. This was chosen over a single monolithic policy because:
- Different deployment contexts need different restrictions (dev vs production vs readonly)
- Presets (permissive, standard, strict, readonly) cover common cases without custom configuration
- Custom policies can override individual layers while keeping defaults for others
- Deny always takes precedence over allow, providing a safe default
Tool Presets
import { createToolRegistry, createCodingTools, createAllTools } from '@nsys-cortex/tools';
// Presets available via createToolRegistry:
// 'coding' → read, write, edit, bash
// 'readonly' → read, grep, find, ls
// 'admin' → all 26 tools (all minus elevated)
// 'minimal' → read, bash
const registry = createToolRegistry(proxy, 'coding', { policy });
Policy Engine
Four composable policy layers:
-
PathPolicy — Glob-based allow/deny for file paths. Default: allow
/home/agent/**,/tmp/**; deny/proc/**,/sys/**,/dev/**,/etc/shadow. -
CommandPolicy — Binary and pattern-based command filtering. Default denied:
shutdown,reboot, fork bombs,mkfs,ddto devices. -
ResourcePolicy — Output truncation limits. Defaults: 50KB max output, 2000 max lines, 10MB max file size.
-
ToolAccessEngine — Group-based tool allow/deny. Groups:
group:runtime,group:fs,group:search,group:neuron,group:memory,group:sessions,group:mcp(external MCP tools — allowed by default, deny to restrict). Deny takes precedence. An effect-class cap (maxEffectClass, ADR-011) is the third deny: a tool whose class ranks above it is inactive —effect class <c> exceeds the policy cap <cap>— unless anallowentry names it exactly (deny still beats that); the MCP carve-out for dynamic names does not pass the cap. Caps per preset:readonly→read,strict→mutate-reversible(an MCP server's tool,mutate-irreversible, needs an exact-name allow),standard→mutate-irreversible(elevatedand anydestructivetool stay inactive even when registered),permissive→destructive; an engine built without a policy carries the standard cap, a custom access policy without one has none.createToolRegistryderives the registry's cap from the policy engine's preset when no access policy is passed, andmcp_find_toolsoffers only deferred tools the cap admits. (#12)
Policy Presets
| Preset | Description | Effect-class cap (ADR-011) |
|---|---|---|
permissive |
Almost no restrictions, large limits | destructive (no cap) |
standard |
Sensible defaults, dangerous paths/commands blocked | mutate-irreversible |
strict |
Whitelist paths, deny sudo/chmod, lower limits | mutate-reversible |
readonly |
Only read operations allowed | read |
import { CortexPolicyEngine } from '@nsys-cortex/tools';
const policy = new CortexPolicyEngine({ preset: 'standard' });
// or custom:
const custom = new CortexPolicyEngine({
toolPolicy: { allowedPaths: ['/workspace/**'], maxOutputSize: 20 * 1024 },
accessPolicy: { deny: ['elevated', 'process_start', 'process_kill'] },
});
Input validation & shell-safety
The policy engine is not the only defense — two layers sit under it, because tool arguments are untrusted model output and several tools build shell commands:
- Schema validation — before a tool runs, the agent loop validates its arguments against the tool's TypeBox
parametersviavalidateToolInput(): stringified scalars are coerced ("5"→5),nullon an optional field is treated as absent (models often emit it for unset params), then anything still mismatched is rejected. A field declaredType.Number()(e.g.limit,pid) cannot arrive as"1; rm". (MCP tools carry a raw JSON Schema and pass through — they call a server, not a shell.) Strict mode (ADR-011, #17;agent.strictToolInput/NSYS_CORTEX_STRICT_TOOL_INPUT=true, orAgentLoopOptions.strictToolInput) refuses any coercion —"5"for a number field is rejected withinvalid arguments at /limit: Expected number, which the model receives as the tool's error result and re-prompts on;nullon an optional field is still absence; the check itself is never bypassed. The default stays coercing, since local models stringify numbers. - Shell escaping — every user-controlled value interpolated into a shell command (paths, globs, patterns, memory keys) is escaped with
shq(), numeric fields are coerced to integers, and free positionals use an-e/--option terminator so a value like--pre=/bin/shcan't become a program flag. Tools that only need file I/O route through the validatedfile_read/file_write/resolve_pathcommands (realpath jail) rather than shelling out.
See Security Posture for the full guarantee-to-test map. As always, Firecracker mode is the actual boundary; these layers are defense-in-depth that also apply in dev mode.
NeuronProxy
Abstraction layer between tools and the sandbox. Tools never interact with vsock directly.
import { NeuronProxyImpl } from '@nsys-cortex/tools';
const proxy = new NeuronProxyImpl(sandboxManager, agentId, cwd);
// proxy.exec(command, options)
// proxy.readFile(path)
// proxy.writeFile(path, content)
// proxy.fileExists(path)
// proxy.stat(path)
// proxy.listDir(path, options)
// proxy.mkdir(path)
// proxy.getHealth()
// proxy.getCwd()
// proxy.getAgentId()
Memory Store
Memory files live inside the neuron overlay at /home/agent/memory/ (or <overlay>/memory/ in dev mode). They persist automatically across neuron restarts.
import { NeuronMemoryStore } from '@nsys-cortex/tools';
const store = new NeuronMemoryStore(proxy);
await store.update('MEMORY.md', 'append', '## Decision\nUse TypeScript');
const results = await store.search('TypeScript');
Session Manager
Manages logical sessions within and across neurons. Supports agent-to-agent communication and sub-agent spawning.
import { CortexSessionManager } from '@nsys-cortex/tools';
const sessions = new CortexSessionManager();
const session = await sessions.createSession({ agentId: 'my-agent', name: 'worker' });
await sessions.sendToSession(session.sessionId, 'process these files');
System Prompt
Three-file prompt architecture (AGENTS.md, SOUL.md, TOOLS.md):
import { buildCortexSystemPrompt } from '@nsys-cortex/host';
const prompt = buildCortexSystemPrompt({
tools: registry.getActiveTools(),
agentId: 'my-agent',
sessionId: 'ses-123',
cwd: '/home/agent/workspace',
sandboxInfo: { mode: 'neuron', workspaceDir: '/workspace', workspaceAccess: 'rw', isElevated: false },
});
MCP Integration
External MCP servers plug their tools into the agent's registry — the standard way to add capabilities (git, web fetch, databases, custom integrations) without writing Cortex tools.
Configuration (the CLI chat and HTTP API load these automatically at startup):
// ~/.nsys-cortex/config.json
{
"mcpServers": [
{ "kind": "stdio", "name": "git", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-git"] },
{ "kind": "http", "name": "fetch", "url": "http://localhost:9090/mcp", "token": "optional-bearer" },
{ "kind": "http", "name": "crm", "url": "https://mcp.example/mcp", "headers": { "x-tenant": "acme" },
"auth": { "kind": "oauth-client-credentials", "tokenUrl": "https://idp.example/oauth/token",
"clientId": "cortex", "clientSecret": "…", "scopes": ["mcp:tools"], "resource": "https://mcp.example/mcp" } }
]
}
An http server is reached over Streamable HTTP (ADR-016); the earlier HTTP+SSE transport is not supported — a server that only speaks it fails to connect and costs you its tools, not your session, with the transport named in the warning. It authenticates with a static bearer (token, sent as Authorization: Bearer), fixed headers (an API-key header, a tenant id — sent on every request), or OAuth 2.1 client credentials (auth): the host posts grant_type=client_credentials to tokenUrl with client_secret_basic, the scopes and the RFC 8707 resource indicator when the server requires one, caches the token, refreshes it 60 s before expires_in (5 minutes when the endpoint states none) and, on a 401 from the server, refreshes once and retries — the bearer is injected per request, so a refresh never reconnects. token and auth are mutually exclusive (the config schema and the env parser refuse both). The secrets live in the 0o600 config file or the env and are never logged: the logger's property-name redaction covers token and clientSecret. (#15)
Or via env (JSON array, wins over the config file): NSYS_CORTEX_MCP_SERVERS='[{"kind":"stdio","name":"git","command":"npx","args":["-y","@modelcontextprotocol/server-git"]}]'.
Behavior:
- Tools are namespaced
mcp__<name>__<tool>(server names limited to[a-zA-Z0-9_-]{1,32}since they become tool-name prefixes). - A server that fails to connect is logged and skipped — it costs you its tools, not your session. The
@modelcontextprotocol/sdkdependency is imported dynamically; a clear install hint fires only if a server is actually configured without it. - MCP tools default to
parallelSafe: false(MCP doesn't declare read-only safety, so we don't race writes against the same external system). - MCP tools are classed
mutate-irreversiblewithidempotency: 'at-most-once'(ADR-011 — the conservative assumption, since a server declares nothing the adapter can read);mcp_find_toolsisread. (#12) - Access policy: MCP tools are allowed by default (they're opt-in via config; their dynamic names can't be enumerated in an allow-list). Restrict them by denying the whole group (
deny: ['group:mcp']) or an exact tool name (deny: ['mcp__github__delete_repo']).
Lazy loading. Every registered tool's full definition (name + description + JSON schema) is sent to the provider on every turn — a large MCP server (GitHub's exposes ~70 tools) can add tens of thousands of context tokens per request. When the configured MCP tool count exceeds NSYS_CORTEX_MCP_DEFER_THRESHOLD (config mcpDeferThreshold, default 10), registerMcpTools() puts them in the registry's deferred pool instead: the model sees only one compact mcp_find_tools meta-tool that advertises the server inventory. The agent calls it with keywords ("create issue", "query database", or "*" to list everything); matching tools activate and become ordinary, schema-enforced tools from the next model step. Activation is sticky for the session (so prompt caching re-stabilizes after one miss) and capped at 8 tools per search. Set the threshold very high to always load eagerly, or 0 to always defer.
import { registerMcpTools, loadConfiguredMcpTools } from '@nsys-cortex/tools';
// Defers behind mcp_find_tools when above the threshold; returns whether it did.
const deferred = registerMcpTools(registry, await loadConfiguredMcpTools());
Programmatic use:
import { loadMcpServerTools, loadConfiguredMcpTools } from '@nsys-cortex/tools';
const tools = await loadMcpServerTools({ kind: 'stdio', name: 'git', command: 'npx', args: ['-y', '@modelcontextprotocol/server-git'] });
registry.loadTools(tools);
// or: everything from NSYS_CORTEX_MCP_SERVERS, failure-tolerant
registry.loadTools(await loadConfiguredMcpTools());
Security: only connect MCP servers you trust. Their tool descriptions reach the model verbatim (truncated to 1KB as a bound, not a defense — a malicious description is a prompt-injection vector), and their tools execute with whatever privileges the server process has, outside the neuron sandbox.