Nsys Cortex

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:

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:

  1. Tools are testable in isolation (mock the proxy, not the transport)
  2. The transport layer can change (vsock, gRPC, HTTP) without touching any tool code
  3. 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:

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:

  1. PathPolicy — Glob-based allow/deny for file paths. Default: allow /home/agent/**, /tmp/**; deny /proc/**, /sys/**, /dev/**, /etc/shadow.

  2. CommandPolicy — Binary and pattern-based command filtering. Default denied: shutdown, reboot, fork bombs, mkfs, dd to devices.

  3. ResourcePolicy — Output truncation limits. Defaults: 50KB max output, 2000 max lines, 10MB max file size.

  4. 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 an allow entry 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 (elevated and any destructive tool 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. createToolRegistry derives the registry's cap from the policy engine's preset when no access policy is passed, and mcp_find_tools offers 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:

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:

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.