Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Architecture

Where this comes from

The design follows the thesis in the companion research vault (../sms-research): safe live self-modification needs a substrate (revertible effects + reactive dependencies, from Cordis / the Spatiotemporal Composability paper), a judge the agent cannot reach (the lesson of the Darwin Gödel Machine’s objective hacking), and persistence (which DeepSeek Harness deliberately skipped: “scratch, not memory”). No shipped system had all three; this repo is an attempt to — packaged so that other applications can adopt it. docs/research-positioning.md maps every concept to code and carries the threat model.

What we copied, and from whom:

FromTakenChanged
Linuxa small generic kernel; drivers behind contracts; everything else loadabledrivers are npm packages picked at composition time
Cordis / dshfiber per plugin, dispose-on-failed-start, inject/provide keys, façade-style ctx, immutable versions with a moving current; “consumers depend on service definitions, never providers”effects are a closed vocabulary so undo is derived by the host, never written by the (LLM) author
dsheverything is a plugin; human approves the visible halfapproval is a judge policy (sensitiveScopes/Points), not baked in; there is no visible half — the baseline is headless
pithe loader loop is enough — no “modify yourself” API beyond the tools the agent hasthe loader is plugin_define → judge → kernel; no file system needed
DGMa judge that is out of reachtrust tiers inside the kernel + authorization tokens, instead of hiding
Strapi / Directusdeclarative collections with auto CRUD/REST/hookscollections are just an extension point owned by a plugin, on any SQL engine

Rings

Deeper is more abstract — the Linux kernel / OSI shape. A package imports its own ring or below; the tree is the diagram, and scripts/quality/layers.ts is the same model as code (pnpm quality enforces it, docs/dependency-graph.md is generated from it by pnpm graph).

ring 4  examples/*                    composition roots: pick drivers, add plugins, listen
ring 3  packages/baseline             @sms/baseline: createBaseline() · createTenantPool() · testkit · re-exports every plugin
ring 2  packages/plugins/*      │     packages/drivers/{store,host,executor,model,objects}/*
        features, platform-free │     one contract each, platform-specific; never import a plugin
        http · collections · judge · agent-loop · admin-api · scheduler · auth · rbac · orm · meta · rpc · automation · audit
ring 1  packages/core/plugin-sdk │    packages/core/contracts
        what agent code sees:    │    the waist: Database · Store · Host · TenantResolver · ModelService · ObjectStore
        cap.provide/contribute/  │    (kernel types only — never plugin-sdk)
        get/invoke/log + channel │
ring 0  packages/core/kernel          loader · registry · fibers · derived undo · event log · trust tiers · Executor interface

The kernel is the single root; every ring above it is a pair of siblings that never import each other and meet only in the ring above. Ring 1 splits ergonomics (plugin-sdk) from vocabulary (contracts); ring 2 is the hourglass: plugins and drivers are siblings on top of contracts, and neither column imports the other. Plugins are also platform-free (no node:*, no SDKs) so they run wherever the kernel runs.

Plugins depend on definitions, never providers (the Cordis rule; Linux modules link against exported symbols, not each other’s source). Each plugin has two faces:

@sms/plugin-x/contract (src/contract.ts)@sms/plugin-x (src/index.ts)
holdsX_KEY/X_POINT constants, service + item types, validators, pure helperseverything in the contract, plus createXPlugin() / xPlugin
imported byother plugins (the only allowed subpath between plugins)composition roots: baseline, apps, tests
may reachcontracts, kernel types, other contractsanything below ring 3

The manifest is the runtime counterpart: inject (hard — the reconciler waits for it and unloads the fiber before its provider) and optional (soft — read with ctx.kernel.getValue, present in FiberView). The gate cross-checks the three: a runtime import of a sibling’s contract, a getValue(key) call, and the manifest must agree. The plugin graph is a DAG.

The kernel

Kernel
├─ keys        name → { value, owner }          + KeyMeta { minProviderTrust, minConsumerTrust }
├─ points      name → Contribution[]            + ExtensionPointMeta { minContributorTrust, validate }
├─ fibers      id   → { manifest, trust, enabled, state, effects[] }
├─ settle()    reconciler: enabled ∧ inject satisfied → activate; disabled ∨ dependency gone → deactivate
├─ log         append-only events; foldTarget(events) = what should be installed/enabled
└─ executor    Executor.load(bundle, HostBridge) for code the kernel will not run in-process

Fiber lifecycle: inactive → loading → active → unloading → inactive, or loading → failed. Activation applies manifest.contributes first (declarative), then runs the plugin’s activate (in-process for system plugins; through the executor for agent plugins). Every provide/contribute pushes an AppliedEffect with its inverse. A throw anywhere unwinds what ran.

Ordering (Cordis Thm 63, “provider outlives consumers”): deactivating a fiber first deactivates every active fiber that injects one of its provided keys, recursively, then undoes its own effects. settle() then re-activates whatever became satisfiable again.

Trust tiers: system (installed by the composition root, in-process, holds ctx.kernel) and agent (proposed at run time; never holds KernelApi). Keys and points carry minimum trust; Registry.get/provide/ contribute check it every time. Kernel.propose() refuses an agent manifest without a MountAuthorization minted by Kernel.authorize() — which is only reachable through ctx.kernel, i.e. from system code. A forged or reused nonce, or a manifest whose code hash differs from the authorized one, is rejected. An agent manifest whose id belongs to a system fiber is rejected (RESERVED_ID) — otherwise “update” would replace the judge itself — and uninstall refuses system fibers unless the composition root passes force.

Executor boundary: plugin code never receives objects. It gets cap whose calls go over a bridge to the kernel’s HostBridge, which re-checks inject and trust on every get/invoke. Functions inside contributed items are marshalled as { $fn: id } and turned into async proxies by the kernel, so a tool handler written by the agent is callable by the host, and stops working the instant the fiber is disposed. Values flowing to plugin code are JSON snapshots.

Drivers (the “device” layer)

@sms/contracts defines what a host must supply; the baseline and plugins depend on that, never on an engine:

ContractUsed byDrivers
Database { dialect, exec, query(sql, ?-params), columns, close }collections (key database), store-sqlsqlite (node:sqlite), postgres (pg/PGlite; schema per tenant), do (Durable Object SqlStorage), d1 (one D1 per tenant)
Store { db, log, blobs, close }kernel (log, blobs)createSqlStore(db) in store-sql turns any Database into a Store
ObjectStore { put, get, head, delete, list }file-handling plugins (key objects)s3 (AWS / MinIO / R2 over S3, SigV4 on fetch), r2 (bucket binding), memory (tests)
Host { listen(handler) }composition rootsnode (node:http), fastify (mount under a prefix, encapsulated scope), cloudflare (Worker fetch + DO class)
TenantResolvercreateTenantPool, createCloudflareApptenantFromHeader, tenantFromSubdomain, custom
Executor (kernel)kernel, judge’s load gatenode (worker_threads), quickjs (WASM)
ModelServiceagent loop (key model)claude (Claude Agent SDK, claude login), gpt (Codex SDK, codex login), claude-api (Messages API), gpt-api (Responses API), openrouter (Chat Completions), gpt-oauth (Codex backend, personal login), scripted (tests)

Every store driver passes packages/core/contracts/test/database-contract.ts and the collections contract suite; object stores pass packages/core/contracts/test/objects-contract.ts; both executors pass the same executor tests; model drivers pass packages/core/contracts/test/model-contract.ts. See docs/drivers.md.

Persistence

The event log is the source of truth; mounted state is a cache. plugin/installed (with manifest and code hash), plugin/enabled, plugin/disabled, plugin/uninstalled fold into a target map. Kernel.restore() re-creates agent fibers from that map (fetching code from the blob store by hash) and settles. System plugins are re-installed by the composition root each boot; their enabled flag is taken from the log. fiber/*, judge/* and chat/* events are audit only. Tables: kernel_events, kernel_blobs, c_<collection>; withdrawing a collection or field keeps its table/column.

Tenancy

The kernel is single-tenant. createTenantPool({ resolve, tenant }) boots one baseline per tenant id on its own store (file / schema / DO) and routes each request; on Cloudflare the Durable Object is that pool entry. Tenant ids are validated because they become file and schema names. See docs/tenancy.md.

The judge (a plugin)

Gates (the gate point, system-only): manifesttrustsource-lintload (activate in a throwaway executor with a recording bridge; provided keys must be declared; unknown points fail). Then policy: sensitive scopes/points → pending human approval (/api/approvals). Then mount, then invariant contributions run against the live kernel; a violation rolls back and, for an update, restores the previous version from the log + blobs.

The judge is unreachable from agent code by four independent facts: judge is a system-only key; gate and invariant are system-only points; authorization minting lives on KernelApi; and system plugin ids are reserved (the judge additionally requires agent ids to start with agent.). The services other plugins rely on (http, collections, model, agent, database) can only be provided by system fibers. Tests: packages/core/kernel/test, packages/plugins/judge/test, packages/baseline/test.

Admin surface (headless)

plugin-admin-api contributes JSON routes only: /api/state, /api/catalog, /api/chat, plugin enable/disable/uninstall, approvals, /api/events. Users, groups and models come from @sms/plugin-auth, @sms/plugin-rbac and @sms/plugin-orm; createBaseline installs all three (auth: false opts out of the first two) and plugs rbac into the ORM’s guard: anonymous requests are denied, users pass the access matrix and record rules ($uid = caller), sudo contexts bypass. Mutating routes require the bearer token (createBaseline({ adminToken })) or the nod of an admin-authorizer contribution (system-only point — an auth plugin admitting its own admin sessions). There is no HTML and no UI extension point in the baseline; a consumer renders whatever it wants on top (docs/building-a-ui.md). Agent-authored UI, if a product wants it, is a consumer-defined extension point marked sensitive.

Containment

ExecutorBoundary
executor-nodeworker_threads: steering, not a security boundary (dsh’s label for node:vm). Removes process, fetch, require; shares nothing but the RPC port; the judge’s source-lint refuses obvious escapes. Dev use.
executor-quickjsQuickJS compiled to WASM: its own heap (memoryLimitBytes), CPU deadline per slice (sliceMs, interrupt handler), no I/O, no import, no timers; the only host function is __host(method, json). Runs on Node and, with the single-file variant, inside a Cloudflare Worker where eval/new Function are unavailable. Production use.

Neither executor can reach the judge or the kernel; that boundary is the trust tier, not the sandbox.

Cloudflare (one Worker, one Durable Object class)

PieceNodeCloudflare
Executorexecutor-node / executor-quickjsexecutor-quickjs (single-file WASM variant)
Storestore-sqlite / store-postgresstore-do (DO SQLite: same tables) / store-d1 (one D1 per tenant)
Objectsobjects-s3objects-r2 (bucket binding) / objects-s3
HTTP entryhost-node / host-fastifyhost-cloudflare: Worker fetchTENANT.get(idFromName(tenant)) → DO → app.handle
Everything elseunchangedunchanged

Dynamic Workers (worker_loader) would give a stronger per-plugin boundary but was ruled out for v1 to keep to one traditional Worker; QuickJS provides the per-plugin boundary inside the DO.

Packaging

Development runs TypeScript directly; releases are git tags pointing at detached release commits with dist/ committed and workspace:* rewritten to github:…#tag&path:/packages/x, cut by .github/workflows/cd.yml when a vX.Y.Z tag is pushed after pnpm check (scripts/release.ts, docs/releasing.md). Package exports carry an sms-source condition for in-repo source resolution and default to dist/.

Extension points shipped

PointOwnerItemAgent may contribute
routehttp{ method, path, handler(req) }yes (human by default)
schedulescheduler{ name, everyMs, handler(run) }yes (human by default)
grouprbac{ name, implies? }no (system)
accessrbac{ model, group, read?, write?, create?, unlink? }no (system)
rulerbac{ name, model, groups?, domain, ops? }no (system)
modelorm{ name, fields, displayField?, order? }yes
model-fieldorm{ model, name, spec }yes
model-methodorm{ model, name, handler(env, ids, args), access? }yes
model-hookorm{ model, on, when, handler(env, payload) }yes
model-onchangeorm{ model, field, handler(env, values) }yes
view / action / menumetaviews with inheritance, actions, menu tree (opt-in)yes
automationautomation{ id, model, trigger, watch?, filter?, action }yes (human by default)
audit-trackaudit{ model, fields[] }yes
admin-authorizeradmin-api{ name, authorize(req) }no (system)
collectioncollections{ name, fields[] }yes
collection-fieldcollections{ collection, field }yes
collection-hookcollections{ collection, on, handler(record, previous) }yes
toolagent-loop{ name, description, input_schema, handler(input) }yes
promptagent-loop{ section, text, order }yes
gatejudge{ name, run(ctx) }no (system)
(auth)authcontributes route ×3 and admin-authorizer auth-session; keys identity / identity-admin
invariantjudge{ name, check(kernel) }no (system)

Keys: database (system↔system), http, collections, model (system provides), judge (system↔system), agent (system↔system).