Skip to content

The rib model

A rib is a capability in its own package, on its own version, bolted onto a harness that never changes. It is a deliberate alternative to the usual ways new capability gets added: forking the harness and maintaining the fork, upstreaming a plugin into a monolith and waiting on review, or running a sidecar and inventing a protocol to reach it. A rib is a dependency you add and remove, nothing more.

The contract that makes that work, and the boundary it rests on, are the rest of this page.

A rib is a Bun package named @keelson/rib-<id>, usually in a repository named keelson-rib-<id>. Installation is dependency management, not integration work: keelson rib add <source> hands the source to bun add, and at the next boot the server discovers whatever rib-* packages sit in the home’s @keelson scope. There is no central registry of ribs, no manifest to edit, and no plugin API key. Anyone can publish a rib; installing one is a one-line decision you can reverse with keelson rib remove.

Discovery also separates two states that plugin systems usually conflate. Installed means the package is present in the home. Active means the server validated and registered it at boot. KEELSON_RIBS filters between them, so an operator can keep five ribs installed and activate one, per process, without uninstalling anything.

The contract is one interface, and every hook on it is optional. A rib implements the subset it needs, and each hook lands its contribution in a specific place in the harness. The figure shows the main destinations:

One rib package on the left, four labeled arrows crossing a dashed harness boundary to four destinations: the tool registry, snapshot keys under the rib's namespace, the workflow catalog, and the actions and auth status probes.

Figure 1. One rib, several destinations. Each optional hook lands its contribution in a specific harness registry; the figure traces the four core ones, and policies, docs, agents, and commands land in registries of their own. Everything the rib touches stays under its own namespace.

HookWhat it contributes
registerToolsTools for the shared registry. They reach the chat agent with no further wiring; a workflow prompt node opts each one in by name.
composeBundleA snapshot composer under the rib’s key namespace. The browser renders the published data as live boards.
views, surfacesStatic declarations of how those snapshots appear, up to a full top-level tab in the browser.
contributeWorkflowsWorkflow definitions built in code and merged into the catalog at activation, optionally bound so a run’s output republishes a snapshot key. A rib can also ship static definitions as plain YAML files in a workflows/ folder at its package root; they merge with the same rib provenance, no hook required.
contributePoliciesGovernance policies merged into the harness policy engine at activation, evaluated at each turn’s hook points.
contributeDocsDocumentation sources the rib folds into the harness docs catalog, listed and sliced on demand by the keelson_docs tool, so an installed rib extends what the agent can read about itself.
onActionA handler for inbound actions over the loopback API, so a board’s buttons can reach the rib.
listAgents, resolveAgent, listCommands, invokeCommand, completeCommandNamed chat agents and slash commands the rib adds to the chat composer.
authStatusA credential probe surfaced in the rib inventory, so the harness can report whether the rib’s external system is reachable.
disposeTeardown. The harness awaits it at shutdown, before the database closes.

The chat composer is a further destination the figure leaves out: the agents and commands hooks feed it. The pattern holds across all of them, though: a rib contributes data and declarations, and the harness owns the machinery. A rib never renders UI, never opens its own port, and never touches the database.

The architecture page states the rule from the harness side: the core never imports a rib. The rib model adds the other half: a rib cannot reach outside its own namespace, and the harness checks rather than trusts.

  • Snapshot keys are namespaced. Everything a rib publishes lives under rib:<id> or rib:<id>:*. The snapshot manager a rib receives is scoped: registering a key outside the namespace throws at activation, and reading another rib’s keys returns nothing.
  • Credentials are namespaced. A rib’s getCredential("token") resolves to a keychain entry under rib_<id>_token. The accessor is read-only and the account format is validated, so one rib cannot enumerate or read another rib’s secrets through the harness.
  • Tool names are global, collisions lose. A tool name already claimed is skipped with a warning, which is why rib tools carry a family prefix, like weather_now.

The model earns its keep when a contribution travels. Follow one tool call end to end:

  1. A chat turn starts. The server assembles the turn’s tools from the shared registry, the same list GET /api/tools reports, with rib tools folded in beside the harness’s own.
  2. The provider adapter projects each tool into its SDK’s format. The agent sees a typed tool; it does not see ribs at all.
  3. The agent calls the tool. The provider validates the input against the tool’s schema, then runs the rib’s execute with the rib’s own context: its process-exec helper, its scoped credentials, its snapshot manager.
  4. The result streams back as a tool_result chunk, through the same pipeline every other chunk uses, into the conversation record.

Nothing in that path is rib-specific. The same registry serves workflow prompt nodes, so a tool written once is callable from chat, from a deterministic workflow, and from any surface that can start either. That is the rib model’s bet: if the contract is narrow and the destinations are shared, capability scales by adding packages, not by modifying the harness.

  • Snapshots and surfaces covers the publishing substrate behind composeBundle, views, and surfaces.
  • The Rib contract is the precise interface, the context the harness injects, and the validation rules it enforces.