Skip to content

Capabilities: the product adapts to what is on

OpenSwitchboard is installed, not forked: a Slack bot with one provider and nothing else is a complete installation, and so is the four-Worker deployment with residents, memory, run history, MCP, costs and schedules. What separates them is one value — Capabilities, computed once at startup from the config and the environment (Fowler's feature toggles resolved once) and handed down through CoreDeps. Every surface reads that value: the command registry hides what is off, the web seed carries it so the dashboard paints only the tabs that exist, the self-description block describes only what is wired, the deploy plan iterates the profile's Workers. No surface reads config.memory?.enabled for itself; a scattered if (config.x) is the smell this feature removes (capabilities are computed once, with a Null Object for every off-state). The value's rules and the Null Object for every off-state are routing-and-config.md item 16; the registry's hiding is command-registry.md item 28.

The proof is a suite that runs the whole surface under three installations expressed as configurations — minimal, local-full, cloud-full — and snapshots each, plus a capability axis on the command conformance suite: a capability changing must change exactly the commands whose enabledWhen depends on it, on every surface, and nothing else.

Behavior

  1. One value, computed once. capabilitiesFrom(config, env) is pure and is called exactly once per process — src/index.ts for the bot, src/cli.ts for the CLI — and the result rides CoreDeps.capabilities. Its axes, each with the rule that turns it on and the value it takes when off:

    AxisOn whenOff value
    executionexecution.type (local | e2b | cloudflare)local
    residentsexecution.resident.baseUrl names the resident Workerfalse
    memorymemory.enabled: truefalse
    runHistorya runHistory block that selects a store: store: file, or a worker whose bearer (tokenEnv, default MEMORY_TOKEN) is in the envfalse
    runLedgerrun history on a state Worker (a file store has no ledger)false
    mcpan mcp blockfalse
    costsa costs block AND its Cloudflare analytics token in the envfalse
    schedulesschedules.worker.baseUrl with its bearer in the envfalse
    githubthe App triple (GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY + GITHUB_APP_INSTALLATION_ID) or GH_TOKENfalse
    ingressSWITCHBOARD_INGRESS_TOKENS names at least one bearerfalse
    readingDiffAbridgethe meat binary resolves on the bot host's PATH (meatOnPath, probed once by the caller and handed in as the HostFacts third argument — the computation stays pure), AND the Anthropic provider's credential is set (anthropicApiKey, the provider's own apiKeyEnv), AND review.readingDiff.provider — env override included — is not off (reading-diff.md item 1)false
    dashboardAuthconfig.dashboard.auth when set (access-gate.md); else access when ACCESS_TEAM_DOMAIN + ACCESS_AUD are set, else none (loopback callers of a localhost deployment only) — the same resolveDashboardAuthMode the verifier runs, so the value names the strategy that gates requestsnone

    The rules mirror the builders that select each subsystem's implementation, and the function throws exactly where they throw (a malformed costs or mcp block) — a bad config is a startup error here as it is there, never a capability silently read as off. ALL_CAPABILITIES is every axis at its on value (the catalogue as the reference docs render it); NO_CAPABILITIES every axis at its off value.

  2. Off is hidden, not refused. A command declares the capability it needs as enabledWhen(caps) on its definition (absent → always on). When the predicate is false for the process's capabilities the command is hidden on every surface — absent from help, <group> help, MCP tools/list, the HTTP catalogue (/api/<id> is 404), the CLI catalogue and chat; a direct invoke is not_found — instead of answering unavailable (command-registry.md item 28 names the gated commands). The handler's own unavailable (a dependency missing at call time) stays as the defence in depth; it is never the way a user learns a feature is off.

  3. The gate is derived, not restated. dependsOn(cmd) (src/core/capabilityGating.ts) reads a command's axes off its own enabledWhen: each axis whose value changes the answer in at least one of the two reference worlds — turned off alone against everything else on, or turned on alone against everything else off. Both directions matter because a predicate may accept more than one backend: repo test is on with residents OR local execution, so it depends on both axes. visibleUnder(cmds, caps) is the catalogue under one value. Every consumer that needs to know which commands a capability decides — the conformance matrix's Depends on: line, the docs matrix's command column (docs-site.md), the capability suite's expectations — asks these, so a new gated command is covered and documented the moment it is registered, and a hand-written list of "commands behind memory" cannot exist to drift.

  4. Three installations, as configurations. src/core/testing/capabilityFixtures.ts exports minimal (Slack plus one provider; tools on the bot host; no Access, so dashboard auth none; nothing optional), local-full (everything a laptop can turn on: memory in-process, run history on disk, GitHub via GH_TOKEN, an mcp block, bearer ingress; no Workers, so no residents, costs, schedules or ledger) and cloud-full (everything on: Cloudflare execution with a resident Worker, memory, run history and the ledger on the state Worker, MCP, costs, schedules, the GitHub App, ingress, Access). Each is a complete config.yaml plus an environment and the Capabilities it must produce; the suite asserts the round trip through capabilitiesFrom, so a fixture is a shape an operator can copy, never a hand-typed set of flags. minimal is NO_CAPABILITIES; cloud-full is ALL_CAPABILITIES; strictly more is on in each than the one before.

  5. Every surface, under every shape, snapshotted. For each fixture the suite drives the conformance world (src/core/testing/conformanceFixture.ts, fixture() with the fixture's Capabilities handed to the CommandRegistry) through the real adapters and snapshots: help on chat, the MCP tools/list names, the HTTP catalogue (the /api/<id> routes that answer an admin with anything but 404), the CLI catalogue, the /runs and /runs/scheduled seeds as src/index.ts wires them (the shell's capabilities stamp, the retention sentence — the configured window with run history on, "kept about a minute" without — the Scheduled panel's schedules and whether it has a firing store), and the self-description block (with the resident Worker's cap where there is a fleet). The deploy plan (formatPlan(planDeploy(…))) is snapshotted for the full example profile (every Worker in the canonical order) and for a profile with only the bot (one step, no state Worker to push the config to). A snapshot changes when — and only when — a surface changes what it shows for that shape; the diff is the review.

  6. The capability axis. For every axis of the contract, turning it on from the minimal world changes exactly the commands whose enabledWhen depends on it — the ones the predicates say it adds and the ones they say it removes (a sandbox execution removes repo test/repo build from a world with no residents), on each adapter, judged by what the adapter shows (presenceOf: HTTP not-404, a tool in tools/list, the CLI grammar knowing the words, chat recognising the form) — and every command that moved depends on that axis. Every fixture shows exactly visibleUnder its capabilities, per adapter, intersected with the surfaces the command exposes. A snapshot of the gating map (command → axes) fences which commands depend on each capability. On the conformance suite (command-registry.md item 25) every command gets a capability row: in every world one axis away from all-on or all-off (and in all-off itself), it exists exactly where its predicate says — hidden is absent on every adapter and not_found on a direct invoke with nothing executed, never unavailable; present is an ok invoke.

Validation criteria

CriterionEvidence
1: readingDiffAbridge is on iff the host fact says the binary is there, the provider's credential (its own apiKeyEnv) is set, and the switch (config or SWITCHBOARD_READING_DIFF) is not off; any one missing is off; meatOnPath answers command -v meat for a PATH[unit] src/core/capabilities.test.ts::capabilitiesFrom — every axis, on and off::readingDiffAbridge…, src/core/meatProcess.test.ts::meatOnPath::*
2: review.abridge is hidden without runHistory or without readingDiffAbridge, present with both; the off Null Object answers unavailable naming the facts[unit] src/core/commands/review.test.ts::review.abridge::is hidden when run history is off…, ::no abridger (the capability's Null Object)…
Each fixture's config.yaml + env produce its declared Capabilities through capabilitiesFrom (item 4)[unit] src/core/capabilitySurfaces.test.ts::capability fixtures — real configurations::$name: its config.yaml and env produce its Capabilities through capabilitiesFrom (the round trip)
minimal is the all-off value with the local-dev dashboard; cloud-full is the all-on value; the three shapes are distinct and monotone; local-full needs no Worker (item 4)[unit] ::minimal is the contract's all-off value with the local-dev dashboard; cloud-full is its all-on value, ::the three shapes are distinct and strictly more is on in each than the one before, ::local-full turns on nothing that needs a Worker: execution local, no residents, costs, schedules or ledger
Under each fixture, help on chat, the MCP tool list, the HTTP catalogue, the CLI catalogue, the web seeds and the self-description block are pinned (item 5)[unit] src/core/capabilitySurfaces.test.ts::capability surfaces — $name::help on chat, ::MCP tools/list names, ::HTTP catalogue: the /api/<id> routes that answer an admin, ::CLI catalogue, ::web seeds: /runs (capabilities, retention sentence) and /runs/scheduled (schedules, firing store), ::self-description block (the resident cap is the Worker's answer — 6 where there is a fleet)
The /runs seed carries exactly the fixture's capabilities; retention reads the configured window with run history on and the one-minute sentence without; the Scheduled panel has a firing store only with schedules on (item 5)[unit] src/core/capabilitySurfaces.test.ts::capability surfaces — $name::web seeds: /runs (capabilities, retention sentence) and /runs/scheduled (schedules, firing store) (the seed's capabilities is asserted equal to the fixture's before the snapshot)
The deploy plan: the full example profile is every Worker in DEPLOY_ORDER; a bot-only profile is one step with no state Worker URL; both texts pinned (item 5)[unit] src/core/capabilitySurfaces.test.ts::deploy plan — what \deploy plan` prints::the full example profile: every Worker, in the canonical order, ::a bot-only profile is a one-step plan with no state Worker to push the config to`
Which commands depend on each capability is derived from enabledWhen and fenced (item 3, 6)[unit] src/core/capabilitySurfaces.test.ts::capability axis — a capability turning on changes exactly the commands its enabledWhen names::which commands each capability turns on, derived from the registry's enabledWhen — update deliberately
Every fixture shows exactly the commands enabled under it, per adapter (item 6)[unit] ::$name: every adapter shows exactly the commands enabled under it
Turning one axis on from the minimal world adds and removes exactly the commands the predicates say, on every adapter, and each of them depends on the axis (item 6)[unit] ::%s: from the minimal world, turning it on changes exactly the commands whose enabledWhen depends on it, on every adapter
In every world one axis away from all-on or all-off, and in all-off, a command exists exactly where its enabledWhen says: hidden is absent on every adapter and not_found on invoke with nothing executed — never unavailable; present invokes ok (item 2, 6)[unit] src/core/commandConformance.test.ts::command conformance — $id::capability axis: in every world one axis away from all-on or all-off, the command exists exactly where its enabledWhen says — hidden is absent on every adapter and not_found on invoke, never unavailable; present is ok
The conformance matrix's Depends on: line is dependsOn (item 3)[unit] src/core/commandConformance.test.ts::command conformance — catalogue fences::scripts/command-conformance-matrix.ts prints this suite's matrix…
The docs matrix's command column is generated per axis from dependsOn, and docs:check fails when the committed page and the registry disagree (item 3)[unit] src/docs/reference.test.ts::renderCapabilityCommands::*, ::the real catalogue::the capability column lists, per axis, exactly the commands dependsOn derives from the registry — a list nobody typed, ::GENERATED_REGIONS::matches the markers actually present in each docs page…; [agent] npm run docs:checkdocs:check ok — 10 file(s) match the command registry, the decision records and the diagram sources