Skip to content

docs(rfc-0046): design @cloudhal/release-verify and deploy --release (#227) - #230

Merged
Madraka merged 6 commits into
mainfrom
claude/rfc46-release-verify
Nov 14, 2026
Merged

Madraka merged 6 commits into
mainfrom
claude/rfc46-release-verify

Conversation

@Madraka

@Madraka Madraka commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

What

This is a docs-only design addendum to RFC-0046, new §11, for the two consumer tools #227 asks for. They are written up here first, and the code follows once the owner accepts this. No code, contract or package changes.

  • §11a @cloudhal/release-verify: the shared reference implementation of §2's steps and §7's limits.
    • Runtimes: Workers, Node 24 and browsers.
    • Runtime dependencies:
      • @noble/hashes for incremental SHA-256 where Workers' DigestStream is absent;
      • Ajv's standalone runtime helpers plus the ajv-formats date-time check, because the generated validators import them;
      • nothing constructs Ajv at runtime, and a test fails on any other import.
    • Keys: PINNED_KEYS with reduce-only trust.
    • entry-path safety: a linear scanner refuses .., ., empty segments, backslashes, control characters, overlong paths and case-fold duplicates.
    • Two-pass tarball API: the tarball's digest is checked first, then entries are yielded one at a time, each only once it is verified.
    • Stable error codes, which are public API.
    • missingScopes: exact matching plus §3's one rule, that admin covers edit.
    • releaseAssetUrls: all five assets, including the Sigstore bundle.
    • Attestation subpath: an availability check first; the exact flags with --bundle; any non-zero exit counts as failed.
  • §11b cloudhal deploy --release:
    • It deploys the CLI's own version only.

    • It refuses with release-unavailable when there is no published bundle, and never falls back to a local build.

    • Revocation: it ships only after Release signing phase 2: signed keys.json and yanked.json, key expiry, KMS custody (RFC-0046 §9) #226's signed keys.json, which it fetches on every run, refusing hard when the fetch fails.

    • Order: verify, then the tarball digest, then the attestation, then extraction, then wrangler.

    • The second factor is required unless --allow-single-factor is given.

    • Extraction goes into a fresh directory, with resolve checks and exclusive create.

    • Wrangler runs with no_bundle and module types taken from the manifest.

    • What --release means (option B, the owner's choice): it installs the official standard theme of this CLI's version. It does not build the project.

    • Theme check: these are compared byte for byte with this CLI version's own starter:

      • src/, public/, astro.config.mjs and tsconfig.json;
      • root build inputs the starter does not have, which must be absent: skills/, jsconfig.json, any PostCSS config, and a postcss key in package.json.

      The list was checked against the installed Astro 7.4.7 and Vite 8.3.5. vite.config.* is not loaded, because Astro passes configFile: false. .DS_Store, Thumbs.db and desktop.ini are skipped by exact name.

      • CRLF is normalised to LF, so a Windows checkout of an unmodified starter passes.
      • Missing and extra files both count as differences.
      • On any difference, the CLI refuses and lists the files.
    • --discard-local-theme: deploys the official theme anyway and lists what is not deployed. The flag is never implied.

    • Older projects: a project whose starter files come from an older version needs the flag. cloudhal starter sync is named for later and is out of scope.

    • Config: build-class cloudhal.json keys are refused, and the flag does not cover them. These are comments.available, and media.transform unless features.mediaTransformVar is true.

      • The table classes every key and sub-key, with no fallback class. search and comments.rateLimit are deploy.
      • A type mapped over ProjectConfig and tests over OWN_KEYS and the integration's option sub-keys fail on any key left unclassed.
    • Wrangler: the installed wrangler must be at least manifest.toolchain.wrangler. The starter's ^4.158.0 range is not trusted.

    • The gh probe requires gh 2.68.0 or later, the first version with --source-ref/--source-digest, plus every flag in --help. An older gh counts as unavailable, which --allow-single-factor covers.

Open points (defaults in bold)

  1. @noble/hashes for SHA-256 outside Workers (audited, zero-dependency, pinned exactly). Workers use the native crypto.DigestStream. Our own ~120-line SHA-256 would be new cryptographic code in the trust package.
  2. --release takes only the CLI's own version, until §10's runtime configuration lands.
  3. When gh is unavailable, the CLI refuses. --allow-single-factor continues on the release key alone and says so; it is never implied. A failed attestation is always a refusal.
  4. The full-tarball test is opt-in (CLOUDHAL_RELEASE_ASSETS), in its own CI job that downloads v0.9.0. The 7.2 MB tarball is never committed. The committed fixtures are the real statement, signature and manifest.
  5. What notAfter means for releases a key already signed is Release signing phase 2: signed keys.json and yanked.json, key expiry, KMS custody (RFC-0046 §9) #226's decision 6. Until then there is no key-expired code, and PINNED_KEYS carries no dates.
  6. Real theme/skills values in later manifests are not part of this change. Consumers ignore the placeholders.
  7. With --from, a keys.json must be fresh. It is refused when its signed issuedAt is more than 30 days old, so an old list cannot replay trust from before a revocation. This needs Release signing phase 2: signed keys.json and yanked.json, key expiry, KMS custody (RFC-0046 §9) #226's format to carry issuedAt; online runs always fetch the current list.

Revised done-when for #227

The issue's "cloudhal deploy --release 0.9.x" can't be met: CLI 0.9.x has no --release, and --release takes only the CLI's own version. Proposed instead:

  • @cloudhal/release-verify is published, with provenance, from release.yml. Its tests pass on the real v0.9.0 statement, signature and manifest: a flipped byte, a wrong key, a revoked key, a wrong version and a wrong channel are all refused. The opt-in CI job verifies the real v0.9.0 tarball.

  • cloudhal deploy --release in the first CLI version that ships it deploys that version's verified bundle. It refuses:

    • a tampered or unverified bundle;
    • a missing bundle (release-unavailable);
    • a project whose src/, public/, astro.config.mjs or skills/ differ from the starter, unless --discard-local-theme is given;
    • a keys.json that can't be fetched or doesn't verify.

    It depends on Release signing phase 2: signed keys.json and yanked.json, key expiry, KMS custody (RFC-0046 §9) #226's keys.json.

Owner steps when the code lands

  • Publish a placeholder @cloudhal/[email protected] (README only, --tag placeholder), then npm deprecate it.
  • Add the trusted-publisher entry (release.yml, the npm-publish environment). The first real version then comes from release.yml with --provenance. A version published by hand would have none, and planPublish would skip it afterwards.
  • The package joins the fixed group in .changeset/config.json (ADR-0013); that edit is part of the code PR.

Notes

Refs #227

🤖 Generated with Claude Code

Madraka added a commit that referenced this pull request Nov 14, 2026
- Replace the theme/skills hash refusals, which compared placeholders
  (v0.9.0 has empty lists and sha256("")), with a defined
  "unmodified starter" check: template files byte for byte, no extra
  src/, public/ or config files, package.json dependencies, and
  cloudhal.json build-input keys from one table that a test keeps equal
  to what buildSite and prepareAdminWorker read. comments.available and
  media.transform (unless features.mediaTransformVar) count as build
  inputs.
- Entry-path safety: an entry-path code (empty, . and .. segments,
  backslash, control characters, case-fold duplicates, length), a
  linear scanner (rule 13), and extraction into a fresh directory with
  resolve checks, exclusive create, and module types from the manifest.
- First publish: a deprecated 0.0.0-placeholder.0, then the trusted
  publisher, so the first real version has provenance; the fixed group
  in .changeset/config.json.
- Attestation: an availability check first, exact flags, --bundle with
  the Sigstore asset (now in releaseAssetUrls and --from), any
  non-zero exit counts as failed, and it runs before extraction. The
  CLI refuses without the second factor unless --allow-single-factor
  is given.
- Revocation: deploy --release waits for #226's signed keys.json,
  fetched on every run with a hard refusal; the package can ship first.
- Runtime dependencies stated honestly: @noble/hashes, ajv runtime
  helpers and ajv-formats, guarded by a test.
- missingScopes points to §3; key-expired moves to the open points
  (#226 decision 6); invariant-5 tests use the synthetic bundle; a
  release-unavailable refusal when there is no public bundle.

Refs #227

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
Madraka and others added 2 commits November 14, 2026 11:08
…227)

Addendum §11 before building #227:

- 11a: the published verifier: runtimes (WebCrypto Ed25519,
  DecompressionStream, incremental SHA-256: Workers' DigestStream, else
  @noble/hashes, the one audited runtime dependency), generated standalone
  validators for the two roots, PINNED_KEYS with reduce-only trust, the
  two-pass tarball API (digest first, then entries verified one by one),
  stable error codes, missingScopes, the Node-only attestation subpath,
  tests on the real v0.9.0 statement/signature/manifest plus an opt-in
  full-tarball test in its own CI job (the 7.2 MB tarball is never
  committed), and the owner's
  first publish and trusted-publisher entry.
- 11b: deploy --release: the CLI's own version only, verified bytes
  deployed through wrangler with no_bundle, refusals for anything the
  bundle bakes in (theme, settings, design, block types, skills, src/,
  attached or multi-site installs, Workers Builds), attestation when gh is
  available, --from <dir> offline, --dry-run.
- 11c: four open points with defaults.

Refs #227

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
- Replace the theme/skills hash refusals, which compared placeholders
  (v0.9.0 has empty lists and sha256("")), with a defined
  "unmodified starter" check: template files byte for byte, no extra
  src/, public/ or config files, package.json dependencies, and
  cloudhal.json build-input keys from one table that a test keeps equal
  to what buildSite and prepareAdminWorker read. comments.available and
  media.transform (unless features.mediaTransformVar) count as build
  inputs.
- Entry-path safety: an entry-path code (empty, . and .. segments,
  backslash, control characters, case-fold duplicates, length), a
  linear scanner (rule 13), and extraction into a fresh directory with
  resolve checks, exclusive create, and module types from the manifest.
- First publish: a deprecated 0.0.0-placeholder.0, then the trusted
  publisher, so the first real version has provenance; the fixed group
  in .changeset/config.json.
- Attestation: an availability check first, exact flags, --bundle with
  the Sigstore asset (now in releaseAssetUrls and --from), any
  non-zero exit counts as failed, and it runs before extraction. The
  CLI refuses without the second factor unless --allow-single-factor
  is given.
- Revocation: deploy --release waits for #226's signed keys.json,
  fetched on every run with a hard refusal; the package can ship first.
- Runtime dependencies stated honestly: @noble/hashes, ajv runtime
  helpers and ajv-formats, guarded by a test.
- missingScopes points to §3; key-expired moves to the open points
  (#226 decision 6); invariant-5 tests use the synthetic bundle; a
  release-unavailable refusal when there is no public bundle.

Refs #227

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
@Madraka
Madraka force-pushed the claude/rfc46-release-verify branch from 9b9258b to 89fa0aa Compare November 14, 2026 08:08
Madraka and others added 4 commits November 14, 2026 11:21
- The unmodified-starter check no longer blocks upgrades and rollbacks.
  - Files are compared with the starter that created the project,
    through a committed cloudhal.starter.json written by init (a new
    starter-lock root schema), or, for older projects, through a frozen
    starter-history.json generated from the tags.
  - Config build keys are compared with the target, which is the
    running CLI.
  - Dependency versions are ignored and only their names are compared,
    since the bundle is self-contained.
  - The real 0.9.0 to 0.10.0 case and a rollback are in the test list.
- Any file outside the base starter, minus a defined ignore list, is
  refused. This catches a root skills/ directory.
- The config table now has deploy, runtime and build classes, with the
  runtime overrides stated (CLOUDHAL_SITE_ID, dataDir unused by the
  cloudflare target, analytics vars).
- The drift guards test the builds themselves: a static guard over
  astro.config.mjs's project keys, override tests, and admin outputs
  byte-identical across deploy keys.
- The gh probe requires gh 2.68.0 or later (source-ref and
  source-digest) and every flag in --help; an older gh is
  "unavailable", which --allow-single-factor covers.
- Open point 7: --from refuses a keys.json older than 30 days by its
  signed issuedAt (needs #226's format).

Refs #227

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
The owner chose the simpler rule: --release installs the official
standard theme of this CLI's version, and it does not build the project.

- It refuses when src/, public/, astro.config.mjs or a root skills/
  differ from this CLI version's own starter. Files are compared byte
  for byte after CRLF is normalised to LF, and both missing and extra
  files count. The refusal lists the files.
- --discard-local-theme deploys the official theme anyway and lists
  every file that is not deployed. It is never implied.
- A project whose starter files come from an older version needs the
  flag. `cloudhal starter sync` is named for later and is out of scope.
- It still refuses on build-class cloudhal.json keys, which the flag
  does not cover. The deploy/runtime/build table and its drift guards
  stay.
- It checks for a minimum wrangler version: the one in
  manifest.toolchain.wrangler, read from the installed wrangler.
- Removed: cloudhal.starter.json, starter-history.json, the ignore list
  and the base-version logic.

Refs #227

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
- Skip OS metadata files (.DS_Store, Thumbs.db, desktop.ini) by exact
  basename under the compared paths; test public/fonts/v1/.DS_Store.
- The key table has no fallback class. search, comments.rateLimit and
  the analytics budget, retention and rate-limit keys are deploy. A type
  mapped over ProjectConfig (+ media) and its mixed sub-keys, plus tests
  over OWN_KEYS and the integration's option sub-keys, fail on any key
  left unclassed.
- Root build inputs, checked against the installed Astro 7.4.7 and Vite
  8.3.5:
  - tsconfig.json is compared with the starter;
  - jsconfig.json, every PostCSS config Vite loads, and a package.json
    postcss key must not exist;
  - vite.config.* is not loaded (Astro passes configFile: false);
  - .env* cannot bake anything into the starter's code.

Refs #227

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
…#230)

Vite 8.3.5's resolvePostcssConfig searches upward from the project root
and stops at searchForWorkspaceRoot: the nearest pnpm-workspace.yaml,
lerna.json, package.json with workspaces, or deno workspace. Without
one it falls back to the nearest package.json. The --release check now
searches the same directories, so a monorepo's root PostCSS config
cannot be dropped silently. There are test cases for a pnpm workspace
root postcss.config.cjs, an intermediate package.json postcss key, and
a config above the stop directory.

Refs #227

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
@Madraka
Madraka merged commit f3c5a2d into main Nov 14, 2026
8 checks passed
@Madraka
Madraka deleted the claude/rfc46-release-verify branch November 14, 2026 11:59
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