A self-hosted platform for MCP Apps and agent automations. Install an MCP bundle and you get more than tools — you get an interactive UI in the sidebar with live agent-UI data sync, and the ability to run the agent on demand or on a cron schedule. Full ext-apps host support on top of an agentic loop with skill-driven prompt composition and multi-agent delegation.
Ships as container images. Also exposes itself as an MCP server via Streamable HTTP so external MCP clients can consume the aggregated toolset.
# Prerequisites: Docker
export ANTHROPIC_API_KEY=sk-ant-...
docker compose up --build
# Web UI: http://localhost:27246
# API: http://localhost:27246/v1/healthOpen http://localhost:27246 in your browser. Auth is configured via instance.json (see Configuration).
# Prerequisites: Bun (https://bun.sh), mpak CLI (https://mpak.dev), Node.js 22+
export ANTHROPIC_API_KEY=sk-ant-...
bun install
cd web && bun install && cd ..
bun run dev
# API on http://localhost:27247 (auto-restarts on file changes)
# Web on http://localhost:27246 (Vite HMR, proxies /v1/* to :27247)One command, one terminal. Output is prefixed [api] / [web]. Ctrl+C stops both.
For API-only development (no web client):
bun run dev:apibun install
bun run dev:tui # Interactive TUI (Ink)User message
│
▼
┌─────────────────────────────────────────┐
│ Runtime.chat() │
│ │
│ 1. Skill matching (triggers → keywords)│
│ 2. System prompt composition │
│ 3. Tool filtering (per-skill scoping) │
│ 4. AgentEngine loop: │
│ LLM call → tool execution → repeat │
│ 5. Conversation persistence │
└─────────────────────────────────────────┘
│
▼
ChatResult { response, toolCalls, tokens, ... }
The engine loops until the model stops calling tools, hits the iteration limit (default 10, max 25), or exceeds the token budget.
bun install
bun run verify # lint → typecheck → test → test:web → smoke
# Or individually:
bun run test # Unit + integration tests
bun run lint # Biome linter
bun run check # TypeScript strict mode
# Web client — build verification
cd web && bun install
bun run build # TypeScript + Vite build → dist/
# Docker — validate configs
docker compose config # Validate compose fileAll endpoints require authentication (Bearer token or session cookie) unless noted. Auth is configured via instance.json — see the identity system docs.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /v1/health | No | Health check |
| GET | /v1/bootstrap | Yes | Bootstrap workspace context (user, workspaces, shell config) |
| POST | /v1/chat | Yes | Synchronous chat |
| POST | /v1/chat/stream | Yes | SSE streaming chat |
| GET | /v1/apps/:name/resources/:path | Yes | Fetch app UI resource |
| POST | /v1/tools/call | Yes | Direct tool invocation |
| GET | /v1/shell | Yes | Shell configuration (placements, endpoints) |
| GET | /v1/files/:fileId | Yes | Serve uploaded file |
| GET | /v1/events | Yes | SSE workspace event stream |
| GET | /v1/auth/authorize | No | OAuth authorization redirect |
| GET | /v1/auth/callback | No | OAuth callback handler |
| POST | /v1/auth/logout | Yes | Clear session cookie |
| POST | /v1/auth/refresh | No | Refresh access token |
| GET | /.well-known/oauth-protected-resource | No | MCP OAuth discovery (RFC 9728) |
| GET | /.well-known/oauth-authorization-server | No | AuthKit metadata proxy (RFC 8414) |
| POST/GET/DELETE | /mcp | Yes | Streamable HTTP MCP server endpoint |
NimbleBrain is both an MCP client (connecting to installed bundles via stdio/HTTP) and an MCP server (exposing composed tools to external hosts via the /mcp Streamable HTTP endpoint). The ToolRegistry aggregates tools from all connected MCP servers into a single namespace, while skills scope tool access per task.
Three port interfaces isolate concerns:
| Port | Purpose | Implementations |
|---|---|---|
ModelPort |
LLM provider | AnthropicModelAdapter (prompt caching), EchoModelAdapter (tests) |
ToolRouter |
Tool discovery + execution | ToolRegistry (MCP sources + inline sources), StaticToolRouter (tests) |
EventSink |
Observability | StructuredLogSink, WorkspaceLogSink, SseEventManager, ConsoleEventSink, CallbackEventSink, DebugEventSink, NoopEventSink |
All system tools are prefixed with nb__ (the nb source name + __ separator).
| Tool | Purpose |
|---|---|
nb__status |
Platform status: overview, bundles, skills, or config (scope param) |
nb__search |
Unified search: installed tools or mpak registry (scope param) |
nb__set_model_config |
Update model provider and limits (admin only) |
nb__set_preferences |
Set user preferences (name, timezone, theme) |
nb__manage_app |
Install, uninstall, or configure an app |
nb__manage_skill |
Create, edit, delete user skills |
nb__delegate |
Spawn child agent for sub-tasks (multi-agent) |
nb__briefing |
Generate personalized activity briefing |
nb__manage_users |
Create or delete users (admin only) |
nb__manage_workspaces |
Workspace CRUD + member management + conversation sharing (admin only) |
Additional internal tools (UI-only, hidden from LLM) are listed in Architecture Reference.
Skills are markdown files with YAML frontmatter. They inject system prompts and scope tool access:
---
name: my-skill
description: What this skill does
metadata:
triggers: ["exact phrase match"]
keywords: [fuzzy, keyword, matching]
category: domain
allowed-tools: ["server__*"]
---
# System prompt content injected when this skill matchesSkill matching is two-phase:
- Triggers — exact substring match on the user message (first hit wins)
- Keywords — count keyword hits, require minimum 2 to qualify
Two categories of skills:
- Core (
src/skills/core/) — always injected into the system prompt (e.g.,bootstrap.mdteaches meta-tool usage) - User-matchable — loaded from
src/skills/builtin/(currently empty),~/.nimblebrain/skills/, and config-specified directories
Bundles are MCPB-format MCP servers. They can be:
- Named — downloaded and cached via
mpak run @scope/name - Local — resolved from a path on disk
- Remote — connected via Streamable HTTP or SSE transport (distributed MCP servers)
Local and named bundles spawn as subprocesses communicating via stdio (MCP JSON-RPC 2.0). Remote bundles connect over HTTP. All three types are aggregated into the same unified tool namespace by the ToolRegistry.
No MCP bundles are installed by default. Platform capabilities (home, conversations, files, settings, usage, automations) are built in as inline tool sources (see src/tools/platform/). Install bundles explicitly via the mpak registry or a local path. Tool visibility follows the tiered surfacing rules described under Tiered Tool Surfacing.
NimbleBrain splits configuration across two files:
nimblebrain.json— instance-level settings (models, HTTP, logging, limits, feature flags). One file per deployment.workspace.json— per-workspace settings (bundles, skill directories, named agent profiles, optional model + identity overrides). One file per workspace under<workDir>/workspaces/<ws-id>/.
This split is the workspace isolation boundary: two workspaces in the same deployment can install different bundles and agents without touching the instance config. See Workspace Isolation below.
Create a nimblebrain.json in your working directory. A minimal file:
{
"$schema": "https://schemas.nimblebrain.ai/v1/nimblebrain-config.schema.json",
"version": "1"
}A fully specified example:
{
"$schema": "https://schemas.nimblebrain.ai/v1/nimblebrain-config.schema.json",
"version": "1",
"models": {
"default": "anthropic:claude-sonnet-4-6",
"fast": "anthropic:claude-haiku-4-5-20251001",
"reasoning": "anthropic:claude-opus-4-6"
},
"providers": {
"anthropic": { "apiKey": "sk-ant-..." },
"openai": { "apiKey": "sk-..." }
},
"http": { "port": 27247, "host": "127.0.0.1" },
"logging": { "dir": "~/.nimblebrain/logs", "level": "normal", "retentionDays": 30 },
"store": { "type": "jsonl", "dir": "~/.nimblebrain/conversations" },
"telemetry": { "enabled": true },
"files": { "maxFileSize": 26214400, "maxFilesPerMessage": 10 },
"features": { "bundleManagement": true, "mcpServer": true },
"maxIterations": 25,
"maxInputTokens": 500000,
"maxOutputTokens": 16384,
"workDir": "~/.nimblebrain"
}Model slots. models takes three named slots — default (chat / general), fast (title generation, briefings, skill matching), and reasoning (complex analysis). Each is a provider:model-id string. providers supplies per-provider API keys when you want to mix providers across slots. The older single-model / defaultModel shape is still accepted for backward compatibility but is deprecated.
Feature flags. All default to true. Disable a flag to remove the capability entirely — the tool is unregistered, not visible to the LLM, and POST /v1/tools/call returns 403. See Feature Flags for the full set.
Deprecated fields. identity and contextFile are ignored with a warning — use a skill with type: "context" instead.
Each workspace has its own config at <workDir>/workspaces/<ws-id>/workspace.json. In dev mode (no instance.json), the runtime uses a single _dev workspace.
{
"id": "ws_product",
"name": "Product",
"members": [{ "userId": "usr_default", "role": "admin" }],
"bundles": [
{ "name": "@nimblebraininc/ipinfo" },
{ "path": "../mcp-servers/hello" }
],
"skillDirs": ["./skills"],
"agents": {
"researcher": {
"description": "Research agent",
"systemPrompt": "You are a research agent...",
"tools": ["search__*"],
"maxIterations": 8
}
},
"models": { "default": "anthropic:claude-opus-4-6" },
"identity": { "name": "Acme Copilot" }
}bundles, skillDirs, agents, and optional models / identity overrides live here, not in nimblebrain.json. Entries placed at the top level of nimblebrain.json are silently stripped on load — the runtime treats them as configuration errors rather than falling back to a global scope.
Bundles, tool registries, and conversation data are scoped to a workspace. Every tool handler resolves its workspace via runtime.requireWorkspaceId() before touching data. In dev mode this returns "_dev"; behind auth it resolves from the request's session or API key.
Two workspaces that install the same bundle spawn independent subprocesses with data directories under <workDir>/workspaces/<wsId>/data/<bundle>/, so their entity data never crosses. Sidebar placements, briefing facets, and the app list are filtered per workspace.
nb Interactive TUI (default) / headless pipe mode
nb serve HTTP API server (production)
nb dev Dev mode: API with file watching + web HMR
nb bundle list|add|remove|search Manage bundles
nb skill list|info Inspect skills
nb config set|get|clear Configure per-bundle credentials (requires `-w <wsId>`)
nb status Workspace status
nb reload Hot-reload bundles and config
nb telemetry on|off|status|reset Manage anonymous telemetry
nb automation Manage automation rules
Run nb --help or nb <command> --help for full usage. If you haven't run bun link, use bun run src/cli/index.ts instead of nb.
| Flag | Scope | Purpose |
|---|---|---|
--config <path> |
Global | Config file (default: ./nimblebrain.json) |
--model <id> |
Global | Override default model |
--workdir <path> |
Global | Override working directory |
--debug |
Global | Enable debug event logging |
--help |
Global | Print help and exit |
--json |
Headless + subcommands | Structured JSON output |
--resume <id> |
TUI/headless | Resume a previous conversation |
--port <number> |
serve, dev | HTTP server port (default: 27247) |
--no-web |
dev | Skip web dev server (API only) |
Model providers
| Variable | Purpose |
|---|---|
ANTHROPIC_API_KEY |
Anthropic API key (required unless set via providers.anthropic.apiKey) |
OPENAI_API_KEY |
OpenAI API key (when using openai:* model slots) |
GOOGLE_GENERATIVE_AI_API_KEY |
Google Gemini API key (when using google:* model slots) |
Runtime
| Variable | Purpose |
|---|---|
NB_WORK_DIR |
Override working directory (takes precedence over config and --workdir) |
ALLOWED_ORIGINS |
Comma-separated allowed CORS origins (for cookie-based auth) |
MCP_MAX_SESSIONS |
Max concurrent MCP sessions (default: 100) |
MCP_SESSION_TTL_MS |
MCP session inactivity TTL in ms (default: 1800000) |
NB_CHAT_RATE_LIMIT |
Chat requests per minute per user (default: 20) |
NB_TOOL_RATE_LIMIT |
Tool calls per minute per user (default: 60) |
NB_TIMEZONE |
Default IANA timezone for time-aware features |
NB_HOST_URL |
Public host URL for OAuth redirects |
Identity & telemetry
| Variable | Purpose |
|---|---|
WORKOS_API_KEY |
WorkOS API key (when auth.adapter: "workos" in instance.json) |
NB_INTERNAL_TOKEN |
Shared secret for service-to-service calls (never forwarded to bundles) |
TURNSTILE_SECRET_KEY |
Cloudflare Turnstile secret (CAPTCHA on auth endpoints) |
POSTHOG_API_KEY |
PostHog key for anonymous product telemetry |
NB_TELEMETRY_DISABLED |
Set to 1 to disable telemetry (also DO_NOT_TRACK=1) |
When stdin is not a terminal (piped), the CLI runs in headless mode:
echo "What is 2 + 2?" | bun run dev:tui
# Multi-turn (conversation carried across lines)
printf "Hello\nWhat did I just say?\n" | bun run dev:tui
# Structured JSON output
echo "List files" | bun run dev:tui -- --jsonEach line of stdin is one message. The conversation ID is carried across lines automatically. Responses go to stdout (plain text by default, JSON objects with --json). Logs go to stderr. EOF exits cleanly.
import { Runtime } from "nimblebrain";
const runtime = await Runtime.start({
model: { provider: "anthropic" },
store: { type: "memory" },
});
const result = await runtime.chat({ message: "What can you help me with?" });
console.log(result.response);
await runtime.shutdown();interface ChatRequest {
message: string;
conversationId?: string; // Resume existing conversation
model?: string; // Override model for this request
maxIterations?: number; // Override iteration limit
workspaceId?: string; // Target workspace
fileRefs?: FileReference[]; // Attached files for context
contentParts?: ContentPart[];
metadata?: Record<string, unknown>;
}
interface ChatResult {
response: string;
conversationId: string;
workspaceId?: string;
skillName: string | null;
toolCalls: Array<{
id: string;
name: string;
input: Record<string, unknown>;
output: string;
ok: boolean;
ms: number;
}>;
inputTokens: number;
outputTokens: number;
stopReason: string;
usage: TurnUsage;
}src/
├── index.ts Public API exports
├── engine/ Agentic loop (model → tool → repeat)
│ ├── engine.ts AgentEngine class
│ ├── types.ts ModelPort, ToolRouter, EventSink interfaces
│ ├── tasks.ts MCP Tasks client (polling, progress, cancellation)
│ └── cost.ts Token cost estimation by model
├── runtime/ High-level orchestration
│ ├── runtime.ts Runtime.start() → runtime.chat()
│ ├── types.ts RuntimeConfig, ChatRequest, ChatResult
│ ├── tools.ts filterTools (skill-scoped tool filtering)
│ ├── features.ts Feature flags resolution and tool gating
│ ├── env-filter.ts Bundle env var allowlist/filter
│ └── workspace-runtime.ts Per-workspace bundle spawning
├── identity/ Authentication adapters
│ ├── provider.ts IdentityProvider interface, UserIdentity type
│ ├── providers/dev.ts Dev mode (no auth)
│ ├── providers/oidc.ts OIDC provider (JWT verification)
│ ├── providers/workos.ts WorkOS provider (OAuth + AuthKit MCP)
│ └── instance.ts Instance configuration loading
├── workspace/ Multi-tenant workspace system
│ ├── workspace-store.ts Workspace CRUD operations
│ ├── types.ts Workspace, WorkspaceMember, WorkspaceRole
│ └── scaffold.ts Workspace initialization helpers
├── bundles/ MCPB bundle lifecycle
│ ├── lifecycle.ts Bundle install/uninstall/start/stop state machine
│ ├── manifest.ts MCPB manifest validation (ajv, v0.3/v0.4)
│ ├── resolve.ts Local bundle resolution
│ ├── types.ts BundleRef, BundleManifest, BundleInstance
│ └── schemas/ Vendored MCPB JSON Schemas (v0.3, v0.4)
├── api/ HTTP API (Hono framework)
│ ├── app.ts Hono app factory, route registration
│ ├── server.ts HTTP server startup
│ ├── auth-middleware.ts Auth middleware with workspace resolution
│ ├── handlers.ts Route handler implementations
│ ├── events.ts SSE event manager (broadcast, heartbeat)
│ ├── routes/ Modular route files (auth, chat, bootstrap, etc.)
│ └── middleware/ Hono middleware (CORS, etc.)
├── tools/ Tool definitions
│ ├── system-tools.ts System tools factory (search, manage, delegate)
│ ├── delegate.ts nb__delegate multi-agent tool
│ ├── registry.ts ToolRegistry (aggregates MCP sources)
│ ├── workspace-mgmt-tools.ts Workspace management tools
│ ├── user-tools.ts User management tools
│ └── conversation-tools.ts Conversation sharing tools
├── adapters/ Pluggable implementations
│ ├── structured-log-sink.ts Per-conversation JSONL logs with cost
│ ├── workspace-log-sink.ts Workspace-level daily JSONL logs
│ ├── console-events.ts Stderr event logging
│ ├── callback-events.ts Callback-based events (Ink UI)
│ ├── debug-events.ts Verbose debug logging
│ └── noop-events.ts Silent event sink
├── files/ File context extraction
│ └── types.ts File config, supported formats (PDF, DOCX, etc.)
├── skills/ Skill discovery and matching
│ ├── loader.ts File parsing (YAML frontmatter + markdown)
│ ├── matcher.ts Two-phase matching (triggers → keywords)
│ ├── types.ts Skill, SkillManifest, SkillMetadata
│ └── core/ Core skills (always injected, e.g. bootstrap.md)
├── conversation/ Message persistence
│ ├── event-sourced-store.ts Event-sourced store (persists engine events)
│ ├── jsonl-store.ts Append-only JSONL (one file per conversation)
│ ├── memory-store.ts In-memory (ephemeral)
│ ├── window.ts History windowing (sliceHistory)
│ └── types.ts ConversationStore interface
├── prompt/ System prompt composition
│ └── compose.ts Multi-layer: identity → core skills → apps → skill
├── model/ LLM provider management
│ ├── registry.ts Provider registry (AI SDK createProviderRegistry)
│ └── stream.ts doStream helper — calls model, emits text deltas
├── telemetry/ Anonymous product telemetry
│ ├── posthog-sink.ts PostHog event mapping
│ └── manager.ts TelemetryManager (opt-in/out, anonymous ID)
└── cli/ Interactive + headless terminal interface
├── index.ts Entry point (Commander program assembly)
├── config.ts nimblebrain.json loading
├── commands/ One file per command group
├── dev.ts nb dev dual-process supervisor
├── app.tsx Ink (React) UI component
└── markdown.tsx Lightweight markdown renderer for Ink
export ANTHROPIC_API_KEY=sk-ant-...
export ALLOWED_ORIGINS=http://localhost:27246 # for cookie-based auth
docker compose up --build
# Platform: internal only (API), Web: localhost:27246 (UI)- Runtime container: Bun + Python 3.13 + Node 22 + mpak
- Web container: Caddy serving the SPA, proxying
/v1/*to the platform
See Dockerfile, web/Dockerfile, and docker-compose.yml for full config.
This section contains detailed internal architecture documentation for contributors.
When cumulative input tokens exceed maxInputTokens, the engine returns immediately with stopReason: "token_budget". Tool calls from the current LLM response are dropped (not executed) to avoid running tools whose results can't be processed.
When total tools ≤30, all are surfaced directly. Above 30 with no skill matched, only nb__* tools are direct (rest via proxy). When a skill matches with allowed-tools, matching tools + system tools are direct. Configurable via maxDirectTools (default 30). Implementation in src/runtime/tools.ts.
Internal System Tools (UI-only, hidden from LLM)
| Tool | What it does |
|---|---|
nb__list_apps |
List installed apps with status, tools, trust scores |
nb__get_config |
Get runtime configuration (providers, model, limits) |
nb__manage_identity |
Write or reset workspace agent identity override (admin only) |
nb__version |
Platform version info |
nb__workspace_info |
Workspace metadata, telemetry status |
BundleLifecycleManager (src/bundles/lifecycle.ts) tracks bundle states:
- Install: mpak download → read manifest → extract UI metadata from
_meta["ai.nimblebrain/host"]→ record trust score → spawn MCP server → register → atomic config write → emit event - Uninstall: check protected → stop server → remove source → atomic config removal → emit event (data NOT deleted)
- States: starting → running → crashed → dead (+ stopped for manual stop)
- Atomic writes: config changes use write-temp-then-rename
nb__delegate (src/tools/delegate.ts) spawns child AgentEngine.run() with scoped prompt and filtered tools. Named agent profiles configured in nimblebrain.json under agents. Child iteration budget capped at min(child.max, parent.remaining - 1). Multiple delegations in the same turn run concurrently via Promise.all().
src/engine/tasks.ts detects CreateTaskResult from MCP tool calls. Polls tasks/get until terminal state (completed/failed/cancelled). Emits tool.progress events during polling. Cancels active tasks on engine abort.
InMemoryConversationStore— default for programmatic useJsonlConversationStore— default for CLI, files in~/.nimblebrain/conversations/. Line 1:{ id, createdAt }metadata. Lines 2+:StoredMessageobjects.EventSourcedConversationStore— persists engine events as JSONL. Append-only after creation. Token totals, cost, and last model derived at read time fromllm.responseevents viaderiveUsageMetrics(). Supports multi-user conversations with ownership, visibility (private/shared), and participant management.
Pluggable authentication via IdentityProvider interface (src/identity/provider.ts). Configured via instance.json in the work directory:
dev— No auth, default when noinstance.jsonexists. All requests get a default identity.oidc— JWT verification via any OIDC provider. Auto-provisions users on first valid login.workos— Full OAuth code flow with PKCE, token refresh, managed users via WorkOS. Supports MCP OAuth for external client access via AuthKit.
Each request carries a UserIdentity (id, name, email, role) threaded through AppContext in Hono middleware.
Multi-tenant workspace isolation (src/workspace/). Key types: Workspace, WorkspaceMember, WorkspaceRole (owner, admin, member).
Bundles can be installed per-workspace (tracked via BundleInstance.wsId). Each workspace gets its own ToolRegistry with unqualified tool names. WorkspaceRuntime handles per-workspace bundle spawning.
createSystemTools() takes getRegistry: () => ToolRegistry (callback) instead of a direct registry reference, enabling dynamic workspace-scoped registries. The runtime maintains a _workspaceRegistries map keyed by workspace ID.
Workspace isolation in tool handlers: All tool handlers that access data must use runtime.requireWorkspaceId() (throws if missing). Do not use getCurrentWorkspaceId() (nullable) or getBundleInstances() (unfiltered) in tool handlers. In dev mode, requireWorkspaceId() returns "_dev".
src/prompt/compose.ts joins layers with ---:
- Layer 0: Identity — context skills or default fallback
- Layer 1: Core skills — always present (bootstrap.md teaches meta-tool usage)
- Layer 2: Installed Apps — dynamically injected list with UI status and MTF trust scores
- Layer 3: Matched skill system prompt
Authentication: Bearer token via Authorization header or HttpOnly session cookie (nb_session). Cookie attributes: HttpOnly, SameSite=Lax, Secure in production. Bearer header takes precedence over cookie.
CORS: Dynamic. Dev mode: Access-Control-Allow-Origin: *. With auth: only ALLOWED_ORIGINS env var origins, with credentials support.
MCP endpoint (/mcp): Streamable HTTP for external MCP clients. 100 concurrent sessions (env: MCP_MAX_SESSIONS), 30-minute TTL (env: MCP_SESSION_TTL_MS). Disabled when features.mcpServer is false. When authkitDomain is configured, returns WWW-Authenticate header on 401 for automatic OAuth discovery by MCP clients. See docs/mcp-oauth.md for setup instructions.
MCP OAuth discovery endpoints:
GET /.well-known/oauth-protected-resource— RFC 9728 Protected Resource MetadataGET /.well-known/oauth-authorization-server— RFC 8414 Authorization Server Metadata (proxied from AuthKit)
Workspace-level (GET /v1/events): Events: bundle.installed, bundle.uninstalled, bundle.crashed, bundle.recovered, bundle.dead, data.changed, config.changed, skill.created, skill.updated, skill.deleted, file.created, file.deleted, bridge.tool.call, bridge.tool.done, heartbeat (30s).
Per-conversation (GET /v1/conversations/:id/events): For multi-participant chat. Security: requireAuth → requireWorkspace → canAccess(). Events: user.message, text.delta, tool.start, tool.done, llm.done, done, heartbeat. Sender excluded from own broadcast.
- Chat: editorial conversation style (serif assistant, italic user), streaming via SSE, inline tool call display
- MCP App Bridge: sandboxed iframes, postMessage proxy for tool calls
- Agent-UI sync:
data.changedevents forwarded to iframes with 100ms debounce - Login:
"__cookie__"sentinel token indicates cookie-based auth (suppresses Authorization header)
The sidebar is data-driven from the placement registry:
| Slot | Purpose | Example |
|---|---|---|
sidebar (priority < 10) |
Ungrouped core nav at top | Home (0), Conversations (1) |
sidebar (priority >= 10) |
Grouped under "general" label | — |
sidebar.<group> |
Named group | sidebar.apps → "Apps" |
sidebar.bottom |
Pinned to bottom zone | Settings |
main |
App routes (pages, not nav) | Third-party apps |
Placements with a route field get React Router routes in App.tsx. Routes from sidebar use /app/<route> (except Home → /).
Files:
nimblebrain.json— instance config. Validated at startup againstsrc/config/nimblebrain-config.schema.json(JSON Schema draft-07, AJV). Unknown keys warn; structural errors throw. Workspace-owned fields (bundles,skillDirs,agents,preferences,home,noDefaultBundles) are silently stripped on load.identityandcontextFileare deprecated with a warning.<workDir>/workspaces/<wsId>/workspace.json— per-workspace config. Ownsbundles,skillDirs,agents, and optionalmodels/identityoverrides.<workDir>/instance.json— auth configuration (OIDC or WorkOS adapter). Absence signals dev mode.
Config resolution for nimblebrain.json (when no --config flag):
--workdir <dir>→<dir>/nimblebrain.json- Otherwise →
./nimblebrain.json(CWD)
NB_WORK_DIR overrides workDir from either the config file or --workdir.
Each entry in workspace.json → bundles[] accepts:
| Field | Type | Description |
|---|---|---|
name |
string | Bundle name from the mpak registry |
path |
string | Local filesystem path (resolved relative to the config file) |
url |
string | Remote MCP server URL (HTTPS; HTTP blocked unless allowInsecureRemotes) |
env |
object | Environment variables passed to the bundle process |
allowedEnv |
string[] | Host env vars this bundle may read |
protected |
boolean | Prevents uninstall via nb__manage_app |
trustScore |
number|null | MTF trust score (0-100) |
ui |
object|null | UI metadata: { name, icon, primaryView? } |
All default to true. Setting to false removes the capability entirely — tool not registered, not visible to LLM, returns 403 via HTTP.
| Flag | Controls | Tool(s) Affected |
|---|---|---|
bundleManagement |
Install/uninstall/configure apps | nb__manage_app |
skillManagement |
Create/edit/delete skills | nb__manage_skill |
delegation |
Multi-agent delegation | nb__delegate |
toolDiscovery |
Tool search (scope=tools) | nb__search |
bundleDiscovery |
Registry search (scope=registry) | nb__search |
mcpServer |
External MCP access via /mcp |
/mcp endpoint |
fileContext |
File upload and context extraction | File processing |
userManagement |
Create/delete users | nb__manage_users |
workspaceManagement |
Workspaces, members, sharing | nb__manage_workspaces |
Enforcement: Three layers — (1) tools excluded from registry at startup, (2) POST /v1/tools/call returns 403, (3) MCP ListTools filters and CallTool rejects. Read-only tools (nb__status) are never gated.
Bundle processes receive a filtered host environment. Default allowlist: PATH, HOME, USER, SHELL, LANG, LC_ALL, LC_CTYPE, TERM, TMPDIR, TZ, XDG_DATA_HOME, XDG_CONFIG_HOME, NODE_ENV, BUN_ENV, NB_WORK_DIR, UPJACK_ROOT, PYTHONPATH, VIRTUAL_ENV, NODE_PATH. Hard deny (never passed): NB_API_KEY, NB_INTERNAL_TOKEN. Opt in via allowedEnv in bundle config.
- Protocol must be
https:(SSRF protection) - Private IP ranges rejected:
10.x,172.16-31.x,192.168.x,169.254.x,::1 - Cloud metadata hostnames rejected
- Embedded credentials rejected
- Dev exception:
"allowInsecureRemotes": trueallowshttp://localhost
- Reserved prefix —
nbcannot be used as a bundle source name - No duplicate sources — registry rejects duplicates; built-in bundles register first
These are non-negotiable patterns. Violating them causes production bugs:
tools/callmust returnCallToolResultas-is — never unwrap or cherry-pick fields- No
data.changedfrom tool proxy — causes infinite loops (tool → SSE → iframe refresh → tool) - Tool errors → JSON-RPC errors —
isError: truemust send error response, not result - Bridge
destroyedflag — React StrictMode double-mounts; guard listeners withdestroyedboolean - Iframe DOM isolation — never put React-managed children in same container as raw DOM iframes
- SlotRenderer effect depends only on
placementKey— callbacks via refs, not dep array (prevents flickering) - Shell components must not consume
ChatContext— useChatConfigContext(stable) to avoid re-renders during streaming "primary"virtual path —GET /v1/apps/:name/resources/primaryresolves toprimaryView.resourceUrifrom manifest- Spec methods only — use ext-apps spec method names in bridge; NimbleBrain extensions use
synapse/prefix ui/initializefield names —hostInfo(notserverInfo),hostCapabilities(notcapabilities),hostContext.themeis string
- Runtime: Bun (not Node). Use
bun run,bun test,bunx. - Module system: ESM only. All imports use
.tsextensions. - Linting: Biome (not ESLint/Prettier).
- Type checking:
bunx tsc --noEmit. Strict mode. - Testing: Bun's built-in test runner. Use
createEchoModel()andStaticToolRouterto avoid LLM calls. - Model types: Vercel AI SDK V3 types from
@ai-sdk/provider. - HTTP: Hono. Typed context via
AppEnv/AuthEnv. - No classes for data — plain interfaces + factory functions.
- Tool results:
structuredContentfor typed data,contentfor human-readable summary. - Prompt security:
sanitizeLineField()and XML containment tags incompose.ts— do not remove without reviewingtest/unit/prompt-injection.test.ts.
| Setting | Value |
|---|---|
models.default |
anthropic:claude-sonnet-4-6 |
models.fast |
anthropic:claude-haiku-4-5-20251001 |
models.reasoning |
anthropic:claude-opus-4-6 |
| Max iterations | 25 (hard cap: 50) |
| Max input tokens | 500,000 |
| Max output tokens | 16,384 |
| Max history messages | 40 |
| Max tool result size | 1,000,000 chars (0 disables) |
| Default bundles | none (platform capabilities are built in) |
| Work directory | ~/.nimblebrain |
| HTTP port | 27247 |
| HTTP host | 127.0.0.1 |
| Conversation store (CLI) | JSONL in ~/.nimblebrain/conversations/ |
| Conversation store (programmatic) | In-memory |
| Package | Purpose |
|---|---|
ai |
Vercel AI SDK core (provider registry, types) |
@ai-sdk/anthropic |
Anthropic provider (prompt caching, streaming) |
@ai-sdk/openai |
OpenAI provider |
@ai-sdk/google |
Google Gemini provider |
@modelcontextprotocol/sdk |
MCP client (stdio transport) |
ajv + ajv-formats |
JSON Schema validation for MCPB manifests |
gray-matter |
YAML frontmatter parsing for skill files |
ink |
React-based terminal UI |
posthog-node |
Anonymous product telemetry (server-side) |
posthog-js |
Anonymous product telemetry (web client) |
hono |
HTTP framework (routing, middleware, typed context) |
StructuredLogSink— Per-conversation JSONL logs with LLM/tool latency, cache tokens, cost. Disable withlogging.disabled: true.WorkspaceLogSink— Workspace-level daily rolling JSONL logs. Only persists workspace events (bundle lifecycle, data/config changes, skill/file operations).ConsoleEventSink— Human-readable stderr for development.DebugEventSink— Verbose JSON dumps (--debug).CallbackEventSink— Bridges events into React state (Ink UI).PostHogEventSink— Anonymous telemetry. No PII. Opt-out:telemetry.enabled: false,NB_TELEMETRY_DISABLED=1, orDO_NOT_TRACK=1.