Skip to content

feat(web): first-class web surface — <plugin> web from the installed artifact, one browser relay, /web in dev (#564) - #620

Merged
ScriptedAlchemy merged 23 commits into
mainfrom
feat/564-web-surface
Sep 5, 2026
Merged

feat(web): first-class web surface — <plugin> web from the installed artifact, one browser relay, /web in dev (#564)#620
ScriptedAlchemy merged 23 commits into
mainfrom
feat/564-web-surface

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Closes #564.

Design note: #564 (comment) (item → file mapping; #592 constraints). Built on the merged composite root (#578, 62b69c068) and the typed App client / bridge (#601, 0d4a37cef).

What lands

One artifact root, one framework-owned command. web: { apps, open? } in agent-bundle.config.ts exposes Apps already declared under mcp.servers.<id>.apps (config/normalize.ts, config/validate.ts, core/types.ts; invalid exposure is AB4341). The composite root's agent-bundle.manifest.json gains a strict web section (web-host/manifest.ts) and bin/<plugin>.mjs carries <plugin> web — compiled into the routed bin by build/entry-shell.ts / build/cli-bins.ts, with the host runtime and the page script inlined (virtual module, AB6005-clean; a routed bin without web is byte-identical to before, hash-pinned in tests/entry-shell.test.ts). No web/ directory, no src/web/** routing universe.

<plugin> web from the installed artifact. web-host/{command,launch,host-server,select-app,session,page,page-script}.ts + src/web-host.ts: reads the manifest beside bin/, spawns the plugin's own packed MCP server over the MCP client SDK (bundled), calls the opening tool once, serves the host page on loopback with the sandbox proxy on a second origin, applies allow consent policy, prints the ready line / --json. cli-entry.ts dispatches web before the authored tree. Requires the installed artifact only — no node_modules/agent-bundle, no checkout.

One browser relay, one bridge. The host-side relay is one module, web-host/browser/frame-relay.ts (McpAppFrameRelay), mounted by the Workbench (packages/workbench/src/mcp/mcp-app-frame.tsx imports it), serve-app, dev /web/<server>/<app>, and the artifact's <plugin> web page (web-host/browser/main.tsdist/web-host/page.js). serve-app-page.ts's inline HOST_SCRIPT duplicate is deleted. Host bridge stays createMcpAppBridge via McpAppRoutes; App side is createAppClient (#601). This PR authors no bridge.

Dev parity. dev/web-host-routes.ts mounts GET /web/<server>/<app> on the foreground server: same page, same relay, same routes; reads the active epoch's manifest web section; 404 for Apps not in web.apps. Each page gets an opaque opening id so concurrent tabs on one session never bind each other's opening call (McpAppRoutes create request accepts opening).

Response boundary. The page binds the opening call's CallToolResult the host already made (#562 path) — the same structured result the MCP projection lowers; no parallel rendering path.

Removed. agent-bundle/serve-app-command (spawnServeApp, serveAppArgv, locateFrameworkCli, ServeAppCommandError), its tests, fixture, docs, and the AB4837 recovery prose that pointed at it. Capability tables / doctor / mcp-app example advertise <plugin> web.

Bug fixed on the way. dev/watcher.ts: the package build stages dist/ in a .dist.stage-* sibling the watcher did not ignore, so every dist/ package build inside agent-bundle dev invalidated the epoch it had just produced (surfaced by the mcp-app example gaining a bin; reproduced with invalidation logging, root-caused, covered in tests/dev-watcher.test.ts).

cargo-hauler dashboard.ts shim (#564 acceptance, cargo-hauler #107 §4)

Dry-run on a local clone (dryrun/564-web, b030a61, never pushed): src/cli/dashboard.ts (181 lines: checkout heuristics, node_modules crawl for agent-bundle, serve-app spawn + stdout regex, SIGTERM relay, --target picker) deleted; replaced by web: { apps: [{ app: 'hauler/dashboard', tool: 'hauler_status', allow: ['call-tool'] }] } (+3 lines) and dashboardweb on the usage line. Net +14 / −193. hauler web forwards through src/scripts/hauler.ts's default case to the generated bin, which dispatches web. Phase B: built with this branch's tarballs, copied artifact/ elsewhere, deleted the build's node_modules, ran node artifact/bin/cargo-hauler.mjs web --no-open --json and node artifact/scripts/hauler.mjs web --no-open --json from an unrelated cwd: both served the host page (<title>hauler/dashboard</title>, fallback hidden), SIGINT → 130. No AB4341/AB6005.

Tests

  • pnpm build && pnpm typecheck && pnpm lint && pnpm test:unit (4076 passed), pnpm test:integration (1117 passed, incl. examples-real.e2e, mcp-app-real.e2e, serve-app.test, dev-web-host-routes, web-command.e2e), pnpm test:packed (34 passed, incl. packed-web-command: packed tarball → clean consumer → build → delete project source → spawn bin/<plugin>.mjs web as a real process), pnpm docs:site:build.
  • New: web-config, web-manifest, web-host-select-app, web-host-page, web-host-frame-relay, web-launch, web-command, dev-web-host-routes, packed-web-command, web-command.e2e (Chromium), bin-process support.

Docs / changeset

en + zh: reference/configuration (web, bin ownership), reference/cli (<plugin> web), guide/authoring/mcp ("Exposing an App in the browser"), guide/development/workbench (/web/...), reference/api, guide/distribution/validation, examples/mcp-app, guide/concepts/architecture (bridge fork marked landed); docs/diagnostics.md (AB4341 shapes 1–8, AB4837 recovery), docs/entry-conventions.md (<plugin> web, agent-bundle/web-host). One minor changeset (.changeset/564-web-surface.md).

Deslop

Deslop: GPT-5.6 Sol (source lane), 36 edits; Grok 4.6 (tests lane), 66 edits — 102 edits over the diff vs origin/main, commit 6756481d1. Restating comments and interface-field doc noise removed; as casts replaced by narrowed types (requireJsonObject, isPlainRecord, instanceof); core/* helpers reused (parseJsonWithoutDuplicateKeys, isErrno, errorMessage); tests import WEB_HOST_SEED_ELEMENT_ID / WEB_HOST_TOKEN_HEADER / WebHostPageSeed instead of redeclaring them; the listener try/catch in web-host/session.ts teardown and the test-side requestBody try/catch dropped. Integrator fix-ups: three frame-relay.ts comments the lane cut pre-existed on main (restored); manifest.ts narrowing via throw instead of casts; the lane's switch of install/doctor.ts to the strict readWebManifest was dropped in favour of the owner's lenient read (74bb06a68).

Self-review

Reviewer: change-risk-reviewer (GPT-5.6 Sol) on the integrated diff vs origin/main; author model Claude Fable 5.1.

Pass 1

# Finding Disposition
1 Blocker — config/normalize.ts: web could silently replace a conventional src/cli.ts bin, or be silently dropped when an explicit bin claims the plugin name / bin: false. Fixed (415bdcef4): web never displaces an authored executable; validation emits AB4341 naming the owner (shapes 7–8), tests in web-config.test.ts.
2 Blocker — dev/web-host-routes.ts: concurrent /web pages sharing a server/tool overwrote each other's opening call. Fixed (415bdcef4): per-page opaque opening id stamped in the seed, sent on create, keyed in the dev routes (bounded FIFO); McpAppRoutes rejects a malformed or unknown opening. Tests in dev-web-host-routes.test.ts, mcp-app-routes.test.ts.
3 Should-fix — web-host/manifest.ts exactKeys duplicated strict-object validation. Fixed: hasDataKeys from core/strict-json.ts.

Codex review (commit ab86e06b8) — all four threads answered inline; fixed in 67047c4fc.

# Finding Disposition
C1 P1 — the web capability row was declared but nothing consulted it for emission. Fixed: every built-in adapter publishes webSurfaceCapability; cli-bins.ts gates emission on cli (routed bin) or web (web-only bin); AB4341 shape 9 warns on a selected target whose web row is missing/degraded/unavailable/prohibited.
C2 P2 — configured MCP server args were dropped by web-host/launch.ts. Fixed: manifest web.apps[].args (validated as strings), expanded and passed after the entry.
C3 P2 — an App omitted from every selected target still appeared in the manifest web section. Fixed: webManifestFor filters by targets ∩ selected; artifact-cli-bin.test.ts asserts claude-only is absent from the portable artifact.
C4 P2 — a top-level command alias web was not reserved. Fixed: AB4341 shape 6 covers aliases; test in web-config.test.ts.

Pass 2 (after the deslop commit 6756481d1 and the fixes above)

# Finding Disposition
4 Should-fix — web-host/session.ts: teardown listeners ran without isolation after deslop dropped the try/catch; a sync throw aborted the remaining listeners and escaped the transport's close callback, a rejection went unhandled. Fixed (67047c4fc): notifyClosed isolates each listener (Promise.resolve(listener()).catch, sync throw caught), matching McpSessionService's watcher dispatch; the lease's watchSessionClosed forwards the listener promise. New tests/web-session.test.ts runs throwing, rejecting, unsubscribed, and late listeners against a real stdio server and asserts no unhandledRejection.
5 Should-fix — build/entry-shell.ts: a web-only bin with configured state/providers still imported and initialised them. Fixed (67047c4fc): a command-less bin mounts neither state nor providers; entry-shell.test.ts asserts a web-only bin with state + providers compiles to the same source as one without.
6 Nit — docs/diagnostics.md said AB4341 has "six message shapes". Fixed: nine (the generated site pages follow).

Pass 3 (after pass-2 fixes)

# Finding Disposition
7 Should-fix — build/cli-bins.ts: every generated bin carries a generatedCli (empty for a web-only plugin), so targetHostsGeneratedBin always judged on cli; a custom adapter without a web row still emitted the web bin, contradicting AB4341 shape 9. Fixed (ef3a25dda): generatedBinCapability decides by command count; adapter-capability-states.test.ts pits opposing cli/web states.
8 Should-fix — docs/diagnostics.md still documents agent-bundle serve-app. Dismissed: the serve-app CLI command still exists (src/cli.ts, src/serve-app/**); this PR removes only the agent-bundle/serve-app-command export (the routed-CLI spawner), so the diagnostics prose is accurate.

Lift the standalone MCP App host out of serve-app/serve-mcp-app.ts into
plain-Node modules under src/web-host/ that both `agent-bundle serve-app`
and the generated `<plugin> web` command (later lanes) run:

- select-app.ts: parseAppSelector (was parseServeAppSelector), appNameOf,
  openApp over an AppSelectionSource, requireJsonObject.
- session.ts: openStdioAppSession / sessionAuthorityFor; the session now
  carries a `selection` adapter over the SDK client.
- page.ts: renderWebHostPage({ script, seed }), WEB_HOST_TOKEN_HEADER
  (x-agent-bundle-web-host), webHostContentSecurityPolicy; the HTML shell,
  style, and escaping move here from serve-app-page.ts, the inline relay
  script is replaced by the caller-supplied built page script, and the
  seed element is #agent-bundle-web-host-seed.
- page-script.ts: readWebHostPageScript() reads dist/web-host/page.js once.
- host-server.ts: startWebHost / validPort / validProfile — the loopback
  HTTP host as plain async acquire/release, closing newest-first.

serve-mcp-app.ts keeps its public surface and becomes the framework-side
orchestration (validate, artifact, launch env, session, selection, host)
inside the existing Effect scope; serve-app-page.ts is deleted and api.ts
switches to parseAppSelector.

page.ts imports WEB_HOST_SEED_ELEMENT_ID / WebHostPageSeed from
./browser/seed.ts, which Lane 2 owns and provides; no stub is committed.
The framework page script (dist/web-host/page.js) is also Lane 2's, so
tests/serve-app.test.ts needs Lane 2's build step to run green.

Tests: web-host-page.test.ts, web-host-select-app.test.ts (unit);
serve-app.test.ts updated for the new seed id and token header.
Generated <plugin> web replaces the spawnServeApp entry, so capability tables, doctor, and the mcp-app example now expose Apps from the composite artifact instead of spawning the framework CLI.
… and runWebCommand (#564)

`web-host/launch.ts#resolveWebLaunch` turns a manifest `web.apps[]` entry into the
StdioLaunch of its MCP server: this Node + the artifact-relative `entry` (refused when
it escapes the plugin root or names no file: WebLaunchError `entry-outside-root` /
`entry-missing`), cwd = plugin root, env = inherited string values, then the declared
static env with `agent-bundle:path:*` tokens expanded (`plugin-data` →
`<root>/.agent-bundle/web/<server>`, created when referenced), then the
`AGENT_BUNDLE_PLUGIN_ROOT` anchor unless declared.

`web-host/command.ts#runWebCommand` is the command: `[<server>/<app>] [--port N]
[--open|--no-open] [--tool T] [--input JSON] [--allow <cap>]... [--profile <id>] [--json]
[--help]`; coded WebCommandError (`usage`, `app-ambiguous`, `app-not-exposed` → 2;
`manifest-missing`, `web-missing`, `manifest-invalid`, `server-exited` → 1); reads the
manifest's web section, picks the App, launches → openStdioAppSession → openApp →
startWebHost, prints the shared ready line or one stable JSON line, and stays until the
signal aborts (0) or the server exits (1 with its stderr tail). The modules it drives are
an injectable runtime so the command is unit-tested against fakes.

`src/web-host.ts` is the public `agent-bundle/web-host` entry (rslib entry and package
export land with Lane 2).

Plain Node: no Effect, no compiler modules (AB6005/AB4837).
…e authored tree (#564)

`RunGeneratedCliOptions.web?: GeneratedCliWebCommand` — when the generated bin passes it,
`<plugin> web ...` reaches `web.run(argv after web, { name, signal, writeErr, writeOut })`
right after the `--version` check and before the compiled command tree is consulted, and
the root `--help` lists `web` among the top-level commands in sorted position. Without the
hook nothing changes: `web` is an unknown command as before.
…#564)

Delete tests/packed-serve-app-command.test.ts, tests/serve-app-command-spawn.test.ts,
tests/serve-app-command.test.ts and fixtures/serve-app-command/ — the
spawnServeApp route is replaced by the framework-owned web command. The
AB4837 recovery expectation in cli-routes-build.test.ts and
route-framework-imports.test.ts now reads the web.apps / <plugin> web sentence.
- tests/packed-web-command.test.ts (packed pool): fixtures/web-surface consumer
  built with the installed CLI, source removed (packed-deleted-source), then
  bin/<plugin>.mjs web --no-open --json as a real process: sorted JSON ready
  document, human ready line, seed element, CSP, 403 without the token header,
  SIGINT 130 / SIGTERM 143 with no surviving descendants, --help listing both
  the authored dashboard command and web, exit 1 without a manifest, exit 2
  for an unexposed App or an --allow outside the vocabulary, no surviving
  agent-bundle or effect import.
- packages/workbench/tests/web-command.e2e.test.ts (integration pool): builds
  examples/mcp-app, runs its bin, opens the page at 1440x900, waits out the
  binding status, asserts the sandbox-origin proxy frame and the srcdoc App
  frame, the rendered status panel, the relayed resources/read and tools/call
  round trips, the hidden consent panel, then SIGINT teardown.
- tests/support/bin-process.ts: spawn/await-line/teardown helpers shared by both.
- artifact-cli-bin.test.ts: a project with web.apps and no src/cli still emits
  bin/<plugin>.mjs whose --help lists web and whose manifest carries the web section.
- rstest.integration-tests.ts: pool membership for the new files.
…command hosts the artifact it ships in (#564 integration)
…e collision); per-page opening id isolates concurrent dev /web pages; manifest keys via hasDataKeys
… every package build); web-config fixtures on #601 route ids; tool match via mcpRouteProtocolName; architecture prose marks the bridge fork as landed
@changeset-bot

changeset-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4a37174

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

This PR includes changesets to release 1 package
Name Type
agent-bundle 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

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-05T08:47:27.529853Z ab86e06 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@pkg-pr-new

pkg-pr-new Bot commented Sep 5, 2026

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

commit: 4a37174

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab86e06b85

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* root on any host (#564). Wave-2 bin emission consumes this row the same
* way `cliBinCapability` gates `bin/<plugin>.mjs`.
*/
export const webSurfaceCapability = 'web';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Wire the web capability into emission

For custom targets whose web support differs from cli, this capability declaration has no effect: a repo-wide search finds it referenced only by its unit test, while planComposite, targetHostsCliBin, and routedCliBinTargetDiagnostics still gate every web-bearing bin exclusively on cliBinCapability. Consequently a target can advertise supported web and have the bin omitted, or advertise unavailable web and still receive it; consume this row in emission and validation instead of leaving the declared capability disconnected.

AGENTS.md reference: AGENTS.md:L11-L16

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 67047c4: every built-in adapter publishes webSurfaceCapability (consumed from capabilityTable.plugin.web), cli-bins.ts gates emission through one targetHostsGeneratedBincli for a routed bin, web for a command-less web-only bin — and validation emits an AB4341 warning (shape 9) naming a selected target whose web capability is missing, degraded, unavailable, or prohibited. Tests: adapter-capability-states.test.ts, web-config.test.ts, artifact-cli-bin.test.ts.

await mkdir(pluginData, { recursive: true });
}
return Object.freeze({
args: Object.freeze([entry]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve configured MCP server arguments

When a compiled or prebuilt MCP server uses the supported mcp.servers.<id>.args setting, its normal host launch appends those arguments after the entry, but the web manifest stores no argument list and this launcher always spawns only [entry]. Any server that requires a flag or positional argument therefore starts differently under <plugin> web and may fail its handshake; carry server.args.slice(1) into the manifest and expand/pass them here.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 67047c4: the manifest web.apps[].args carries the server's declared arguments after the entry (server.args.slice(1)), web-host/manifest.ts validates them as strings, and web-host/launch.ts expands and passes them after the entry. Tests: web-launch.test.ts, web-manifest.test.ts, packed-web-command.test.ts.

relative(options.artifactRoot, entry.output).replaceAll('\\', '/'),
]));
const servers = new Map(options.model.mcpServers.map((server) => [server.id, server]));
const apps = options.model.web.apps.map((app) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude target-omitted Apps from the web manifest

When an exposed App is scoped away from the current build—for example, its server targets both portable and claude, the App targets only claude, and the artifact selects portableplanCompiledMcpApps correctly omits its HTML, but this unconditional mapping still advertises it in the manifest. The generated command and dev /web route then launch the server and fail because its resource list cannot contain that App; validate the target mismatch or filter exposure using the same selected-target predicate as App compilation.

Useful? React with 👍 / 👎.

@ScriptedAlchemy ScriptedAlchemy Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 67047c4: webManifestFor filters exposed Apps by targets ∩ selected, so an App omitted from every selected target does not appear in the manifest's web section. Test: artifact-cli-bin.test.ts (status/claude-only is absent from the portable artifact's web.apps); documented in the targets-artifacts reference (en/zh).

}
const bins = model.packageBuild?.bins ?? [];
for (const bin of bins) {
if (bin.generatedCli?.commands.some((command) => command.path[0] === 'web') !== true) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve web aliases as well as command paths

When a top-level authored command has another path but declares aliases: ['web'], this check emits no AB4341, yet runGeneratedCliEntry dispatches any first argument equal to web before consulting the authored command tree. The alias is therefore silently unreachable in every web-enabled artifact; include top-level aliases in the reserved-name collision check.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 67047c4: validate.ts reserves web for top-level command aliases too (AB4341 shape 6, CLI alias "web" of <command> is reserved by the web surface). Test: web-config.test.ts (dashboard aliased to web); docs in docs/diagnostics.md and the CLI/configuration references.

ScriptedAlchemy and others added 5 commits September 5, 2026 08:49
…t-read rationale

The comment said to switch to readWebManifest "once web-host/manifest.ts
lands (Lane 3)" — that module landed in this same branch, and switching
would be wrong anyway: doctor reads the web section leniently by design so
it reports what a bundle contains instead of failing on a malformed
manifest (the strict parser rejects the minimal manifests doctor accepts).

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Sol lane (source, 36 edits): drop comments that restate code, replace
type-bypassing casts with narrowed types, reuse core/strict-json and
core/errors helpers (parseJsonWithoutDuplicateKeys, isErrno,
errorMessage), and make install/doctor read the manifest web section
through web-host/manifest.ts instead of its own lenient parser.
Grok lane (tests, 66 edits): drop restating comments, import
WEB_HOST_SEED_ELEMENT_ID / WEB_HOST_TOKEN_HEADER / WebHostPageSeed instead
of redeclaring them, narrow with instanceof instead of casts.
Integrator: restore the three relay comments that pre-existed on main,
fix manifest.ts narrowing with throw, give the doctor test a valid
manifest web section.
…te/providers

- web-host/session.ts: each close listener runs isolated (sync throw and
  rejection), matching McpSessionService's watcher dispatch; lease
  watchSessionClosed forwards the listener's promise so rejections are caught
- entry-shell.ts: a command-less (web-only) bin mounts neither state nor
  providers, so their modules cannot keep <plugin> web from starting
- docs/diagnostics.md: AB4341 documents nine shapes
- tests: web-session.test.ts (throwing/rejecting listeners, lease), web-only
  bin with state+providers equals the plain web-only bin
generatedCliBinEntry gives every generated bin a generatedCli (empty for a
web-only plugin), so targetHostsGeneratedBin always consulted cli and a custom
adapter without a web row still emitted the web bin. generatedBinCapability
decides by command count; test pits opposing cli/web states.
api.mdx: main's per-module table with the web-host row in place of the removed
serve-app-command; api/_meta.json follows; package.json keeps the web-host
tsconfig in typecheck.
@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) September 5, 2026 10:05
@ScriptedAlchemy
ScriptedAlchemy merged commit 512ddaa into main Sep 5, 2026
17 checks passed
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
…ath in cliBinSourceInputs; re-pin non-web template hash
cursor Bot pushed a commit that referenced this pull request Sep 5, 2026
…'s changeset untouched

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
…clared-projection web launch, identity-keyed sessions, opening-tool policy, out-of-artifact web state (#628)

* fix(564): move framework-owned web state outside the installed artifact

<plugin> web anchored per-server plugin data at <pluginRoot>/.agent-bundle/web,
mutating the installed artifact. Durable web state now lives under the user's
home (~/.agent-bundle/web-data/<plugin>-<digest>/<server>), keyed by the
resolved plugin root so two installs never share it, and a read-only install
still launches when the server declares plugin-data state. The configured
args still pass through app.args.map(expand) unchanged.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>

* fix(564): one effective web launch from declared projections; identity-keyed sessions, epoch retirement, opening-tool policy

The dev /web route hardcoded target 'portable', so browser presentation
forced the portable projection: a Claude- or Codex-only build could not open
/web/<server>/<app> without mcp.json. The launch now resolves from the
projections the artifact manifest declares, over the same canonical
resolution mcp run and MCP sessions use (readTargetMcpServer +
resolveMcpPathTokens, env values re-anchored through the target's stdio
argument rule): an explicit ?target= is validated against the declared
projections that launch the server (invalid is an error, never a fallback);
projections sharing one normalized launch descriptor (command, args, cwd,
declared env, runtime binding) open unprompted whatever the host order;
materially different launches answer 409 naming the choices; no candidate
reports the missing binding instead of synthesizing portable. Selection
settles before any spawn.

Web sessions are cached by epoch, server, and resolved launch identity, and
retire on successful epoch publication only (artifact.available): new
acquisitions use the new epoch, an old session leased only by this registry
closes and releases its process and epoch reference, sessions pages still
lease stay valid, and a failed rebuild retires nothing.

Opening an App page is no longer an unbounded mutation: an opening tool
annotated readOnlyHint: true runs on every load, any other opening tool runs
once per session, tool, App, and input, and refreshes rebind the retained
result. The page's previewProfile stays presentation-only.

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>

* docs(564): document web launch selection, opening-tool policy, web state location, and session retirement

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>

* chore(564): own changeset for the #620 follow-up; leave the merged PR's changeset untouched

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>

* docs(website): dev /web launch selection, opening-tool policy, session retirement, web state location (en+zh)

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>

* chore: changeset PR number (#628)

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>

* fix(564): close a retired leased web session at its last lease release; share one in-flight opening call across concurrent loads

Self-review findings on #628: a session retired by epoch publication while
pages still leased it was deleted from the registry and never closed —
closeSessionWhenUnleased on McpSessionService now closes it at the release
of its last lease (immediately when nothing leases it). The opening-result
cache retains the pending call rather than its settled value, so concurrent
first loads of a mutating opening tool share one tools/call, and a failed
call is dropped so the next load retries. AB8023 documents the /web launch
selection statuses (404 invalid target or no candidate, 409 ambiguous).

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>

---------

Co-authored-by: Zack Jackson <ScriptedAlchemy@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant