Installing & Operating Plugins
The operator lifecycle: install, enable, grant, configure, monitor, revoke, and remove.
This is the operator's side of the platform. Plugin administration requires the organization:manage capability — workspace owners and admins only.
Install a bundled plugin (Tier 1)
Bundled plugins are composed at build time, so installation is a code change, not a dashboard action.
Install the package
Add it as a direct root dependency (dependencies or optionalDependencies) from the npm registry. Codegen verifies the installed package identity, the bun.lock integrity entry, and realpath containment. Aliases, git/URL/file/workspace sources, and external symlinks fail closed.
Register it
bun packages/plugin-cli/src/index.ts add @vendor/owlat-plugin-example
This edits the checked-in plugins.config.ts and prints the capability diff the change introduces. Run it with --dry-run first to review the diff and the proposed file without writing.
Regenerate the composition
bun run plugins:codegen
Commit plugins.config.ts and the generated composition files in the same change. CI runs bun run plugins:check, which fails when generated output is stale or invalid.
Rebuild and deploy
The plugin now exists in the deployment. Its feature flag sits at the manifest's declared default — normally false — and no capability is granted, so nothing runs yet.
The diff is the security review surface for a bundled plugin: it is the complete list of host operations the package can ever request. Bundled code runs with the same trust as your own deployed code — read the diff, not just the README.
Enable and grant
Settings → Plugins lists every registered plugin with its state, capabilities, and required environment variables.
Enabling a bundled plugin is an explicit consent action:
- The deployment must have every
flag.requiredEnvVarsvalue present. Enabling fails with the list of missing variables otherwise. - The enable request must approve every capability the manifest declares — no more, no fewer, no duplicates. A partial or unknown approval is rejected.
- Disabling the plugin deletes its grant record entirely.
That is how "grants can only restrict the manifest" appears in practice for Tier 1: the operator's choice is to consent to the full declared set or to leave the plugin off. Connected apps (Tier 2) are the tier where a subset of the manifest is granted per app.
Both the flag state and the grant record are read fresh inside each caller's transaction, so disabling a plugin or revoking a grant takes effect on the very next operation — no redeploy and no cache to wait out.
Configure
If the plugin declares a settingsSchema, its detail page renders a generic form: string, number, boolean, and select fields with the declared labels, descriptions, defaults, and bounds. The host validates input server-side against the schema.
A secret field is not part of the form. Owlat stores no plugin credentials: the field names a PLUGIN_-prefixed environment variable you set on the deployment, and the page shows only that variable's name and whether it is present. A save that carries a value for a secret key is rejected. "Reset to defaults" clears stored values back to the schema defaults.
If a plugin is removed from the build while settings rows remain, the index page offers "Clear plugin settings" for that orphaned id.
Operate the tiers
Tier 2 — connected apps
Registered from the dashboard; no rebuild. Each app is bound to a plugin id, requests a subset of that plugin's declared capabilities, and holds an encrypted shared secret used to sign hooks. Statuses are enabled, disabled, and revoked (terminal). See Connected Apps.
Tier 3 — sandboxed jobs
The code-worker retains the dormant plugin-job polling protocol, but no host enqueue adapter or operator job UI is shipped. The plugin-tasks profile is therefore not a usable plugin surface today:
COMPOSE_PROFILES=plugin-tasks docker compose up -d code-worker
The same container actively serves the coding-agent queue under the inbox-codetasks profile. See Sandboxed Jobs for the reserved Tier-3 contract.
Monitor
| Surface | What it shows |
|---|---|
| Audit log | Every hosted plugin operation, attributed with organizationId and pluginId, recording fixed operation and outcome codes only |
| Hook delivery logs | Per-app, per-kind signed-hook resolutions: attempted or not, which side won, the fixed fallback reason, and network duration. Retained for 30 days |
| Webhook delivery logs | Outbound webhook deliveries, including plugin-published events |
| Plugin task list | Recent Tier-3 jobs with status, attempts, and timing |
None of these surfaces contain payloads, prompts, generated text, mail content, secrets, or signatures — the redaction is structural, so no read path can leak them.
Revoke and remove
| Goal | Action | Takes effect |
|---|---|---|
| Stop a bundled plugin and withdraw its authority | Disable its feature flag — this also deletes its grant record | Next operation |
| Stop a connected app now | Disable it | Next hook call or API request |
| Permanently kill a connected app | Revoke it (terminal) | Next hook call or API request |
| Revoke a plugin-bound API key | Revoke the key, or disable the plugin it is bound to | Next request — the effective scope set is re-derived per request |
| Remove a bundled plugin | owlat-plugins remove <package>, regenerate, rebuild, deploy | After redeploy |
Removing the package from plugins.config.ts also removes its Convex component registration. Plugin storage rows and settings survive the removal; use "Clear plugin settings" on the plugins index to purge the orphaned settings.
If a plugin is misbehaving, disable the flag first — it is instant and needs no deploy. Only then investigate, and remove the package afterwards. Do not start with a rebuild: it is the slowest lever you have.
Zero-plugin deployments
The zero-plugin composition is a valid no-op deployment. plugins.config.ts with an empty bundledPluginPackages array generates a composition with no plugins, and every registry falls back to its built-in members with their original order, defaults, and error semantics.