Skip to content

feat(manifest): authoritative agent-bundle.manifest.json v2 — consumers read the composite root through the manifest (#592 step 3, #555 W2/S3) - #604

Merged
ScriptedAlchemy merged 151 commits into
mainfrom
feat/592-authoritative-manifest
Sep 5, 2026
Merged

feat(manifest): authoritative agent-bundle.manifest.json v2 — consumers read the composite root through the manifest (#592 step 3, #555 W2/S3)#604
ScriptedAlchemy merged 151 commits into
mainfrom
feat/592-authoritative-manifest

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Implements #592 step 3 — the authoritative manifest together with #555 W2/S3 — consumers read the composite root through the manifest. Design comment: #592 (comment). Owner guidance from the 08:30 review is implemented, not noted — see "Owner review (08:30)" below.

What changes

agent-bundle.manifest.json is manifestVersion: 2: closed keys, canonical stableJson bytes, every path root-relative POSIX, and it is serialized from the compiler's own IR (NormalizedPluginCompiledRouteGraphTargetArtifactPlans) rather than reconstructed. The host documents (.claude-plugin/plugin.json, .codex-plugin/*, .cursor-plugin/plugin.json, .mcp.json, hooks/hooks.json, marketplace catalogs) are projections the manifest points at; nothing reads them for identity any more.

Public contract (durable)

section fields
manifestVersion 2
application id, name, version, description? — identity, once
projections[] one row per selected host: host (the selected name), builtInHost? (the shipped adapter that planned it — identity, never the name), documents{plugin?,marketplace?,mcp?,hooks?} pointers, marketplace{name}?
routes the compiled route graph: digest, servers[]{id,name,mode,routes[]}, events[]{…,execution{preflight?,providers[],runtime,fallback}} (#618: the preflight export and required providers artifact-only inspection needs), scripts[], providers[], layouts[], cli?{mode,routes[],commands[]}commands[] carry options[].aliases and projection?{module,mapInput,defaults?,relaxed?} serialized from CompiledCliProjection (#616), contracts?[]{id,input,origin,routes[]} (reserved shape for #593)
executables bins[]{name,path,worker?,hosts[]}, hooks[]{id,name,event,host,kind,path,routeId?,timeout?}, mcpServers[]{id,name,kind,transport,hosts[],launch?{entry,worker?,args[],env},apps[]{id,name,resourceUri,path?,prebuilt?}}kind is compiled/prebuilt/command/remote; launch is present exactly for compiled and prebuilt and is the one launch record every host MCP document projects and <plugin> web / mcp run start (args[] are {kind:'artifact',path} for root-relative references written from agent-bundle:path:plugin-root/…, else {kind:'literal',value}; tokens in literals and env are expanded by the launcher, never by the manifest), scripts[]{id,name,mode,path,worker?,hosts[],rendered?}
distribution channels[] (local/npm), install?{instructions?,script?} — pointers only; S5 owns the contents — and payloads[]{name,hosts[],runtimeDependencies[]}: every definePrebuilt directory a selected host packages (#630), the files[] rows under <name>/ are its prebuilt contents
files[] path, bytes, sha256, kind, mode? — the authoritative inventory (no provenance here)
runtime node
web? #564's section minus the copied launch fields: apps[]{app,name,resourceUri,server,tool?,input?,allow[]} + open; apps[].server names an executables.mcpServers[] row that carries launch (#628/#633 — one launch owner, no second server definition)

Compiler record (operational, versioned separately)

compiler.recordVersion: 1 with producer{name,version}, project{configPath,configDigest,modelDigest,revision,sourceInputs[],packageName?,packageVersion?}, provenance[]{path,sourceInputs[]} (paths exactly match files[]), adapters[]{host,adapterRevision,observedVersion,schemas[]}, agentSkills, validation. Changing anything here never changes the public contract; manifestVersion and recordVersion move independently.

targets[]projections[]; validation.targets[]validation.projections[]; files[].sourceInputscompiler.provenance[]; producer/project/agentSkills/validation moved under compiler. The agent-bundle.hooks.json sidecar is gone (build/hook-index.ts deleted); hook rows are executables.hooks[].

Shipped schema

packages/agent-bundle/schemas/agent-bundle.manifest.schema.json (draft 2020-12, additionalProperties: false everywhere, in files), exported as artifactManifestSchema and validateArtifactManifestSchema from agent-bundle and agent-bundle/api, beside readArtifactManifest, parseArtifactManifest, artifactManifestName, artifactManifestVersion, artifactCompilerRecordVersion. tests/manifest-schema.test.ts proves the writer's output validates, the schema's keys agree with the TypeScript type, and closed-key extras are rejected by both.

Consumers rewired (probing deleted in the same change)

consumer reads deleted
install, uninstall, doctor --from install/identity.ts readBundleIdentity: identity from application, projection by builtInHost === host, host document + marketplace pointers from projections[].documents; copy set and content hash from files[] (install/receipt.ts manifestInventory) resolveBundleRoot, readIdentity, manifestPath/marketplacePath/readRecord/readString in doctor.ts and install.ts, the plugin.json / marketplace readers, the directory walk that produced the content hash
mcp list|invoke|run, serve-app build/manifest-projection.ts resolveManifestHost / resolveManifestMcpDocument: server rows from executables.mcpServers[], --target optional when one projection runs the server; mcp run launches the host document's own line, cross-checked against executables.mcpServers[].entry hostManifestPaths, mcpDocumentPath, configureArtifactOptions target-required paths
hooks list|simulate, AB6018 executables.hooks[] readArtifactHookIndex, parseArtifactHookIndex, writeHookIndex
inspect --json, new inspect --artifact <root> services/inspect-artifact.tsdev/artifacts/application-explorer.ts applicationExplorerFor — a thin UI projection of the manifest, not a mirror
inspection service / Workbench artifacts page artifact-executables.ts from executables.*; scripts, bins, MCP servers, hooks from rows artifact-script-catalog.ts (directory listing of scripts/)
dev installs (dev/host-install-manager.ts) build/manifest-reindex.ts reindexArtifactManifest re-indexes the variant's files[] + provenance[] after it edits mcp.json/adds markers ad-hoc manifest rewrites
pack-inventory, eval --artifact, test installed files[], projections[] targets[] readers

Diagnostics (docs/diagnostics.md)

  • AB6010 now also judges adapter identity: projections[].builtInHost must equal TargetRegistry.builtInHost(host) besides revision/version/schemas.
  • AB6018 is judged from executables.hooks[], not the deleted sidecar.
  • AB7001 sources from the manifest: missing or non-canonical manifest, no projection whose builtInHost is the host, missing documents.plugin, pointer to a file the root lacks, a files[] row missing or digest-mismatched, unsafe Cursor name, Claude/Codex without marketplace.name. doctor --from reports the same AB7001 (with a Doctor recovery) instead of folding it into AB7306; AB7306 remains for non-diagnostic failures.
  • AB6039/AB6040 existed in an earlier revision as drift detectors between the manifest and host documents; removed per the 08:30 review — the adapter runtime/hook contracts are the single source for documents.mcp/documents.hooks, so there is no second source to drift.

Owner review (08:30)

  1. Serialized from IR, no drift detector. manifestFor takes the CompiledRouteGraph, the NormalizedPlugin, the selected TargetArtifactPlans and the registry contracts; documents.mcp/documents.hooks come from registry.mcpRuntime(host).manifestPath / registry.hookContract(host).manifestPath. AB6039/AB6040 deleted.
  2. Operational metadata separated. compiler record with its own recordVersion (above).
  3. Relocatable paths. core/paths.ts isRelocatablePosixPath gates every path field in writer and parser (must be a safe relative POSIX path); tests/manifest-relocatable.test.ts builds a four-host fixture with hooks, scripts, a CLI bin, a compiled MCP server, an MCP App and web, asserts no machine path appears in the bytes, every pointer is a files[] row, moves the root to another directory, and re-runs validateArtifact, readArtifactManifest, readBundleIdentity (three hosts) and the MCP pointer resolution against the moved root — byte-identical manifest, same answers. Forged absolute executables.mcpServers[].entry.path and web.apps[].entry are rejected.
  4. Copied-artifact proof. Below.
  5. Inline threads. None were left on the PR; the review body's points are the five above.

cargo-hauler proof (#107 §1) — copied artifact, no source, no node_modules

Full report and traces: /tmp/592-proof3/report.md, /tmp/592-proof3/{out,traces}/. Head proved: 464f30ca8 (the reviewer fixes after it touch doctor error codes, mcp run argument order and tests only). Packed from a detached worktree: agent-bundle-0.1.0.tgz 164cd05d…111a (3,403,441 B), agent-bundle-runtime-0.0.0.tgz 25e9cd0b…9343, rsc-markdown-stream-0.1.0.tgz 5c154ac6…01fa. Scratch cargo-hauler at /tmp/592-proof3/hauler (source checkout untouched), pnpm-workspace.yaml overrides → the tarballs, pnpm install + agent-bundle build → composite artifact/ (no artifact/<host>), validate --artifact 0 errors, Claude plugin validate loaded.

Manifest: manifestVersion 2, compiler.recordVersion 1, application cargo-hauler@0.6.0, projections claude/codex/cursor/portable, 37 files[], executables.bins[0] bin/cargo-hauler.mjs + bin/cargo-hauler-flight.mjs worker, one compiled MCP server hauler with App hauler/dashboard, 12 hooks.

cp -r artifact /tmp/592-proof3/root-… then test ! -e for agent-bundle.config.ts, src, node_modules, package.json under the root and the cwd. Every command traced with strace -f (openat/statx/getdents64/execve/clone), filtered to the copied root:

command resolves the copied root first touch = manifest no directory listing by agent-bundle no <root>/<host> result
doctor --host cursor|claude|codex --from ROOT --json yes: bundleRoot, 0.6.0, artifactContentHash 56c1eff722af… from files[] yes yes yes exit 1 for cursor is the live ~/.cursor 0.6.4 comparison (AB7309), 0 for claude/codex
install cursor --from ROOT --json (no --replace) yes yes yes yes AB7005 version collision vs installed 0.6.4; wrote nothing
mcp list --target cursor 7 tools yes novalidateArtifact walks the tree first yes exit 0
mcp list (no --target) AB5000 names the four projections yes yes yes closed failure, manifest only
mcp invoke hauler_status --target cursor server spawned from ROOT/mcp/mcp-hauler-2b8242f9-flight.mjs yes no — same walk yes exit 0 (tool reports 0.6.4 daemon vs 0.6.0 CLI skew; daemon not restarted)
serve-app hauler/dashboard --target cursor --no-open --no-env HTTP 200, <title>hauler/dashboard</title> yes no — same walk yes exit 0
inspect --artifact ROOT [--json] { manifest, application } — the ApplicationExplorer payload the Workbench renders yes yes yes manifest is the only file touched
validate --artifact ROOT 0 errors walk first expected — its job is tree vs files[] yes exit 0
doctor --from ROOT/.cursor-plugin, --from ROOT/cursor AB7001 No agent-bundle.manifest.json in …

Corrupted copies (/tmp/592-proof3/hauler-corrupt): deleting projections[cursor].builtInHostAB7001 … built for projections [claude, codex, cursor, portable]; none is the shipped cursor adapter; garbled JSON → AB7001 … not a valid canonical artifact manifest: Artifact manifest is not valid JSON.; documents.plugindoes-not-exist.jsonAB7001 … names "does-not-exist.json", which is not a manifest file. — on both doctor and install --json.

Residual filesystem access, stated plainly:

  • mcp list|invoke and serve-app still run validateArtifact before launching — the pre-existing tamper gate that compares the tree against files[] (that is the getdents64 in the table). It is not probing for structure; every launch decision comes from the manifest. Narrowing it to the executed rows is a follow-up, not this PR.
  • doctor stats fixed names (state, .env, .env.local; for Claude .lsp.json, monitors/monitors.json, settings.json) — operator data and Claude's project layout, not artifact structure; and the host CLIs it spawns (claude plugin list) walk the root themselves.
  • Workbench is project-bound (dev server over a project root); its artifacts page renders the same ApplicationExplorer that inspect --artifact prints, from the manifest only (tests/application-explorer.test.ts, packages/workbench/tests/examples-real.e2e.test.ts).
  • cargo-hauler's own tests/version-consistency.test.ts still opens artifact/cursor/.cursor-plugin/plugin.json and fails with ENOENT — the consumer change meta-framework: execution plan and sequencing #107 §1 asks for; not modified here.

Absorbed from main after the owner's go (#616 / #630 / #629)

Merged origin/main twice (7769b21fc: #629 + #630; 472d64d1a: #616 + #628). Nothing on the branch keeps a second copy:

Owner audit (17:11) — contract table

Each row done on the integrated head; the fresh reviewer passes below cover the result.

# row where it lands proof
1 #616 CLI projections routes.cli.commands[].projection{module,mapInput,defaults?,relaxed?} and options[].aliases, serialized from CompiledCliProjection by build/manifest-routes.ts; defaults are the CLI-only flags.<key>.default literals (the canonical Zod defaults are not re-derived). cli.projectionSources is deliberately not recorded: it maps route ids to absolute module paths for the bundler (build-side only, excluded from the graph digest for the same reason) and would violate the relocatability rule; the relative projection.module is the recorded identity. tests/cli-routes-build.test.ts, tests/manifest-schema.test.ts, tests/manifest-key-inventory.test.ts
2 #618 preflight / providers routes.events[].execution{preflight?,providers[],runtime,fallback} from routes/event-execution.ts eventRouteExecutionFor — the same helper config/normalize.ts uses, so there is one derivation tests/manifest-combined-proof.test.ts asserts the block; tests/preflight-artifact-graph.test.ts
3 #628 web launch selection web-host/manifest.ts readWebManifestDocument reads projections[].host (v1 targets[].name gone) and now requires manifestVersion === 2; the session-selection path (dev/web-host-launch-selection.ts, web-host/launch.ts) takes the launch from document.launches.get(app.server) tests/web-manifest.test.ts (hosts, launches, version rejection for undefined/1/3/'2'), tests/web-host-routes-unit.test.ts, tests/dev-web-host-routes.test.ts
4 canonical launch ownership executables.mcpServers[].launch is the only launch record; web.apps[] lost entry/args/env; <plugin> web resolves it through the shared expandLaunchTokens (web-host/manifest.ts), artifact arguments under the root, env tokens under the durable root, AGENT_BUNDLE_PLUGIN_ROOT anchored; mcp run launches the host document's line for the record of the same name and refuses a document that renames or omits a launchable server (AB6017, Pass 8) tests/manifest.test.ts, tests/prebuilt-payload.test.ts (prebuilt args/env through the web launcher), tests/build-compose.test.ts (custom adapter order; renamed document server refused), tests/packed-readonly-state-root.test.ts
5 #629 Workbench Application explorer consumes ArtifactInspection.application (dev/artifacts/application-explorer.ts); Routes/Hooks/Playground pages and tests stay deleted as on main; transport added so the tree keeps labelling servers by transport packages/workbench/tests/application-tree-model.test.ts, artifacts-page.test.ts, examples-real.e2e.test.ts
6 #630 prebuilt dependencies distribution.payloads[]{name,hosts[],runtimeDependencies[]} from NormalizedPlugin.payloads (selected hosts only); prepack behaviour untouched; artifact-only consumers read the deployment requirement from that row, and prebuilt servers carry kind: 'prebuilt' + launch tests/prebuilt-payload.test.ts, tests/manifest-schema.test.ts, tests/manifest-combined-proof.test.ts
7 compatibility rule Strict policy, documented in the reference "Versioning": any change an old closed reader would reject bumps manifestVersion — adding an optional public key included; "optional" is never "backward compatible". compiler.recordVersion governs only the compiler subtree. Enforced by tests/manifest-key-inventory.test.ts: the shipped schema's full key inventory (public and compiler, split) is checked into fixtures; changing either without bumping the matching version fails. Every reader — parseArtifactManifest and the lean web reader — refuses any other manifestVersion. tests/manifest-key-inventory.test.ts, tests/manifest.test.ts, tests/web-manifest.test.ts
8 combined proof tests/manifest-combined-proof.test.ts: one fixture with a projected CLI tool (submit.cli.ts), a preflight event, an App exposed through web, durable framework state (agent-bundle:path:plugin-data/state in env), and a definePrebuilt payload with runtimeDependencies; built, copied to a relocated directory (no source, no node_modules), byte-identical manifest, validate/doctor/inspect --artifact from the copy, then install.mjs install → same-version replace (after reindexArtifactManifest) → doctoruninstall with the exact success lines pinned (Installed …@… at <destination> (content …), Replaced … (content … -> …), Uninstalled … for cursor (local mode) at <destination>) and collision/daemon version mismatch/Already installed asserted absent, and the same artifact exercised through the fixture's package.json bin tests/manifest-combined-proof.test.ts (integration pool)
state root ≠ code root (#637/#640/#642) The manifest records nothing path-shaped about state: launch env keeps the agent-bundle:path:plugin-data/… token unexpanded, web records no data root, and the launcher (not the manifest) derives ~/.agent-bundle/web-data/… / ~/.agent-bundle/state/…. After merging #640 and #642, install/state-root.ts locates a declared AGENT_BUNDLE_STATE_ROOT through the installed manifest's projections[host].documents.mcp pointer instead of probing .cursor-plugin/mcp.json/mcp.json; coordination comments exchanged on #604 (17:25 / 17:28). tests/packed-readonly-state-root.test.ts, tests/uninstall.test.ts, tests/doctor.test.ts, tests/manifest-relocatable.test.ts

Merged origin/main through #636, #638 (compile evidence record — admitted as root metadata in validate-artifact.ts; the hook-index half of that merge stays deleted), #640 and #642.

Deslop

Deslop: gpt-5.6-sol-medium, 11 edits proposed / 9 applied on the pre-merge diff (365939501, 0e34c653a; two reverted: the "Reserved keys" sections in the en/zh manifest reference, which the brief requires) + 1 edit on the post-merge delta (ff3d3efc0) + 1 edit on the #616/#630 absorption delta (59dbb1ee5; the other proposals touched main's own comments outside the delta and were not applied) + 2 edits on the 17:11 contract-table delta (6b10803f2: temp-home cleanup in prebuilt-payload.test.ts) + 2 edits on the #638/#642 merge delta (2387162ac: two restating comments) + 2 edits on the P2 reader/reindex delta (c78bfc8d2) + 5 file-level edits on the post-74ccb99 delta after the #634 merge (bb241b281: 12 restating comments in build.ts, build-compose.test.ts, prebuilt-payload.test.ts, web-manifest.test.ts; three non-null assertions in parseRoutes replaced by a type-narrowing filter) + 10 edits on the Pass 8 fix delta (1db705bc5, Sol: 13 proposed, 3 reverted where the comment gave the reason rather than restating the code). Changeset condensed by hand.

Self-review

Reviewer: gpt-5.6-sol-medium (author lanes: Fable/Sol/Grok; integrator Fable).

Pass 1 (on 464f30ca8) — 4 findings, all fixed:

  1. tests/preflight-artifact-graph.test.ts still read agent-bundle.hooks.json → reads manifest.executables.hooks (6f69911f2).
  2. doctor --from folded every AB7001 into AB7306, contradicting docs/changeset → malformedBundle preserves DiagnosticError diagnostics; AB7306 only for non-diagnostic errors; test updated (ff3d3efc0).
  3. mcp run rebuilt the launch as [manifestEntry, ...hostArgs.slice(1)], wrong for adapters that put flags first or use the entry as command → the host document's line is used verbatim; a document that skips the compiled entry is already refused at build (AB6017), so no runtime re-check (ff3d3efc0, fe49a137a).
  4. tests/manifest-relocatable.test.ts omitted web.apps[].entry → fixture gained an MCP App + web; absolute and unlisted web entries asserted rejected (ff3d3efc0).

Pass 2 (on 60148f923) — 5 findings, 4 fixed, 1 dismissed (fe49a137a):

  1. Schema relativePath accepted C:escape while the parser rejects it → (?![A-Za-z]:) lookahead added; parity case in tests/manifest-schema.test.ts.
  2. No test covered a host launch line with arguments before and after the entry → tests/build-compose.test.ts launches a synthetic adapter whose document is --enable-source-maps <entry> --stdio and asserts the exact sequence, and asserts the build refuses (AB6017) a document that skips the entry.
  3. The non-DiagnosticError AB7306 fallback lost its test → tests/doctor.test.ts makes .cursor-plugin a regular file (ENOTDIR on the pointer check) and asserts AB7306 with no AB7001.
  4. AB7306 had no row in docs/diagnostics.md → "Read-only Doctor bundle resolution (AB7306)" section added; coverage check green.
  5. cli.ts inspect --artifact uses await import('./api.ts')dismissed: that is the module's documented policy (header comment, lines 7–16) and every other action in the file does the same so --help/--version do not load the product graph.

Pass 3 (merge delta vs origin/main after absorbing #616/#630/#629, on 59dbb1ee5) — 5 findings, all fixed (2d5eb053a), reviewer re-run: "No findings":

  1. manifestPayloads host filtering untested → tests/prebuilt-payload.test.ts builds with targets: ['claude', 'portable'] against payloads targeting codex and claude, asserts only runtime is recorded with hosts: ['claude'] and no app/ rows are staged.
  2. Workbench Application tree used ArtifactInspectionMcpServer.kind as the transport label after kind changed meaning → transport added to the inspection row, service, Workbench validator, model, and fixtures.
  3. distribution.payloads[<name>] location string → distribution.payloads[<index>].name, matching the rest of the parser.
  4. docs/diagnostics.md omitted defaults? from the projection field list → added.
  5. {@link CompiledCliProjection} in route-manifest.ts/manifest.ts relies on TypeDoc's project-wide resolution — dismissed: pnpm docs:site:build resolves it (0 broken links) and the file's other links use the same form.

Pass 4 (contract-table delta, on 74ccb996b) — 5 findings, all fixed (17edaf120, 6b10803f2):

  1. mcp run's no-host-document fallback dropped the manifest launch args/env → built from row.launch with expandLaunchTokens (shared with resolveWebLaunch), artifact arguments under the root, env under the durable root.
  2. web-host/manifest.ts never checked manifestVersion → requires 2; rejection tests for undefined/1/3/'2'.
  3. Combined-proof install/replace/uninstall assertions matched prefixes → pin the whole first stdout line (destination, content hashes, for cursor (local mode)).
  4. No test passed prebuilt args/env through the launch record and web launcher → prebuilt-payload.test.ts adds a server with literal/artifact args and tokenized env, asserts the record and the resolved launch (state under home, not the artifact).
  5. Schema descriptions said launch/web servers are compiled-only → say compiled or prebuilt.

Pass 5 (fresh, integrated result incl. #640/#636, on 6b10803f2) — 2 findings, both fixed (9b8ce27e9, 66ac3a7bf), reviewer re-run: "No findings" on code, docs precision only:

  1. The fallback in (4.1) did not inject AGENT_BUNDLE_PLUGIN_ROOT, so a generated server could derive state from the artifact → anchored on the durable root; build-compose.test.ts reaches the fallback through a registry whose reader aliases server names (AB6017 still holds) and asserts the anchor.
  2. Reference prose said every reader enforces canonical bytes and the top level has exactly nine keys → "Lean reader" row names the one non-parser consumer (<plugin> web support) and what it checks; "nine required keys plus optional web"; Versioning notes the lean reader rejects the version mismatch first.

Pass 6 (#638/#642 merge delta, on 93fa0e1c9) — 1 finding, fixed (2387162ac): the doctor row claimed a manifest-less root always gets the derived state root; it falls back to an inherited AGENT_BUNDLE_STATE_ROOT first. Verified by the reviewer: readArtifactManifest never throws on arbitrary directories, no import cycle from install/ to build/manifest-file.ts, install copies the manifest so the three callers see it, the manifest is excluded from the ownership walk, no live reference to the hook index remains.

Pass 7 (owner P2 + evidence items, on d31aca73ad1018dedb) — 7 findings, 6 fixed, 1 dismissed:

  1. Lean reader did not check launch.entry/worker/artifact args against files[]parseFilePaths + requireLaunchFiles shared with the full parser (8fa261880).
  2. parseProjectionHosts/parseServerLaunches validate only the slices the lean reader launches from — dismissed by design, documented in the "Lean reader" row: it refuses rather than filters those slices and ignores the rest.
  3. copyInventoryFiles verified the source and copied without re-hashing (TOCTOU) → the copy is re-measured and refused when it is not the verified inventory (8fa261880; test in install.test.ts).
  4. "Written once" contradicted reindexArtifactManifest → reference rows say reindexing re-measures non-compiled rows and refuses bundle rows and the compile evidence record (8fa261880).
  5. A prebuilt payload named state was indexed by the manifest but skipped by treeInventory, so a valid artifact failed the copy hash → state/ is reserved everywhere: AB4741 for the payload, both parsers and the schema refuse files[] rows under it in any letter case (c97457521).
  6. The lean reader still admitted launches under state/ → one parseArtifactFilePath used by both readers (d1018dedb).
  7. .agent-bundle-install.json passed the parser but treeInventory rejects it → the same rule reserves the receipt entry (d1018dedb).

Pass 8 (fresh, rebased result through #634/#646, on bb241b281; change-risk-reviewer could not start over tracedecay transport, fell back to generalPurpose on Sol) — 3 findings, all fixed (1af72fec00, 66c72f458):

  1. requireLaunchFiles checked path membership only: a compiled server's launch.entry could name a generated/copy row (bytes no compile evidence describes) and two servers' records could be swapped without any check against the host documents → (a) both readers require the entry to be the row its server kind starts (bundle for compiled, prebuilt for prebuilt) and the worker a bundle row (parseFileKinds, shared artifactManifestFileKinds); (b) validateMcpCoherence holds every host document to the launch records projected to its host: every launchable manifest server for the target must appear in the document under the same name (validateDeclaredServersPresent), and each stdio document server's artifact-local paths must include its record's entry and artifact arguments (validateLaunchAgreement, a subset check so a ./payload/file literal the adapter resolves artifact-locally is not rejected), else AB6017. With drift refused at validation, mcp run's manifest-only fallback became unreachable for any validated root and is deleted (66c72f458): the host document is required and is the launch line. Tests: web-manifest.test.ts, manifest.test.ts (wrong-kind entry, prebuilt at a bundle), artifact-validator.test.ts (swapped record → AB6017; renamed server → AB6017; agreeing record with an artifact-local literal → none), build-compose.test.ts (aliased document names → mcp run refuses with AB6017, nothing spawned).
  2. manifestInventory compared sha256 only, so a post-build chmod on an indexed row installed as authoritative → uses matchesManifestFile (bytes, mode, path, digest) like validate-artifact; install.test.ts asserts AB7001 on an executable bit the row does not declare. Test fixtures (support/install-fixture.ts, public-api-packed.test.ts) now record mode and bundle kinds the way the compiler does.
  3. The manifest path rule admitted names the receipt reader rejects (NUL, CON.txt, a:b, trailing dot/space, control characters), so a valid manifest could be uninstallable → one isPortablePathSegment in core/paths.ts used by isRelocatablePosixPath (both parsers) and isReceiptPath; the schema's relativePath pattern mirrors it; five parity rules added to manifest-schema.test.ts.

Pass 9 (re-run after the Pass 8 fixes, on 1db705bc52, Sol) — 3 must-fix, 2 should-fix, all fixed (1ee3cffae5):

  1. Agreement compared an unordered set, so node payload/bootstrap.mjs mcp/server.mjs passed while Node ran the bootstrap → validateLaunchAgreement takes the document's artifact-local paths in launch order: the first must be the record's entry, the record's artifact arguments must follow it as an ordered subsequence; flags and literals may interleave.
  2. A non-stdio document server with the same name as a launchable record skipped agreement and failed only in mcp runAB6017 when the document reaches a launchable record over another transport.
  3. Validation read the document at runtime.manifestPath while mcp run reads projections[host].documents.mcp, so a manifest missing the pointer validated and then failed to launch → the pointer must equal the runtime's document path whenever either exists (AB6017); artifact-inspection-service.test.ts's pointerless-host case now asserts refusal at publish.
  4. docs/diagnostics.md AB6017 said "exactly" → describes the ordered rule, the pointer, and the transport case.
  5. Schema parity covered one device name → con.txt, COM¹.log, lpt³ rejected by both; COM10.log, console.txt, nulled/index.json, lpt.txt accepted by both.
    Re-run on 1ee3cffae5 (Sol): no findings.

Gates

On 1ee3cffae5 (after merging origin/main through #634/#646): pnpm build && pnpm typecheck && pnpm lint && pnpm test:unit green (4228 passed); the touched integration suites green (234 passed: combined proof, host install proofs, prebuilt payload, web command, compile evidence, artifact validator, dev host install, build, build-compose, install, doctor, uninstall, dev web-host routes, package build); pnpm test:packed (38) green; pnpm docs:site:build green (parity ok). Earlier on 66c72f458: pnpm build && pnpm typecheck && pnpm lint && pnpm test:unit green (4221 passed); the touched integration suites green (209 passed on 1af72fec00: combined proof, host install proofs, prebuilt payload, web command, compile evidence, artifact validator, dev host install, build, build-compose; 172 passed on 66c72f458: build-compose, package-build, artifact validator, combined proof); pnpm test:packed (full packed pool, 37) green; pnpm docs:site:build green (0 broken links, parity ok). Full pnpm test:integration:run last ran green on 2387162ac. Earlier heads: e91b06825, 565bd77e5.

Hand-offs (posted on #555 and #592 at merge)

  • S4 (npm root): distribution.channels already carries 'npm' when compiler.project.packageName is set; add the npm-specific pointers under distribution (closed keys → schema + parseDistribution + website/docs/*/reference/artifact-manifest.mdx), never under projections[].
  • S5 (INSTALL.md / install.mjs): distribution.install.instructions / .script are the pointers; AB6023/AB6024 already require the files per builtInHosts(selected). Author the contents; keep them files[] rows (the writer indexes them automatically). The generated install.mjs (install/surface.ts) still reads .cursor-plugin/plugin.json and probes .cursor-plugin/mcp.json/mcp.json for runtimeStateRoots (fix: manage derived state across lifecycle #642) because it also installs Agent Plugins packs that have no artifact manifest; when S5 rewrites it, read agent-bundle.manifest.json (projections[cursor].documents.{plugin,mcp}) first and keep the probe only for manifest-less packs.
  • Step 4 (shared compiler policy): compiler is the seam — add policy inputs (cache keys, tool versions, timings) there and bump recordVersion only; manifestVersion stays 2. reindexArtifactManifest is the one path that rewrites a manifest outside build.

…555 S1)

- config/normalize.ts no longer synthesizes a plugin lowering target or a
  shared plugin skill document; loweringHosts is a pure isSkillHost filter.
- adapters/registry.ts registers portable, codex, claude, cursor only;
  adapters/plugin.ts is deleted together with the composite-only capability
  helpers (intersect/union/mergeCapabilityEvidence, capabilityBooleanView).
- targets: ['plugin'] and --target plugin fail with the existing AB4100.
- plugin branches removed from api.ts, pack-inventory, package-build,
  entry-shell, events/projection, lifecycle replay, install/surface,
  install/install, create-agent-bundle options/scaffold, capability JSON,
  and the CLI help text.
- Tests: plugin-bundle.test.ts deleted; plugin rows dropped from the
  adapter, install-surface, normalization and lifecycle suites; acceptance 3
  covered by normalization.test.ts and cli.test.ts.
 S2)

build/build.ts stages every selected host projection into one root at
artifactRoot; build/compose.ts merges byte-identical entries once, fails
same-path collisions with AB4103 in host-name/path order, and refuses
host-scoped components another selected host would discover conventionally
with AB4105. Codex and Cursor hook/MCP documents move beside their manifests;
shared hooks compile to hooks/<name>.<host>.mjs; compiled surfaces are
attributed to the sorted composite identity; the artifact manifest records
only the selected projections. Install, doctor, dev services, eval harnesses,
Workbench, examples, and tests follow the composite root.
…inspect judge the composite root; port #569 consumer and docs fixes

- config/validate.ts: AB4106 when an adapter registered on an advanced
  TargetRegistry is selected beside any other target; the built-in host list
  moves to adapters/composite-layout.ts and install/surface.ts imports it.
- build/compose.ts: planComposite returns the plan beside its diagnostics;
  dev/project-service.ts prepare uses it so validate, inspect, dev report
  AB4103/AB4105 exactly where build refuses (caught by the composite-rules
  parity test ported from #569).
- Ported from #569 (superseded): tests/support/mcp-conformance.ts pluginRoot,
  tests/skill-document-service.test.ts decoy, examples/audiobook-curator and
  examples/hooks-and-scripts READMEs, scripts/measure-hook-cold-start.mjs.
- tests/composite-rules.test.ts: layout-independent rules from #569 (four-host
  root, commands/ dialect collision, INSTALL.md sections, validate parity).
- Stale per-host wording: Cursor capability evidence, doctor/types/cursor
  comments, create-agent-bundle README, docs/diagnostics.md, canvas sample.
…akes into a generated-route server; AB4105 trigger names skills
… root; mcp-apps-compile test selects the portable projection
…e selection identity (codex+portable), matching planCompiledMcpApps after the #585 merge
…ot-w1

# Conflicts:
#	rstest.integration-tests.ts
…nested-root and shared-root tests (#592 boundary)

The generated MCP entry and every hook wrapper derived the event runtime's
endpoint id from `<epoch>:<selection identity>:<root>`, so the composite
selection (`claude+codex`) had become runtime identity, and the runtime
took `events.target` as the tool-call lineage host fallback, reading the
selection as a host. Both are projection selection leaking into runtime
identity (#592 §2). The endpoint is now `<epoch>:<root>` on both sides
(entry-shell, hook-contract, `agent-bundle/test` installed harness), the
`target` field leaves `GeneratedRouteMcpEntryOptions` and
`GeneratedEventRuntimeBinding`, and the lineage fallback is the one host a
single-projection root serves, or none for a composite root; `entries.ts`
requires the selection instead of defaulting it from the composite name.

`AB4105` never fired for a skill: normalization gives every skill every
selected target and per-host frontmatter extensions collide as `AB4103`
instead, so the dead skill branch leaves `compose.ts` and the diagnostics,
reference, and framework-mode prose say so. `AGENT_BUNDLE_HOOK_HOST` leaves
the runtime-environment reference (en/zh) and the test env fixtures; `api.ts`
reuses `isBuiltInHost`; `compose.ts` reuses `sortedProjections`; stale
`<target>/…` doc comments in `test/packed.ts` and `routes/graph.ts` name the
root layout.

Ports from #569: install refuses `--from` naming a directory above the plugin
root for all three hosts (`AB7001`, no host CLI runs), doctor lists Claude
plugins from the root `--from` names and never from a nested `claude/`, and
the Codex validator judges only `.codex-plugin/*` in a root shared with
Claude's `.mcp.json` and `hooks/hooks.json`. Lane C's docs parity pass:
tree drawings gain `commands/` and `rules/`, the folder-discovery shield
names all three guarded paths, over-wide code samples re-padded to 90
columns, `AB4808`/`AB4809` prose and the rsc-agent-runtime README describe
one composite root.
…ost-packs/runtime/…), not under a target partition
… server; document AB7001's composite-root trigger

Self-review pass 1 read the fallback as inferring the root's cardinality
from `allowedTargets`. It is `server.targets ∩ selected` — the hosts whose
MCP documents list the server, so the hosts that can have spawned it — and a
Claude-only server in a Claude+Codex root is correctly assumed to be Claude's.
The comment and the two test names now say so. `docs/diagnostics.md` names
`AB7001` in the `AB700x` family row: the host manifest sits directly under
`--from`, never under `<from>/<host>`.
…rst generated server; judge built-in hosts by adapter identity for the install surface (#578 review)

- planMcpEntriesSurface no longer attaches every event route to the first
  generated-route server: eventRuntimeHosting hosts the runtime in the first
  generated server each selected host's MCP document lists (one process when
  they agree, one per host otherwise) and every hosting server accepts the
  same allowed set; a Claude-only server in a Claude+Codex root no longer
  refuses Codex wrappers while Codex's own server hosts nothing.
- The generated entry carries `hosts` (the selected hosts whose documents
  list the server) separately from `allowedTargets`; the lineage fallback
  reads `hosts`, so a Claude-only server hosting a two-host runtime still
  assumes Claude for an anonymous MCP client (#592).
- TargetRegistry.builtInHost()/builtInHosts() judge the four shipped adapters
  by identity; compose and artifact validation use it for INSTALL.md and
  install.mjs, so an advanced registry's adapter named `portable` earns no
  install surface it never asked for. The pack inventory (manifest names
  only) keeps the name-based requirement it had on main.
…s, distribution; hook index folded into executables.hooks (#592 step 3)
…ion, never as the selection identity

The browser pool compiles every app once for the project's whole selection
(as the build stages it, #555) but the registry's `target` — the preview
profile and the binding's `target` the page sees — is the host the app
mounts as: the override, or the app's first declared target the project
selects, as before. `claude+codex+portable` had leaked into the binding
(#592) and failed examples/mcp-app's browser-app suite in CI.
The hook-index sidecar is gone; these tests now assert executables.hooks
on agent-bundle.manifest.json and pass the required route graph into build().
@changeset-bot

changeset-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bb241b2

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

# Conflicts:
#	docs/diagnostics.md
#	website/docs/en/guide/distribution/installation.mdx
#	website/docs/en/reference/cli.mdx
#	website/docs/en/reference/configuration.mdx
#	website/docs/zh/guide/distribution/installation.mdx
#	website/docs/zh/reference/cli.mdx
#	website/docs/zh/reference/configuration.mdx
… AB4100 lists the four hosts, AB6023/AB6024 and AB7001 describe the one root
@ScriptedAlchemy
ScriptedAlchemy enabled auto-merge (squash) September 5, 2026 19:58
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

CI integration-2 diagnosis: I downloaded artifact 9976012542 and inspected the Playwright trace. The product close completed; the trace shows the Close MCP session click at ~6.67s, the button disabled, and the terminal render replaced it with Reset MCP session by ~6.70s. The test then waited 30s on expect(closeSession).toBeDisabled() because the old locator no longer existed. This is a fixture assertion race, not the expected syntax-error fixtures or a manifest regression. The focused test passes when asserting the phase transition instead: after the click, assert .mcp-page-phase contains /Closing|Session closed/ at the existing browser timeout. No timeout increase is needed. I have that exact fix validated locally and will carry it in #639; #604 currently needs the same two-line assertion fix (or cherry-pick after I push) before its Verify gate can turn green.

…e server identities, malformed launch and projection rows; reindex refuses compiled files and compile evidence
…; copies re-measure to the verified inventory
…AB4741 for payloads, files[] parser and schema, installers
# Conflicts:
#	packages/agent-bundle/tests/web-host-routes-unit.test.ts
…ifest, the runtime state root, or the install receipt entry
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

Re the integration-2 diagnosis (20:12): the assertion race is already fixed on this branch — 9f29d10ef test(workbench): avoid terminal close locator race replaces the toBeDisabled() wait on the old locator with the phase-transition assertion (.mcp-page-phase contains /Closing|Session closed/, existing browser timeout, no timeout increase), after settling the scroll first. It is the same two-line fix you validated; #639 can drop its copy after this merges or keep it, the two are identical.

Head is now 6b7955d69: origin/main (#634 compile-evidence-gated module walk, #646) merged in with the manifest v2 rows kept (AB700x row keeps the AB7001 manifest detail, AB7010 row takes #634's AB7014 consumer-runtime-evidence wording; the architecture consumer table keeps the v2 field names and gains #634's build/compiler.ts/build/external-policy.ts row and the residual-walk description of validate-artifact-modules.ts). Local gates on that head: build, typecheck, lint, unit (4220 passed), the touched integration suites (157 passed), packed pool (37 passed), docs:site:build — all green. Deslop of the post-74ccb99 delta and the fresh reviewer pass follow before auto-merge is armed.

@ScriptedAlchemy
ScriptedAlchemy merged commit 632847a into main Sep 5, 2026
16 checks passed
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
ScriptedAlchemy added a commit that referenced this pull request Sep 5, 2026
…r, transport, pointer (AB6017); install re-measures mode; one portable path rule (#604 follow-up) (#650)

* fix(manifest): launch entries are the rows their server kind compiles to; host documents agree with the launch record; install re-measures bytes and mode; one portable path rule for files[] and receipts

* mcp run: host document must project every launchable manifest server; drop manifest-only fallback

* chore: deslop the Pass 8 fix delta

* fix(validate-artifact): host documents project the launch record in order, over stdio, from the manifest's own pointer (AB6017); schema device-name parity cases

* changeset: manifest launch agreement follow-up
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.

1 participant