The problem
A useful agent is never one object. It is an agent plus two MCP instances plus three skills plus a cron trigger plus a spend cap — and the agent references the MCP instances by id, and the trigger references the agent by id, and every id is workspace-local. Sharing that setup by writing instructions produces a twenty-step guide where step fourteen silently depends on step three. Sharing it as a database export produces something that cannot be installed anywhere else, because every reference is a UUID that means nothing in another workspace. And in both cases the person installing it finds out what credentials it needs by hitting an error part-way through, after resources have already been created.How AgentArea approaches it
One fully-inlined document, referenced by key
Schema version0.1.0. Every entity has a key — a stable in-document
identifier matching [a-zA-Z][a-zA-Z0-9_]* — and entities reference each other
by key, never by database id. An agent lists mcps: [github] and skills: [triage]; an automation names agent: triage_bot. The installer resolves keys
to real ids at install time, which is what makes the document portable.
Secrets never appear in the document
ASetupField declares a key, a label, a type, and whether it is required.
Everything else in the bundle references it as ${setup.<key>}:
env_schema entry as a secret based on
SetupField.type, rather than relying on the MCP service’s name-based
heuristic. The bundle already declared which inputs are credentials, so guessing
from the variable name is unnecessary and less accurate.
A URL MCP that verifies the signed call context
(tool call context) takes its signing key
the same way. context_secret is a ${setup.<key>} reference to a setup field
of type secret, never the key itself:
context_secret, which stores it in the secret
manager; the instance then shows context_signing: true. A literal value fails
parsing, a reference on a non-URL MCP or to a non-secret field blocks the
bundle, and a value shorter than 32 or longer than 512 characters refuses the
install with a message that names the field, not the value. Exporting a
workspace turns a signed instance’s key into a fresh secret setup field.
A workspace exports as a bundle
GET /v1/workspaces/{workspace}/export (admin only; Settings → Workspace →
Export workspace in the webapp) writes the workspace’s agents, MCP
connections, skills, cron automations and supported webhook channels as one
bundle. Each agent’s MCP reference, an instance id, resolves to the exported
MCP key. An agent that references an MCP or skill the export cannot
represent fails the export with a 422 naming it, so tools are never silently
dropped.
A GitHub-imported skill exports as source_type: github with its
source_url, so the import fetches the whole package again. Exporting only its
SKILL.md would drop the files the skill references.
An exported bundle is meant to be shared, so no credential travels in it.
Secret-backed values are never read; each becomes a secret setup field.
Plain MCP configuration is screened as well. These values are replaced by a
${setup.<key>} reference to a secret setup field:
- an environment variable or header whose name looks like a credential
(
token,secret,password,api_keyand similar), or whose value is a credentialed URL or anAuthorization-style value (Bearer …,Basic …); - an
argsentry that is a credentialed URL, the value of a credential flag (--api-key=x,--token x,TOKEN=x), or a credential header argument (Authorization: Bearer x); - an
endpoint_urlthat is a credentialed URL.
environment and headers is exported as written. Setup keys are derived from
the MCP key and the position, so the same workspace always exports the same
document.
On install, references in args and endpoint_url are resolved into the
values the runtime reads, because an MCP server takes its arguments verbatim.
An export that refused every MCP with a credential in its arguments would
refuse most real workspaces: a Postgres MCP has nowhere else to take its
connection string. Templating keeps the export usable, and the person
installing the bundle supplies their own values.
Agent settings that are policies
Per-agent behaviour that governance owns travels as a policy bound to the agent’s key. The final-answer check, for example (see answer check):Analyze, then install
POST /v1/workspaces/{workspace}/bundles/analyze parses the source and returns an ImportPreview —
every entity tagged CREATE, REUSE, or SKIPPED with a reason, plus a list
of issues. It writes nothing. This is where a missing setup field, an agent
pointing at a nonexistent MCP key, or a command runtime the platform cannot run
is surfaced.
POST /v1/workspaces/{workspace}/bundles/install takes the canonical bundle and the setup values.
Missing required setup blocks the whole install before anything is written.
Inline skill content is parsed with the same SKILL.md parser the skill service
uses, so a frontmatter that is not valid YAML (an unquoted value containing
: is the usual cause) is a blocking issue in the preview, naming the skill
and the line.
Install answers 422 when the bundle or a setup value is invalid (anything
analyze blocks, missing required setup, a refused context_secret, a skill the
skill service refuses, an MCP the MCP service refuses) and 400 when an
install step fails for another reason, such as a GitHub skill that cannot be
fetched. Both are problem+json: the message in detail, code
bundle_invalid or bundle_install_failed, and the blocking issues.
Analyze accepts exactly one of source (pasted text) or source_url (fetched
server-side behind an SSRF guard, capped at 5 MB).
Install order is dependency order
MCPs, then skills, then agents, then channels, then automations, then policies. Each step returns the key-to-id map the next one needs. An agent whose MCP was skipped is still created; an automation whose agent was not created is skipped with that reason recorded, rather than failing the install.Idempotency follows what the last install recorded
AnInstalledBundle row is upserted by bundle name, recording the canonical
document and the full install result: each entity’s key, action and id. The next
install of the same bundle reuses an MCP instance, skill or agent only by the id
that result recorded for its key, and only while it still exists (an archived
agent is installed again). It is marked REUSED and not duplicated. Triggers are
named {bundle_name}:{key}; one is reused only when it carries that name and runs
the agent this install bound.
An entity that merely carries a name from the bundle is never reused. Names are
not unique and any member picks them, so an agent called Triage Bot that the
bundle did not install is someone else’s: reusing it would wire the bundle’s
triggers, channels and policies to it. The install creates its own alongside.
An agent’s MCP tools name the bundle’s MCP instances by id. Re-running an install
is therefore close to a no-op for what the bundle installed before.
A failed install leaves nothing behind
Each domain service commits on its own, so install is made all-or-nothing by compensation. Every entity the install creates is recorded with how to delete it; when a later step fails, those entities are deleted again, newest first, before the error is returned: policies, triggers and their stored channel credentials, agents, skills, MCP instances (with their stored secrets) and the MCP server specs created for them. Entities that already existed and wereREUSED are never touched. A retry after a failure therefore creates everything
fresh instead of reusing a half-configured MCP instance from the failed attempt.
Automations and channels install disabled
enabled defaults to false on both, and the installer explicitly disables a
cron trigger after creating it — trigger creation schedules unconditionally, so
without that step an imported automation would start running before the user had
confirmed its credentials. Connect, verify, then activate.
Bundles in the catalog
A registry of typebundles holds catalog items whose spec is the bundle
document, keyed by the bundle’s name. Nothing is materialized on sync; the
bundle is provisioned into a workspace on demand through the same install path.
Why not a repository or an archive
A directory layout or a tarball is the familiar packaging shape, and it gives up the property that matters most here: a bundle must be analyzable before anything is written. A single inlined document can be pasted into a form, fetched in one request, diffed in a pull request, reviewed by a human, and previewed against a live workspace. An archive means fetch, unpack, and then trust — and the fetch itself becomes an attack surface with a much larger blast radius than a size-capped text fetch behind an SSRF guard.Why not reference resources by id
Ids are workspace-local. A bundle that names them can be exported from the workspace that built it and installed nowhere else, including back into the same workspace after those resources were recreated. Keys cost an indirection at install time and buy portability, which is the entire point of the format.Why not install first and report failures after
The preview exists because a half-finished install is worse than a refused one. An MCP instance created with a credential, orphaned because the agent step then failed, is a live secret nobody is tracking. Blocking on missing required setup before the first write, and reporting unsupported entities asSKIPPED in a
preview, moves those failures to a point where nothing has been created yet.
Limits
- Only canonical YAML or JSON is parsed.
parse_bundleloads the document and validates it against theBundleschema. There is no adapter that converts a Claude plugin manifest, a repository layout, or any other external format — despite what the format module’s own docstring implies. - Rollback is compensation, not a database transaction. If deleting a created entity also fails (the database is down, say), that failure is logged and the rest are still removed; the original error is what the caller sees.
- Command MCPs are restricted to eight runtimes.
npx,uvx,uv,python,python3,node,bunx,deno. A command that is a local path or references${CLAUDE_PLUGIN_ROOT}is skipped with a reason recorded, not failed. Docker and URL MCPs are always accepted. - Channels are Telegram only and automations are cron only in
0.1.0. The schema is a closed literal in both cases, so anything else fails validation. - Skills support inline content and GitHub only. Zip and S3 import are not in
0.1.0. - Export screening is heuristic. A credential in plain MCP configuration
that matches none of the rules above (a short token in an unnamed positional
argument, for example) is exported as written. Store credentials as secret
values (
isSecretin the spec’senv_schema) and they never leave the workspace. - There is no uninstall.
InstalledBundlerecords what was installed, and removing those resources is manual. The API surface is analyze and install. - A reused entity is not reconciled. An agent the bundle installed before is reused as it is now, edits included; its configuration is not brought back to the document.
extra="forbid"everywhere. An unrecognized field anywhere in the document is a validation error, not a warning. A bundle authored against a newer schema version is rejected outright rather than partially understood.
Related
Registry and catalog
How a bundle reaches a workspace as a catalog item
MCP
What the
mcps section provisions