Skip to content

Architecture

Keelson is a harness with a typed extension contract, not a monolith. This page is the map of the system: what the harness owns, how the pieces wire together at boot, where ribs attach, and the one rule that keeps the harness and the ribs apart.

Two surfaces drive one server, and the server fans out into the subsystems that do the work. The CLI and the browser both speak to the Keelson server over HTTP and WebSocket. The server owns the provider registry, the tool registry, the workflow executor, and the snapshot manager, and each of those grounds into local state: your OS keychain, the rib packages you install, the SQLite store, and the WebSocket frames the browser renders as live surfaces. The tool registry also faces outward: the server re-exposes it over the Model Context Protocol at /api/mcp, so an external agent can call the same rib and workflow tools the chat agent uses.

A layered runtime diagram. The CLI and Browser SPA sit at the top and connect through an HTTP / WebSocket bus to the Keelson server. The server fans out to four subsystems: providers, the tool registry, workflows, and snapshots. Each subsystem grounds into a stack below it: providers into credentials and the OS keychain, the tool registry into rib tools and ribs, workflows into YAML DAG runs and the SQLite store, snapshots into live surfaces and WebSocket frames. The tool-registry column is brass, marking the path where ribs attach.

Figure 1. The runtime shape. The two surfaces drive one server, which fans out into four subsystems, each grounding into local state. Navy is the harness, ocean blue is something attached to it, brass is a rib, and dashed slate is external state. The brass column is the seam where ribs attach.

The split that defines keelson is the split between the harness and the ribs. The harness is the part that ships in this repository: the provider registry, the tool registry, the workflow executor, the chat surface, the SQLite store, and the credential vault. It is the same on every install.

A rib is an extension that lives in its own package, named keelson-rib-<name> on GitHub and @keelson/rib-<name> on npm. A rib is the only place an external system is touched. A rib can contribute tools to the agent path, workflows to the catalog, and surfaces to the browser, all without changing the harness or the SPA. Install one as a dependency and the server discovers it at boot; nothing about the harness changes.

The same system reads as a ship’s frame, the picture the project is named for. The harness is the keelson beam running the length of the build. The two surfaces, the CLI and the browser, drive the same server. Ribs are the frames fastened to the beam, each owning one external system.

A blueprint side elevation of a hull. The keelson beam is the harness; three brass frames rise from it, bolted to the beam, each labeled as a keelson-rib package. A midship section inset shows one rib bolted to the keelson. The CLI is the rudder, the browser SPA a surface, and a dashed superstructure (wheelhouse, mast, and rigging) marks structure still under construction.

Figure 2. The harness is the keelson beam. Ribs fasten on as separately versioned packages, the CLI steers from the shell, and the harness has no compile-time dependency on any rib: installed ribs are discovered and loaded through the rib contract at boot.

The harness is a Bun monorepo. Each workspace owns one responsibility, and the server is the composition root that wires them together at startup.

apps/cli

The keelson command. Routes to the server over HTTP and WebSocket when it is up, and falls back to in-process execution when it is down.

apps/server

The HTTP and WebSocket server on port 7878, and the composition root. It wires the snapshot manager, providers, credentials, and ribs, and owns the SQLite store and the policy engine.

apps/web

The React SPA. Renders the built-in Chat, Workflows, Memory, and Usage surfaces (Usage a read-only token ledger), plus any surface a rib contributes, with no per-rib UI code.

packages/shared

The public types and contracts: the Rib interface and the snapshot streaming substrate. It carries no dependency back on the harness.

packages/workflows

The DAG executor and YAML schema. The node taxonomy and substitution model borrow concepts from Archon.

packages/providers

The coding-agent SDKs behind one interface: Copilot, Claude, Codex, the multi-vendor Pi, a no-auth stub for offline use, and any configured OpenAI-compatible gateway.

The keelson home

The runtime home at ~/.keelson: the SQLite database, the bundled starter workflows, the ribs you install, and the named commands and scripts that workflow nodes run.

startServer() builds the system in a fixed order, because later stages close over earlier ones. The registries have to be populated before the chat and workflow handlers capture them, and the snapshot manager has to exist before a rib can bind to it.

  1. Bootstrap providers. Build the provider registry over the enabled provider SDKs (Copilot, Claude, Codex, Pi, and the offline stub) plus any configured OpenAI-compatible gateway, each resolving its own credentials.

  2. Create the snapshot manager. Stand up the generic streaming substrate that ribs publish boards through, before any rib can bind to it.

  3. Create the credential store. Open the keyring-backed vault, so each rib can be handed a namespaced, read-only reader scoped to its own keys.

  4. Discover and activate ribs. bootstrapRibs() scans the keelson home for @keelson/rib-* packages, validates each, imports it, and filters the set by KEELSON_RIBS.

  5. Register rib tools. Each active rib’s tools join the shared registry that the chat agent and workflow prompt nodes read.

  6. Prepare rib workflows. Narrow each contributed workflow definition against the schema and collect the bindings that republish a run’s output to a snapshot key.

  7. Open the SQLite stores. Conversations, workflow runs, memory, and projects, all in the one database at the keelson home.

  8. Merge the workflow catalog. The bundled workflows from the home and the rib-contributed definitions become one catalog.

  9. Build the prompt handler and controller. Wired after the registries are populated; rib tools are passed in default-off, so a workflow prompt node sees one only when it allows it by name.

  10. Mount the MCP gateway. When mcp.enabled (the default), the server mounts /api/mcp, exposing the now-populated tool registry to external agents over the Model Context Protocol. State-changing tools are included by default.

  11. Schedule surfaces and serve. A heartbeat scheduler keeps snapshot-backed regions fresh on their cadence, then Bun serves the HTTP and WebSocket routes on 127.0.0.1:7878.

Ribs are not registered by hand. The server discovers them at boot and folds each kind of contribution into the surface that already reads it, so a rib reaches chat, workflows, and the browser without per-rib wiring.

  1. Discover. The server walks the keelson home and keeps the rib-* packages it finds. A package that fails to import or validate is skipped with a warning, never fatally.

  2. Activate. KEELSON_RIBS filters which discovered ribs turn on. When it is unset, every discovered rib activates.

  3. Validate. Each candidate is checked against the contract: a well-formed id, a display name, and namespaced keys. A failure costs you that rib, not the harness.

  4. Register tools. Each rib’s tools land in the shared registry and become reachable from both the chat agent and workflow prompt nodes.

  5. Publish snapshots and surfaces. A rib’s composeBundle, views, and surfaces become live boards the SPA renders, each under the rib’s own namespace and refreshing on its own cadence.

  6. Merge workflows. Contributed workflow definitions fold into the catalog, and a bound run’s output republishes to the rib’s snapshot key.

  7. Expose actions and auth. onAction handles a board’s buttons over the loopback API; authStatus surfaces a credential probe through GET /api/ribs.

  8. Dispose. At shutdown the harness awaits each rib’s dispose before the database closes, so a rib holding sockets or child processes tears down cleanly.

The exact shapes, rules, and order live in the rib contract.

One rule keeps the harness and the ribs apart, and it shows up as four concrete boundaries.

  • Static dependency. The harness has no compile-time dependency on any rib. Installed ribs are discovered and imported at boot through the Rib contract, which is what lets the harness ship once and stay stable while any number of ribs come and go.
  • Namespace. Everything a rib publishes lives under rib:<id> or rib:<id>:*. Registering a snapshot key outside that namespace throws, and reading another rib’s keys returns nothing.
  • Credentials. Each rib receives a namespaced, read-only credential reader scoped to rib_<id>_*. A rib cannot read another rib’s secrets, or the harness’s.
  • Failure isolation. A malformed rib, a bad tool, or a duplicate tool name is skipped with one warning. One broken rib costs you that rib, never the server.

This page is the map; the rest of the tier goes one level deeper on each part of it.