nimblebrain.json
NimbleBrain splits configuration into two files:
nimblebrain.json— instance-level settings (models, HTTP, logging, limits, feature flags). One file per deployment. This page.workspace.json— per-workspace settings (bundles, skill directories, named agent profiles, optional model + identity overrides). Seeworkspace.json.
Top-level bundles, skillDirs, agents, home, and preferences entries in nimblebrain.json are silently stripped at load — they belong in workspace.json now. See workspace configuration for where they live.
Config resolution
Section titled “Config resolution”NimbleBrain looks for the config file in this order, stopping at the first match:
--config <path>— Explicit path passed as a CLI flag..nimblebrain/nimblebrain.json— Project-local config directory in the current working directory.<defaultWorkDir>/nimblebrain.json— Inside the command’s default working directory (~/.nimblebrainforbun run start, the project-local workdir for worktree dev)../nimblebrain.json— Current working directory (bare file).
If no config file exists at the resolved path, NimbleBrain auto-creates a minimal default file ({ "$schema": …, "version": "1" }) as long as the parent directory exists.
The override file
Section titled “The override file”A sibling nimblebrain.overrides.json next to the resolved config file is deep-merged on top of it at load. The seed (nimblebrain.json) is operator-managed and overwritten on every deploy; the override is written by the admin set_model_config tool and preserved across deploys. The merge is one level deep — top-level objects (e.g. models) are merged key-by-key so an override of models.fast doesn’t clobber the seed’s models.default; scalars and arrays are replaced wholesale. A malformed override file is logged loudly and skipped (the seed still boots) rather than taking down startup.
Complete annotated example
Section titled “Complete annotated 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" },
"providers": { "anthropic": { "apiKey": "sk-ant-..." }, "openai": { "apiKey": "sk-..." } },
"maxIterations": 25, "maxInputTokens": 500000, "maxOutputTokens": 16384, "maxToolResultSize": 1000000,
"logging": { "dir": "~/.nimblebrain/logs", "disabled": false, "level": "normal", "retentionDays": 30 }, "http": { "port": 27247, "host": "127.0.0.1" },
"features": { "bundleManagement": true, "skillManagement": true, "delegation": true, "toolDiscovery": true, "bundleDiscovery": true, "fileContext": true, "userManagement": true, "workspaceManagement": true, "compaction": true },
"connectors": { "providers": { "composio": { "monitorEnabled": true } } },
"files": { "maxFileSize": 26214400, "maxTotalSize": 104857600, "maxFilesPerMessage": 10, "maxExtractedTextSize": 204800 },
"allowInsecureRemotes": false, "telemetry": { "enabled": true }, "workDir": "~/.nimblebrain"}Top-level fields
Section titled “Top-level fields”| Field | Type | Default | Description |
|---|---|---|---|
$schema | string | — | JSON Schema URI. Set to "https://schemas.nimblebrain.ai/v1/nimblebrain-config.schema.json" for editor autocompletion. |
version | string | — | Config file version. Must be "1". |
models | object | — | Role-based model slots. See models. |
modelPolicy | object | — | Which models this organization permits. See modelPolicy. |
providers | object | — | Per-provider API keys. See providers. |
model | object | { "provider": "anthropic" } | Legacy single-provider config. Use providers instead. |
defaultModel | string | — | Deprecated. Use models.default instead. |
maxIterations | integer | 25 | Max agentic iterations per request. Range: 1—50. |
maxInputTokens | integer | 500000 | Max input tokens per request. Engine stops when this budget is exceeded. |
maxOutputTokens | integer | 16384 | Max output tokens per LLM call. |
maxToolResultSize | integer | 1000000 | Max characters for a single tool result. 0 disables the cap. |
thinking | "off" | "adaptive" | "enabled" | enabled on reasoning models | Extended-thinking mode, applied to every provider that supports reasoning. off: do not reason — not enforceable on Anthropic’s adaptive-only models (Opus 4.7/4.8, Sonnet 5, Opus 5), which have no such state: the AI SDK validates thinking.type=disabled and then never sends it, so the model applies its own default (#738). adaptive: model decides per call, at no stated depth. enabled: reason every turn, at thinkingEffort. Applies only to models the catalog carries and flags reasoning-capable — an override can ask for reasoning but cannot make the parameter exist. A pinned model id, or one served by an OpenAI-compatible proxy under its own name, isn’t in the catalog and so gets no reasoning options; the runtime logs a warning naming the model when it drops an override this way. Typically set via the admin set_model_config tool, which writes it to nimblebrain.overrides.json. |
thinkingEffort | "low" | "medium" | "high" | "xhigh" | "max" | medium | How hard to think when reasoning is on. The portable control — see reasoning depth for how it reaches each provider. Ignored for off and adaptive. |
thinkingBudgetTokens | integer | — | Explicit token budget, for metering thinking in tokens instead of naming a depth. Only honored by providers that meter it that way (Anthropic up to 4.6, Gemini 2.5); elsewhere thinkingEffort applies — Gemini 3 takes a level, not a budget. Counts toward maxOutputTokens. Anthropic requires a minimum of 1,024. Prefer thinkingEffort unless you need a hard token cap. |
sessionStore | object | { "type": "memory", "ttlSeconds": 28800 } | MCP session metadata store for /mcp. See sessionStore. |
logging | object | — | Structured logging config. See logging. |
http | object | { "port": 27247, "host": "127.0.0.1" } | HTTP server config. Omit for programmatic-only use. |
features | object | all true | Feature flags. See Feature Flags. |
connectors | object | — | Managed-connector provider settings (Composio). See connectors. |
files | object | — | File upload limits. See files. |
allowInsecureRemotes | boolean | false | Allow HTTP (non-TLS) remote bundle connections. Dev only. |
telemetry | object | { "enabled": true } | Anonymous usage telemetry. See telemetry. |
workDir | string | "~/.nimblebrain" | Working directory for runtime state (conversations, logs, workspaces). |
Reasoning depth
Section titled “Reasoning depth”Providers disagree about how to ask a model to think harder. Two take a named effort tier; two meter it in tokens. NimbleBrain’s config is the effort tier, and the runtime translates.
| Provider | What it receives |
|---|---|
| Anthropic — Opus 4.7/4.8, Sonnet 5, Opus 5 | thinking: {type: "adaptive"} + output_config.effort, the tier verbatim |
| Anthropic — up to Sonnet 4.6 | thinking: {type: "enabled"} + a budgetTokens sized from the tier |
| OpenAI, and Nebius-hosted open-weight models (Qwen, Kimi, GLM, gpt-oss, Cosmos3) | reasoningEffort. Its usable range stops at high, so xhigh and max both land there. On OpenAI, acceptance is also per model — see below |
| xAI (Grok) | reasoningEffort, tops out at high. Acceptance is per model and some Grok models take no effort parameter at all — see below |
| Google Gemini 3 | thinkingConfig.thinkingLevel, tops out at high |
| Google Gemini 2.5 | thinkingConfig.thinkingBudget, sized from the tier and held inside the model’s documented range |
The translation only runs one way. A depth is converted into a token budget where a provider needs one, because the model’s output ceiling is a reasonable scale to express a chosen depth against. The reverse is not done: a token budget carries no depth to recover, and deriving one from a ceiling the operator never set would turn a number nobody chose into an instruction to reason as hard as possible on every call.
thinkingBudgetTokens is optional and independent — it does not replace the
depth. Where a provider meters thinking in tokens, the budget is used and is
capped to leave room for the answer; everywhere else the depth applies as
usual. Setting one never voids the other.
Google and OpenAI support is per model, not per generation, and is read from hand-maintained tables measured against the live APIs.
On Google, gemini-3-pro-preview accepts only low and high where
gemini-3.6-flash accepts all four, and gemini-2.5-pro cannot stop thinking
where the flash models can. A Gemini model with no verified entry receives no
thinking options at all and runs at its own default, because guessing the
dialect is how a stock install starts returning 400s.
On OpenAI, most models take all three tiers, but the GPT-5 -pro models do not
(o1-pro and o3-pro take all three):
gpt-5-pro accepts only high — and rejects medium, the default depth, so
it is the one to know about. gpt-5.2-pro, gpt-5.4-pro and gpt-5.5-pro
reject low. An OpenAI model with no entry is assumed to take all three, the
opposite of the Google default, because nearly every model does and the
adapter already skips non-reasoning ones.
In both cases a requested depth a model doesn’t offer steps down to the
nearest one it does, never up. Where nothing at or below the request is
offered, no depth is sent and the model runs at its own default — so an
explicitly set thinkingEffort can end up not applied on a model that doesn’t
accept it, rather than the call failing.
off is not universally enforceable, and where it isn’t, nothing is sent
rather than a flag that would be rejected or ignored:
- Anthropic — the AI SDK only serializes
thinkingforenabledandadaptive, so adisabledflag never reaches the wire for any model (#738).offworks where it works because Anthropic’s own default is not to think. - OpenAI —
reasoningEffort: "none"is documented as GPT-5.1-only and an error elsewhere. On o-series models reasoning can’t be turned off at all. - Gemini — 2.5 Flash takes a zero budget; 2.5 Pro cannot disable thinking.
On Gemini 3 it depends on the model: those offering a
minimallevel get it, and those that don’t (both Pro entries) get nothing rather than the next tier up, which would answer “don’t reason” with an instruction to reason.
models
Section titled “models”The models object declares two role-based slots. Each value is a provider:model-id string (e.g. "anthropic:claude-sonnet-4-6").
{ "models": { "default": "anthropic:claude-sonnet-4-6", "fast": "anthropic:claude-haiku-4-5-20251001" }}| Slot | Used for |
|---|---|
default | Chat, general requests, most tool-using turns |
fast | Conversation titles, the home briefing, and both history folds — end-of-turn compaction and the mid-turn fold. Cheap/fast workloads, but still needs a large context window: see below |
A workspace can override individual slots via workspace.json → models. When a nb__delegate call or agent profile specifies a slot name (e.g. "model": "fast"), it’s resolved against these slots. "alias:fast" is the same reference written explicitly; both spellings resolve identically.
A bare model string that is neither a slot name nor a model in the catalog is passed through to the provider unchanged, with anthropic: prepended — the deliberate fallback for pinned or bespoke ids served under their own name (for example through an OpenAI-compatible proxy). That means a typo here is not rejected at config load; it fails at the provider on the first call that uses the slot, reporting a model you did not configure.
Changing a slot affects new conversations only
Section titled “Changing a slot affects new conversations only”A conversation is bound to a model when it is created, and that binding does not
move. Changing models.default — in this file, through the override file, or
via the admin set_model_config tool — applies to conversations started after
the change; conversations already under way keep running on the model they began
on. Start a new conversation to pick up the new slot.
The binding is the main chat model only. Auxiliary work always follows the
current slots: title generation and history compaction take fast, and a
nb__delegate call takes whatever slot it names.
Precedence for a chat turn, highest first:
| Source | |
|---|---|
| 1 | The conversation’s binding, set when it was created |
| 2 | A per-request model, on the turn that creates the conversation |
| 3 | models.default from this file (or the override file) |
Conversations created before this behavior existed carry no binding and resolve from the current slot on every turn, as they did before.
modelPolicy
Section titled “modelPolicy”Which models this organization permits. Absent — or present with an empty
allowed — allows every chat-capable model from every configured provider, so
an existing deployment changes nothing and an org opts into restriction.
{ "modelPolicy": { "allowed": ["anthropic:claude-sonnet-5", "anthropic:claude-haiku-4-5-20251001"] }}| Field | Type | Description |
|---|---|---|
allowed | string[] | Qualified provider:model-id strings. |
Policy can only subtract. It narrows what the configured providers can already reach — naming a model whose provider has no key does not make it reachable, and naming a non-chat model does not make it selectable.
This file is the only place it is set. There is no runtime surface for it — the allowlist is deployment configuration, so it lives in version control with the rest of the tenant’s setup and a redeploy always restores it.
Every slot in models should name an allowed model. Nothing stops
you writing a file where one does not: the runtime logs an error naming the
slot at startup and then runs, because a deployment that boots with an error
you can act on beats one that will not start. Until you fix it, turns resolve
to that model while the picker excludes it.
Once the file is loaded the runtime does hold the line — repointing a slot at a forbidden model, or clearing one so it falls back to a forbidden model, is refused. A user’s saved model preference needs no such care: one outside the list falls back to the configured default on their next turn.
providers
Section titled “providers”Per-provider API keys. Takes precedence over the legacy model object when both are present.
{ "providers": { "anthropic": { "apiKey": "sk-ant-..." }, "openai": { "apiKey": "sk-..." }, "google": { "apiKey": "AIza..." } }}Each provider key is optional. If a provider’s apiKey is omitted, the runtime falls back to that provider’s standard env var (ANTHROPIC_API_KEY, OPENAI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, NEBIUS_API_KEY, XAI_API_KEY).
Nebius Token Factory
Section titled “Nebius Token Factory”nebius is an OpenAI-compatible gateway for open-weight models (Qwen, Kimi, GLM, gpt-oss). Configure it like any other provider and reference models with the nebius: prefix:
{ "providers": { "anthropic": { "apiKey": "sk-ant-..." }, "nebius": { "apiKey": "..." } }, "models": { "default": "anthropic:claude-sonnet-4-6", "fast": "nebius:openai/gpt-oss-120b" }}The key falls back to NEBIUS_API_KEY. Unlike the other providers, this key is required when nebius is configured — the runtime fails to start if neither providers.nebius.apiKey nor NEBIUS_API_KEY is set, because the Nebius adapter reads no environment variable of its own and a keyless provider has nothing to authenticate with. Override the endpoint with baseURL (default https://api.tokenfactory.nebius.com/v1). A curated set of flagship models ships in the catalog so they appear in the model picker with pricing; any other Nebius model id still routes as free text (nebius:<org>/<model>), just without catalog metadata. Nebius requests use the Chat Completions API and rely on the provider’s automatic prefix caching — the Anthropic-style explicit cache breakpoints don’t apply.
Reasoning models hosted here stream their thinking back and it is shown in the chat. Those blocks carry no provider signature, so a Nebius turn that hits the output limit mid-answer stops there and reports why, instead of automatically continuing the way an Anthropic turn does.
The exact model lineup is account- and tier-specific and changes over time. The authoritative list for your key is GET https://api.tokenfactory.nebius.com/v1/models; use those ids verbatim (they’re case-sensitive, e.g. Qwen/Qwen3-235B-A22B-Instruct-2507).
xAI (Grok)
Section titled “xAI (Grok)”xai reaches Grok over the Chat Completions API. Configure it like any other provider and reference models with the xai: prefix:
{ "providers": { "anthropic": { "apiKey": "sk-ant-..." }, "xai": { "apiKey": "xai-..." } }, "models": { "default": "xai:grok-4.5" }}The key falls back to XAI_API_KEY, and baseURL overrides the endpoint (default https://api.x.ai/v1). Five models ship in the catalog: grok-4.5, grok-4.3, grok-4.20-0309-reasoning, grok-4.20-0309-non-reasoning, and grok-build-0.1. All five support tool calling.
Reasoning support is uneven in a way worth knowing before you point a slot at one:
| Model | Effort tiers accepted |
|---|---|
grok-4.3 | low, medium, high, plus none to suppress reasoning entirely |
grok-4.5 | low, medium, high |
grok-4.20-0309-reasoning | none — it reasons, but rejects the effort parameter |
grok-build-0.1 | none — same |
grok-4.20-0309-non-reasoning | none — does not reason |
Where a model takes no effort parameter, the runtime sends no reasoning options and the model applies its own default; asking for a tier would fail the call outright. grok-4.3 is the only model in an effort dialect where thinking: "off" sends a suppressing value rather than nothing — none genuinely zeroes the reasoning trace. (Gemini 2.5 models that can disable thinking also honor off, via a zero thinkingBudget; that is the budget dialect, not a tier.)
Two other notes. xAI publishes no per-model max-output cap and accepts max_tokens up to the full context window, so grok-4.5 and grok-build-0.1 are pinned to a 16,384-token output ceiling in the catalog — without it the message budget resolves to zero and every turn fails. And grok-4.20-multi-agent-0309 is deliberately absent: the Chat Completions endpoint refuses it (Multi Agent requests are not allowed on chat completions).
Conversation storage
Section titled “Conversation storage”Conversations are workspace-owned and persisted automatically — there is nothing to configure. Every turn is written to a per-conversation JSONL file at {workDir}/workspaces/<wsId>/conversations/<ownerId>/<convId>.jsonl in the event-sourced format (one append-only event log per conversation), which is what enables crash-resilient run replay and history compaction. Legacy message-format JSONL files are still read.
sessionStore
Section titled “sessionStore”Pluggable metadata store for MCP HTTP sessions. Tracks each Mcp-Session-Id and its idle TTL across the cluster.
{ "sessionStore": { "type": "memory", "ttlSeconds": 28800 }}Process-local Map with periodic TTL sweep. No external dependencies. Default; fine for any single-replica deploy.
{ "sessionStore": { "type": "redis", "ttlSeconds": 28800, "redis": { "url": "${REDIS_URL}", "keyPrefix": "nb:mcp:session:" } }}Hash-per-session with native PEXPIRE TTL. Required when platform.replicas > 1; also useful at single-replica if you want cluster-wide visibility into active sessions for ops/admin tooling. The redis.url field expands ${VAR} placeholders from process.env at boot, so the deployment configmap can carry a literal "${REDIS_URL}" and have the actual URL injected from a secret via envFrom.
| Field | Type | Default | Description |
|---|---|---|---|
type | "memory" | "redis" | "memory" | Provider. redis requires redis.url. |
ttlSeconds | integer (s) | 28800 (8 h) | Idle TTL. Each request resets the clock — actively-used sessions never expire. |
redis.url | string | — | Redis connection URL (redis:// or rediss://). Required when type is "redis". Supports ${VAR} env expansion. |
redis.keyPrefix | string | "nb:mcp:session:" | Hash key prefix for session entries. |
Why a session store at all
Section titled “Why a session store at all”The MCP /mcp endpoint is stateful. Each session has two parts:
- Metadata —
sessionId,identityId,workspaceId, timestamps. Just data; lives in the session store. - Live transport — open HTTP response stream, SDK
Serverinstance, registered request handlers, in-flight JSON-RPC state. Process-bound — holds open file descriptors and JS object references that cannot be serialized or moved.
Metadata is shareable across processes; the live transport is not. Routing requests to the process that owns a session’s transport is the load balancer’s job (cookie stickiness, header-hash on Mcp-Session-Id), not the session store’s. The session store is deliberately deployment-vocabulary-free — no notion of “which pod owns this” — so the same interface fits any deployment topology.
When a request arrives at a process that doesn’t have the live transport for the requested session, the 404 carries error.data.reason:
| Reason | Meaning |
|---|---|
not_found | Session store has no record. Idle-TTL eviction, or session never existed. |
unavailable | Session exists in the store, but the live transport isn’t on this process. Possible causes: process restart, sticky-routing miss, transport closed locally. |
Spec-compliant clients re-initialize on either reason. Operators distinguish unavailable causes via deploy timing, process uptime, and “session-store size vs local transport count” signals.
Configuration matrix
Section titled “Configuration matrix”| Replicas | Store | Use case |
|---|---|---|
| 1 | memory | Default. Dev, programmatic use, simple production. Sessions die on process restart; clients re-initialize. |
| 1 | redis | Single-process with cluster-shared metadata for ops visibility, audit trails, or future scale-out. |
| N | redis | Horizontal scale. Pair with sticky load-balancing (e.g. ALB lb_cookie) — without it, every cross-process request returns unavailable and clients re-initialize constantly. |
| N | memory | Not supported. Sessions partition across processes. |
When type: "redis" is set without a redis.url, NimbleBrain refuses to fall back to in-memory and fails the boot — silently degrading would mask a misconfiguration.
Scaling to multiple replicas
Section titled “Scaling to multiple replicas”platform.replicas > 1 requires all of:
- Storage that supports concurrent multi-process mounts. The default chart’s PVC is
ReadWriteOnceand blocksRollingUpdaterollouts. Move workspace data out of the PVC (Postgres/S3) or relocate toReadWriteMany(EFS). - Routing strategy keyed on
Mcp-Session-Id. ALBlb_cookiestickiness is the simplest path on AWS; alternatives are NGINX/Envoy with header-hash routing, or application-level cross-process proxying. - Cluster-shared session store.
sessionStore.type: "redis". - Deploy strategy that doesn’t drop all replicas at once.
platform.strategy.type: RollingUpdate(with the storage from (1) in place).
Drop any one of these and multi-replica is broken in a different way: PVC deadlock, re-init storms, partitioned session view, or deploy-time outages.
Tuning ttlSeconds
Section titled “Tuning ttlSeconds”The 8 h default is tuned for a connector left open during a working day. Tighten it (e.g. 1800 for 30 minutes) if you want sessions to evict faster on idle; loosen it if your typical usage pattern leaves connectors open across days.
MCP_SESSION_TTL_SECONDS env var overrides this when set.
logging
Section titled “logging”{ "logging": { "dir": "~/.nimblebrain/logs", "disabled": false, "level": "normal", "retentionDays": 30 }}| Field | Type | Default | Description |
|---|---|---|---|
dir | string | {workDir}/logs | Log directory. |
disabled | boolean | false | Disable structured logging entirely. |
level | "normal" | "debug" | "normal" | "debug" persists additional verbose fields. |
retentionDays | integer | — | Auto-delete log files older than N days on startup. Omit for no cleanup. |
connectors
Section titled “connectors”Managed-connector providers. See Connector Providers for what one is, how it differs from a connector, and which providers ship.
A provider is registered — and its callback routes, liveness probe, and vendor SDK reached — only when everything it needs to identify itself resolves: the broker credential for both, plus a namespace for Smithery (see its table below). A deployment without one carries no provider: the /v1/composio-auth/* routes are not mounted, no revalidation sweep runs, and the vendor SDK is never loaded.
{ "connectors": { "providers": { "composio": { "apiKey": "…", "authConfigs": { "gmail": "ac_xxxxxxxx" }, "baseUrl": "https://backend.composio.dev", "monitorEnabled": true }, "smithery": { "namespace": "my-namespace", "monitorEnabled": true } } }}connectors.providers.composio
Section titled “connectors.providers.composio”| Field | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Platform-wide Composio API key — the broker credential. See The broker credential. |
authConfigs | Record<string, string> | {} | Per-toolkit auth-config ids (ac_…), keyed by the toolkit slug the catalog entry names. See Per-toolkit auth configs. |
baseUrl | string | https://backend.composio.dev | Composio API base URL override (self-hosted / staging). Must be http(s) — any other scheme is rejected at startup, because it would turn /v1/composio-auth/proxy into an open redirect. |
monitorEnabled | boolean | true | Run Composio’s arm of the connection-revalidator probe, which detects connections whose downstream vendor account lapsed while the platform key stayed valid. |
The probe’s sweep cadence is not here. One revalidator serves every provider, so the interval is provider-agnostic and is set with NB_CONNECTION_REVALIDATE_INTERVAL_SECONDS (Environment Variables). Only the enable/disable is Composio’s.
Per-toolkit auth configs
Section titled “Per-toolkit auth configs”Every auth: composio connector needs an auth config created in the Composio dashboard (how). Its ac_… id varies per Composio account, so it cannot live in the catalog, which is shared across deployments.
"authConfigs": { "gmail": "ac_xxxxxxxx", "posthog": "ac_yyyyyyyy" }The key is the catalog entry’s composio.toolkit — an identifier that entry already carries, rather than a third name invented to carry the value. It is still matched exactly, so a mistyped key resolves to nothing; the runtime checks the whole set at boot and logs any key matching no catalog toolkit.
A toolkit you leave out is not reported — wiring a subset of the catalog is the normal case, so a standing boot warning listing the rest would only teach operators to ignore [composio]. Installing a connector whose toolkit has no id fails at that moment, naming the toolkit and the config key to set.
These are identifiers, not secrets: an ac_… is inert without apiKey, the way a client_id is inert without a client_secret. They live in config next to the toolkits they name rather than in your secret store.
One instance serves one tenant, so this is a single config layer: there is no per-tenant merge. Intra-tenant isolation is unchanged — the single broker account still serves every workspace and user in the tenant, namespaced by the Composio user_id.
connectors.providers.smithery
Section titled “connectors.providers.smithery”| Field | Type | Default | Description |
|---|---|---|---|
apiKey | string | — | Platform-wide Smithery API key. Falls back to SMITHERY_API_KEY. Its presence, with a namespace, is the gate on registering the provider at all. |
namespace | string | — | Smithery namespace brokered connections are created under. Required — a namespace is globally unique and account-owned at Smithery, so there is no safe default; without one the provider stays unregistered even with a credential present. |
baseUrl | string | https://api.smithery.ai | Connect API base URL override. Must be http(s) — the session URL derived from it becomes an installed connector’s remote MCP target. |
monitorEnabled | boolean | true | Run Smithery’s arm of the connection-revalidator probe. It detects a connection deleted at the broker; a lapsed authorization is logged, not flipped, until a verdict can carry a remedy URL. |
Smithery contributes no callback routes: it brokers OAuth on its own hosted setup page rather than redirecting back to us, so there is no equivalent of /v1/composio-auth/* to mount. See Connectors Catalog for authoring an auth: smithery entry.
The broker credential
Section titled “The broker credential”apiKey is the platform-wide Composio API key and the gate on registering the provider at all — without it (here or in COMPOSIO_API_KEY) the integration stays dormant.
It is declarable because nothing persisted points at where it lives. A connector installed through Composio records auth: { type: "provider", provider: "composio" } in workspace.json — it names the credential it needs, not a place to find it — so the key never sits at rest and resolution stays the provider’s own business. Connectors installed before that change carry a ${COMPOSIO_API_KEY} environment reference and are mapped forward when they start, so both resolve identically with no operator action.
The broker credential is the only env fallback
Section titled “The broker credential is the only env fallback”Settings resolve from this block only — there is no environment fallback for them. The broker credential is the exception: apiKey also reads COMPOSIO_API_KEY / SMITHERY_API_KEY, because a secret has a legitimate home in the environment. One value, one source, with that single documented exception.
NB_TENANT_ID is likewise not part of this block. It is a deployment identity, stays env-only, and is still required when the OAuth bouncer is configured.
File upload limits applied when features.fileContext is enabled.
{ "files": { "maxFileSize": 26214400, "maxTotalSize": 104857600, "maxFilesPerMessage": 10, "maxExtractedTextSize": 204800 }}| Field | Type | Default | Description |
|---|---|---|---|
maxFileSize | integer (bytes) | 26 MB | Maximum size of a single uploaded file. |
maxTotalSize | integer (bytes) | 100 MB | Maximum total size of all files per message. |
maxFilesPerMessage | integer | 10 | Maximum number of files per chat message. |
maxExtractedTextSize | integer (bytes) | 200 KB | Maximum extracted text size per file. |
telemetry
Section titled “telemetry”NimbleBrain collects anonymized, aggregate usage data with no PII — no bundle names, tool names, error messages, or file paths are sent.
{ "telemetry": { "enabled": false } }Disable with NB_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 env vars, or set telemetry.enabled: false as shown above.
Legacy model object
Section titled “Legacy model object”The older single-provider model shape is still accepted for backward compatibility but deprecated. Prefer providers for mixing API keys and models for model selection.
{ "model": { "provider": "anthropic", "apiKey": "sk-ant-..." }, "defaultModel": "claude-sonnet-4-6"}| Field | Type | Description |
|---|---|---|
provider | "anthropic" | "openai" | "google" | Default provider when providers isn’t set. |
apiKey | string | Falls back to the provider’s env var when omitted. |
Validation
Section titled “Validation”NimbleBrain validates nimblebrain.json at startup against the JSON Schema (draft-07) using AJV.
- Unknown keys produce a warning on stderr and are ignored.
- Structural errors (wrong types, missing required fields) throw an error and prevent startup.
- Workspace-owned and legacy keys (
bundles,skillDirs,agents,home,preferences,noDefaultBundles, and the legacyskills) are silently stripped — no warning. - The override file (
nimblebrain.overrides.json) is validated structurally, but unknown-key warnings are suppressed for it. Every keyset_model_configwrites is in the published schema; the suppression is for version drift, since the file is preserved across deploys and can outlive the schema of whichever image reads it. - Deprecated keys (
identity,contextFile) produce a warning pointing at context skills.
[config] Warning: unknown key "foo" in /home/user/.nimblebrain/nimblebrain.json (ignored)Error: Invalid config in /home/user/.nimblebrain/nimblebrain.json: - /maxIterations: must be <= 50CLI flag overrides
Section titled “CLI flag overrides”CLI flags take precedence over values in the config file:
| Flag | Overrides |
|---|---|
--config <path> | Config file location |
--workdir <dir> | Default location of the config file and workDir fallback |
--model <id> | defaultModel field (legacy) |
--port <n> | http.port field |
--debug | Enables verbose debug event logging |
Minimal config
Section titled “Minimal config”The smallest valid config file:
{ "$schema": "https://schemas.nimblebrain.ai/v1/nimblebrain-config.schema.json", "version": "1"}This uses all defaults: Anthropic provider (key from ANTHROPIC_API_KEY), default model, no MCP bundles installed (platform capabilities are built in), in-memory conversation storage, and structured logging enabled.