Skip to content

feat(runtime): separate the framework state root from the plugin code root - #640

Merged
ScriptedAlchemy merged 4 commits into
mainfrom
feat/state-root-separation
Sep 5, 2026
Merged

feat(runtime): separate the framework state root from the plugin code root#640
ScriptedAlchemy merged 4 commits into
mainfrom
feat/state-root-separation

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Closes #637 (follow-up to #628; part of #592).

Problem

resolveWebLaunch() sets AGENT_BUNDLE_PLUGIN_ROOT to the installed artifact, resolvePluginRoot() computed stateRoot = join(root, 'state'), and generatedStateOwner() opened the SQLite kernel there — so a plugin with a workspace-durable src/state.ts tried to create SQLite beneath a possibly read-only installed artifact. #628 moved only the author-facing web ${PLUGIN_DATA} directory out of the artifact.

Contract

Two roots, resolved once per generated process by resolvePluginRoot (@agent-bundle/runtime):

Root Source Value
code root (plugin.root) AGENT_BUNDLE_PLUGIN_ROOT, else the shell's fallback installed code and assets — unchanged
state root (plugin.stateRoot) AGENT_BUNDLE_STATE_ROOT (explicit override, resolve()d, ${…} token = unset with one stderr warning) exact directory
else stateAnchor: 'user-data' (every artifact shell: MCP entry, Flight worker, artifact CLI bin + render worker, standalone hook wrappers) ~/.agent-bundle/state/<plugin>-<digest>, or $XDG_STATE_HOME/agent-bundle/<plugin>-<digest> when set
else stateAnchor: 'root' (default; npm package bin, route-unit / in-memory harnesses) <root>/state — unchanged

<plugin> is the code root's basename when it is a safe path segment (else plugin); <digest> is the first 16 hex chars of SHA-256 over the code root's realpath — the same spelling as the web-data segment from #628 (pinned by web-launch.test.ts). Two installs never share state; symlinked spellings of one install do. ResolvedPluginRoot gains stateSource: 'native' | 'derived'.

New public surface: PLUGIN_STATE_ROOT_ENV_ANCHOR, PluginStateAnchor, pluginStateSegment, userStateHome, userDataStateRoot (@agent-bundle/runtime); pluginStateRootEnvAnchor (agent-bundle). resolveWebLaunch and launch.ts behavior are unchanged: the web launch still names only the code root and never points AGENT_BUNDLE_PLUGIN_ROOT at a data directory.

Existing installs relocate their state: after upgrading and rebuilding, an installed plugin starts from an empty state root; AGENT_BUNDLE_STATE_ROOT=<old root>/state keeps using the old one. The changeset says so (minor, agent-bundle + @agent-bundle/runtime).

Manifest v2 (#604) does not record the state location; commented there that it must stay that way (never artifact-relative).

Tests

  • Packed regression packages/agent-bundle/tests/packed-readonly-state-root.test.tsserves a state-writing tool from a read-only installed artifact without writing beneath it: installs the packed tarballs into a clean consumer, builds the new durable-web-surface fixture (generated MCP server + web-exposed App + workspace-durable state + CLI command), removes the source, chmods the artifact tree read-only, launches through resolveWebLaunch with no state env, records twice, asserts the artifact listing is byte-identical, <artifact>/state absent, SQLite under userDataStateRoot(artifact, env), the artifact CLI bin reads the same entries, a fresh MCP process sees CLI-visible state, and the manifest never mentions AGENT_BUNDLE_STATE_ROOT.
  • packed-stdio-projection.test.ts now asserts the derived state root and that nothing lands under the artifact.
  • plugin-root.test.ts: override, blank/token override, user-data derivation (home, XDG_STATE_HOME), unsafe basenames, realpath.
  • entry-shell.test.ts: stateAnchor: 'user-data' emitted for 'artifact' shells, absent for 'cwd'; byte pins updated.
  • rstest.worker-isolation.ts sets a per-worker XDG_STATE_HOME so spawned shells never write under the developer's home.

Docs

runtime-environment (new AGENT_BUNDLE_STATE_ROOT / XDG_STATE_HOME rows, rewritten "Durable state"), mcp, package-entries, cli, installation — en + zh; docs/entry-conventions.md, docs/framework-mode.md.

Gate

pnpm build && pnpm typecheck && pnpm lint && pnpm test:unit green; test:route-unit, test:projection, targeted integration (generated-route-server, hooks, artifact-cli-bin, workbench-surface, test-harness-manifest, mcp-session-service, cli-routes-build) green; packed pool: packed-readonly-state-root + packed-stdio-projection green; pnpm docs:site:build green.

Deslop: Claude Fable 5.1, 6 edits (deduplicated the three identical launch.ts doc comments, tightened launch-env-shell.ts / hook-contract.ts comments, removed a repeated clause in en/zh cli.mdx).

Self-review

Reviewer: GPT-5.6 Sol (generalPurpose; change-risk-reviewer was blocked on the TraceDecay daemon). Author lanes: Grok / Sol / Fable-high; orchestrator Claude Fable 5.1.

Pass 1 — 5 findings:

  1. Rendered script render workers (scripts/<name>-flight.mjs) kept the 'cwd' state fallback — fixed: build/entries.ts passes stateFallback: 'artifact'; cli-routes-build.test.ts asserts the emitted worker carries stateAnchor: 'user-data'.
  2. Dev / Workbench MCP sessions used the build epoch as AGENT_BUNDLE_PLUGIN_ROOT and would have derived one ~/.agent-bundle/state/<epoch>-<digest> per rebuild — fixed: dev/mcp-session/mcp-session-launch.ts pins AGENT_BUNDLE_STATE_ROOT=<epoch>/state (main's location; declared env still wins); mcp-session-service.test.ts asserts it.
  3. uninstall --purge-data leaves the derived state root — deferred to uninstall --purge-data and doctor do not address the derived framework state root (~/.agent-bundle/state) or web-data (follow-up to #637, #628) #641 (same gap fix(564): port the #620 review follow-ups from the merged branch — declared-projection web launch, identity-keyed sessions, opening-tool policy, out-of-artifact web state #628 left for web-data); disclosed in the changeset and in en/zh cli.mdx / installation.mdx.
  4. doctor inventories only <root>/statedeferred to uninstall --purge-data and doctor do not address the derived framework state root (~/.agent-bundle/state) or web-data (follow-up to #637, #628) #641, same disclosure.
  5. Relative XDG_STATE_HOME would anchor state on the shell's cwd (the read-only artifact) — fixed: userStateHome honors absolute values only (XDG spec); test + docs updated. Empty os.homedir() left as is: Node returns the platform home or throws; no artifact shell runs without one.

Pass 2 — no code blocker; 3 doc warnings, all fixed: --purge-data row scoped to legacy state (en/zh), zh plugin.value.root, changeset says "absolute path" for XDG_STATE_HOME.

… root

Artifact-hosted shells (stdio MCP entry, Flight worker, artifact CLI bin and
render worker, standalone hook wrappers) resolve their state root under
~/.agent-bundle/state/<plugin>-<digest> (or $XDG_STATE_HOME/agent-bundle/...)
instead of <plugin root>/state, so a read-only install launches and two
installs never share state. AGENT_BUNDLE_PLUGIN_ROOT keeps naming the code
root; AGENT_BUNDLE_STATE_ROOT overrides the state root.

Closes #637
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@changeset-bot

changeset-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f04259b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agent-bundle Minor
@agent-bundle/runtime Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 5, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@640
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@640
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@640
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@640

commit: f04259b

…ons to <epoch>/state, ignore relative XDG_STATE_HOME

Self-review findings: rendered script workers kept the cwd fallback; dev and
Workbench MCP sessions would have derived one user-data root per build epoch;
a relative XDG_STATE_HOME would have anchored state on the shell's cwd.
uninstall --purge-data / doctor coverage of the derived root is #641.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated durable state anchors inside the installed artifact — separate state root from code root (follow-up to #628; #592)

1 participant