Skip to content

Capability packs

Status: Current | Last reviewed: 2026-09-21

The extensible, tenant-scoped capability registry — so an industry pack (e.g. healthcare Adverse Event reporting) can be authored once and reused across agents. The registry, CRUD API, expander, constraints, library picker, Studio workspace editor and capability testing are implemented. Signed packaging remains design. Read this before extending any of it.

Implementation status

Capability-owned testing

Studio scaffolds testing.json and its JSON schema beside the capability sections. The Testing button opens its custom text editor. Generate missing responses from tool contracts, edit variants/examples, define named scenarios, check them, and run Python implementation tests. The form and JSON share one VS Code document. Check/run flush pending edits before assembling source files.

The manifest's optional testing field has schema_version: 1, default_scenario, tools, scenarios and implementation_tests. Each scenario can choose tool_variants, initial_state, and runtime responses for supervisor, authentication, payment or UI. Full contract validation runs on workspace publish and registry create/update. Saved tool references use mocks from the saved tool's own Testing section.

Each implementation test names a Python tool and supplies input, expected, optional variables, and dependency fixtures (module, attribute, result, async). Dependencies replace callable return values, including dotted attributes; the tool's actual source executes in code-executor's network-isolated jail. Fixtures must match the callable's return contract; JSON values do not automatically become HTTP response objects. Input/output schemas and the exact expected result are checked. Execution receives no integration credentials.

The authenticated POST /api/capabilities/:id/testing supports inspect, generate, check and run. Studio proxies it through /api/studio/capability/testing with the capability identity from its scoped editor session. Neither generation nor checking publishes a capability. Published agent snapshots and already queued test runs retain their pinned definitions. See Agent evaluation for runtime adapters.

Built (phases 1–2):

  • Registry table capability_definitions + built-in seeds — 045_capability_definitions.sql (single table with a version column; the separate versions table is deferred until the phase-3 editor — provenance immutability comes from the manifest snapshot instead).
  • Manifest contract + validator + code-derived built-in manifests — capability-manifest.ts (client-safe); seed drift pinned by capability-seeds.test.ts.
  • Server reads/writes + ref resolution — capability-registry.ts; CRUD at GET/POST /api/capabilities, GET/PUT/DELETE /api/capabilities/[id] (AIManager:ManageTools; shared scope platform-admin only; a manifest change requires a version bump).
  • Publish-time expander — capability-expander.ts, wired inside publishAgentVersion (db.ts) so BOTH publish paths (Studio route + management/MCP) expand and enforce constraints.
  • Prompt-authoring projection — draft capability: refs resolve their manifest tool names into the @ / {{ menus; published provenance restores the same capability footer while hiding the exact generated prompt suffix and grouping expanded tools under the capability instead of ordinary Actions. Runtime and Copy full prompt continue to use the untouched expanded snapshot.
  • Generic constraint interpreter in validate-node-capabilities.ts — customer auth's conversational stage-type rule is now manifest data (the parity port).
  • UI: enabled-capability cards + a searchable library dialog (not a card per registry row — the registry grows) — GenericCapabilityCard.tsx, CapabilityLibraryDialog.tsx, SchemaConfigForm.tsx, rendered from NodeCapabilitiesAndBehaviors.tsx. Built-in cards keep their explicit rendering (zero-risk deviation).

Built (phase 3 — pro-code authoring; the earlier form/JSON editor dialog was REPLACED after user review):

  • Capabilities tab in Tools & Templates — CapabilitiesPanel.tsx: list + launch + publish, not an editor. Built-ins are deliberately NOT listed (not authorable — they read as a confusing tools/capabilities mix; they're enabled from the agent editor's Capabilities library). "New capability" collects metadata only (name / immutable slug / description / category), creates the row, and opens its code-server workspace. Row actions: Open in editor, Publish from workspace, Delete.
  • Capability workspaces — capability-workspace.ts: SPLIT into section files so each gets its own JSON Schema AND its own rich custom editor — capability.json (metadata only), tools.json, variables.json, logic.json, config-schema.json, constraints.json, prompt.md, tools/*.py (python config.source_file refs, inlined at publish). Schemas under .trueserve/; .vscode/settings.json wires both json.schemas and workbench.editorAssociations (the section files open in the Studio extension's Capability Editor by default, workspace-scoped — the extension contributes it at priority option so files with these names elsewhere are untouched). BACK-COMPAT: sections inline in capability.json (the pre-split layout) still assemble; a section FILE wins when present. Naming mirrors catalogs (/workspaces/capability-{shared-<slug>|tenant-<tenant_slug>-<slug>}); scaffold is lazy (editor-link creates it from the CURRENT row when missing, never overwrites WIP — delete the folder to re-scaffold a pre-split workspace into the new layout). ai-manager owns both directions — capability workspaces are data-only, so catalog-builder is not involved.
  • Editor link + auth — editor-link route (catalog-clone JWT with capability_id claims); code-server-verify accepts the capability- folder prefix; studio-api claims are a union (catalog OR capability) and the impersonation client sends the matching X-Target-Capability binding.
  • Publish/validate from workspace — publish-from-workspace
  • validate-workspace: assemble → validateCapabilityManifest → auto version bump when content changed; the capability.json slug is IMMUTABLE (mismatch is a hard error — it names the workspace dir and every agent ref).
  • TrueServe Studio extension — RICH SECTION EDITORS (capability-section-editor.ts
  • section-editor-html.ts): one CustomTextEditorProvider serving the five section files as form UIs — tools (type dropdowns, per-type fields, parameter rows, tools/.py picker/create/open), variables, logic (tool dropdown read from tools.json, fail-closed gate fields), config fields (typed rows generating the JSON Schema), constraints (stage-type checkboxes, requires picker fed by the tenant registry via a new studio-api /capability/registry route). It's a TEXT editor provider on purpose: the form and raw JSON edit the SAME document ("Open as JSON" always available; save/dirty/undo are VS Code's; edits made by Claude or git show up live in the form). Plus TrueServe: Validate Capability / Publish Capability commands (they saveAll first — ai-manager assembles from DISK) opening the Capability panel, a webview with Validate/Publish buttons + the error/warning list. Webview, not host: studio-api auth rides the per-connection cs_auth COOKIE, which only webview fetches carry — the extension host has no token, so every studio-api call (the panel, the saved-tool + registry pickers in the section editors) must originate in a webview, the same reason the catalog preview panel's Publish is a webview button. Claude inline* gets the capability-authoring skill (auto-symlinked by the image entrypoint). Extension changes ship via a code-server image rebuild.
  • Library-first agent UX (same review): the Capabilities section renders only ENABLED capabilities as cards; discovery — built-ins included — goes through one always-present searchable library (CapabilityLibraryDialog.tsx, category-grouped, "Built-in" first). Each built-in entry reuses the SAME buildEnablePartial helper its card switch uses (exported from the four capability components), so the two paths cannot drift; Multimodal UI and Video enable via their config keys, not refs.
  • End-to-end coverage: capability-lifecycle.spec.ts (mini-form create → workspace files written → validate + publish v2 → agent library enable → agent publish; falls back to API authoring when the test host cannot see the workspace bind mount). User-facing docs: userguide/tools/capabilities.md.

Findings from the first real capability (adverse-event-reporting, 2026-08-28)

A GVP-compliant pharmacovigilance intake capability (7 manual-API tools against a FHIR wrapper, identity-gated writes, config form) was authored end-to-end as a stress test. It published cleanly and surfaced seven gaps. Findings 1–5 were fixed on 2026-08-28 (the same pass that built and deployed the wrapper — fhir-sfdc-wrapper-lambda):

  1. ADDRESSED — config→tool substitution. {{capability_config.<key>}} now substitutes into inline tool CONFIG string leaves at expansion (capability-expander.ts substituteToolConfigValue): whole-string placeholders keep the configured value's type, an unconfigured whole-string placeholder drops that config key (an unresolved auth.integration_id must not ship literal), embedded unresolved placeholders stay literal with a warning. The AE capability's tools now read {{capability_config.fhir_api_base_url}} + {{capability_config.fhir_oauth_integration}}.
  2. ADDRESSED — integration pickers. Config-schema fields with format: 'trueserve:integration' (+ optional integration_source / integration_capability filters) render an integration Select on the capability card (SchemaConfigForm + hooks/use-integrations-list.ts); the workspace config-schema editor gained an "Integration" field kind and the tools editor an Auth control (none / fixed integration / integration from a config field), fed by GET /api/studio/capability/integrations. The generic oauth2_api integration source (migration 046) closes the auth gap: client-credentials Bearer tokens are minted live in all THREE runtimes (see the root CLAUDE.md cross-cutting bullet).
  3. ADDRESSED — manifest tool_logic. New logic kind {kind: 'tool_logic', tool, available_when?, unavailable_message?, bind?, capture?} expands per-FIELD into config.tool_logic[tool] (field-level collision errors; available_when stays as gate-only sugar). The AE capability captures $.data.id → ae_case_reference (note: capture paths address the tool RESULT envelope {status, data}, so wrapper response fields live under $.data.*), and variables.json now legitimately declares it.
  4. ADDRESSED — richer Manual-API form. The tools editor gained header rows, a body-template JSON editor, the Auth control, and a live cross-check that warns on {param} placeholders not in declared parameters, declared params never used, and {{capability_config.X}} not matching a config-schema field.
  5. ADDRESSED — unresolved-placeholder stripping is now a cross-runtime guarantee. After templating, a string that is still exactly {name} for a DECLARED parameter is stripped from body/headers (recursively; containers emptied by stripping are dropped — FHIR forbids empty arrays) and from URL query pairs, in BOTH runtimes (inline_api_tools.py, db_loader.py). Undeclared literal braces are untouched. Related fix from the live E2E: both runtimes now parse +json structured-syntax content types (RFC 6839 — application/fhir+json) as JSON, or the $.data.id capture would have addressed a string.

Finding from the second capability (appointment-management, 2026-09-01):

  • ADDRESSED — GET args no longer clobber a templated URL query. httpx params= REPLACES an existing URL query (it does not merge), and both executors passed the raw LLM args there — so an authored ?name={patient_name}&birthdate={patient_birthdate}, correctly templated to ?name=Golden&birthdate=1989-11-25, went on the wire as ?patient_name=Golden&patient_birthdate=1989-11-25: keys the wrapper never reads, a 400 (provide at least one of ?name=) on EVERY call. The bug hid because a tool whose parameter names equal its query keys gets rewritten to an identical query. Both runtimes now exclude args a URL placeholder consumed and merge the remainder (plus auth query) onto the URL by hand, sending no params= at all. Regressions: test_get_args_must_not_clobber_the_templated_url_query in test_saved_api_tools.py and test_oauth2_client.py.

Still open:

  1. No enum support on tool parameters — value sets (seriousness, outcome, …) live in description prose per the "name the allowed values" rule; schema-level enums would constrain the model harder.
  2. No dry-run affordance — nothing lets an author fire a tool with sample args from the workspace (catalogs have live preview); testing requires an agent publish.

The fixed pipeline is proven live: agent ae-reporting-e2e (TrueServe tenant) publishes with the capability configured against the deployed wrapper + the oauth2_api integration, and the orchestrator executor path (expansion → live token mint from the encrypted integration row → API Gateway → Lambda → JWT Bearer → hlsorg) created real AdverseEventEntry records (7aeVa000000035xIAA, 7aeVa000000037ZIAQ, 7aeVa000000039BIAQ — left as demo data).

What worked as designed: requires: [customer_authentication] + seven fail-closed available_when gates on customer_auth_status == "verified" composed with the built-in exactly as intended; the structured body_template JSON (string-leaf {param} substitution) carries full FHIR resources; the SFDC-subset rules (single coding per CodeableConcept, onsetDateTime-only, narrative into reaction.description) live cleanly in tool descriptions + the prompt.

Deliberately deferred: generation-MCP capability tools (the in-chat AI authoring surface — Claude inline in the workspace covers the AI path today), a unified Tools + Capabilities add-surface in the agent editor (both ultimately write config.tools refs — ToolSelector's popover and the capability library are the two halves of that seam), a custom config-form component per capability (letting a workspace ship its own UI replacing SchemaConfigForm on the agent card — tenant-authored UI inside ai-manager needs the catalog-style sandboxed-bundle treatment; the likely shape is referencing an A2UI component rendered in an iframe, not raw JS in the Studio page), constraints.scope: 'agent' (publish rejects it), install-time integration binding (superseded in practice by the oauth2_api + format: 'trueserve:integration' config-field pattern), signed export/import (phase 4), the observer primitive. Preview/draft expansion parity moved from this list to BUILT on 2026-08-28 (see Draft/Preview parity).

Why

What a capability is today

A built-in capability (Customer Authentication, Payments, External Consult, Message Injection, IVR navigation, transfer_agent) is ~12 hand-coded sync points across two languages:

Sync point Where
Registry constants (×5) node-builtins.ts — NODE_BUILT_IN_TOOLS, CAPABILITY_BUILTIN_IDS, SINGLETON_BUILTIN_IDS, BUILTIN_PROMPT_TOOL_NAMES, default configs
Closed configEditor union service-definitions/types.ts
Bespoke React editor components/tools/{payments,customer-authentication,…}/
Config TS interface pipeline-types.ts BuiltinToolConfigs
Hand-edited schema generator generate-config-schemas.ts
Publish validators validate-node-capabilities.ts, agent-config-validation.ts
Runtime gates node_builtin_config.py
Tool wiring tool_wiring.py + per-capability helpers
Prompt token name map token_resolver.py
Frame processors payments/consult/injection processors in lib/pipecat/pipeline/

The only drift guards are text-parsing tests. Adding a capability means editing all of it; a tenant cannot add one at all.

Goals

  • Tenant-scoped custom capabilities with the same authoring surface as built-ins (Capabilities card, prompt token, config form, publish validation).
  • One consistent model for what a capability contributes: tools, prompt fragment, variables, logic, config schema, constraints.
  • Signed, portable packs — the A2UI catalog export/import model — for industry bundles.
  • AI-assisted authoring as a first-class path, not an afterthought.
  • No new code-execution surface for tenant-authored content.

Design overview

Two tiers, joined by a publish-time expander:

  • Tier 1 — declarative manifests (extensible). A capability is a DB row whose manifest declares tools, prompt, variables, logic, config schema, and constraints. Anything a tenant or pack can author lives here.
  • Tier 2 — runtime primitives (first-party code only). Frame processors, media-plane behavior, secure capture. Built-in capabilities bind to them via a platform-only runtime field; tenant manifests never can.
flowchart LR
  A[Author: editor / code-server / generation MCP] --> R[(capability_definitions\n+ versions)]
  R -- "capability:<slug> ref + config" --> D[agent draft config]
  D -- publish --> E[Expander]
  E --> P[published agent_versions.config\ninline tools + logic + prompt + variables\n+ _capability_provenance]
  P --> O[ai-orchestrator: existing paths only]
  R -- signed export/import --> R2[(another env / tenant)]

Publish-time expansion, not runtime resolution

The single most important decision. A draft stores capability:<slug> refs in stage config.tools plus config.capability_configs.<slug> (parallel to today's builtin_tool_configs). The publish route expands each ref into config shapes the orchestrator already executes, and stamps _capability_provenance (slug, definition id, manifest version) on the published config.

Why publish time:

  • Published agents stay immutable. A capability update lands on the next publish, and every agent version records exactly which capability version it embeds — the property regulated tenants (the target market) need.
  • Zero runtime interpreter. Expanded output is ordinary config: inline tools (inline_python_tools.py, inline_api_tools.py), Agent Logic rules, prompt text, session variables.
  • Prompt tokens just work. Expanded tools are real tools in config, so {{TOOL:…}} resolution needs no new name map — and does not inherit the known a2ui data-tool literal-token gap.

Existing builtin:<slug> refs are unchanged (storage compatibility, collab allow-lists, published agents). The six built-ins become shared-scope registry rows whose refs resolve through the same registry; only new capabilities use the capability: prefix.

Draft/Preview parity (added 2026-08-28)

Publish-time expansion left one authoring hole: a Preview/test session runs the DRAFT, which is refs-only — so the capability's tools, prompt, and gates were simply absent in the one surface authors rehearse in, and "the capability doesn't work" was the natural (wrong) reading. Drafts still store refs (nothing about the storage or publish contract changed); parity comes from expansion at draft resolve time in the orchestrator:

  • fetch_pipeline_config_from_db (db_pipeline_store.py) is the one choke point every draft passes through (live session builds AND the /preflight route). When it resolves a status='draft' config whose stage tools carry a capability: ref, it POSTs the config to ai-manager's internal POST /api/internal/capabilities/expand (route, bridge-API-key auth — the same service-to-service mechanism ari-controller uses) and runs the returned config. The TypeScript expander stays the ONE implementation; nothing was ported to Python (draft_capability_expansion.py).
  • Fail-open: any error — ai-manager unreachable, a 422 from a mid-edit draft (unknown slug, logic collision), missing bridge key — logs a WARNING and runs the unexpanded draft, exactly the pre-parity behaviour. Preview must start; constraint violations come back as warnings, and publish is still the blocking gate.
  • Published configs never take this path (already expanded at publish), so version immutability and provenance are untouched. The in-memory expanded draft carries _capability_provenance for the session but nothing is written back to the draft row.

Data model

-- As built (045_capability_definitions.sql). The separate versions table is
-- deferred to the packaging phase: `version` is a column, a manifest change
-- requires bumping it, and each published agent embeds the full
-- manifest_snapshot in its provenance, so history is reconstructible.
CREATE TABLE capability_definitions (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id uuid NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
  scope text NOT NULL CHECK (scope IN ('shared','tenant')),
  slug text NOT NULL,
  name text NOT NULL,
  description text, icon text,
  category text,                 -- pack/industry grouping
  version text NOT NULL DEFAULT '1',
  manifest jsonb NOT NULL,
  created_by uuid, created_at timestamptz, updated_at timestamptz
);
-- UNIQUE (tenant_id, slug) + partial UNIQUE (slug) WHERE scope='shared':
-- a tenant slug may not shadow a visible shared slug (creation rejects it;
-- the resolver still prefers the tenant row on a tie, defensively).

Scope model follows a2ui_catalogs: built-ins belong to the TrueServe tenant; scope='shared' rows are visible to all tenants and writable only by platform admins; scope='tenant' rows are RLS-scoped. Provisioning may clone selected baselines like provision-tenant-baselines.ts does for catalogs (one-time fork; no update propagation — same as catalogs).

Manifest contract

Field Contents Executes via
tools[] Inline tool defs (python_sandboxed, manual/openapi, knowledge), {tool_ref: <uuid>} references to SAVED tools (how MCP servers / A2A agents are carried — expansion pushes the row's UUID into config.tools and the runtimes resolve it live, so server URLs/auth/filters stay on the saved row), or platform:<name> refs (shared scope only). Tool CONFIG string leaves substitute {{capability_config.<key>}} at expansion — whole-string placeholders keep the value's type, unconfigured whole-string placeholders DROP the key Existing inline-tool + saved-tool wiring
integration_requirements[] Integration type + required capability flags; bound to a tenant integration at install Secret storage
prompt Fragment template (config placeholders + {{TOOL:…}} tokens) and the editor token name Prompt assembly
variables[] Session variables the capability publishes (from tool capture / updated_variables) Existing variable scope
logic[] available_when gates (fail closed), tool_logic entries (gate + bind literal|variable + capture result→variable projections, merged per-FIELD into config.tool_logic[tool]), LogicTrigger rules Agent Logic evaluator
action_capture[] A2UI action projections (action?, path, declared variable, optional editor-only component) merged into config.multimodality.action_capture Agent Logic UI-action capture
config_schema JSON Schema driving a generated config form; format: 'trueserve:integration' (+ integration_source/integration_capability filters) renders an integration picker Generic SchemaConfigForm
constraints singleton, stage_types, scope: stage\|agent, requires[] (other capability slugs), expression-based publish checks Publish validation
egress_domains[] Domains the pack's sandboxed tools call; surfaced at install code-executor egress policy
tenant_settings_key Optional ceiling settings key (payments/customer_auth precedent) tenant_settings
runtime Built-in slug binding to Tier-2 code — platform-only, rejected in tenant scope Frame processors

Sketch (abridged):

{
  "slug": "adverse-event-reporting",
  "tools": [
    { "kind": "python_sandboxed", "name": "check_seriousness", "parameters": ["…"], "code": "def handler(…): …" },
    { "kind": "manual", "name": "report_adverse_event",
      "integration_requirement": "safety_api",
      "capture": { "ae_case_id": "$.case_id" } }
  ],
  "integration_requirements": [ { "id": "safety_api", "type": "openapi", "label": "Safety system endpoint" } ],
  "prompt": { "token": "ADVERSE_EVENT_REPORTING", "fragment": "If the caller mentions … you MUST … {{TOOL:report_adverse_event}} …" },
  "variables": [ { "name": "ae_case_id", "type": "string" }, { "name": "ae_serious", "type": "boolean" } ],
  "logic": [ { "kind": "available_when", "tool": "report_adverse_event", "expression": "ae_intake_complete == true" } ],
  "config_schema": { "type": "object", "properties": { "products": { "type": "array", "description": "Monitored products" } } },
  "constraints": { "scope": "agent", "requires": ["warm_transfer"] },
  "egress_domains": ["safety.example.com"]
}

What the expander emits

Per enabled stage (or all stages when scope: 'agent'): the manifest's tools as inline_tools entries with install-time integration bindings resolved by reference (never copied — the knowledge-tools rule), logic[] merged into the stage's rules, action_capture[] merged into the stage's multimodality config, the rendered prompt fragment appended (or its token replaced where the author placed it), and declared variables registered. requires[] is checked against the stage's other refs; platform: tools pass through as the built-in names.

Validation and schemas

Tenant config_schemas cannot join the static generated schema, so the publish route validates capability_configs.<slug> dynamically against the manifest's schema — warnings-only, matching the config JSONB policy. The static schema declares capability_configs as a described open map. Constraint checks (singleton, stage_types, requires, expressions) run in validate-node-capabilities.ts's slot as a manifest interpreter.

Note: capability_configs is STAGE-level, and stage config keys pass through both Y.Doc shape converters generically — verified, no converter change was needed. The round-trip is pinned in pipeline-shape.test.ts so a later "optimization" that enumerates stage-config keys fails loudly. A config-ROOT key would still need MISC_ROOT_FIELDS in both copies — _capability_provenance avoids that by existing only on published rows, which collab never reads.

Runtime impact

Declarative capabilities need no orchestrator changes — expansion lands on existing execution paths. Code-backed built-ins keep their gates and frame processors, now bound via runtime.

One future Tier-2 primitive is anticipated (v2): an observer — the copilot gate (small-model classifier, runs_on, debounce) generalized to run inside a primary agent's pipeline, parameterized entirely by manifest data, emitting a session variable the capability's logic and #when sections react to. It covers "watch the conversation deterministically" needs (compliance monitoring) without opening any tenant-code path. Until it exists, packs use prompt-driven detection plus a post-call reconciliation trigger (below).

Authoring surfaces

Three surfaces, one contract. The security boundary is the publish validator + manifest schema, never the editor — so all three are equally safe, and the AI path gets full freedom.

In-app editor

List panel + detail page + versions tab, following the catalog shell pattern (MultimodalPanel / CatalogDetailPage), with plain REST saves (no Y.Doc room). The form assembles from components that already exist: the prompt editor for the fragment (live token pills), WhenConditionBuilder / NewVariableDialog for logic[], ParameterDefinitions for tool schemas, CapabilityCard to preview what agent authors will see. Writes gated on AIManager:ManageTools; shared-scope writes platform-admin-only.

code-server workspace — the AI-assisted path

The code-server flow catalogs already use, minus the build pipeline (manifests are pure data — no esbuild, no MinIO artifacts). POST /api/capabilities/[id]/editor-link mints a short-lived JWT scoped to a capability workspace containing:

  • capability.json — the manifest, with its JSON Schema wired up so the IDE (and Claude inline) get completion and validation
  • prompt.md — the fragment
  • tools/*.py — python_sandboxed tool bodies
  • AGENTS.md — the authoring protocol + docs bundle, seeded the same way the Generate with AI workspace is

Claude inline in code-server authors and iterates freely; Publish posts the workspace back through the validator into a capability_versions row. Nothing the editor writes executes anywhere until publish accepts it, and the manifest cannot express in-process code, so an AI (or human) author cannot escalate past the contract.

Generation MCP tools

Capability-authoring tools (create_capability_draft, set_manifest, validate_manifest, preview_expansion) join the generation MCP server, so the sandboxed Generate-with-AI CLI and the editor Assistant can build capabilities conversationally — same effectors-only pattern, same per-job token auth as agent generation.

Security model

Hard line Rationale
Tenant manifests get no runtime binding and no new execution surface Custom behavior runs only via code-executor's nsjail sandbox (namespaces, egress proxy, no ambient credentials), remote MCP/REST, or knowledge
agent-host a2ui_tool_plugins (unsandboxed importlib) excluded from tenant packs Platform-trust only; the clone path already resets is_platform_approved — follow that precedent
Packs never carry credentials integration_requirements bound at install; expansion resolves by reference, never copies (knowledge-tools lesson)
Prompt fragments in shared/imported packs are a supply-chain surface ed25519 signing + env promotion policy + an import review screen showing the full fragment and tool list before install
Expressions are the whitelist-AST Agent Logic language No eval, no calls; tool gates fail closed
Sandboxed-tool egress is declared egress_domains[] surfaced at install so the code-executor allowlist is updated deliberately, not discovered as an opaque failure

Tenant-scope fragments are the same trust level as prompts tenants already write; the controls above exist for shared distribution.

Packaging and sharing

Factor the signing machinery out of catalog-export.ts into a shared lib parameterized by artifact type — ed25519 detached signature over the manifest, *_TRUSTED_KEYS verification, env promotion policy, provenance on the imported row, 409 on provenance conflict, createIfMissing. Capability export is the same .tgz: manifest.json + manifest.sig + the capability manifest and tool sources.

An industry pack is an umbrella signed bundle referencing artifact types that all have tenant-scoped DB storage: capability manifests, an A2UI catalog (intake forms), agent templates, workflows. The install wizard walks: signature/provenance → prompt + tool review → integration binding → egress review → import each artifact.

Worked example: Adverse Event reporting

The manifest sketched above, plus the pack pieces:

  • Intake enforcement — report_adverse_event's required schema + an available_when gate holding submission until captured variables show the four minimum elements; the pack's A2UI catalog ships a structured intake form for visual surfaces.
  • Seriousness — check_seriousness is a deterministic python_sandboxed check; the model never self-asserts seriousness (the KBA rule: the runtime observes tool results).
  • Read-back — reports the stored tool result (ae_case_id), never a derived value (the payments rule).
  • Escalation — requires: ["warm_transfer"]; config names the consult destination. Compose, don't reimplement.
  • Detection — v1: prompt-driven in-call detection plus a conversation.end workflow trigger using the LLM-judge condition kind in conditions.py to scan the transcript and file a reconciliation case for anything missed — a recognized pharmacovigilance pattern. v2: the observer primitive sets ae_suspected in real time.

Phasing

Phase Deliverable Proving case
1 ✅ Table + manifest contract; the SEVEN built-ins (incl. message_injection) seeded as shared rows with runtime bindings; registry lib + CRUD API Behavior identical everywhere (built-in cards stay explicit; only custom capabilities render from the registry)
2 ✅ Publish-time expander, capability_configs, generic schema form + library dialog, provenance, constraint interpreter Customer auth's stage-type constraint + customer_auth_status variable moved to manifest data (parity-tested); demo order-status-lookup fixture expands end-to-end
3 ✅ Pro-code authoring: capability workspaces in code-server (JSON-schema validation, Claude inline + skill), Studio-extension validate/publish, workspace publish routes; library-first agent enablement. Generation-MCP capability tools remain deferred Capability authored in the workspace, published from it (auto version bump), enabled via the searchable library, agent published (capability-lifecycle.spec.ts)
4 Shared signing lib, export/import, install wizard Cross-env promotion of that capability
5 Pack bundle format; observer primitive Adverse Event reporting pack

Open questions

  • Baseline update propagation: catalogs deliberately fork-once; do shared capability updates ever push to tenants, or is re-import the only path?
  • Draft agents referencing a capability whose definition was deleted or un-shared — block publish, or expand from the last-embedded version?
  • Does the expander run on Preview (draft) calls, or does Preview interpret refs live? (Draft-parity argues for expanding on draft save too.)
  • Marketplace-style discovery across tenants is out of scope here; the signed export file is the v1 distribution unit.

See also