# Plugin system

inseam's extension surface. A plugin is an NPM package whose identity is its package name (`@inseam/plugin-<name>` first-party, `inseam-<name>` third-party); the loader verifies NPM provenance attestation, materializes a durable `plugin_package` row, and admits the plugin to an ephemeral `LoadedPluginSet`. Plugins contribute through typed registries on `RegistrationContext` and run handlers against a freshly-scoped `RuntimeContext` whose capabilities the host has granted.

**See also:** [design](../design/plugin-system.md) · [spec](../spec/plugin-system.md)

## What it does

Two surfaces, split by package:

- **Contract surface** (`@inseam/plugin-contract`) — `PluginPackageName`, `PluginPackageRow`, `PluginManifest`, `PluginModule`, `RegisterFn`, `RegistrationContext` (with its `Registries` bag), `RuntimeContext` (with `source` / `emit` / `kv` / `http` / `clock` / `log` / `self`), `Capability`, and every error class. Zero runtime deps.
- **Core surface** (`@inseam/core`, plugin-system contribution) — `openPluginRegistry`, `PluginRegistry`, `AttestationVerifier` (test-injection seam — production uses the sigstore-backed default), `DEFAULT_PLUGIN_SYSTEM_LIMITS`, `ALL_CAPABILITIES`. Owns the `plugin_package` / `plugin_kv` tables, the seven-step load sequence, capability enforcement, and the registry-side auto-tagging that stamps `packageName` onto every plugin contribution and write.

A plugin's `packageName` is captured in the closure that builds its contexts; plugins never pass their own name in, so they cannot forge another plugin's tag or register under another plugin's identity. The `plugin_package` row is durable and network-synced (rides the outbox); the `LoadedPluginSet` is process-local and regenerated on every host boot.

Capability authority is the intersection of three layers: the plugin's manifest declares `requested`, the host config declares the `allowed` ceiling, and installation persists the `granted` set that the loader passes in per `PluginLoadRequest`. Registration-time `add` calls and runtime-time method calls both check the granted set and throw `CapabilityDeniedError` synchronously when missing.

## How to use it

A host wires the Store, events, and area registries, then opens the plugin registry. In production, omit `attestationVerifier` — the sigstore-backed default takes over.

```ts
import { openPluginRegistry } from "@inseam/core";

const plugins = await openPluginRegistry({
  store, events, access, source, connections,
  developmentMode: false,
});

const outcomes = await plugins.load([
  {
    packageName: "inseam-plugin-acme",
    version: "0.3.1",
    tarball,            // bytes from node_modules
    module,             // dynamically imported plugin module
    manifest,           // parsed `inseam` block of package.json
    grantedCapabilities: ["register_connections", "source_write", "http", "kv"],
  },
]);

for (const o of outcomes) {
  if (!o.ok) console.error(o.packageName, o.error);
}
```

A failing plugin lands its error in its own `PluginLoadOutcome` slot; sibling plugins in the batch continue. Admin paths (`revoke`, `uninstallPlugin`, `getPackage`) throw `UnknownPluginError` for names the registry has never observed.

Full type signatures and the seven-step load sequence: [spec — Public API](../spec/plugin-system.md#public-api) and [spec — Behavior](../spec/plugin-system.md#behavior).

## How to extend it

Every inseam plugin — Connection, Location Kind, identifier kind, listener, or any future area — ships through this same shape. A third-party plugin depends on `@inseam/plugin-contract` plus whichever area contract(s) it implements against, and on nothing else from the framework.

```ts
// package.json
{
  "name": "inseam-plugin-acme",
  "type": "module",
  "inseam": {
    "capabilities": {
      "requested": ["register_connections", "register_location_kinds", "source_write", "http", "kv"]
    }
  }
}
```

```ts
// src/index.ts
import type { PluginModule, RegistrationContext, RuntimeContext } from "@inseam/plugin-contract";

export const register: PluginModule["register"] = (ctx: RegistrationContext) => {
  ctx.registries.connections.add(acmeConnection);
  ctx.registries.locationKinds.add(acmeLocationKind);
};
```

The minimum a plugin author needs:

1. Name the package `@inseam/plugin-<name>` or `inseam-<name>`. The name is the durable identity recorded on every row the plugin asserts.
2. Publish with `npm publish --provenance` from CI. The loader refuses tarballs without a valid attestation.
3. Export a `register(ctx)` function. Pure declaration — `ctx.registries.*.add(...)` only. No I/O, no fetch, no KV. A throw fails the load and rolls back partial contributions.
4. Run runtime work inside the handlers registered through the area registries. Each receives `RuntimeContext` as its first argument: write triples through `ctx.source.write` (auto-tagged), keep cursors / refresh tokens in `ctx.kv`, fetch through `ctx.http`, time through `ctx.clock`, log through `ctx.log`.
5. Declare every capability the plugin will exercise in `inseam.capabilities.requested`. The host's `granted` set is `requested ∩ allowed`; touching a service without its capability throws `CapabilityDeniedError`.

For in-tree development, prefix the name with `workspace:` (e.g. `workspace:inseam-plugin-foo`) and open the registry with `developmentMode: true` — attestation is skipped, and the prefix is stored verbatim on every asserted row so dev data is visibly tagged. Production hosts refuse `workspace:*` names outright.

A worked end-to-end author example (Connection + Location Kind + cursor in KV): [spec — Plugin author perspective](../spec/plugin-system.md#plugin-author-perspective).

## Where the code lives

- Contract package: `pkgs/plugin-contract/src/index.ts`
- Core surface entry: `pkgs/core/src/plugin-system/index.ts`
- Loader, contexts, capability enforcement, KV, registry methods: `pkgs/core/src/plugin-system/registry.ts`
- Types, capability vocabulary, error classes, default limits: `pkgs/core/src/plugin-system/types.ts`
- Migrations (`plugin_package`, `plugin_kv`): `pkgs/core/src/plugin-system/migrations.ts`
- Behavior tests, one file per concern: `pkgs/core/src/plugin-system/*.test.ts`
- Test doubles (fake verifier, fake plugin, fake registry wiring): `pkgs/core/src/plugin-system/__fixtures__/`

The sigstore-backed `AttestationVerifier` default lives at `pkgs/core/src/plugin-system/sigstore-verifier.ts` and is the only file in this contribution that touches sigstore primitives.

## Related

- [Spec — plugin-system](../spec/plugin-system.md) — types, the seven-step load sequence, errors, acceptance criteria
- [Design — plugin-system](../design/plugin-system.md) — why identity is the package name, why `plugin_package` is a row, why registration and runtime are split
- [Monorepo package model](./monorepo-package-model.md) — `@inseam/plugin-<name>` vs `inseam-<name>` naming, contract-package dependency rules, workspace resolution
- [Source](./source.md) — Location Kinds are plugin contributions; `uninstallPlugin` triggers the envelope-keyed sweep this port owns
- [Access](./access.md) — `asserted_by_plugin_package` foreign-keys into `plugin_package`; identifier kinds and envelope-party roles flow through `ctx.registries`
- [Connection](./connection.md) — `ConnectionDefinition`s are plugin contributions; fetch handlers receive `RuntimeContext`
- [Events](./events.md) — `plugin_package.observed` / `plugin_package.revoked` ride the outbox; listeners are a plugin extension point, materializers are not
