Plugins

A plugin is a typed library shipped inside the beebox package and reached through the public subpath beebox/plugins/<name>. A box uses a plugin by listing it in _config/box.json and writing small stubs that import and extend it. Nothing new runs at runtime: a stub is an ordinary box schema, view or trick, loaded by the mechanisms that load every other one. The design and its decisions: plugins plan, design notes. The standing position ("knowledge, not plugins": no registry service, no marketplace, no lifecycle framework) is in extensibility and still holds.

Three tiers

Before writing a plugin, pick the tier.

TierUse it whenArtifact
InstructionThe agent can call a service with what it already has (omdb, tmdb)A documentation entry; the service is consumed, not integrated
TrickA script that integrates with nothingA well-structured trick the docs point at; the agent copies it into src/tricks/scripts/
PluginThe engine must know about it: a card type it validates, a view, a lint over cards, a health checksrc/plugins/<name>/

A plugin exists only for the third case.

Vocabulary

Layout

src/plugins/<name>/
  plugin.ts     default-exports definePlugin({...}); Node-safe
  view.tsx      optional; browser-safe; the stub re-exports it
  README.md     What it is · Setup · Conventions · Migration · Uninstall
  skill.ts      exports the skill body as a string; the engine writes the frontmatter
  <own files>
src/plugins.ts  the registry; add the member here

plugin.ts and view.tsx are separate entries because plugin.ts may import node:fs for a health check while view.tsx must bundle for the browser. The build emits dist/plugins/<name>/plugin.js and view.js; the export map names both.

The contract

import { definePlugin } from "../../cards/plugin-definition.js";
export default definePlugin({
  name: "courseware",                       // equals the directory name
  description: "Courses, lesson plans, learner progress",   // passes the brief lint
  docs: "src/plugins/courseware/README.md",
  skill: SKILL_BODY,
  schemas: { course: courseBase, "lesson-plan": lessonPlanBase },  // key = default type name
  views: [{ name: "concept-map", rendersCardTypes: ["concept-map"] }],
  healthChecks: async (boxRoot) => [...],   // read-only
  lintCards: async (input, ctx) => [...],   // box-aware; uses ctx, never engine imports
});

The declarative fields are read without running anything. The two hooks are the whole hook surface; add a new hook kind only with a real consumer and a plan.

What stays core

The connectors (Gmail, Google Calendar, Google Drive) stay core. They are the box's intake for mail, calendar and drive, and intake is medium; they share Google auth, the admin connect flow, the scheduler's auth alerts and the default schedules with the engine, and import 44 engine modules the boundary above forbids. The sizing and the decision are in docs/implemented-plans/plugins-slice-3.md. A source a box is about (a feed reader, say) is content and would be a plugin; it would be the real consumer for a connectors hook, which does not exist until then.

The import boundary

A file under src/plugins/<name>/ may import only beebox/{cards,schema,view-widgets} through src/exports/*, src/cards/plugin-definition.ts, its own directory, and external packages. view.tsx may not import node:*. The plugin-imports layout rule and an ESLint block enforce this. The rule is what keeps an in-repo plugin from being core by another name: if a plugin needs an engine internal, the engine is missing a public piece, and that is a separate change.

Stubs

Schema stub, <box>/src/schemas/progress.ts:

import { cardSchema, extendSchema } from "beebox/cards";
import { z } from "beebox/schema";
import courseware from "beebox/plugins/courseware";
export default cardSchema("progress", extendSchema(courseware.schemas.progress, {
  fields: { mood: z.string().optional() },
}));

extendSchema merges fields (a key present in both is a type error), runs both validate and both superRefine hooks, chains summarize, and concatenates instructions. Scalar options are delta-wins.

View stub, <box>/src/views/concept-map.tsx:

export { default } from "beebox/plugins/courseware/view";
export const rendersCardTypes = ["concept-map"];

Stubs use static imports; the health checks find plugin references by scanning import specifiers.

Activation is agent-run

The README's Setup section lists the exact stubs. The agent adds the name to plugins in _config/box.json, writes the stubs, and commits. No plugin code runs at activation. bbx plugins list shows every installed plugin, active or not, with its description and README path; the agent guide carries one standing line pointing at it.

Health

Core owns these checks; they derive from effective schemas and the active list together:

bbx upgrade validates the whole box under the new engine after migrations, then runs bbx health --json there and prints the failing rows above under Plugin health after upgrade:. Validation errors and warnings and failing health rows are reported and the upgrade finishes; they never block it.

Deactivation, removal, rot

Remove the name from box.json. The stubs stay as box code and keep compiling; the skill the engine wrote is retired (marked files only). A plugin removed from the engine breaks its stubs' imports: the loader reports it, plugin-stub-missing names it, and bbx upgrade fails typecheck before committing. The project never removes a plugin in the release that adds its replacement, and the README's Migration section names the script to run.

Conventions

test/plugins.test.ts loops every registered plugin and checks the rules marked (checked). The rest are convention only.