CONTEXT.md
Rama main · réplica de lectura del repositorio original.
# 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: `<publicOrigin>/mcp/<wsId>`
(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
`<wsId>`, checked per request, authorizes** — the audience only prevents replay.
An MCP request is bound to (identity, workspace); `/mcp/<wsId>` keeps no session (ADR-0048).
REST addresses a workspace the same way: `/v1/workspaces/<wsId>/…` (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 `<ownerId>`
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
`<source>__<tool>` — 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/<wsId>/<primitive>/<ownerId>/`
(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/<wsId>/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/<app>/`. `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: <type>` 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/<wsId>` 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/<wsId>` 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