Skip to main content

Bundles

Experimental

A bundle is a versioned file that lifts a working configuration out of one instance and puts it into another: an agent, an MCP connector, an app, or a knowledge set. Export produces a YAML or JSON document; install creates the resource from it and records exactly what it wrote — so a later version can be merged in three-way without overwriting local edits.

Experimental — what that means here

Export and install work and are supported, but they are not yet stable:

  • The file format may change between releases. A bundle is meant for moving and provisioning, not as a long-term backup. Re-export rather than keeping old files around.
  • There is no registry. Bundles are files you move between instances yourself.
  • Updating is API-only. When a new version and a local edit touched the same field the route reports the conflict and overwrites nothing — that case still needs an interface.
  • Uninstalling deletes. The resources the bundle created are removed; only what you have edited locally since installing is kept.
Who can reach which route

The routes are not all protected the same way:

  • GET /api/agents/{id}/bundle and GET /api/widgets/{public_id}/bundle are reachable by any signed-in caller holding the relevant permission, exactly like the routes beside them (GET /api/agents/{id}/export and the rest of the app catalog). The handler enforces agent:create/agent:update/agent:delete and app:manage respectively; the app export is additionally scoped to what the caller can see, so another user's private app cannot be exported.
  • GET /api/knowledge-sets/{id}/bundle requires knowledgeset:manage — the same permission as every other knowledge-set route. A set an agent or a thread created for itself cannot be exported, and neither can one that has been deleted.
  • GET /api/tool-references/{id}/bundle and the five /api/bundles/* routes (install, update, list, drift, uninstall) are restricted to instance administrators.

What a bundle never contains​

This is the load-bearing property of the format, not a side effect:

  • An agent's env values are blanked. Only the name and description travel.
  • MCP header values must be ${VAR} placeholders (optionally preceded by an auth scheme such as Bearer). Anything else is blanked and reported as a warning.
  • Reserved fields are dropped: system, systemAccessFeature, systemAccessAdminOnly, default, alias, tokenLimits.
  • A knowledge bundle carries no documents. It describes the sources a knowledge set draws on; the corpus itself still travels by knowledge-set export and import.
  • Whatever ties a knowledge source to the exporting organisation does not travel: login names (username for WebDAV, Nextcloud and SMB, user for SFTP), the Windows domain, the OneDrive shared links, and the S3 endpoint. The endpoint selects which object store is contacted — left in place, a re-pointed bucket name would still be read from the exporting organisation's server. All of them are set afresh on the target instance.
  • Credentials, threads, runs, memories, team and workspace data are never part of the projected structure in the first place.

Most of these rules are applied again on install, so a hand-written bundle cannot get around them: reserved fields are dropped again, and an MCP header carrying a literal value instead of a ${VAR} placeholder is blanked and reported again.

One exception: an agent's env values are not re-blanked on install. Export always blanks them, but a hand-written bundle that carries an env value is installed with that value. Update is different — there the value configured on this instance is kept, because env values count as instance-local.

Endpoints​

MethodPathPurpose
GET/api/agents/{id}/bundleExport an agent
GET/api/tool-references/{id}/bundleExport an MCP connector
GET/api/widgets/{public_id}/bundleExport an app (latest unarchived version)
GET/api/knowledge-sets/{id}/bundleExport a knowledge set (sources only, no corpus)
GET/api/assistants/{assistant_id}/projects/{project_id}/bundleExport a project as a composite bundle (see Project bundles)
POST/api/bundles/installInstall a bundle
POST/api/bundles/installations/{id}/updateMove an installation to a new version
GET/api/bundles/installationsList installations
GET/api/bundles/installations/{id}/driftReport local edits
DELETE/api/bundles/installations/{id}Uninstall

Export accepts ?format=yaml (default) or ?format=json, plus ?id= and ?version= to set the bundle identity. Without them you get local/<name> at version 0.0.0.

Example: agent​

The output of GET /api/agents/{id}/bundle, trimmed of empty fields: the agent payload also carries about fifteen null and "" entries, omitted here for readability. Every field shown matches the real output exactly and is pinned against it by a test. A real export's metadata also carries a provenance block (instance, user, timestamp); without ?id=/?version= the defaults are local/<name> and 0.0.0. Note value: "" — the env value that was set on the agent was blanked on export.

apiVersion: bundle.basepeak.ai/v1
kind: Bundle
metadata:
id: bp/bauamt-assistent
version: 1.0.0
spec:
type: agent
payload:
name: Bauamt Assistent
description: Beantwortet Fragen zu Bauanträgen.
prompt: Du bist ein hilfsbereiter Assistent der Bauverwaltung.
model: m1abc
tools:
- office-bundle
env:
- name: REGION_CODE
description: Regionalschlüssel
value: ""
existing: false
requires:
- kind: model
ref: m1abc
policy: substitute
- kind: tool
ref: office-bundle
policy: strip

Example: MCP connector​

Bearer ${MCP_HEADER_0} survives; the X-Leaked header carried a literal value and was blanked.

apiVersion: bundle.basepeak.ai/v1
kind: Bundle
metadata:
id: bp/nextcloud-connector
version: 1.0.0
spec:
type: connector
payload:
name: nextcloud-mcp
reference: mcp://nextcloud-mcp
bundle: true
toolMetadata:
name: Nextcloud
mcpServer:
url: https://cloud.example.test/mcp
headers:
- name: Authorization
value: Bearer ${MCP_HEADER_0}
- name: X-Leaked
allowedTools:
- list_files
- read_file

A connector carrying mcpServer.auth (the universal OAuth adapter) is refused on install — it would otherwise look configured and silently fail to authenticate.

Example: app​

document is an embedded JSON object, not a string.

apiVersion: bundle.basepeak.ai/v1
kind: Bundle
metadata:
id: bp/antragsstatus
version: 1.0.0
spec:
type: app
payload:
name: Antragsstatus
description: Zeigt den Status eines Antrags.
document:
root: r
elements:
r:
type: Card
state: {}

Example: knowledge​

A knowledge bundle describes sources, not content. Federal and state law travel verbatim — every instance ingests the same statutes from the same source, which is what makes a sector package installable at all. A tenant-specific address is declared as substitute under spec.requires instead: bucket (s3:), host (sftp:), server (smb:), base URL (webdav:, nextcloud:) — and the host of a council information system (ris:).

Publicly reachable is not the same as identical everywhere

A RIS belongs to exactly one municipality. Travelling verbatim, it would have the installing administration ingest the exporting one's council documents into their own knowledge set — publicly available, and entirely wrong. That is why a RIS host is substitutable like a bucket.

A website source's URL list (websiteCrawlingConfig.urls) does travel verbatim: a list cannot be expressed as a single substitutable reference. It is visible in the payload — check it after installing, or your instance keeps crawling the exporting organisation's site.

apiVersion: bundle.basepeak.ai/v1
kind: Bundle
metadata:
id: bp/bauamt-wissen
version: 1.0.0
spec:
payload:
dataDescription: Bebauungspläne, Satzungen und einschlägige Gesetze.
flowBlueprint: bpai
name: Bauamt-Wissen
sources:
gesetze-1:
gesetzeConfig:
selectedLaws:
- baugb
- bauo-nrw
syncSchedule: 0 3 * * *
s3-1:
s3Config:
bucket: acme-docs
prefix: satzungen/
region: eu-central-1
requires:
- kind: knowledgeSource
policy: substitute
ref: s3:acme-docs
type: knowledge

Install it with a mapping of the same shape:

curl -X POST https://<instance>/api/bundles/install \
-H 'Content-Type: application/json' \
-d '{"bundle": "<contents of the yaml file>",
"substitutions": {"s3:acme-docs": "s3:dormagen-docs"}}'

Only the address itself is replaced. prefix, region, port, dir and share travel verbatim and can be changed after install like any other setting — the same folder layout under a different bucket is the ordinary case, and demanding a mapping for each of those fields would hold the install up on input nobody needed to change.

The keys (gesetze-1, s3-1) are a source's identity across instances: a later version is merged against them source for source. A real export assigns them as <type>-<short hash>. A source you add yourself after the install carries no such key: it is yours, it does not count as local divergence, and an update leaves it untouched. Uninstalling is then refused — deleting the set would take your own source with it, so remove it first or keep the set.

Dependencies: strip, block, substitute​

Every external reference is listed under spec.requires with a policy:

  • strip — if it is missing, drop it and report a warning.
  • block — if it is missing, the install fails.
  • substitute — the administrator must supply a local equivalent at install time. Models, model providers, an app's tools and the addresses of tenant-specific knowledge sources are classified this way because they are instance-specific.

For a knowledge source (kind: knowledgeSource) the instance checks only the shape of the mapping — its type, and a non-empty address. Whether a bucket exists and is readable from here is answered by the first sync; a check at this point could only ever say yes.

curl -X POST https://<instance>/api/bundles/install \
-H 'Content-Type: application/json' \
-d '{"bundle": "<contents of the yaml file>",
"substitutions": {"m1abc": "m1xyz"}}'

Updating, drift, uninstalling​

An installation remembers the exact bytes it wrote. An update is a three-way merge against them:

  • If nothing was changed locally, the new version applies cleanly.
  • Local edits to fields the update did not touch survive.
  • On a genuine conflict nothing is written and the response is 409 with the conflict list. Resolve with onConflict: "keep-mine" or "take-theirs".

GET .../drift reports local divergence as JSON Pointer paths. DELETE removes what the installation still owns unmodified, leaves locally edited resources in place and reports them. Apps are archived rather than deleted, so running instances keep working.

In the UI​

Every endpoint is also reachable from the admin area.

Exporting — wherever the resource is managed:

TypeLocation
AgentAgent detail page → Export → Export as bundle (with dependencies)
MCP connectorTools → select the connector → Export as bundle
AppApps → row action Export as bundle
KnowledgeKnowledge Sets → row action Export as bundle

The agent export still offers the existing manifest formats (YAML/JSON). A manifest and a bundle are different artifacts: only a bundle installs through POST /api/bundles/install.

If the export blanked or dropped anything, a dialog says so. That is the only place you find out — the bundle file itself looks unremarkable.

Substitutions are API-only

The install dialog does not yet collect substitutions. A bundle carrying a tenant-specific address — any S3, SFTP, WebDAV, Nextcloud, SMB or RIS source, and equally an agent with a model reference — is therefore installed through POST /api/bundles/install as shown above. A bundle whose sources are all public installs from the UI.

Installing and managing — under Bundles (instance administrators only): install, list installed bundles, detect local edits (drift), and uninstall. Uninstalling removes the resources it created, provided they are unchanged since the install; locally edited ones are left in place and reported.

On instances with the deletion bin enabled a knowledge set goes there rather than being destroyed; without it the deletion is final, including the documents already ingested. While an agent or a thread still uses the set the uninstall is refused — the same protection DELETE /api/knowledge-sets/{id} enforces.

Limits of this phase​

Beyond the caveats above:

  • agent, app, connector, knowledge and project. toolConfig and dataProtection parse but are refused on install.
  • Nested bundles (spec.includes) are only allowed inside a project bundle, where they carry its agent, knowledge sets, connectors and app. See Project bundles.
  • A file without metadata.id installs but cannot later be updated.