From 9c959ea2a5a3702973a3b87f545b51a98824f47e Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Sat, 1 Aug 2026 15:15:37 +0900 Subject: [PATCH] docs: plan release and boot integrity work --- .../2026-08-01-release-boot-integrity.md | 344 ++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-01-release-boot-integrity.md diff --git a/docs/superpowers/plans/2026-08-01-release-boot-integrity.md b/docs/superpowers/plans/2026-08-01-release-boot-integrity.md new file mode 100644 index 0000000..e9cf9d8 --- /dev/null +++ b/docs/superpowers/plans/2026-08-01-release-boot-integrity.md @@ -0,0 +1,344 @@ +# Release and Boot Integrity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make generated Release Manifest V2 artifacts verifiable and make the browser accept only coherent V1/V1 or V2/V2 boot protocol pairs. + +**Architecture:** Zod schemas define artifact shapes and version-specific token projection. Runtime config preserves an exact V1/V2 discriminator through release-manifest loading, where mixed pairs fail before contract or application composition. + +**Tech Stack:** TypeScript 7, Node.js 24, Zod 4, Vitest 4, Vite 8, pnpm 11. + +## Global Constraints + +- Preserve all pre-existing dirty-worktree changes; never reset or restore them. +- Do not stage or commit mixed existing source/test files without explicit user authorization. +- Runtime Config versions accepted by browser boot are exactly `"1"` and `"2.0"`. +- Release Manifest V1 is read-only compatibility; all writers emit V2. +- V2 never requires or emits `API_CONTRACT_VERSION`/`apiContractVersion`. +- V2 contract identity is `contractSet.setDigest` and the full package set. +- Local/development endpoints allow only HTTP or HTTPS; staging/production allow only HTTPS. +- Every production behavior change must be preceded by a failing test. + +--- + +### Task 1: Executable release artifact schemas and token projection + +**Files:** +- Create: `scripts/contracts/release-artifacts.ts` +- Create: `tests/unit/release-artifacts.test.ts` +- Modify: `src/contracts/release-tokens.ts` + +**Interfaces:** +- Produces: `releaseManifestV1ArtifactSchema`, `releaseManifestV2ArtifactSchema`, `releaseManifestArtifactSchema`, `runtimeConfigV1ArtifactSchema`, `runtimeConfigV2ArtifactSchema`, `runtimeConfigArtifactSchema`, and `buildManifestArtifactSchema`. +- Produces: `parseReleaseArtifact(value)`, `parseRuntimeConfigArtifact(value)`, `parseBuildManifestArtifact(value)`. +- Produces: `projectReleaseTokens(release)` returning common tokens plus exactly one of `apiContractVersion` or `contractSetDigest`. +- Consumes: `contractSetSchema` from `src/contracts/contract-set.ts`. + +- [ ] **Step 1: Write failing V2 projection tests** + +```ts +it("projects the nested V2 contract-set digest without a legacy scalar", () => { + const release = parseReleaseArtifact(v2ReleaseFixture); + expect(projectReleaseTokens(release)).toMatchObject({ + schemaVersion: 2, + contractSetDigest: v2ReleaseFixture.contractSet.setDigest, + }); + expect(projectReleaseTokens(release)).not.toHaveProperty("apiContractVersion"); +}); + +it("rejects a V2 release carrying the removed scalar", () => { + expect(() => parseReleaseArtifact({ + ...v2ReleaseFixture, + apiContractVersion: "1", + })).toThrow(); +}); +``` + +- [ ] **Step 2: Run tests and confirm RED** + +Run: `corepack pnpm exec vitest run tests/unit/release-artifacts.test.ts` + +Expected: module/export resolution failure because the artifact contract module does not exist. + +- [ ] **Step 3: Implement discriminated artifact schemas** + +```ts +export const releaseManifestArtifactSchema = z.discriminatedUnion( + "schemaVersion", + [releaseManifestV1ArtifactSchema, releaseManifestV2ArtifactSchema], +); + +export type ReleaseArtifact = z.output; + +export function projectReleaseTokens(release: ReleaseArtifact) { + const common = { + schemaVersion: release.schemaVersion, + appVersion: release.appVersion, + buildId: release.buildId, + commitSha: release.commitSha, + configSchemaVersion: release.configSchemaVersion, + assetManifestHash: release.assetManifestHash, + releaseId: release.releaseId, + builtAt: release.builtAt, + } as const; + return release.schemaVersion === 1 + ? { ...common, apiContractVersion: release.apiContractVersion } + : { ...common, contractSetDigest: release.contractSet.setDigest }; +} +``` + +Build Manifest V1 must include the fields currently emitted by the generator: +`releaseId`, `moduleInventoryHash`, `buildContext.sourceDateEpoch`, and output +paths for module inventory, route chunks, and runtime-config schema. + +- [ ] **Step 4: Run focused tests and confirm GREEN** + +Run: `corepack pnpm exec vitest run tests/unit/release-artifacts.test.ts tests/unit/release-coherence.test.ts` + +Expected: all tests pass; V1 projection retains `apiContractVersion`; V2 projection contains only `contractSetDigest`. + +### Task 2: Generate and verify artifacts through the same contracts + +**Files:** +- Modify: `scripts/generate-build-manifest.ts` +- Modify: `scripts/verify-release.ts` +- Modify: `schemas/artifacts/build-manifest.schema.json` +- Test: `tests/unit/release-artifacts.test.ts` + +**Interfaces:** +- Consumes Task 1 parsers and token projection. +- Produces V2 release and V1 build manifest that have been parsed before write. + +- [ ] **Step 1: Add failing parser/writer round-trip tests** + +Assert that the exact generator shapes parse, that unknown root/output fields +fail, and that a runtime-config V2 artifact parses without +`API_CONTRACT_VERSION`. + +- [ ] **Step 2: Run the tests and confirm RED** + +Run: `corepack pnpm exec vitest run tests/unit/release-artifacts.test.ts` + +Expected: current build-manifest schema/parser rejects emitted fields or the V2 runtime parser requires the removed scalar. + +- [ ] **Step 3: Parse before every write and parse before verification** + +In `generate-build-manifest.ts`, wrap the existing build-manifest object with +`buildManifestArtifactSchema.parse(...)`, wrap the existing release-manifest +object with `releaseManifestV2ArtifactSchema.parse(...)`, and replace the +runtime-config parser with `runtimeConfigV2ArtifactSchema.parse(...)`. Preserve +the exact existing values and output paths; the schema call is the only writer +boundary added in this step. + +In `verify-release.ts`, replace `CompatibilityTuple` parsing and the loop over +all registry keys with version-specific projection. Keep legacy coherence +fixtures on the existing numeric compatibility policy, but do not apply that +legacy tuple parser to V2 artifacts. + +- [ ] **Step 4: Generate JSON Schema from the executable build schema** + +Replace the checked-in `schemas/artifacts/build-manifest.schema.json` with the +deterministic `z.toJSONSchema(buildManifestArtifactSchema)` representation. +The generated schema must use draft 2020-12 and `additionalProperties: false`. + +- [ ] **Step 5: Run focused tests and confirm GREEN** + +Run: `corepack pnpm exec vitest run tests/unit/release-artifacts.test.ts tests/unit/release-coherence.test.ts` + +Expected: all release artifact and legacy compatibility tests pass. + +- [ ] **Step 6: Run the actual release pipeline in an isolated temporary copy** + +Run the existing contract generation, app build, manifest generation, and +`node scripts/verify-release.ts` with local build environment values. + +Expected: release verification exits 0 and reports no +`releaseToken:apiContractVersion` or `releaseToken:contractSetDigest` mismatch. + +### Task 3: Exact runtime-config version and endpoint selection + +**Files:** +- Modify: `src/bootstrap/runtime-config-schema.ts` +- Modify: `tests/runtime-schema/runtime-config.test.ts` + +**Interfaces:** +- Produces `RuntimeConfigValidation` with a reliable `schema: "V1" | "V2"` discriminator. +- Keeps the existing normalized `RuntimeConfig` facade for downstream callers. + +- [ ] **Step 1: Add failing future-version and protocol tests** + +```ts +it.each(["0", "1.0", "2.0.1", "3.0"])( + "rejects unsupported boot config version %s", + (version) => { + expect(validateRuntimeConfig({ + ...validV1Config, + CONFIG_SCHEMA_VERSION: version, + }).success).toBe(false); + }, +); + +it.each(["file:///tmp/api/", "data:text/plain,x", "blob:https://test/id"])( + "rejects non-http endpoint %s", + (API_BASE_URL) => { + expect(validateRuntimeConfig({ ...validConfig, API_BASE_URL }).success).toBe(false); + }, +); +``` + +- [ ] **Step 2: Run tests and confirm RED** + +Run: `corepack pnpm exec vitest run tests/runtime-schema/runtime-config.test.ts` + +Expected: `3.0` and at least `file:`/`data:` cases are currently accepted. + +- [ ] **Step 3: Implement literal version dispatch and scheme allow-list** + +```ts +export const runtimeConfigV1Schema = base.extend({ + CONFIG_SCHEMA_VERSION: z.literal("1"), + API_CONTRACT_VERSION: z.string().regex(VERSION_PATTERN), +}).strict().superRefine(runtimeConfigInvariants); + +const selectedSchema = declared === "1" + ? runtimeConfigV1Schema + : declared === "2.0" + ? runtimeConfigV2Schema + : null; +``` + +`assertEndpointUrl` must reject every protocol outside `http:` and `https:` +before applying the non-local HTTPS rule. + +- [ ] **Step 4: Run tests and confirm GREEN** + +Run: `corepack pnpm exec vitest run tests/runtime-schema/runtime-config.test.ts` + +Expected: exact V1/V2 cases pass and future/non-HTTP cases fail. + +### Task 4: Enforce config/manifest protocol pairing + +**Files:** +- Modify: `src/bootstrap/load-release-manifest.ts` +- Modify: `tests/runtime-schema/release-manifest.test.ts` + +**Interfaces:** +- Adds `MANIFEST_PROTOCOL_PAIR_MISMATCH` to `ReleaseManifestErrorCode`. +- Requires V1 runtime config with V1 manifest and V2 runtime config with V2 manifest. + +- [ ] **Step 1: Replace the permissive compatibility test with a pairing matrix** + +```ts +it.each([ + ["V1", 2], + ["V2", 1], +] as const)("rejects %s runtime with manifest V%s", async (configSchema, schemaVersion) => { + await expect(loadReleaseManifest( + runtimeFor(configSchema), + { fetcher: async () => jsonResponse(manifestFor(schemaVersion)) }, + )).rejects.toMatchObject({ code: "MANIFEST_PROTOCOL_PAIR_MISMATCH" }); +}); +``` + +Also test that a V1 scalar mismatch fails, and that V2 contract-set verification +is mandatory rather than conditional. + +- [ ] **Step 2: Run tests and confirm RED** + +Run: `corepack pnpm exec vitest run tests/runtime-schema/release-manifest.test.ts` + +Expected: V2 runtime plus V1 manifest currently resolves successfully. + +- [ ] **Step 3: Implement pair validation before tuple checks** + +```ts +const expectedManifestVersion = runtime.configSchema === "V1" ? 1 : 2; +if (manifest.schemaVersion !== expectedManifestVersion) { + throw new ReleaseManifestError("MANIFEST_PROTOCOL_PAIR_MISMATCH", identity); +} +``` + +For V1, require both legacy scalar values and compare them. For V2, require the +contract set and always call `verifyContractSet`. Do not use presence checks to +choose security validation. + +- [ ] **Step 4: Run tests and confirm GREEN** + +Run: `corepack pnpm exec vitest run tests/runtime-schema/release-manifest.test.ts tests/runtime-schema/runtime-config.test.ts` + +Expected: complete pairing matrix passes and all tampered V2 sets fail. + +### Task 5: Close boot cancellation and timing semantics + +**Files:** +- Modify: `src/bootstrap/read-bounded-boot-json.ts` +- Modify: `src/bootstrap/load-runtime-config.ts` +- Modify: `tests/runtime-schema/runtime-config.test.ts` +- Create: `tests/unit/read-bounded-boot-json.test.ts` + +**Interfaces:** +- Pre-aborted external signals prevent fetch admission. +- `validationDurationMs` excludes network acquisition. + +- [ ] **Step 1: Add a failing pre-abort test** + +Create an already-aborted controller, call `readBoundedBootJson`, and assert the +fetcher is never called and the outcome is a stable fetch/abort failure. + +- [ ] **Step 2: Add a failing network-exclusion timing test** + +Use a deferred fetcher and a deterministic `now()` sequence. Assert that elapsed +network time does not contribute to `validationDurationMs`. + +- [ ] **Step 3: Run both tests and confirm RED** + +Run: `corepack pnpm exec vitest run tests/unit/read-bounded-boot-json.test.ts tests/runtime-schema/runtime-config.test.ts` + +- [ ] **Step 4: Implement admission precheck and move the timer start** + +Check `options.signal?.aborted` before installing listeners or invoking fetch. +In `loadRuntimeConfig`, set `startedAt` immediately after a successful bounded +read and before safe-name/schema validation. + +- [ ] **Step 5: Run both tests and confirm GREEN** + +Run the same focused command and expect all tests to pass with no leaked abort +listeners or timers. + +### Task 6: Full verification and handoff + +**Files:** +- Verify all files touched by Tasks 1-5. + +- [ ] **Step 1: Run focused suites** + +Run: + +```sh +corepack pnpm exec vitest run \ + tests/unit/release-artifacts.test.ts \ + tests/unit/release-coherence.test.ts \ + tests/unit/read-bounded-boot-json.test.ts \ + tests/runtime-schema/runtime-config.test.ts \ + tests/runtime-schema/release-manifest.test.ts +``` + +- [ ] **Step 2: Run repository static and non-browser suites** + +Run `corepack pnpm check:types`, `corepack pnpm lint`, and +`corepack pnpm test:all`. + +- [ ] **Step 3: Run release verification from a clean generated output** + +Run the complete local build and `corepack pnpm verify:release`. Record the +actual exit status and mismatch list. + +- [ ] **Step 4: Run diff hygiene** + +Run `git diff --check` and a NUL-byte scan. Do not attribute pre-existing +unrelated failures to this sub-project. + +- [ ] **Step 5: Report exact remaining gates** + +List passing commands, failing commands, files changed, and any browser-only +coverage that still requires a Playwright-capable environment.