Plugin Platform
How Owlat is extended: one validated manifest, a capability model, and three execution tiers.
Owlat's variation points — send transports, agent steps, draft strategies, autonomy gates, automations, webhook events, import providers, crons, navigation, settings — are all reachable from plugins. A plugin is an npm package that exports one validated manifest and, optionally, static modules behind the contracts in @owlat/plugin-kit.
The platform has three deliberate properties:
- One manifest. Everything the host needs to compose, permission, flag, budget, and render a plugin is data in
definePlugin(...). Validation never invokes plugin code. - Declaration is not authority. A manifest requests capabilities. An operator grants them. Both are rechecked at the moment of every hosted operation.
- No arbitrary runtime code. Nothing loads JavaScript into Convex or the browser at runtime. Bundled code is composed at build time; untrusted code runs out of process (a connected app) or behind an OS boundary (a sandboxed worker job).
@owlat/plugin-kit is the public compatibility boundary and the only package a plugin author imports. Its major version is the compatibility line — individual contribution interfaces do not carry their own versions. Pre-1.0 it tracks the Owlat repository version and contracts may break with the application.
The three tiers
| Tier | What it is | Installation | Trust |
|---|---|---|---|
| 1 — Bundled plugin | An npm package listed in plugins.config.ts, composed into the Convex and Nuxt builds | Edit config → codegen → rebuild → deploy | Same trust as the operator's own deployed code. Capabilities and flags reduce authority; they are not a sandbox |
| 2 — Connected app | An external HTTPS service with plugin-bound API keys and webhooks, plus a specified signed synchronous hook protocol whose runtime adapter is not shipped — see Connected Apps | Register in the dashboard; instant, no rebuild | Untrusted network peer. Every response is strictly validated, scrubbed, clamped, and bounded |
| 3 — Sandboxed worker job | A specified worker protocol behind the code-worker OS boundary; no host enqueue adapter is shipped | Reserved | Untrusted process. No ambient credentials, hard CPU/memory/time ceilings, host-controlled command |
One maintained reference plugin covers each tier:
| Tier | Reference | What it contributes |
|---|---|---|
| 1 | examples/plugins/escalation-guard | An agent step, a draft strategy, all three automation registries, a webhook event, and nav/settings entries — all in-process |
| 2 | examples/plugins/slack-approvals | A restrict-only hold gate served out of process over the signed hook protocol; no in-process contributions at all |
| 3 | examples/plugins/deliverability-lab | A seed-list job on the sandboxed queue, plus a Tier-1 send gate, cron and UI entries and llm:invoke for host-mediated generation |
One plugin may use several tiers at once, as the Deliverability Lab does. examples/conformance drives all three through the real host, codegen and CLI on every test run.
Anatomy of a plugin
import { definePlugin } from '@owlat/plugin-kit';
export const helloPlugin = definePlugin({
id: 'hello-owlat',
version: '0.1.0',
capabilities: ['plugin-storage:read', 'plugin-storage:write'],
// Storage capabilities REQUIRE a flag: access must be revocable at runtime.
flag: { default: false },
});
| Field | Meaning |
|---|---|
id | Lowercase kebab-case, ≤ 64 characters. Namespaces flags, storage, spend, audit attribution, and every contributed kind |
version | Semantic version |
capabilities | Requested host operations, domain:action, exact match, at most 64 |
contributes | Data-only descriptors per contribution bucket, at most 256 per bucket |
flag | { default, requiredEnvVars? }. Required whenever the plugin contributes anything, requests plugin storage, or requests llm:invoke |
llmBudget | { dailyUsd }. Required with llm:invoke; a hard per-UTC-day ceiling |
component | One condition-independent Convex component export, installed under an injective plugin_<id> namespace |
settingsSchema | Declarative operator form; the host renders, validates, and redacts it |
How a bundled plugin reaches production
- The package name is added to the checked-in root
plugins.config.ts. bun run plugins:codegen(orowlat-plugins codegen) verifies the installed package's identity, lockfile integrity and realpath containment, imports only its manifest entry, validates one immutable snapshot, and writes the checked-in Convex and Nuxt composition files ordered by manifest id.- Builds and CI run
bun run plugins:check, so stale or invalid composition fails the build closed. - The deployment rebuilds. The plugin's feature flag starts at its declared default — normally
false— so nothing runs until an operator enables it and grants capabilities.
Bundled plugin code runs in Owlat's own processes. Capability declarations, grants, flags and host-mediated services reduce what a bundled plugin can reach and make its actions attributable — they do not contain hostile code. Anything you do not fully trust belongs in a connected app (Tier 2) or a sandboxed job (Tier 3).
Package map
| Package | Role |
|---|---|
packages/plugin-kit | Public contracts: definePlugin, capability constants, every contribution type. The only package a plugin imports |
packages/plugin-host | Private, runtime-neutral enforcement kernel shared by Convex and Nuxt: capability checks, flags, ordering, gate results, untrusted-text policy |
packages/plugin-codegen | Build tool that owns the single composition point and the generated catalogs/registries |
packages/plugin-cli | owlat-plugins — create, add, remove, codegen, dev |
examples/plugins/* | Maintained reference plugins exercising the real contribution points |
examples/conformance | The conformance gallery: lifecycle (install/add/remove/disable/upgrade) and full-pipeline replay suites over those references, plus the plugin-provider parity proof — a fixture ESP bundle driven through the shipped routing, dispatch, measurement, return-path and credential-form modules |
Read next
- Building a Plugin — scaffold, manifest, modules, tests, ship
- Contribution Reference — every bucket, its capability, and its host semantics
- Capabilities & Trust — the permission model and the invariants a plugin can never weaken
- Installing & Operating — the operator lifecycle
- Connected Apps and Sandboxed Jobs
- Troubleshooting
Architecture decisions: ADR-0049 (platform contract), ADR-0050 (draft strategies), ADR-0051 (ordered autonomy gates), ADR-0052 (connected apps and signed hooks), ADR-0053 (sandboxed plugin jobs).