Composition, forks, and distribution

This page shows how to fork a plugin, select its claims, and distribute the result. The kernel keeps these actions separate.

An install adds a claimant. A winner change makes that claimant active. A fork records where its source came from.

Use this when

  • Change a first-party screen. Fork the plugin that wins its surface contract.
  • Replace a backend service. Keep the parent contract ID so current consumers follow the winner.
  • Test a change safely. Install a fork and flip one winner without removal of the parent.
  • Publish a plugin. Distribute a verified artifact through npm, Git, or a marketplace.
  • Recover from a bad override. Start safe mode and restore a default winner.

What you build

You will build acme.thread-ui, a fork of bb.thread-ui. The fork claims the parent-owned bb.thread-ui.list surface.

You will install the fork from a local path. You will then flip the global winner and flip it back.

Before you fork

First, inspect the active contract and its source.

bb plugin winners bb.thread-ui.list
bb plugin source bb.thread-ui
bb plugin doctor bb.thread-ui

Use a contract when consumers must follow the current winner. Use a module import when consumers need one exact implementation.

Only a replaceable single contract has one global winner. A replaceable keyed contract has one winner for each key.

A list contract uses all visible claims. A chain contract uses all enabled members in the stored order.

Use the agent-driven fork loop

The normal fork loop starts from a contract, not from a package name.

  1. Ask an agent to resolve the contract and its current winner.
  2. Ask the agent to copy that winner's source into a new package.
  3. Give the fork a new plugin ID.
  4. Record the parent ID and the copied parent revision.
  5. Keep only the parent contracts that the fork still implements.
  6. Build and install the fork.
  7. Review the new claimant before you change the winner.

A useful request to an agent is:

Fork the current winner of bb.thread-ui.list as acme.thread-ui.
Keep the bb.thread-ui.list contract compatible.
Record the parent revision, build the plugin, and install it from its path.
Do not change the winner until I review the result.

The recorded revision gives the agent a merge base when the parent changes. The fork remains an ordinary plugin.

Record lineage and parent claims

Write the fork identity and claim in bb.plugin.jsonc.

{
  "$schema": "https://getbb.app/schemas/plugin-v2.schema.json",
  "schemaVersion": 2,
  "id": "acme.thread-ui",
  "version": "1.0.0",
  "name": "Acme Thread UI",
  "description": "A compact thread list for Acme projects.",
  "engines": {
    "bb": "^2.0.0",
    "sdk": "^2.0.0"
  },
  "fork": {
    "parent": "bb.thread-ui",
    "parentRevision": "2.0.0"
  },
  "claims": [
    {
      "surface": "bb.thread-ui.list",
      "version": "^1.0.0"
    }
  ],
  "artifacts": {
    "app": "./dist/app.mjs"
  }
}

The new id prevents an identity conflict. The fork record supplies lineage only.

Lineage does not select a winner. It does not give the fork the parent's storage.

The fork claim keeps the parent-owned contract ID. The build checks that claim against the parent's generated contract.json.

Remove every parent claim that the fork no longer implements. The install fails if a claim has an incompatible kind, key, or version.

Provide the claimed surface

Use the app factory from @get-bb/plugin/app.

// src/app.tsx
import { definePlugin } from "@get-bb/plugin/app";
import { CompactThreadList } from "./components/CompactThreadList.js";

export default definePlugin((api) => {
  api.surfaces.provide(
    "bb.thread-ui.list",
    ({ Original, props }) => (
      <CompactThreadList {...props} Original={Original} />
    ),
  );
});

The factory receives a typed api object. The runtime removes the registration when this plugin generation stops.

Original is the typed first-party default. Only the winner of a single surface receives it.

A backend service never receives Original. Its owner declares a default provider for failure recovery.

Build and install the fork

Build all declared artifacts. The build also generates contract.json and artifact.json.

bb plugin build .
bb plugin install path:.
bb plugin winners bb.thread-ui.list

A path: source has a visible local-unverified trust mark. The development loop checks its content before each reload.

Use bb plugin dev . when you want build, path install, file watch, and reload in one command.

The install adds acme.thread-ui to the winner picker. It does not replace the active winner.

Flip the winner

Select the fork after you review it.

bb plugin winner set bb.thread-ui.list acme.thread-ui
bb plugin winners bb.thread-ui.list

The surface remounts with the new winner. The kernel does not preserve the old surface-local state.

Flip back with the same operation.

bb plugin winner set bb.thread-ui.list bb.thread-ui

The kernel groups the fork with its parent in the winner picker. This group does not change arbitration.

For a service winner, the kernel starts and stages the candidate first. It then drains old calls and restarts required dependents.

Optional and watched consumers stay active. They observe service availability through their declared edges.

Install from another source

The source form states how bb resolves and verifies a plugin.

Source Example Result
Local path path:./acme-thread-ui Links a development path and marks it as local.
Built-in builtin:bb-threads Uses the signed bb release index.
npm npm:@acme/bb-thread-ui@^2 Selects one version and verifies registry integrity.
Git ref git:https://host/repo.git#ref:v2.1.0 Resolves one immutable commit.
Git range git:https://host/repo.git#semver:plugin-v:^2 Selects the highest matching tag and records its commit.
Collection A source with --plugin thread-ui Selects one safe entry from bb.plugins.json.
Marketplace marketplace:community/acme.thread-ui Resolves an entry to an npm or Git source.

The installer never runs package lifecycle scripts. It verifies the manifest, contract.json, file digests, engines, and contract rules.

An interrupted promotion leaves the active install unchanged. A failed candidate also leaves the current generation and winner active.

Publish through a marketplace

A marketplace is an index. It does not contain plugin code and does not give code a trust grant.

{
  "$schema": "https://getbb.app/schemas/marketplace-v2.schema.json",
  "schemaVersion": 2,
  "name": "acme",
  "displayName": "Acme Plugins",
  "description": "Verified Acme plugins for bb.",
  "plugins": [
    {
      "id": "acme.thread-ui",
      "displayName": "Acme Thread UI",
      "description": "A compact thread list for Acme projects.",
      "icon": {
        "url": "./icons/acme.thread-ui.svg",
        "sha256": "sha256-value-from-the-published-icon"
      },
      "tags": ["threads", "interface"],
      "author": {
        "name": "Acme",
        "url": "https://example.com"
      },
      "source": {
        "npm": {
          "package": "@acme/bb-thread-ui",
          "range": "^2.0.0"
        }
      }
    }
  ]
}

After a user adds the index, these commands use it.

bb marketplace refresh acme
bb plugin search "thread ui"
bb plugin install marketplace:acme/acme.thread-ui

The installer resolves the marketplace entry to its exact npm or Git source. It then verifies the plugin artifact separately.

Removal of a marketplace does not remove installed plugins. The kernel keeps their direct resolved source records.

Update and roll back

Managed sources keep the current and previous verified artifacts.

bb plugin outdated
bb plugin update acme.thread-ui
bb plugin rollback acme.thread-ui

An update uses the same stage and cutover transaction as a winner change. A rollback also stages the previous verified artifact.

A rollback does not copy storage from the parent. Each plugin ID keeps its own storage.

Recover with safe mode

Start safe mode when an override prevents a normal boot.

bb --safe

Safe mode loads only verified bundled defaults. It ignores user winners, list rows, and chain rows.

Safe mode does not start user server artifacts or host roles. It keeps plugin controls, logs, diagnostics, and recovery routes available.

The kernel ports remain available. They are bb.storage, bb.secrets, bb.preferences, bb.realtime, bb.http, bb.rpc, and bb.plugins.

Inspect the problem and restore the default.

bb plugin doctor acme.thread-ui
bb plugin logs acme.thread-ui
bb plugin winner set bb.thread-ui.list bb.thread-ui
bb plugin disable acme.thread-ui

You can also roll back the fork before the next normal boot. Safe mode does not delete plugin data or stored winner choices.

What happens at runtime

The kernel reads bb.plugin.json and contract.json before it imports plugin code. It validates parent claims during this step.

The kernel stages all factory registrations in one candidate generation. It commits the plugin only after every declared artifact becomes ready.

A winner change updates one global arbitration record. A single surface remounts, and a service uses a bounded cutover transaction.

A failed surface uses Original or its declared default claimant. A failed service uses its declared default provider.

Pitfalls

  • A fork must use a new plugin ID.
  • A fork receives fresh storage. Lineage never shares storage.
  • A parent contract claim must keep the parent's dotted contract ID.
  • Installation does not select a new winner.
  • A direct module import does not follow winner changes.
  • Original exists only for a single app surface.
  • A non-replaceable collision fails before activation.
  • A marketplace entry is an index record, not a security review.
  • Safe mode keeps stored choices. It only ignores them for that boot.

See also