ADR-034: Integrations ship as bundles under the assets root, and memcastle integration copies and registers them¶
Status¶
Accepted.
Amends ADR-013 (the assets root gains a second consumer),
ADR-020 (skills are now packaged, because an installed integration
reads them) and ADR-022 (an integration is also
distributed as one bundled file, still tested from its sources).
ADR-035 adds web/ as a further tree under the same root.
Context¶
Issue #200 asks for the Pi and OpenCode integrations to be installable: documented, packaged with releases, and usable
from a package and from a development checkout.
Until now the only way to use one was to point the agent at a path in a checkout of the repository,
which ties an integration to a layout (skills/ three levels above src/), to node_modules that nothing installs,
and to a git clone of the version you happen to run.
Four forces shape the answer. An integration is tightly coupled to the MCP contract, the shared skills and the MemCastle version, so it should be released with them and not on its own lifecycle. Installing one edits another program's configuration, so it must be idempotent, report what it changed, preserve everything else and be removable exactly. A developer must be able to install what is in their working tree without touching an installed MemCastle, and the code that does it must be the code users run. And installing is administrative: it changes what code an agent runs (the reasoning of invariant 10 for sources).
ADR-013 already chose how files that belong to a package are found: an explicit directory, the installed
share/memcastle, then what is built in.
ADR-033 put the bundled sources there.
That directory is the natural place for integrations too, and the same rule makes a checkout a valid root.
Decision¶
- One assets root holds everything MemCastle ships.
integrations/<id>/andskills/sit besidesources/, under the directory ADR-013 resolves (--assets-dir,MEMCASTLE_ASSETS_DIR,assets.dir, else<prefix>/share/memcastle). A checkout already has exactly that layout, so development mode is--assets-dir <checkout>and shares the discovery, the manifest and the installer with a package.assets.diralso becomes the root of the bundled sources when it holds asources/memcastle-index.json(it was logged and otherwise ignored), andmining.bundled_dirstill outranks it. - An integration is described by
memcastle-integration.toml. Its format, version, supported MemCastle and agent ranges (semver), the agent it is for, the files to install and the file the agent loads. Unknown keys are refused, a newer format is refused whole, and asset paths may not climb out of their directory. What a file list cannot express is an adapter chosen byagent.kind, so a new agent is code and a newkind, and not a new manifest format. - Releases carry bundles, not sources.
packaging/integrations/build.shbundles each integration withbun buildinto one file (the agents' own SDKs stay external), so installing needs nonode_modules, no bun and no npm package. The release tarballs,.deb,.rpm, AUR package and Homebrew formula carryshare/memcastle/{integrations,skills}, andmemcastle_<version>_integrations.tar.gzcarries them alone for the recipes that fetch a single binary. memcastle integration list|install|update|removeis local tooling. It reads files and runs the agent's own program; it opens no palace, calls no daemon and uses no network, likememcastle source build.tests/integration_isolation.rsholdssrc/integrationto touching neither the store, the jobs, the client nor the network, andcrate::app,mcpandapito not calling it: there is no MCP tool and no route.- Install copies, then registers, and records a receipt.
The files go to
<data>/memcastle/agents/<id>/, which is the user's and survives a package upgrade or removal. They are staged, swapped in whole and put back if the agent refuses. The receipt (.memcastle-install.json) holds the version, the origin, a SHA-256 per file and what the agent was told, solistcan tell installed from outdated from modified, andremoveneeds neither the manifest nor the package. Content is compared as well as the version, because a development build never changes the version. - Registration uses the agent's own mechanism and nothing else.
Pi:
pi install,pi listandpi removeon the installed directory, so Pi alone edits itssettings.json. OpenCode: oneplugins/memcastle.tscarrying a marker, removed only when the marker is there, and never overwritten when it is not. Claude Code:claude plugin marketplace add,claude plugin install, and their matching removal commands for the uniquely named local marketplace; its settings files are never opened. Neither adapter opensopencode.json, adds anmcp.memcastleentry or writes a token. - Compatibility is checked before anything is written, against
CARGO_PKG_VERSIONand the agent's--version. An agent whose version cannot be read is refused when the manifest requires a range, because guessing installs something that may not load. - The skills are installed beside the integration, and the manifest names them.
skill-text.tslooks forskills/next to the bundle first and for the checkout's second, so one source file serves both layouts. A[[skills]]entry per skill replaces the first form's all-or-nothing[skills] install = true: anamealone references the sharedskills/<name>/of the assets root, so nothing is duplicated, andlocal = truetakes it from the integration's ownskills/<name>/for a skill only that integration needs. Only the named skills are installed, a named skill without aSKILL.mdrefuses the installation before anything is written, and the receipt records the names. Both layouts resolve a skill through the same assets root as everything else, andbuild.shships an integration's ownskills/inside its package directory. The agent is told about the skills where they sit in the installed copy (Pi throughresources_discover, which anoffsession never answers, and OpenCode throughskills.paths), and the installer never writes into the directories a user keeps skills in, so skills stay independently installable and the integration never overwrites one of the user's.
Alternatives rejected¶
- Publishing
memcastle-piandmemcastle-opencodeto npm. It splits the release of code that is coupled to the daemon's contract, needs a registry account and a second versioning story, and makes the official path depend on a network and a package manager. An integration that genuinely needs its own lifecycle can be published later; the bundle does not prevent it. - Running
bun installat install time. It needs bun and the network on the user's machine, and Pi does not install a local package's dependencies anyway. - Registering the files in place under the assets root. It breaks the agent whenever the package is upgraded or moved, and ties a development install to a build directory.
- Editing
settings.jsonoropencode.jsondirectly. It means owning the formats of two programs we do not control, and a mistake corrupts the user's configuration. The agents' own commands, or one marked file, cannot. - Asking the daemon to install integrations. The daemon is a memory server and has no business changing another program's configuration; installation must work before a daemon has ever run, and a route would make "an agent can change the code it runs" a one-request mistake.
- A new
integrations_dirsetting.assets.diralready means "where the files that ship with MemCastle are", and a second root for the same kind of thing would mean two answers to where a package was installed. - A
pluginalias. The issue left it open. The concept is an integration with an agent, and an alias for one word would be a second name to document and keep in step. It can be added without changing anything here.
Consequences¶
- A release is larger by the Pi and OpenCode bundles, the Claude Code declarative plugin assets, and the skills, and
building the JavaScript integrations needs bun.
mise run integrations:checkbuilds the bundles and installs them from both layouts, so a release that cannot be installed fails before a tag. - The integration's version in
memcastle-integration.tomlis maintained by hand with its compatibility ranges.tests/integration_bundle.rschecks that each accepts the MemCastle that ships it. - An installed integration is a copy: a bug fixed in a later release reaches the user through
integration update, not through the package manager alone, andlistsays when one is outdated. - OpenCode's plugin options live in
opencode.json, which MemCastle does not edit, so an installed plugin is configured through the environment. - The agents' command-line surface (
pi install,pi list,pi remove,--version) is now a dependency of the installer. The tests stand in for the programs, so a change to one of those commands is found by running the installer against the real agent, not by the suite. - Claude Code adds an adapter and a
kind, a manifest and declarative plugin assets, and nothing to the installer's shape.