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 aversioncolumn; 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 bycapability-seeds.test.ts. - Server reads/writes + ref resolution —
capability-registry.ts; CRUD atGET/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 insidepublishAgentVersion(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 fromNodeCapabilitiesAndBehaviors.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(pythonconfig.source_filerefs, inlined at publish). Schemas under.trueserve/;.vscode/settings.jsonwires bothjson.schemasandworkbench.editorAssociations(the section files open in the Studio extension's Capability Editor by default, workspace-scoped — the extension contributes it at priorityoptionso 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 withcapability_idclaims);code-server-verifyaccepts thecapability-folder prefix; studio-api claims are a union (catalog OR capability) and the impersonation client sends the matchingX-Target-Capabilitybinding. - Publish/validate from workspace —
publish-from-workspace validate-workspace: assemble →validateCapabilityManifest→ auto version bump when content changed; the capability.jsonslugis 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): oneCustomTextEditorProviderserving 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,requirespicker fed by the tenant registry via a new studio-api/capability/registryroute). 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). PlusTrueServe: Validate Capability/Publish Capabilitycommands (theysaveAllfirst — 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-connectioncs_authCOOKIE, 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 thecapability-authoringskill (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 SAMEbuildEnablePartialhelper 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):
- ADDRESSED — config→tool substitution.
{{capability_config.<key>}}now substitutes into inline tool CONFIG string leaves at expansion (capability-expander.tssubstituteToolConfigValue): whole-string placeholders keep the configured value's type, an unconfigured whole-string placeholder drops that config key (an unresolvedauth.integration_idmust 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}}. - ADDRESSED — integration pickers. Config-schema fields with
format: 'trueserve:integration'(+ optionalintegration_source/integration_capabilityfilters) 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 byGET /api/studio/capability/integrations. The genericoauth2_apiintegration 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). - ADDRESSED — manifest
tool_logic. New logic kind{kind: 'tool_logic', tool, available_when?, unavailable_message?, bind?, capture?}expands per-FIELD intoconfig.tool_logic[tool](field-level collision errors;available_whenstays 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.*), andvariables.jsonnow legitimately declares it. - 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. - 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+jsonstructured-syntax content types (RFC 6839 —application/fhir+json) as JSON, or the$.data.idcapture 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 noparams=at all. Regressions:test_get_args_must_not_clobber_the_templated_url_queryin test_saved_api_tools.py and test_oauth2_client.py.
Still open:
- 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.
- 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
runtimefield; 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/preflightroute). When it resolves astatus='draft'config whose stage tools carry acapability:ref, it POSTs the config to ai-manager's internalPOST /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_provenancefor 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_configsis 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 inpipeline-shape.test.tsso a later "optimization" that enumerates stage-config keys fails loudly. A config-ROOT key would still needMISC_ROOT_FIELDSin both copies —_capability_provenanceavoids 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 validationprompt.md— the fragmenttools/*.py—python_sandboxedtool bodiesAGENTS.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'srequiredschema + anavailable_whengate holding submission untilcaptured variables show the four minimum elements; the pack's A2UI catalog ships a structured intake form for visual surfaces. - Seriousness —
check_seriousnessis a deterministicpython_sandboxedcheck; 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.endworkflow 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 setsae_suspectedin 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.