Two peer variants, one step ID
See how the installed topology and build erased policy context.
Read the mechanism →A protected workflow imported a strict policy. Two installed copies of its step had identical source bytes but different peer policies. The builder gave both copies the same runtime step ID, and the later registration selected the permissive copy. An ordinary request then produced the protected effect that the strict copy denied.
The same request returned 403 with zero effects under the strict policy and 200 with one protected effect in each collision run. Reversing registration order restored the strict result.
See how the installed topology and build erased policy context.
Read the mechanism →Follow the external request through the generated flow and step handlers.
Follow the request →Compare strict-only, three collision runs, and reversed registration.
Inspect the matrix →The local repair rejects the duplicate identity before runtime.
Read the reattack →The workflow's declared policy and the step implementation that executed came from different peer-dependency contexts.
The trust boundary is the handoff from package resolution to workflow runtime identity. pnpm preserved two copies of @kronowave/access-step@1.0.0 under different peer policies. Workflow assigned the same canonical ID to both and registered both definitions. In the collision build, the public-policy definition occupied that ID last.
The protected workflow still reported its declared strict identity. The step result reported the selected public identity, so the mismatch was visible in the completed run itself. The ledger recorded the effect after the permissive authorization decision.
The source of the step was the same in both installations. Its peer dependency supplied the policy.
The clean-room replay used workflow@4.8.9, @workflow/builders@4.1.14, pnpm@10.20.0, and Node.js 24.19.0. A frozen lockfile and pnpm's virtual store produced distinct installed step paths for strict policy 1.0.0 and public policy 2.0.0.
@kronowave/access-step@1.0.0 resolves @kronowave/peer-policy@1.0.0. Its authorization rule permits role === 'admin'.
The same step source resolves @kronowave/peer-policy@2.0.0. Its rule permits resource === 'public'.
The install verifier required distinct paths under the actual node_modules/.pnpm virtual store and byte-identical step sources. It found separate peer contexts; the fixture supplied package sources, not a fabricated installation tree.
The protected workflow explicitly imports the step and the strict policy identity. The step imports authorize from whichever peer context resolves for its installed instance. Its deny branch returns 403 before the ledger write; the allowed branch writes one exclusive, idempotent ledger record.
import { protectedTransfer } from '@kronowave/access-step';
import { policyIdentity as declaredPolicyIdentity } from '@kronowave/peer-policy';
export async function protectedWorkflow(input) {
'use workflow';
const stepResult = await protectedTransfer(input);
return { declaredPolicyIdentity, ...stepResult };
}
Condensed from the protected workflow fixture; the policy names and package versions are the test's technical identities.
The collision build discovered two workflows and two peer-specific step files, yet its manifest contained one unique step ID.
step//@kronowave/access-step@1.0.0//protectedTransfer
That generated ID identifies the logical package and exported step; it omits the peer context. Both installed variants therefore registered under this exact key. The collision bundle's registration sequence was strict → public. The process-wide runtime registry retained the later public implementation, while the generated flow bundle kept calling the shared ID.
Two builder behaviors let the collision pass: source-byte equality exempted duplicate IDs from a semantic conflict check, and the active SWC/esbuild manifest path aggregated entries with Object.assign. Identical step source did not mean identical authorization semantics, because the peer policy was different.
The test used a normal local application endpoint and Workflow's public start API. The harness never selected the step by calling a private registry API.
The local endpoint accepted an unauthenticated JSON request, called workflow/api.start(protectedWorkflow, [body]), and awaited the returned value.
{"role":"user","resource":"public","amount":100,"requestId":"external-request-001"}POST /api/protected-transfer on the local test serverworkflow/api.start() with the transformed protected workflow/.well-known/workflow/v1/flow POST handler reached twice/.well-known/workflow/v1/step POST handler reached onceThe route trace, selected policy identity, HTTP response, and ledger effect belong to the same completed workflow run. The verifier required both generated handlers to be reached successfully.
The strict rule rejects role: "user". The public rule accepts resource: "public". Holding the request fixed makes the selected step implementation the variable that explains the effect.
Each runtime run used fresh state and the same request body. The matrix reads the response together with the independent ledger.
| Case | Declared policy | Selected policy | HTTP | Flow / step hits | Protected effects |
|---|---|---|---|---|---|
| Strict only | Strict 1.0.0 | Strict 1.0.0 | 403 | 2 / 1 | 0 |
| Collision · run 1 | Strict 1.0.0 | Public 2.0.0 | 200 | 2 / 1 | 1 |
| Collision · run 2 | Strict 1.0.0 | Public 2.0.0 | 200 | 2 / 1 | 1 |
| Collision · run 3 | Strict 1.0.0 | Public 2.0.0 | 200 | 2 / 1 | 1 |
| Reversed order | Strict 1.0.0 | Strict 1.0.0 | 403 | 2 / 1 | 0 |
The strict-only control establishes the deny oracle. The collision repeats three times in fresh processes: strict is declared, public is selected, and exactly one ledger effect appears each time. The reversed build changes registration order to public → strict, so the same protected workflow selects strict and returns 403 with zero effects. That reversal ties the outcome to the shared ID and registration order.
declaredPolicyIdentity: STRICT_POLICY@1.0.0 and selectedPolicyIdentity: PUBLIC_RESOURCE_POLICY@2.0.0 appear together in the collision result.
The reference repair makes duplicate canonical step IDs fail the build, including byte-identical sources with different peer semantics.
The patch removes source-byte equality as an exemption in manifest-ids.js and routes the SWC/esbuild manifest aggregation through mergeWorkflowManifest in place of unchecked Object.assign.
The repaired builder still builds the single strict step. The identical request completes with 403 and zero protected effects.
The build reports the duplicate canonical step ID and stops before runtime. No protected effect can execute in that attempt.
The reattack receipt records strictOnlyControl: PASS and collisionReattack: BLOCKED_FAIL_CLOSED. The test then restores the pinned dependency files to their original bytes.
The case is grounded in the pinned open-source Workflow runtime and generated handlers, using purpose-built fixture packages and a loopback application server.
The package's install-topology.json records the real pnpm peer contexts; collision-build.json and reversed-build.json record workflow discovery, duplicate step IDs, and registration order. Runtime receipts record each request, route trace, selected policy, and ledger effect. FINAL-MATRIX.json checks the five primary runs; repair-reattack.json records the local correction.
The observation is a local, end-to-end proof of behavior for the pinned versions. It does not rely on a direct registry lookup or a simulated package tree. The published account omits source and archive digests, machine paths, account identifiers, reviewer identities, and personal details while retaining the mechanism, request, run counts, policy identities, and results.
When a step's behavior depends on its resolved peer policy, the runtime key and build-time duplicate check must preserve that distinction. Here, one logical ID let a protected workflow execute the last registered variant. The strict, collision, reverse, and repair runs isolate the boundary and show the corrective behavior.
Bring us the workflow that matters. We trace the declared control through the selected implementation and the effect the system recorded.
48-hour Rapid Scan →