Repository navigation
Phase 2 step 3: the host-action foundation (H1, H2, H4) - #106
Conversation
…l-first Tests for Phase 2 step 3 (docs/blocks/PHASE-2-RELEASE-PLAN.md, §7), before the code they test: - H1 at load: an action node needs an id built only from literals and enum props, and a template's possible names must equal its declared actions, or B0 disables the block (actions.test.ts, the ratchet). - Every example and boundary call dispatches only declared names, each with an empty context; auth-form's largest call too, with no field bound to a data model. - The beta's action manifest binds actions by surface and name; forged, misfiled or stale provenance, edited surfaces, undeclared actions and non-empty contexts bind nothing (manifest.test.ts, the runtime tests). - The default Catalog export stays byte-identical to main's (catalog-export test and the composer smoke suite), and the beta's export carries the manifest end to end (composer-blocks e2e). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6Ax8qvfN3gYUC85oSGnoi
…host guide Phase 2 step 3 (docs/blocks/PHASE-2-RELEASE-PLAN.md, §7), limited to H1, H2 and H4. Composer still executes no action. - H1 (composer-core blocks/actions.ts, in B0): every node the profile gives an action has an id built only from literals and enum props, and the names a template can emit equal its declared actions, exactly. Either failure disables the block at load. - H2 (blocks/manifest.ts; the runtime's withActionManifest): in an experimental build, Preview's "export catalog" adds an actionManifest that binds each surface's actions by surface and name. A surface gets a block binding (id, version, expansion identity, declared actions, streaming policy) only when its provenance re-expands, under an approved bundled sidecar, to exactly the stored surface and the exported messages. - The default export: Preview's builder moves to app/catalog-export.ts, unchanged; the default build's export is byte-identical and carries no manifest code. - H4: docs/blocks/HOST-ACTIONS.md, with server-side authorization, idempotency, streaming, untrusted model content and the auth-form rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6Ax8qvfN3gYUC85oSGnoi
|
CI This failure is not caused by this PR:
No fix exists yet. The owner has asked that this timing-sensitive test be left unaltered. I am re-running the failed job once. Generated by Claude Code |
There was a problem hiding this comment.
🟡 Changes recommended
H1’s name enumeration and limit enforcement do not yet guarantee the promised bounded, exactly reachable action declarations.
3 open findings
What changed in this PR
Adds the experimental host-action foundation without executing actions or changing default Catalog exports.
Changes:
- Validates block action names at load.
- Adds verified action manifests to experimental Catalog exports.
- Documents host responsibilities and adds regression coverage.
| File | Description |
|---|---|
| packages/composer-core/src/blocks/view.ts | Exposes action-bearing components. |
| packages/composer-core/src/blocks/provenance.ts | Exposes provenance validation. |
| packages/composer-core/src/blocks/manifest.ts | Builds verified action manifests. |
| packages/composer-core/src/blocks/manifest.test.ts | Tests manifest binding safeguards. |
| packages/composer-core/src/blocks/load.ts | Integrates action validation into B0. |
| packages/composer-core/src/blocks/index.ts | Exports action and manifest APIs. |
| packages/composer-core/src/blocks/actions.ts | Analyzes action names and events. |
| packages/composer-core/src/blocks/actions.test.ts | Tests action validation and contexts. |
| e2e/composer-prod-smoke.spec.ts | Guards default exports and bundle isolation. |
| e2e/composer-blocks.spec.ts | Tests experimental manifest downloads. |
| docs/blocks/PHASE-2-RELEASE-PLAN.md | Records step 3 status. |
| docs/blocks/HOST-ACTIONS.md | Defines host authorization responsibilities. |
| docs/BLOCKS.md | Summarizes the host-action foundation. |
| apps/composer/lib/blocks-ratchet.test.mjs | Checks declarations and empty contexts. |
| apps/composer/experimental/blocks-runtime.ts | Adds manifests to experimental exports. |
| apps/composer/experimental/blocks-runtime.test.ts | Tests export integration. |
| apps/composer/app/views/preview-view.tsx | Connects Preview to manifest exports. |
| apps/composer/app/state.tsx | Shares project provenance access. |
| apps/composer/app/catalog-export.ts | Extracts existing export helpers. |
| apps/composer/app/catalog-export.test.ts | Guards default export byte identity. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review round 1 (Copilot, #106). Tests that the previous analysis fails: - a name read twice holds one value, so `mode-mode` cannot emit `reset-sign-in`; - an $if no call can take counts nothing, and an $if on an enum or a boolean narrows it in each branch, the default included; - an optional enum may name an action only where an $if guarantees it; - an id read straight from an enum or through a $case is capped at 256 names, and one with more than 4096 combinations is refused; - on narrowing templates and on the shipped auth-form, H1's names equal what the real expander and emitter dispatch over every call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6Ax8qvfN3gYUC85oSGnoi
Each action id is evaluated as the expander evaluates it, over every combination of the enum and boolean props it reads, so a name read twice holds one value. An $if that tests a prop's truthiness or its equality with a literal narrows that prop in each branch, and a branch no call can take counts nothing. The 256-name cap now holds for every id, read straight, through a $case or in a $concat, and an id with more than 4096 combinations is refused. Conditions and cross-field constraints beyond that reading still count, so H1 may count a name no call reaches, never miss one a call can emit. HOST-ACTIONS.md says how the names are counted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6Ax8qvfN3gYUC85oSGnoi
… guard, fail-first Review round 2 (the independent security review of 6eb9d72). Tests the code before this change fails, or that pin what nothing tested: - H1 refuses a template value shaped like an action event, literal or built by a $map (Astryx passes table data through verbatim), and a signature field named event, which a call could fill; - an optional $case with a default names its default where the prop is absent; - each manifest entry carries messagesSha256, the sha256 of its exported messages, end to end in the beta's export; - the manifest's empty-context guard, alone: the emitter is made to write a context, so only the guard can refuse the binding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6Ax8qvfN3gYUC85oSGnoi
…pen the host guide - H1 refuses a template value shaped like an action event, which a passthrough prop would carry into the messages, and a signature field named event, so the load-time proof holds under B2's own reading of an action. - Each manifest entry carries messagesSha256, the sha256 of the messages it describes, so a host can tell them from altered ones. - HOST-ACTIONS.md: look entries up by surface, never by surfaceId (the intent's slug, shared); "declared for that surface" means the entry's own actions with declared true; render only messages matching messagesSha256; derive idempotency keys on the server and bound the store; every string in a call is untrusted; a binding certifies structure, not authorship; an export without a manifest binds nothing; edited tokens or profiles bind nothing. The slug and the confirm-label fallback are stated exactly. - The release plan's status note says where the Catalog export is built now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6Ax8qvfN3gYUC85oSGnoi
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
dspack-studio-composer | 5740707 | Oct 16 2026, 05:26 PM |
…pe, fail-first The B0 issue for an action node without an id now has to name every fallback the emitter would use, an alert dialog's confirm label first; it fails against the current message. A guard pins the recipe the host guide is about to state: a host that parses the pretty-printed download and hashes the UTF-8 bytes of JSON.stringify(messages) gets the entry's messagesSha256 as lowercase hex, with non-ASCII text unescaped, and an escaping serializer gets another hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6Ax8qvfN3gYUC85oSGnoi
…n ids, and H1's exact reach The host guide's two rendering rules conflicted: a host that renders under its own surfaceId changes the bytes messagesSha256 names. It now says to check the hash on the messages exactly as exported, then substitute, and states the recipe: lowercase hex sha256 of the UTF-8 bytes of JSON.stringify(messages), non-ASCII unescaped, with the Python equivalent. Idempotency: a key made only of the session, the effect and its target would answer a second, intended operation with the first one's result. The key is now an operation id the server issued (the surfaceId it renders under can serve), bound to the session, effect, target and parameters. H1's reach is stated exactly: it refuses action events in template props and text and call fields named event, not A2UI function calls, data bindings or object node ids, none of which reaches a client-evaluated property on either packaged reference (now a limit in §7 and a threat row). Also: ids may use a boolean $case, the 4096-combination refusal, which edits drop a binding (a profile edit, or color.primary on shadcn/ui), the missing-id issue names the confirm label, and the release plan's status note names actionEvents. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01R6Ax8qvfN3gYUC85oSGnoi


Step 3 of the Phase 2 release plan (
docs/blocks/PHASE-2-RELEASE-PLAN.md, §7), limited to H1, H2 and H4 and their tests. Composer still executes no action. No deploy, no Cloudflare change and no paid model call. Not merged: the owner decides.Head:
5740707f300f013d3131958ff0cdda1cb8d14354, independently security-reviewed at this exact SHA (no blocker; four nits left for the owner, below).What changes
H1: B0 proves the action allowlist at load (
packages/composer-core/src/blocks/actions.ts, called fromb0Issues).surfacePlan.actionProp, or a v2 route fromsynthesized.action. On both references that meansbuttonandalert-dialog. It names the action with a slug of the node's id. Without an id, it falls back to an alert dialog's confirm label or the node's text, both of which a model can write, and then to the component's name.$caseover an enum or boolean, or joined with$concat. That rules out a string the model fills,$index, and an optional enum without a default.$ifthat tests a prop's truthiness, or its equality with a literal, narrows that prop in each branch.$mapor$concat) is refused, and so is a call field namedevent.HOST-ACTIONS.md, §7).actions, exactly: nothing undeclared, nothing declared but unreachable, nothing declared twice.H2: the beta's Catalog export carries an action manifest (
blocks/manifest.ts;withActionManifestinapps/composer/experimental/blocks-runtime.ts).actionManifest(v1).messagesSha256: the lowercase hex sha256 of the UTF-8 bytes ofJSON.stringify(messages), as exported. The recipe is inHOST-ACTIONS.md§3, with its Python equivalent. A host renders only the messages the entry describes.expansionDigest), declared actions and streaming policy, anddeclared: trueon its actions.block: null, every actiondeclared: false). A surface name plus an action name grants nothing. Verification never throws, and an export without a manifest binds nothing.The default export stays byte-identical.
apps/composer/app/catalog-export.ts; the manifest is inserted only in the experimental branch.blockProvenanceForon the composer context, whichexportProjectnow shares. In a default build it returns{}.H4:
docs/blocks/HOST-ACTIONS.md.messagesSha256recipe, and how a binding is earned.surface, never by the sharedsurfaceId;messagesSha256on the messages as exported, then render them under a surfaceId of your own;Docs. A status line in the release plan and a pointer in
docs/BLOCKS.md.Not in this PR
check-default-artifact.mjs,check-beta-deploy.mjs, both wrangler configs and every workflow are untouched.actionManifestkey is added only to the composer smoke suite's chunk scan, which is test-only.Reviews
Copilot.
6eb9d72and raised three findings. All three were confirmed and fixed, with the tests written first (aa4e3f7, then60c80ae):$concat;$ifbranches no call can take were counted.47958c0marks those resolved and raises one low-severity finding on host rule 4: a key made only of the session, the effect and its target would answer a second, intended operation with the first one's result. This is fixed in5740707.Independent security review. A separate read-only reviewer ran mutation and differential checks at each head.
At
6eb9d72: no blocker. It made four minor findings and three nits:surfaceIdanddeclaredActions;The cap was fixed in
60c80ae; the rest were tested first in97073d2and fixed in47958c0.At
47958c0: no blocker.messagesSha256recipe was under-specified, and rule 2 told hosts to render under their own surfaceId, which changes the hashed bytes.Where testable, these were tested first in
5f8431f; all were fixed in5740707.At
5740707, this head: no blocker.47958c0..5740707that weakens an earlier property.Open nits at this head. These are the owner's call. They are left unfixed so the reviewed head does not move.
$concat.iis the surface whosenameis the entry'ssurface.Evidence
Fail-first.
cad7954: composer-core 64 of 66 new tests fail.actionManifest).aa4e3f7: 10 new H1 tests fail against6eb9d72'sactions.ts. They include a brute-force check that H1's names equal what the real expander and emitter dispatch over every call.97073d2: 7 new tests fail before47958c0: three event-shape refusals and fourmessagesSha256checks.5f8431f: 1 of 85 fails before5740707: the missing-id issue must name the confirm label.messagesSha256as the guide says. It is a guard, so it passes before and after.Mutation check.
manifest.tsfails at least one test: messages equality, stored-surface digest, recorded digest, declared names and empty context, unique surface name, approved block, approved sidecar, trusted provenance, own-property lookup, B0-usable only.manifest-context.test.ts. That test makes the emitter write a context, so only the guard can refuse it.At
5740707, run locally. These are CI's steps, in order. They were run at47958c0and again at this head, with the same results apart from the one test round 3 adds. Playwright ran the repo's configs through local wrappers that point at this container's Chromium.pnpm test.47958c0), composer 420 of 420, block-bench 268 of 268.apps/agentrediscover > restoredConflict5 s timeout (5615 ms here). That test is untouched, and it passes on GitHub.pnpm -r typecheckis clean, and the contractscheck:syncpasses.check-default-artifact(29 files clean of 7 markers);check-beta-deploy.mjs(configs and artifact pass);CI.
5740707:testpassed (run 38939609613) and Workers Builds passed; GitHub reports the PR as mergeable (clean).47958c0:testpassed too (run 38935608343).6eb9d72:testfailed once on the knownapps/agenttimeout, which is not this PR's (see the comment below). It passed on the one re-run.Default artifact. Main and this branch were built at the same path.
blockProvenanceForfolds to() => ({});exportProjectcalls it;{a2uiCatalog, surfaces, exportedAt}and downloads it with the same code;import()and no experimental branch remain.47958c0and at5740707. The default build is byte-identical, in all 26 JS, CSS and media files, to the artifact analysed above. Its HTML and TXT files differ only in the build id and Next's per-build random key. So none of the review-round changes reaches the default bundle.Security notes
context: {}, tested for every example and for the largest call on both references. Its fields carry no data-model binding, so nothing is synced when an action fires.🤖 Generated with Claude Code
https://claude.ai/code/session_01R6Ax8qvfN3gYUC85oSGnoi