Documentation source
Plugin Bundles (Engine Reference)
The BundleManifest engine — the installable-artifact format and SQL transaction that powers Plugins. User-facing docs live at /docs/features/plugins.
> **User-facing surface:** The admin UI for discovering, installing, uninstalling, and authoring plugins is **Admin > Plugins** (`/admin/plugins`). See [Plugins](/docs/features/plugins) for the full user and agent guide.
>
> This page documents the **engine internals** — the `BundleManifest` schema, the `install_bundle()` SQL function, and the provenance model — for engineers and agents working at that layer.
## Overview
A **plugin bundle** (engine term: `BundleManifest`) is the installable-artifact format that powers the Plugins surface. It is a Zod-validated JSON document — `@amble/<venture>` or `@<tenant>/<slug>` — declaring an additive set of: entity types (data types), agents, tasks, tool refs, view configs, block registrations, nav patches, skill refs, theme tokens, criteria sets, custom pages, seed data, workspace config, and permissions. Installation fans the manifest into the existing platform tables in a single Postgres transaction. Every installed row is tagged with `installed_by_bundle`, `bundle_version`, and (for workspace-scoped sections) `installed_by_workspace_id` so upgrades, uninstalls, and audits are trivial.
**Architectural decisions:**
- bundles are additive overlays, not fork replacements — see
[ADR-0007](/docs/adr/0007-plugin-bundles-as-overlays);
- the install boundary is the workspace, not the tenant — see
[ADR-0008](/docs/adr/0008-workspaces-as-install-boundary);
- the consolidation that made `BundleManifest` the one engine — see
[ADR-0058](/docs/adr/0058-plugins-unified-install-surface).
**Where to find it:** Admin > Plugins (`/admin/plugins`), gated on `workspaces.team.manage`. This replaced the earlier `/admin/workspaces/install` and the deleted `/admin/extensions` and `/admin/templates` surfaces.
**Who can use it:** tenant admins (install / uninstall / author via
`workspaces.team.manage`); system admins (manage curated registry).
## Engine Status
The `BundleManifest` engine is complete and live. The original ADR-0007 6-PR ladder (PRs 3–6 covering API routes, gallery convergence, reference bundle, and upgrade flow) was superseded by the Plugins consolidation epic (PR #2617, ADR-0058), which delivered the full surface in a focused four-PR sequence instead.
## Key Concepts
| Concept | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Bundle** | A versioned npm package that declares additive platform configuration via `bundle.manifest.json`. |
| **Manifest** | The Zod-validated JSON document at the root of the package. Sole contract between bundle author and the install transaction. |
| **Install transaction** | `install_bundle(tenant_id, workspace_id, manifest, options)` — single Postgres transaction that fans manifest sections into `entity_types`, `agents`, `tasks`, `views`. |
| **`installed_by_bundle`** | Column on every owned table. Lets us list, diff, upgrade, and uninstall a bundle's footprint atomically. |
| **`installed_by_workspace_id`** | Column on every owned table. NULL = tenant-scoped (shared across workspaces); UUID = owned by that workspace's install of this bundle. |
| **Install scope** | Per-section `install_scope: 'workspace' \| 'tenant'`. Default `workspace`. Tenant-scoped sections are idempotent across re-installs into other workspaces of the same tenant. |
| **Per-workspace scoping** | Bundles install per-workspace. The same bundle can be live in `marketing` workspace and absent in `sales` workspace within the same tenant. |
| **Runtime entry point** | Compiled JS in the bundle's npm package that registers tools, blocks, and skills at app boot. The DB row is the install marker; the package is the code. |
## Manifest Reference
The manifest lives at `features/bundles/lib/manifest.ts` and is parsed
by `parseBundleManifest()` (or its non-throwing variant
`safeParseBundleManifest()`).
### Identity
```ts
{
name: "@amble/demo-tracker", // scoped npm-style name
version: "0.1.0", // strict semver
displayName: "Demo Tracker",
description: "...", // optional
author: { name: "...", url: "..." }, // optional
license: "MIT", // optional
homepage: "...", // optional
}
```
### Platform compatibility
```ts
{
amblePlatform: {
minVersion: ">=0.1.0",
maxVersion: "<2.0.0", // optional
},
}
```
### Sections (all optional)
| Section | Becomes a row in / triggers |
| ------------- | ------------------------------------------- |
| `entityTypes` | `public.entity_types` |
| `agents` | `public.agents` |
| `tasks` | `public.actions` (action registry) |
| `tools` | runtime registration via `registerTool()` |
| `views` | `public.views` |
| `blocks` | runtime registration via `registerBlock()` |
| `nav` | merged into `nav_config` jsonb |
| `skills` | runtime registration via `registerSkill()` |
| `theme` | merged into `tenant_settings.theme_tokens` |
| `seedData` | initial rows in `public.entities` + `public.entity_relations` |
### Seed data — entities + relations
Bundles ship starter content via `seedData`. Both arrays are optional;
caps are 1000 entities + 2000 relations per bundle (more than that
should ship a runtime initializer, not the manifest).
```ts
{
seedData: {
entities: [
{
entity_type_slug: "project",
slug: "alpha", // required — idempotency key
name: "Project Alpha",
fields: { stage: "draft" }, // → entity.content jsonb
lockedFields: ["stage"], // → entity.metadata.lockedFields
},
{
entity_type_slug: "project",
slug: "beta",
name: "Project Beta",
content: { description: "..." }, // alternative to `fields`
},
],
relations: [
{
source_entity_type_slug: "project",
source_slug: "alpha",
predicate: "depends_on", // → relationship_type
target_entity_type_slug: "project",
target_slug: "beta",
},
{
source_entity_type_slug: "atlas-person",
source_slug: "alice",
predicate: "owns", // → relationship_type
target_entity_type_slug: "atlas-entity",
target_slug: "family-trust",
metadata: { // optional — merged into entity_relations.metadata
ownershipPct: 100,
ownershipKind: "beneficial",
},
},
],
},
}
```
`install_bundle()` resolves `entity_type_slug` against tenant entity
types and resolves relation endpoints by `(entity_type_slug, slug)`
against tenant entities (whether seeded by THIS bundle in the same
transaction or pre-existing). Type-scoped resolution is required
because `entities.slug` is unique only within
`(tenant_id, entity_type_slug)` — a bundle can ship two entities with
the same slug across different entity types, so resolution by slug
alone would pick non-deterministically. Idempotency keys:
`(tenant_id, entity_type_slug, slug)` for entities (enforced by
`uq_entities_tenant_type_slug` + `ON CONFLICT DO NOTHING`),
`(tenant_id, from_entity_id, to_entity_id, relationship_type)` for
relations (enforced by `uq_entity_relations_unique_relation` +
`ON CONFLICT DO NOTHING`). Re-running install on the same manifest
produces the same N entities and same M relations.
Missing endpoints abort the install with PG error code `P0001` so
typo'd slugs surface at install time instead of silently shipping a
disconnected graph.
Seed relations accept an optional `metadata` object. The value is merged
into `entity_relations.metadata` alongside the `installed_by_bundle`
marker (migration `20260612060000_install_bundle_seed_relation_metadata.sql`
redefines `install_bundle` to support this). Use it to stamp domain fields
such as ownership percentage directly from the bundle manifest — the
Structure Map workspace uses this to seed `ownershipPct`, `ownershipKind`,
`shareCount`, and `shareClass` on family-structure ownership edges.
### Runtime + permissions
```ts
{
runtime: {
main: "dist/runtime.js",
register: "registerBundle",
},
permissions: ["entities.team.read", "entities.team.create"],
dependencies: { "@amble/portfolio-base": "^1.0.0" },
}
```
The `permissions` field uses the same `app_permission` enum as user
roles. A pgTAP test in PR 2 enforces parity between the manifest's
permission union and the DB enum.
## How It Works
```
┌──────────────────────────────────────────────────────────────────┐
│ npm registry │
│ @amble/rock-hill@1.3.0 @amble/ember@2.0.0 @amble/ims@0.4.0 │
└────────────────────┬─────────────────────────────────────────────┘
│ fetch + verify (PR 3)
▼
┌──────────────────────────────────────────────────────────────────┐
│ Bundle resolver │
│ - npm tarball fetch + sha verify │
│ - manifest.json parse via parseBundleManifest() │
│ - dependency resolution + platform version check │
└────────────────────┬─────────────────────────────────────────────┘
│ preview / apply
▼
┌──────────────────────────────────────────────────────────────────┐
│ install_bundle(tenant_id, manifest, options) [SQL fn — PR 2] │
│ atomic transaction: │
│ INSERT INTO entity_types (... installed_by_bundle, version) │
│ INSERT INTO agents (... installed_by_bundle, version) │
│ INSERT INTO tasks (... installed_by_bundle, version) │
│ UPDATE nav_config (... merge bundle nav patches) │
│ INSERT INTO views (... installed_by_bundle, version) │
│ INSERT INTO bundle_installations (...) │
└────────────────────┬─────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ Runtime registration (boot-time) │
│ - Bundle code loaded from `node_modules/<package>/dist/...` │
│ - registerTool() / registerBlock() / registerSkill() │
│ - Tenant scoping via existing `installed_by_bundle` filter │
└──────────────────────────────────────────────────────────────────┘
```
## API Reference
```ts
import {
parseBundleManifest,
safeParseBundleManifest,
findIntraBundleSlugCollisions,
type BundleManifest,
} from "@/features/bundles/lib/manifest"
```
| Function | Use when |
| ------------------------------------------- | ----------------------------------------------------------------------------------- |
| `parseBundleManifest(data)` | Strict parse; throws `ZodError` on invalid input. CLI / install API entry point. |
| `safeParseBundleManifest(data)` | Non-throwing parse; returns `{ success, data \| error }`. Use in admin preview UI. |
| `findIntraBundleSlugCollisions(manifest)` | Read-only structural lint — duplicate slugs in the same section. |
Server-side install / uninstall functions (`installBundle()`,
`uninstallBundle()`, `previewBundle()`) ship in PR 2 under
`features/bundles/server/`.
## For Agents
Bundles are the canonical way to ship a venture into Amble. When an
agent is asked "add the Rock Hill schema to this tenant" or "install
the demo tracker for me," the agent should:
1. POST `/api/plugins` with `{ action: "preview", … }` (server-side:
`serializeTenantArtifactsToManifest` + `safeParseBundleManifest`) to
validate the manifest and render what it installs.
2. Surface the preview to the user via the **Plugins** admin UI
(`/admin/plugins`) or chat.
3. On confirmation, POST `/api/plugins` with
`{ action: "install", install: { source, … } }` — this calls
`installBundle()` (`features/workspaces/server/install.ts`) under the hood.
For authoring a new bundle (rare today; common once bundle authoring DX
matures in PR 6+), follow the manifest reference above and ship the
runtime entry point as compiled JS.
## Design Decisions
- **Plugin overlay, not fork-replace.** See ADR-0007.
- **No new primitive.** Bundles fan into the existing six (Record,
Agent, Action, Work, Message, Context). `bundle_installations` is a
thin install-record table, not a 7th primitive.
- **`.passthrough()` during PRs 1-3.** The Zod schemas use
`.passthrough()` so downstream PRs can append optional fields without
breaking already-published bundles. Locked to `.strict()` before PR 4
ships.
- **Runtime ref, not inline code.** The manifest declares
`runtime_ref` strings (e.g., `dist/tools/foo.js#registerTool`); the
actual code lives in the npm package. Mirrors the `agent_connections`
pattern.
- **Per-tenant by default.** A bundle installed by `tenant-a` is
invisible to `tenant-b` even when both tenants share the same Amble
deployment.
## Related Modules
- [Agent System](/docs/features/agent-system) — bundles seed agents
and assign role permissions.
- [Tasks](/docs/features/tasks) — bundles seed action registry rows.
- [View System](/docs/features/view-system) — bundles ship view
configs against the unified surface registry.
- [Block System](/docs/features/block-system) — bundles register
custom block components.
- [Multi-tenant](/docs/features/multi-tenant) — bundles install
per-tenant via the same RLS / GUC pattern as everything else.
- [Interactivity](/docs/features/interactivity) — the four declarative
Specs that spec-payload bindings (and the slot resolver) interpret.