Skip to content

Expose the resolved plugin root / durable-state anchor on the request context (and to providers) instead of making plugins re-derive AGENT_BUNDLE_PLUGIN_ROOT #468

Description

@ScriptedAlchemy

Symptom in movie-library

movie-library (feat/react-app-shell @ d6a247b) keeps an 80-line resolver, scripts/state-root.ts, whose only job is to answer "where is this plugin installed / where does its durable state live" — the same question every generated agent-bundle worker already answers privately:

  • scripts/state-root.ts:55-61 re-reads AGENT_BUNDLE_PLUGIN_ROOT, guards against a literal unexpanded ${...} token, then falls back to source-layout / artifact-layout heuristics / process.cwd().
  • src/providers/tracker-session.ts:78 — the request-context provider exposes stateRoot: resolveStateRoot(env) so layouts, the session/start event route (src/events/session/start.tsx:47) and the CLI can print where state lands.
  • agent-bundle.config.ts:23-26 documents the arrangement: "every adapter injects the well-known AGENT_BUNDLE_PLUGIN_ROOT anchor into generated stdio entries, and scripts/state-root.ts resolves it".
  • PORT-NOTES.md § Durable state: "The kernel anchors on AGENT_BUNDLE_PLUGIN_ROOT … and falls back to the artifact root, which is different from the plugin's own .runtime root (scripts/state-root.ts …)". Two anchors for one install, because the plugin cannot read the framework's.
  • PR build(deps): bump @mantine/core from 9.5.1 to 9.5.2 #8 framework-mode audit, finding 7: "scripts/run-stdio.sh + scripts/state-root.ts hand-roll launcher/state-root resolution that overlaps the framework's stdio entry, output.distPath, and defineState roots" — category (b), left in place because nothing public replaces it.

Any plugin with pre-existing on-disk state that is not a defineState store (caches, token files, a Python sidecar's data dir) has to do the same.

Root cause in agent-bundle (main @ 284141958)

The request context has no plugin-root / durable-anchor axis; the anchor is computed inside each generated shell and never surfaced:

  • packages/rsc-runtime/src/agent-request.ts:199-220AgentRequestContext exposes invocation, host, session, actor, workspace, lineage, progress, signal, providers, state. workspace (:83-85) is { root: string } and is populated with the caller's cwd (packages/agent-bundle/src/build/entry-shell.ts:331, :528workspace: available({ root: cwd }, 'derived')), i.e. the user's project, not the plugin install.
  • packages/agent-bundle/src/build/entry-shell.ts:188, :712, :987 — three separate const durableAnchor = process.env.AGENT_BUNDLE_PLUGIN_ROOT ?? <fallback> computations (state kernel, notice ledger, lineage registry), each private to the emitted source.
  • packages/agent-bundle/src/routes/public.ts:96-102AgentProviderContext = { invocation, signal }; a provider cannot read the anchor either (Pass request identity, lineage, and read-only state/notices handles to context providers #459 widens providers to identity/lineage/state, but does not add a plugin-root/anchor field).
  • packages/agent-bundle/src/core/types.ts:792 / adapters/types.ts:117-122pluginRootEnvAnchor is a public env contract; there is no public runtime accessor, and agent-bundle/meta (packages/agent-bundle/src/meta.ts) carries only name/version.
  • None of the framework's own anchor computations apply movie-library's ${ guard (state-root.ts:57-60): a host that passes manifest env through literally would give the kernel join('${CLAUDE_PLUGIN_ROOT}', 'state') under cwd.

Workaround in movie-library

scripts/state-root.ts (resolvePluginRoot / resolveStateRoot, five-tier fallback), consumed by the provider, the layouts' footer, the event route, and every legacy domain module.

Proposed fix

Expose the resolved anchor once and let routes, layouts, providers, and event routes read it:

interface AgentRequestContext {
  // new
  readonly plugin: Observed<{
    readonly root: string;        // resolved AGENT_BUNDLE_PLUGIN_ROOT, or the fallback the shell used
    readonly stateRoot: string;   // join(root, 'state') — where defineState / notices / lineage live
  }>;
}
  • source: 'native' when AGENT_BUNDLE_PLUGIN_ROOT was set and expanded, 'derived' for the artifact/cwd fallback; unavailable with a typed reason for an unexpanded ${...} token (and have the kernel treat that token as unset instead of creating a literal ${...} directory).
  • Compute it in one place in the emitted shells (state kernel, notice ledger, lineage registry, and the request context all read the same value) instead of three inline expressions.
  • Add the same field to AgentProviderContext (fits naturally alongside Pass request identity, lineage, and read-only state/notices handles to context providers #459).
  • Document in docs/entry-conventions.md (state section) and the runtime reference.

Acceptance

  • (await agent()).plugin.root / .stateRoot are available in a generated MCP tool route, a rendered CLI command, a hook-backed event route, and the route-unit harness (fixture seam for tests), on portable/claude/codex/cursor artifacts and under mcp run.
  • A provider receives the same field.
  • entry-shell.test.ts proves the kernel, notice ledger, lineage registry, and plugin.stateRoot agree on one path, and that an unexpanded ${...} anchor is reported unavailable rather than joined into a path.
  • movie-library can delete scripts/state-root.ts's anchor tier and read plugin.root in src/providers/tracker-session.ts.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestmeta-frameworkAgent Bundle compiler-coupled meta-frameworkruntimeRuntime context, state, rendering, or execution

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions