# CONTEXT — NimbleBrain runtime The domain model for `nimblebrain/code`: what the nouns mean and the invariants that hold. This is the *what is true* reference. For *how to work in the repo* see `AGENTS.md`; for *why a thing is the way it is* see the decision log in `adr/`; for the codebase map and API reference see `README.md`. > Authoring rule: this file is present-tense and describes the current system. > History and rationale live in `adr/` and git, not here. ## What NimbleBrain OSS is — the three golden rules NimbleBrain is a generic agent runtime with the smallest kernel that does three things. Everything in the kernel serves one of these; if a capability doesn't, it belongs in an MCP server, a Synapse app, or upstream MCP — not the runtime. 1. **Manage skills** — discover, compose, and apply `SKILL.md`-format guidance into the agent's prompt without busting the prompt cache. *(Domain terms: skill, role/channel, catalog, provenance. ADRs 0009–0019.)* 2. **Orchestrate over remote MCP servers** — treat every tool/resource provider (a directly-addressed server, a gateway-brokered one, a Synapse app) as an interchangeable MCP source through one boundary, and keep those connections healthy. The runtime connects; it does not acquire, verify, or execute a server's code. *(Domain terms: source, connection, brokered connector, task augmentation. ADRs 0005, 0020–0032.)* 3. **Provide secure RBAC** — isolate everything behind the workspace boundary, private to its owner by default, with no ambient cross-scope authority. *(Domain terms below; ADRs 0001–0008.)* ## Domain glossary ### Workspace The authorization boundary (ADR-0001). A session reaches exactly **one** workspace plus the caller's identity tools — never a union across workspaces. A workspace has an opaque, generated id (`ws_<16-hex>`, the only form boot accepts, ADR-0042), members, and roles (`admin` | `member`). How a workspace came to exist never changes what it is (ADR-0039). An MCP connection is addressed to one workspace by URL: `/mcp/` (ADR-0036). Bare `/mcp` names none and is refused. A token from the MCP authorization server is valid only at the URL its `aud` names exactly. A first-party credential is bound to no resource: the instance's login session, or an authorization-server token issued to a client the operator configures as its own (ADR-0038). A credential's audience says where it may be used; the client it was issued to says whose app holds it, and only a configured first-party client gets first-party standing. Either way, **membership of ``, checked per request, authorizes** — the audience only prevents replay. An MCP request is bound to (identity, workspace); `/mcp/` keeps no session (ADR-0048). REST addresses a workspace the same way: `/v1/workspaces//…` (ADR-0037), admitted by the same membership check. A route either acts on a workspace and names it in its path, or acts on the caller or on a primitive its own id locates (bootstrap, the event stream, a conversation, a file) and names none. No header, argument, qualified name or default selects a workspace for a request, and a conversation is resumed only at its own workspace's path. The URL is the only thing that names a workspace: bootstrap returns the caller's workspaces and no focus (ADR-0044). A kernel tool that acts on the caller or the org needs no workspace and is called without one (ADR-0043). A workspace exists because `create` made it and stops existing because `delete` archived it. **No write brings one into being**: a writer may create paths inside a workspace root, and fails naming the workspace when the root is gone. So a workspace directory on disk is always one a create produced, which is what lets `list()` treat an unparseable `workspace.json` as corruption rather than as a tree some writer conjured. ### First workspace A user who belongs to no workspace gets one at bootstrap, named for them with them as admin. It is an ordinary workspace — it can be shared, and nothing marks how it was made (ADR-0039) or makes it the one the user opens. `/` takes a user in exactly one workspace into it and shows anyone else all of theirs (ADR-0044). ### Identity provider What authenticates a request and says who made it, named by `auth.adapter` in `instance.json`: `dev` (every request is one local developer, an org admin), `oidc` or `workos`. It is always a choice: with no `instance.json` (and none passed in) the runtime does not start, and no request is admitted as `dev` because the file is absent. Every run names its caller and its workspace (ADR-0040). ### Owner The authenticated principal a primitive belongs to. Stored as an `` sub-partition in the path (ADR-0003), which makes owner-privacy structural. ### Visibility A mutable field on a primitive, `private` (default, fail-closed) or `shared` (ADR-0004). v1 is private-only; `shared` is groundwork, not yet read. `shared` never crosses the workspace wall — it means "to this workspace," never beyond. ### The wall The workspace boundary, enforced **structurally** (the storage path, ADR-0003) and **at dispatch** (`routeToolCall`, ADR-0005). Reaching another workspace is **denied, not gated**. The one sanctioned crossing is a personal-connector grant (ADR-0006), which fails closed with `ConnectorGrantDenied`. ### Identity door / tool namespacing A tool name's shape is its scope (ADR-0005). **Workspace tools** are bare `__` — the workspace is not in the name, so another one cannot be addressed. **Personal connectors** carry a reserved `my_` marker (`my_gmail__send`). **Identity tools** (kernel sources — conversations, files, tasks) are bare too, owned by the user, outside any workspace. The source segment alone decides which door a call takes. **Two senses of "owned" meet here.** A kernel source is *identity*-owned — the tool is the user's and takes the identity door in every workspace. The data it reaches is *workspace*-owned — each conversation, file, and task sits in one workspace's partition (ADR-0003). Identity-owned door, workspace-owned data; both are true at once, and the wall is the second one. ### Primitive (conversation / file / task) Workspace-owned, stored at `workspaces////` (ADR-0003). The path is authoritative; `workspaceId`/`ownerId` on the record are denormalised. Private to the owner by default (ADR-0004). **Active use** (resuming a conversation, running a task, reaching the workspace's tools) requires **current membership** of the primitive's workspace — checked at session establishment, so offboarding revokes reach; **reading** your own authored primitive stays owner-gated (ADR-0007). ### File A workspace-owned primitive with a globally-unique id (`fl_<24 hex>`). Addressed by the bare id: the server resolves its workspace from the id within the caller's own owner partitions, via the `FileLocator` (ADR-0002). The owner partition is both the gate and the search scope. ### Notification A fact a connector recorded that nobody asked for, pulled from an outbox the server declares and held in a workspace-owned inbox at `workspaces//notifications/` (ADR-0003, ADR-0008). Unlike the other primitives it has **no owner sub-partition**: it is authored by a connector, not a user, so any member who can reach that connector's tools can read it. The runtime stamps `source`, `workspaceId`, `receivedAt` and a per-workspace monotonic `seq`; a server can set none of them. The runtime reads the envelope fields it defined and never the server's opaque `data`. ### Source Any tool/resource provider behind the MCP boundary. Every source is one `McpSource` over one transport union (ADR-0022): a remote server over HTTPS, or one of the kernel's own capabilities — conversations, files, tasks, skills — served by a real MCP server on an in-memory linked-pair transport. The platform's own capabilities are MCP servers to themselves, so nothing above the source can tell them apart. The runtime **connects**; it does not acquire, verify, or execute a server's code (ADR-0020). A persisted reference is a URL plus how to reach it and who it speaks as, and discovery is one catalog over a directory of published `ServerDetail` files — the shape the upstream MCP registry publishes, so anything that can write files can populate it. The transport arm is also a trust boundary: in-memory means the bytes never left the process, which is what host-owned `_meta` markers are conditioned on (ADR-0014, ADR-0024). ### Platform app One of the kernel's own capabilities, expressed as a source: conversations, files, tasks, usage, skills, instructions, hooks, notifications, compose. Each is an in-process MCP server the runtime hosts rather than connects to — there is no URL, no credential, and no connection to supervise, so none of what the *Connection* entry below describes applies to one. An app is one unit — its tools, the code behind them, and its UI — and it lives in one directory, `src/platform//`. `createPlatformSources` starts the set. Being a source is the whole of its interface: the registry, the prompt, and `/mcp` cannot tell it from a remote server, which is what keeps the kernel small enough to have only one extension mechanism. ### Connection The supervised link to a remote source, carrying the `(serverName, workspaceId, principalId, ref)` tuple. Two disjoint loops watch it (ADR-0028): **process liveness** (`HealthMonitor` — is the transport up? restart it, indefinitely, because an outage self-heals) and **credential liveness** (`ConnectionRevalidator` — did the upstream authorization lapse? flip to reauth-required and stop, because only a human can fix it). Neither loop concludes anything about the other's question. A connection's secret is named, never inlined: persisted state carries a credential reference resolved through one store (ADR-0027). A connection records **state, not the handle**. The `McpSource` lives in the workspace's registry, addressed by the `(workspaceId, serverName)` the connection is already keyed on, and a consumer that needs it resolves it there each time. `running` therefore means one thing — the credential is good and a source is registered — and a reconnect, which replaces the source object wholesale, cannot leave a second reference pointing at the predecessor. ### Brokered connector A connector whose auth *and* hosted MCP session come from a third party. `ManagedConnectorProvider` is the seam, and it owns the **auth-and-session broker role only — never invocation** (ADR-0026); the session it returns is an ordinary remote source. `dcr` and `static` are runtime-native and stay outside the seam. ### Task augmentation How a long-running tool call is made without holding a socket open: the 2026-07-28 tasks extension, and only that (ADR-0046). The server decides per call whether to answer with a task; the runtime polls it and can cancel (ADR-0029). A 2025-era connection never carries a task: `/mcp` serves only 2026-07-28 (ADR-0048), and the runtime calls a 2025-era connector's tools inline or refuses one that requires a task. The retry policy inverts with it: an inline call is re-issued once on a transport error, and a task call is **never** retried — it has already created server-side state, so replaying it would duplicate the side effect. ### Task *(proposed, ADR-0045)* The unattended primitive: a definition (what to do, optional schemas and acceptance criteria, limits, and an **optional** trigger) and its **runs**. A scheduled task has a trigger; a one-off is a task run once with none; a batch is one task run over many inputs. A run's outcome has two parts: *execution* (how it ended) and *assessment* (whether the deliverable is acceptable). Not the same word as *task augmentation*: that is how a run travels over MCP, not what a run is. ### Skill A unit of guidance in the Agent Skills format. The file is the standard, unmodified, with the runtime's own configuration nested under `metadata.nimblebrain` and validated by one schema (ADR-0009). A skill comes off the filesystem, from an MCP server's `skills/list` under the MCP Skills Extension (a peer, not a lesser kind; its body fetched when needed and verified against the listed digest — ADR-0011), or from a curated connector overlay (ADR-0013). ### Role / channel A skill's declared `loading-strategy`, and the prompt channel that follows from it (ADR-0010). `always` composes into the system prompt every turn. `dynamic` reaches the model by tool-affinity (selected once at turn start), by an explicit trigger phrase (matched per message, the deterministic must-fire channel), or — carrying neither — by the catalog. The sets are disjoint by construction, so a skill is never in two channels, and one predicate answers "how would this skill load?" for every read surface. ### Catalog The model-facing index of activatable skills: name and description only, sorted and deduplicated (ADR-0015). It lists what **can** be activated and never what **is** loaded, so its bytes move only on install and authoring events and the cached prompt prefix holds. ### Provenance Who authored a skill. `chat`, `admin`, `connector`, and `import` are audit facts written to disk. `vendored` is not: it is the trust marker for platform-shipped skills, stamped in memory at load on the platform's own source-tree directories and absent from the on-disk schema, so a file cannot forge it (ADR-0012). ## Decisions The decision log is `adr/`. A proposed ADR is a draft and changes in place until it is accepted; a new ADR amends or supersedes only an accepted one. Foundational (secure RBAC): - [0001](adr/0001-workspace-is-the-boundary-not-identity.md) — the workspace is the boundary, not the identity - [0002](adr/0002-files-resolve-by-bare-id.md) — files resolve by bare id via a workspace locator - [0003](adr/0003-primitives-are-workspace-owned-and-path-authoritative.md) — primitives are workspace-owned and path-authoritative - [0004](adr/0004-private-by-default-visibility-is-a-field.md) — private by default; visibility is a mutable field - [0005](adr/0005-no-cross-workspace-reach-tool-shape-is-scope.md) — no cross-workspace reach; a tool name's shape is its scope - [0006](adr/0006-personal-connector-use-requires-a-grant.md) — personal-connector use in a shared workspace requires a grant - [0007](adr/0007-offboarding-revokes-active-use.md) — offboarding revokes active use; ownership is necessary, not sufficient - [0008](adr/0008-notifications-are-pulled-and-routed-by-the-operator.md) — notifications are pulled into a workspace inbox and routed by the operator - [0036](adr/0036-an-mcp-connection-is-addressed-to-one-workspace-by-url.md) — an MCP connection is addressed to one workspace by URL; a token is valid only for its resource; membership authorizes (amended by 0048) - [0037](adr/0037-a-workspace-is-addressed-by-url-on-every-surface.md) — a workspace is addressed by URL on every surface; a route is workspace-scoped or identity-scoped, and its path says which (amended by 0044) - [0038](adr/0038-a-credential-is-first-party-by-the-client-it-was-issued-to.md) — a credential's audience says where it may be used; the client it was issued to says whose app holds it; only a configured first-party client gets first-party standing - [0039](adr/0039-the-first-workspace-is-an-ordinary-workspace.md) — the workspace provisioned for a user is an ordinary workspace (id retention superseded by 0042, the landing preference by 0044) - [0040](adr/0040-the-identity-provider-is-chosen-never-defaulted.md) — `instance.json` names the identity provider, `dev` included; with none, the server does not start - [0042](adr/0042-a-workspace-id-is-generated-and-boot-refuses-any-other.md) — a workspace id is generated to one grammar; boot refuses a workspace directory with any other - [0043](adr/0043-a-tool-that-acts-on-the-caller-or-the-org-needs-no-workspace.md) — a kernel tool that acts on the caller or the org declares that it needs no workspace - [0044](adr/0044-the-url-is-the-only-thing-that-names-a-workspace.md) — the URL is the only thing that names a workspace; the server chooses none Manage skills: - [0009](adr/0009-skills-are-the-agent-skills-standard-plus-a-nested-runtime-block.md) — a skill is the Agent Skills standard plus one nested runtime block - [0010](adr/0010-role-decides-the-channel.md) — a skill's role decides its prompt channel (amended by 0053) - [0011](adr/0011-a-server-published-skill-is-a-peer-of-a-filesystem-skill.md) — a server-published skill is a peer of a filesystem skill - [0012](adr/0012-vendored-provenance-is-loader-stamped.md) — vendored provenance is loader-stamped, never on disk - [0013](adr/0013-connector-overlays-are-curated-pinned-and-reconciled-at-boot.md) — connector overlays: one curated repo, pinned, content-addressed, reconciled at boot (amended by 0053) - [0014](adr/0014-skill-markers-are-host-owned-meta.md) — skill activation and suppression markers are host-owned `_meta` - [0015](adr/0015-the-catalog-lists-what-can-be-activated.md) — the catalog lists what can be activated, never what is loaded - [0016](adr/0016-a-curated-overlay-binds-by-the-connectors-derived-identity.md) — a curated overlay binds by the connector's derived identity - [0017](adr/0017-the-trigger-matcher-is-the-must-fire-channel.md) — the trigger matcher is the must-fire channel, not a relevance hint - [0018](adr/0018-skill-channels-and-the-cache-breakpoints.md) — no system-prompt section depends on the turn's tool set - [0019](adr/0019-the-most-local-skill-wins-a-shared-name.md) — the most local skill wins a shared name: user, workspace, platform and org, server-published, overlay - [0034](adr/0034-a-skills-scope-names-where-it-is-stored.md) — a skill's scope names where it is stored; its name prefix names who published it - [0053](adr/0053-a-bound-skill-reaches-the-model-before-its-tools-first-call.md) — a skill bound to a tool reaches the model before that tool's first call; the engine holds the call until it does - [0055](adr/0055-guidance-for-one-app-is-a-skill-bound-to-its-tools.md) — guidance for one app is a skill bound to its tools Orchestrate over remote MCP: - [0020](adr/0020-the-runtime-connects-it-does-not-acquire.md) — the runtime orchestrates over remote MCP; it does not acquire or execute a server's code - [0021](adr/0021-one-run-start-door.md) — one run-start door; delegation is not a kernel capability - [0022](adr/0022-one-source-type-over-one-transport-union.md) — every source is one `McpSource` over one transport union - [0023](adr/0023-client-capability-advertisement.md) — the client advertises `extensions` and nothing it does not serve; `tasks` only as the 2026 extension (amended by 0046, 0050) - [0024](adr/0024-private-extensions-live-under-one-reverse-dns-namespace.md) — private extensions live under `ai.nimblebrain/*`, and reuse the spec's schemas (amended by 0055) - [0025](adr/0025-hook-declarations-come-from-the-operator-trusted-catalog.md) — hook declarations come from operator-trusted catalog metadata - [0026](adr/0026-a-brokered-provider-owns-auth-and-session-only.md) — a brokered provider owns auth-and-session only, never invocation - [0027](adr/0027-persisted-state-names-the-credential-not-the-value.md) — persisted state names *what* credential it needs, never *where* the value lives - [0028](adr/0028-two-disjoint-health-loops.md) — two disjoint health loops: process liveness and credential liveness - [0029](adr/0029-long-running-tools-are-task-augmented-and-never-retried.md) — long-running tools are task-augmented; task calls never retry (amended by 0046) - [0030](adr/0030-a-resource-update-is-a-hint-on-a-poll.md) — a resource update is a latency hint on a reader that already polls; nothing else subscribes - [0031](adr/0031-the-consent-model-reads-no-tool-annotation.md) — consent and tool policy read no annotation; no annotation relaxes either - [0032](adr/0032-persisted-connector-state-carries-one-brokered-block.md) — a brokered install persists one block, `{ provider, connectorId, providerRef }`; the auth kind is an open string - [0033](adr/0033-an-events-family-names-its-subject.md) — an event's family names its subject; process liveness is a connector fact - [0035](adr/0035-the-secrets-backend-is-configuration-and-a-sealed-value-opens-or-refuses.md) — the secrets backend is configuration; a value that claims to be sealed opens or refuses - [0041](adr/0041-event-streams-send-typed-payloads-and-cloudevents-is-for-an-outside-consumer.md) — event streams send `event: ` and the typed payload; CloudEvents is for an outside consumer - [0045](adr/0045-a-task-is-the-unattended-primitive-and-its-trigger-is-optional.md) — a task is the unattended primitive; its trigger is optional; a run's outcome separates execution from assessment - [0046](adr/0046-task-augmentation-is-the-2026-tasks-extension-only.md) — task augmentation is the 2026-07-28 tasks extension, inbound, outbound and app to host; the 2025-11-25 tasks utility is not spoken (amended by 0048) - [0047](adr/0047-the-door-decides-a-run-and-an-execution-provider-runs-it.md) — *(superseded by 0056 and 0057)* the door decides a run and an execution provider runs it - [0048](adr/0048-mcp-serves-2026-07-28-only.md) — `/mcp/` serves 2026-07-28 only, per request, with no session; the runtime as a client still falls back to 2025-11-25 for a connector - [0049](adr/0049-the-agent-reads-a-connectors-records-as-resources.md) — a connector's resource templates are named in its prompt entry, bounded; a non-skill `read_resource` returns up to the tool-result limit (amended by 0055) - [0050](adr/0050-a-servers-url-request-reaches-a-person-or-comes-back-as-a-link.md) — an inline `/mcp/` call carries the outside client's capabilities to the connector and relays its input requests back, each elicitation naming the connector; the connector's `requestState` travels sealed to the caller, workspace, tool and arguments; tasks and the runtime's own turns claim nothing - [0052](adr/0052-a-view-opens-what-it-names-by-uri.md) — a view or the agent opens a place by URI (`ai.nimblebrain/open`) and the host finds who shows it: a kernel scheme, an app's `ui://` root, or the one app whose resource template matches; opening one's own connector settings is a separate parameterless request - [0056](adr/0056-an-agent-runtime-executes-what-the-door-decided.md) — the door decides a run and an agent runtime executes it, narrowing and never widening; which runtime is installation configuration; no agent runs as a child of the runtime; an external agent reaches tools only through a per-run projection of the router (amended by 0057) - [0057](adr/0057-an-external-agent-runs-out-of-process-on-credentials-only-the-runtime-mints.md) — an external agent runs out of process in a pool of long-lived workers, one session per turn, or as a durable job when a run must survive a restart; its tools travel on its ACP connection, through the run's projection, with no listener and no tool credential; the runtime mints each turn a model credential, and the installation's model gateway holds the keys and the meter