Runtime environment
Node.js
The compiler and the Workbench need Node.js 22.19 or later; generated executables target
Node.js 22.12 or newer unless runtime.node raises that floor. Both are stated in
Installation, and the selected executable floor is recorded as
runtime.node in the artifact manifest.
Host path tokens
Generated documents reference the plugin root, the composite root every selected host loads, through the token each host substitutes, and the compiler writes the right spelling into each host's own documents rather than assuming one:
Substitution is positional, not global. Codex's ${PLUGIN_ROOT} is the command root of its
generated hook commands and nothing else: the Codex MCP runtime interpolates no path tokens, so a
plugin-root token in an MCP command, args, or env value is accepted only when it leads the
value and the server's cwd is the plugin root, in which case the compiler rewrites it to a
./-relative path under cwd: "./"; an embedded token, a token without that cwd, or any
plugin-data or workspace-root token is a build error. Claude Code substitutes ${CLAUDE_PLUGIN_ROOT} and
its siblings in Skill and agent content, hook and monitor commands, MCP servers, and LSP servers
only, never in settings.json, which is why claude.settings rejects path tokens outright.
Cursor's pinned loader has its own substituted-field table, and a token outside it reports
AB6028 at build time and AB7320 from Doctor.
Environment variables
The three native-smoke opt-ins exist because those runs need a real signed-in CLI; they are never part of an ordinary test run.
.env precedence under mcp run
agent-bundle mcp run composes the launch environment in three layers, lowest priority first:
- The generated server's own env, with path tokens already resolved.
- The
.envfile layer. - The operator's real
process.env, so an exported variable always wins.
The .env layer is the conventional Rsbuild set (.env, .env.local, .env.<mode>,
.env.<mode>.local) at the project root, which the runtime calls the workspace root, unless
--env-file <path> replaces it with exactly the files you name, or --no-env removes it. The two flags are mutually exclusive. Files are read into a
scratch object, so the real process.env is never mutated.
Operator .env files in installed packs
A host launches an installed pack directly, so the pack's own shells apply the same layer
themselves: the stdio MCP entries, the hook wrappers that run plugin code, and the artifact CLI
bin/<name>.mjs read <plugin root>/.env then .env.local at launch (or the files
AGENT_BUNDLE_ENV_FILE names), filling only variables the host did not set, an exported variable
wins, a manifest env default the host passed through yields to the file (the stdio entry carries
its server's declared block to tell the two apart), .env.local beats .env, values are never
logged, a missing file costs nothing, an unreadable one is skipped. The layer is applied before the
server, handler, route, provider, and state modules evaluate, so a module-level process.env read
sees it. The plugin root is AGENT_BUNDLE_PLUGIN_ROOT when the host set it, otherwise the
directory above the shell's own. No ${VAR} interpolation is performed.
Under mcp run the plugin root is the project root, so the shell re-reads the files mcp run
already composed and changes nothing; --env-file and --no-env travel down as
AGENT_BUNDLE_ENV_FILE. The npm package bin reads no pack file. Doctor reports the presence and
variable count of each file (AB7331). Details and the precedence table:
Installation.
Durable state
Installed artifacts, the MCP entry, Flight worker, artifact CLI bin, render worker, and standalone
hook wrappers, derive ~/.agent-bundle/state/<plugin>-<digest> as their framework state root.
<plugin> is the code root's basename, or plugin when that basename is not a safe path segment;
<digest> is the first 16 hexadecimal characters of the SHA-256 digest of the code root's real
path. Two installs therefore never share state, and a read-only install launches. XDG_STATE_HOME
moves the derived root as described above, and AGENT_BUNDLE_STATE_ROOT overrides it. The npm
package bin keeps ./.agent-bundle/state.
Only a workspace-durable state definition uses the SQLite driver; other lifetimes use the
in-memory driver and keep nothing on disk. Every generated process resolves the code root and state
root exactly once (resolvePluginRoot from @agent-bundle/runtime) and publishes them as
(await agent()).plugin, { root, stateRoot }, so a route or provider that keeps its own files
beside the framework's reads plugin.value.stateRoot instead of re-deriving the path.
Under mcp run, plugin-root anchors in env values expand to the project root by default,
not the artifact: the artifact is an ephemeral build product there, and anchoring durable state on
it would fragment that state on every rebuild. Pass --plugin-root <path> pointing at the
artifact root itself, the composite plugin root, not a host subdirectory, for a byte-faithful
rehearsal of a copied-artifact launch. The state root then derives from that expanded code root
(~/.agent-bundle/state/<project>-<digest>) unless AGENT_BUNDLE_STATE_ROOT names one.
Workbench MCP sessions and Workbench route invocations launched by agent-bundle dev pin
AGENT_BUNDLE_STATE_ROOT to <project>/.agent-bundle/state instead, one state root per project,
beside the build epochs rather than inside them, so it survives republishes and epoch retirement
without accumulating one user-data directory per rebuild. The warm dev runtime itself does not set
that variable.
A per-server state directory uses the server name when it is a single safe segment; any other
name becomes a content-addressed server-<digest> segment, so a name like ../shared can never
traverse out of the state root.
Default state projection budgets are 5,000 ms per commit, 262,144 bytes per event, 1,048,576 bytes
of state, and 100,000 revisions. agent-bundle inspect --state reports the resolved driver,
lifetime, durable location, and budget source for each definition.
Stores written before the current SQLite layout
The driver reads only the layout it writes today. It neither adopts nor migrates a store written before that layout, in either of two ways.
A database under the pre-#201 file name, whose suffix was the state id's own leading bytes in hex
rather than a SHA-256 digest, is never opened. When a root-mode store finds that file and no
database under the current name, the open fails with a corrupt AgentStateError that names the
old file, instead of creating an empty store beside it. The old file is left untouched. Once a
database under the current name exists, the old file is not consulted.
A database under the current file name whose agent_state_journal table differs from the one the
driver creates fails at open with a corrupt AgentStateError naming the table. Column names,
their order, and their NOT NULL flags all count, so a journal whose result_state column is
nullable, as an older runtime's ALTER TABLE left it, is rejected before any row is read.
Recover from either by moving the stale database files out of the state root to keep a copy, or
deleting them: each *.sqlite plus its -wal and -shm sidecars, or the whole state root. The
next launch creates a fresh store at the definition's initial state. Nothing reconstructs the old
history.