Sprinter Docs

Plugin Bundles

Versioned npm packages that install entity types, agents, tasks, tools, views, blocks, nav, skills, and seed data into an Amble workspace.

Overview

A plugin bundle is a versioned npm package — @amble/<venture> or @<venture-org>/<bundle> — whose bundle.manifest.json declares an additive set of: entity types, agents, tasks, tool refs, view configs, block registrations, nav patches, skill refs, theme tokens, and seed data. 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;
  • the install boundary is the workspace, not the tenant — see ADR-0008.

Status: PR 1 (manifest schema + parser + ADR) and PR 2 (install / uninstall SQL — workspace-scoped) shipped. PRs 3 (API routes + perms + template adapter wire-up), 4 (gallery convergence into /admin/workspaces/install), 5 (first reference bundle), and 6 (upgrade + curated registry) follow.

Where to find it: Admin > Workspaces > Install (/admin/workspaces/install). The install gallery in PR #931 is the single entry point — first-party templates and external bundles share the same gallery and the same install pathway.

Who can use it: tenant admins (install / upgrade / uninstall via workspaces.team.manage); system admins (manage curated registry).

Key Concepts

ConceptDescription
BundleA versioned npm package that declares additive platform configuration via bundle.manifest.json.
ManifestThe Zod-validated JSON document at the root of the package. Sole contract between bundle author and the install transaction.
Install transactioninstall_bundle(tenant_id, workspace_id, manifest, options) — single Postgres transaction that fans manifest sections into entity_types, agents, tasks, views.
installed_by_bundleColumn on every owned table. Lets us list, diff, upgrade, and uninstall a bundle's footprint atomically.
installed_by_workspace_idColumn on every owned table. NULL = tenant-scoped (shared across workspaces); UUID = owned by that workspace's install of this bundle.
Install scopePer-section install_scope: 'workspace' | 'tenant'. Default workspace. Tenant-scoped sections are idempotent across re-installs into other workspaces of the same tenant.
Per-workspace scopingBundles install per-workspace. The same bundle can be live in marketing workspace and absent in sales workspace within the same tenant.
Runtime entry pointCompiled 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

{
  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

{
  amblePlatform: {
    minVersion: ">=0.1.0",
    maxVersion: "<2.0.0",  // optional
  },
}

Sections (all optional)

SectionBecomes a row in / triggers
entityTypespublic.entity_types
agentspublic.agents
taskspublic.actions (action registry)
toolsruntime registration via registerTool()
viewspublic.views
blocksruntime registration via registerBlock()
navmerged into nav_config jsonb
skillsruntime registration via registerSkill()
thememerged into tenant_settings.theme_tokens
seedDatainitial 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).

{
  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",
      },
    ],
  },
}

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.

Runtime + permissions

{
  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

import {
  parseBundleManifest,
  safeParseBundleManifest,
  findIntraBundleSlugCollisions,
  type BundleManifest,
} from "@/features/bundles/lib/manifest"
FunctionUse 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. Call previewBundle(packageName) to render the diff.
  2. Surface the preview to the user via the admin UI or chat.
  3. On confirmation, call installBundle(packageName) (PR 2 surface) or POST /api/bundles/install (PR 3+ surface).

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.
  • Agent System — bundles seed agents and assign role permissions.
  • Tasks — bundles seed action registry rows.
  • View System — bundles ship view configs against the unified surface registry.
  • Block System — bundles register custom block components.
  • Multi-tenant — bundles install per-tenant via the same RLS / GUC pattern as everything else.
  • Interactivity — the four declarative Specs that spec-payload bindings (and the slot resolver) interpret.

On this page