Skip to content

Bundles — the Bundle Contract

A bundle is the modular unit of Crow's extension layer: a directory under bundles/<id>/ described by a manifest.json. A bundle may provide any combination of surfaces — a containerized service (Docker), an MCP server (tools), a dashboard panel, and skills — hence "bundle = service + tools + skills". The contract is surface-based: a bundle is only required to satisfy the rules for the surfaces it actually declares.

Where bundles come from

  • Source of truth: each bundles/<id>/manifest.json.
  • Install catalog: registry/add-ons.json is generated from the manifests by npm run build-registry — never hand-edit it. It is committed (lockfile model) and a test fails if it drifts.

Universal required fields

Every manifest must have:

FieldRule
idmust equal the directory name
namenon-empty
descriptionnon-empty
typebundle | mcp-server | skill (a coarse category tag, not what drives required fields)
categorynon-empty

version (semver) and author are optional but shape-checked when present (some first-party model/media bundles ship without them).

Surfaces (declare what you provide)

A surface is "declared" by the presence of its key. Each declared surface is validated for shape and that its referenced files exist under the bundle dir:

SurfaceShapeIntegrity
docker{ "composefile": "docker-compose.yml" }the composefile exists
server{ "command": "node", "args": ["server/index.js"], "envKeys": [...] }, or nullentry file checked only when command is node and args[0] is a path (external npx/uv servers are exempt)
panel"panel/<id>.js" or { "id": "...", "extends": "..." }string form: the file exists; object form: shape-only (resolved at runtime)
panelRoutes"panel/routes.js"the file exists
skills["skills/<id>.md", ...]every path exists
ports / port / webUI.portintegers (1–65535); webUI may also be null
requires.bundles / optional_bundles["<bundle-id>", ...]each id is a bundles/<id> dir with a manifest.json (a real bundle)
env_vars[{ "name": "X", "description": "...", "required": false, "secret": false, "default": "" }]each entry has a name

Unknown fields are allowed (the schema is lenient) — bundle-specific extras like capabilities, companion, storage, providers, sttProfileSeed pass through untouched. The canonical shape is registry/manifest.schema.json.

Draft / unpublished

  • "draft": true excludes a bundle from the generated registry.
  • An untracked bundle dir (not committed to git) is treated as an implicit draft — excluded and reported, never auto-published. This keeps work-in-progress out of the registry.

Validate + generate

bash
npm run build-registry -- --check   # validate all manifests + drift-check (CI)
npm run build-registry              # regenerate registry/add-ons.json
npm run test:bundle-contract        # the node:test gate

--check prints a per-bundle audit (id, type, surfaces, status) and exits nonzero on any invalid manifest or if the committed registry is out of date.

Minimal example

bundles/your-bundle/
├── manifest.json
├── docker-compose.yml      (if it ships a service)
├── server/index.js         (if it provides MCP tools)
├── panel/your-bundle.js    (if it adds a dashboard panel)
└── skills/your-bundle.md   (if it adds skills)
json
{
  "id": "your-bundle",
  "name": "Your Bundle",
  "version": "1.0.0",
  "description": "What it does",
  "type": "bundle",
  "author": "You",
  "category": "utilities",
  "docker": { "composefile": "docker-compose.yml" },
  "server": { "command": "node", "args": ["server/index.js"], "envKeys": ["YOUR_API_KEY"] },
  "panel": "panel/your-bundle.js",
  "skills": ["skills/your-bundle.md"],
  "requires": { "env": ["YOUR_API_KEY"] },
  "env_vars": [
    { "name": "YOUR_API_KEY", "description": "API key", "required": true, "secret": true }
  ]
}

After adding or editing a bundle, run npm run build-registry and commit both the manifest and the regenerated registry/add-ons.json.

Released under the MIT License.