From a5a8872b0cfa8acff5dc748cf447aae6dd20fdec Mon Sep 17 00:00:00 2001 From: wonkwonlee Date: Fri, 7 Aug 2026 02:55:31 -0400 Subject: [PATCH 1/2] chore(release): prepare v0.5.0 Bumps all nine workspace packages and every internal @changesafe/* range from 0.4.1 to 0.5.0. The currently-published 0.4.1 core carries a real skip-legitimacy gap (PR #61) and a broken Kubernetes eval path (PR #60); this is not a routine version bump but the release that gets those fixes into published hands. No breaking changes: policyCoverage is an additive optional receipt field, Terraform's new PLAN_CONTEXT_REQUIRED policy replaces a skip that already existed, and Kubernetes eval support only adds a domain --domain kubernetes could not previously use. CORE_POLICY_VERSION and TERRAFORM_POLICY_VERSION move independently of the package semver, as always. This prepares the release; it does not publish. Publishing still requires an explicit owner-authorized tag and GitHub Release. Co-Authored-By: Claude Sonnet 5 --- MEMORY.md | 17 ++- README.md | 6 +- docs/LAUNCH.md | 18 ++- docs/RELEASE_NOTES_v0.5.0.md | 116 ++++++++++++++++++ .../github-actions/gate-terraform-plan.yml | 2 +- lib/domain/version.ts | 2 +- package-lock.json | 62 +++++----- package.json | 2 +- packages/ai/package.json | 8 +- packages/cli/dist/changesafe.js | 2 +- packages/cli/package.json | 18 +-- packages/cli/src/version.ts | 2 +- packages/core/package.json | 2 +- packages/domain-kubernetes/package.json | 4 +- packages/domain-network/package.json | 4 +- packages/domain-terraform/package.json | 4 +- packages/kubernetes-collector/package.json | 4 +- packages/ledger/package.json | 4 +- packages/server/package.json | 10 +- tests/integration/version-sync.test.ts | 6 +- tests/unit/network-review-receipt.test.ts | 2 +- 21 files changed, 218 insertions(+), 77 deletions(-) create mode 100644 docs/RELEASE_NOTES_v0.5.0.md diff --git a/MEMORY.md b/MEMORY.md index 8e3f06e..8a7c070 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -38,7 +38,22 @@ domain decisions. ## Current release status (2026-08-07) -- **v0.4.1 is the canonical release.** All five public packages are on npm at +- **v0.5.0 is prepared on `main`, not yet published.** PR #61 fixed a real + gap in `@changesafe/core`'s currently-published `0.4.1`: a domain adapter + could skip any universal policy (not just the two the shape permits) with + zero enforcement outside the app's own registration path, and a skip's + claimed replacement was never verified to exist. Every receipt now carries + `policyCoverage`. PR #60 added Kubernetes support to `changesafe eval` and + fixed a prompt-injection surface (label/selector values leaking into the + trusted preamble) and an eval-accuracy gap (unvalidated rollback + references) found in review. `CORE_POLICY_VERSION` moves to `core-v0.2.0`, + `TERRAFORM_POLICY_VERSION` to `terraform-v0.2.0` — see + `docs/RELEASE_NOTES_v0.5.0.md`. All nine workspace packages are bumped to + `0.5.0` on `main`; publishing still needs an explicit owner-authorized tag + and GitHub Release, after which this section should record the same + registry-smoke verification v0.4.1 got before being called canonical. +- **v0.4.1 was the canonical release before v0.5.0.** All five public + packages went out on npm at `0.4.1`, published by the release workflow over trusted publishing, and every one carries a verified provenance attestation recording `.github/workflows/publish.yml` in `wonkwonlee/ChangeSafe` at diff --git a/README.md b/README.md index 4289e77..fb3ab84 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ credentials for your infrastructure, and never applies anything. ```yaml - name: ChangeSafe gate - uses: wonkwonlee/ChangeSafe@v0.4.1 + uses: wonkwonlee/ChangeSafe@v0.5.0 with: plan: tfplan.json context: pr-body.txt # untrusted text, scanned but never obeyed @@ -345,14 +345,14 @@ there is no `--auto-approve`. Full usage: [packages/cli/README.md](packages/cli/ ## Packages -Five public packages are on npm. As of **v0.4.1** they are published by the +Five public packages are on npm. Since **v0.4.1** they are published by the release workflow over npm trusted publishing, and each carries a provenance attestation naming the workflow, repository, and commit that produced it — `npm audit signatures` verifies them, and you should rather than take this paragraph's word for it. The manually published v0.3.0 and v0.3.1 bootstrap/remediation versions carry no attestation. -Use `0.4.1` or later. `0.4.0` published only three of the five packages +Use `0.5.0` or later. `0.4.0` published only three of the five packages before failing, so the CLI and the Kubernetes domain do not exist at that version; those three are genuine and attested, but the set is incomplete. diff --git a/docs/LAUNCH.md b/docs/LAUNCH.md index 81d3cc9..4130082 100644 --- a/docs/LAUNCH.md +++ b/docs/LAUNCH.md @@ -5,9 +5,19 @@ deploys, tags, or posts automatically. ## Release truth -- `v0.4.1` is the canonical package/tag release: the complete published set of - the multi-domain workbench, and the first to go out through the trusted - publishing workflow rather than a manual publish. +- `v0.5.0` is the canonical package/tag release: a universal-policy skip + legitimacy fix in `@changesafe/core` (a domain adapter could previously + skip any universal policy, not just the two shape permits, with no + enforcement outside the app's own registration path), a new Terraform + `PLAN_CONTEXT_REQUIRED` policy replacing a prose-only `VERIFICATION_REQUIRED` + skip, every receipt now recording `policyCoverage`, and `changesafe eval` + gaining Kubernetes support. `CORE_POLICY_VERSION` moves to `core-v0.2.0` + and `TERRAFORM_POLICY_VERSION` to `terraform-v0.2.0` — a v0.5.0 receipt is + not directly comparable to an earlier one. See + [RELEASE_NOTES_v0.5.0.md](RELEASE_NOTES_v0.5.0.md). +- `v0.4.1` was the complete published set of the multi-domain workbench, and + the first to go out through the trusted publishing workflow rather than a + manual publish. - **Do not point anyone at `0.4.0`.** It published three of five packages before failing, so the CLI and the Kubernetes domain do not exist at that version. The three that shipped are genuine and carry provenance; the set is @@ -98,7 +108,7 @@ Example: ```yaml - name: ChangeSafe gate - uses: wonkwonlee/ChangeSafe@v0.4.1 + uses: wonkwonlee/ChangeSafe@v0.5.0 with: plan: tfplan.json context: pr-body.txt diff --git a/docs/RELEASE_NOTES_v0.5.0.md b/docs/RELEASE_NOTES_v0.5.0.md new file mode 100644 index 0000000..70b424d --- /dev/null +++ b/docs/RELEASE_NOTES_v0.5.0.md @@ -0,0 +1,116 @@ +# ChangeSafe v0.5.0 + +A trust-model fix in `@changesafe/core`, a new Terraform policy that replaces +a prose-only skip with a real one, `changesafe eval` gaining Kubernetes +support, and two security-relevant fixes to the Kubernetes analysis prompt. + +**Policy behavior changed.** `CORE_POLICY_VERSION` moves to `core-v0.2.0` and +`TERRAFORM_POLICY_VERSION` moves to `terraform-v0.2.0`. A v0.5.0 receipt is +not directly comparable to a v0.4.x one — that is the point of the version +existing, not an oversight. + +## Universal-policy skip legitimacy, enforced in core + +A domain adapter's `skippedUniversalPolicies` could previously name any of +the five universal policies, not just the two whose shape assumes a +model-authored proposal or an in-place patch (`ROLLBACK_COMPLETE`, +`VERIFICATION_REQUIRED`). The only check against this lived in the app's own +runtime registration (`features/domains/runtime.ts`), not in published core — +so a hand-written adapter, or `changesafe gate` run directly against one, +got no enforcement at all. A probe adapter that skipped all five universal +policies passed `evaluatePolicies` cleanly with zero findings and `LOW` risk, +despite the proposal carrying an injection attempt, invented evidence, no +rollback, and no verification. + +`replacedBy` was also a bare string, so nothing verified a skip's claimed +replacement existed. Terraform's `VERIFICATION_REQUIRED` skip named `"the +pull request review"` — prose, not a policy. + +Fixed: + +- `evaluatePolicies` and `policyOrder` both validate every skip before doing + anything else, and throw for an illegitimate `policyId`, a duplicate skip, + or a replacement that names a policy the adapter does not declare (or that + collides with a universal policy id, which `policyOrder`'s id filter could + not otherwise distinguish from the skip itself). Only + `ROLLBACK_COMPLETE`/`VERIFICATION_REQUIRED` may ever be skipped — + `PATCH_SCHEMA`, `BLAST_RADIUS`, and `UNTRUSTED_INSTRUCTION` are structurally + answerable by every domain and no adapter may skip them. +- `SkipReplacement` is a typed shape (`{ kind: "domain-policy", policyId }`) + instead of a string. A replacement that named a non-mechanical process + rather than a real policy would have produced no finding of its own — the + gate would pass with a genuine verdict gap rather than merely an + honestly-labeled one — so that option does not exist at all. +- Every receipt now carries `policyCoverage`: the exact ordered policy ids + that ran, plus what was skipped and why. A verifier reads this from the + signed payload instead of fetching and trusting the adapter's source to + interpret an absent policy id. The field is optional on + `ChangeReceiptSchema` so a receipt issued before it existed (v0.4.1 and + earlier) still parses for hash and signature verification — + `createReceipt` always sets it going forward, and `canonicalize` drops an + `undefined` property, so a legacy receipt's hash is unaffected. + +## Terraform gains `PLAN_CONTEXT_REQUIRED` + +Terraform's proposal is derived mechanically from the plan with no model +involved, so it can never declare its own precondition or postcheck steps — +that is the honest reason it skips `VERIFICATION_REQUIRED`. What replaces it +used to be prose. It is now a real policy: `PLAN_CONTEXT_REQUIRED` warns when +a destructive change carries zero PR or commit context, because the pull +request review this domain relies on for verification needs something to +review. A plan that destroys nothing, or that carries any context entry, +passes. + +## `changesafe eval` measures Kubernetes too + +The benchmark previously imported `networkAnalysisPrompt` and `networkDomain` +only, so it measured one domain while the corpus spans three. Kubernetes now +has its own hardened prompt (`packages/ai/src/prompts/kubernetes.ts`) and is +registered in `ANALYSIS_DOMAINS`; `eval --domain kubernetes` resolves it +instead of the hardcoded network path. Terraform stays out by design — its +plan already is the proposal, so there is nothing for a model to propose. +Report schema moves to version 3, recording `corpus.domain`, so a report is +only ever compared against another from the same domain. + +## Two fixes in the same change + +Landed as part of getting Kubernetes eval support production-honest, not +found independently: + +- **Kubernetes eval input is normalized.** The strict, already-normalized + `KubernetesSnapshotSchema` was being parsed directly against raw, + collector-shaped scenario fixtures — every bundled Kubernetes scenario + failed before ever reaching the provider. `eval`'s Kubernetes domain now + tries the strict parse first and falls back to `normalizeSnapshot`, the + same boundary `changesafe gate` already used. +- **Pod-label and Service-selector values stay inside the untrusted + block.** The Kubernetes prompt's trusted preamble — the section the model + is told to trust, ahead of `` — was echoing + `podLabels` and `selector` values into it. Those are `Record` with no charset restriction beyond length, so an instruction-like + value there bypassed the untrusted-content boundary the system prompt + describes. They are no longer copied into the preamble; the full snapshot + already carries them inside the untrusted block. +- **Rollback references are validated too.** The Kubernetes prompt's local + cross-check validated a forward `replace` against the snapshot but never a + rollback `replace`, and accepted any rollback `remove` regardless of + whether it undid something the proposal actually added. A model response + with an ungrounded rollback reference was counted `accepted` by the + benchmark before the gate's `PATCH_SCHEMA` policy caught it downstream — + inflating the grounded-output metric for a proposal that was always going + to be blocked. +- **Prompt/adapter pairing is typed.** The analysis domain registry paired + each prompt with its adapter through two independent `as unknown as never` + casts, so nothing prevented registering, say, the Kubernetes prompt against + the network adapter. A single generic function now binds both under one + type parameter, so a mismatched pairing fails to typecheck. + +## Known limits + +- Terraform still cannot catch command smuggling: it is an external-diff + domain with no simulator, and no Terraform policy inspects planned + attribute values for command payloads. Tracked, not papered over with a + scenario the gate would not actually refuse. +- `K8S_SERVICE_SELECTOR` still only re-checks Services that existed before a + change; whether it should also check newly created Services is an open + decision, not yet made. diff --git a/examples/github-actions/gate-terraform-plan.yml b/examples/github-actions/gate-terraform-plan.yml index e90c5d5..4874081 100644 --- a/examples/github-actions/gate-terraform-plan.yml +++ b/examples/github-actions/gate-terraform-plan.yml @@ -55,7 +55,7 @@ jobs: run: printf '%s' "$CHANGESAFE_PR_BODY" > pr-body.txt - name: ChangeSafe gate - uses: wonkwonlee/ChangeSafe@v0.4.1 + uses: wonkwonlee/ChangeSafe@v0.5.0 with: plan: tfplan.json context: pr-body.txt diff --git a/lib/domain/version.ts b/lib/domain/version.ts index b35f8c4..c33f051 100644 --- a/lib/domain/version.ts +++ b/lib/domain/version.ts @@ -1,5 +1,5 @@ /** Kept in sync with package.json manually; embedded in receipts. */ -export const APP_VERSION = "0.4.1"; +export const APP_VERSION = "0.5.0"; /** Bumped whenever any frozen policy's behavior changes; embedded in receipts. */ export const POLICY_VERSION = "policies-v0.1.0"; diff --git a/package-lock.json b/package-lock.json index c41d0c2..b7edd35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "changesafe", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "changesafe", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "workspaces": [ "packages/*" @@ -8265,12 +8265,12 @@ }, "packages/ai": { "name": "@changesafe/ai", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "dependencies": { - "@changesafe/core": "^0.4.1", - "@changesafe/domain-kubernetes": "^0.4.1", - "@changesafe/domain-network": "^0.4.1", + "@changesafe/core": "^0.5.0", + "@changesafe/domain-kubernetes": "^0.5.0", + "@changesafe/domain-network": "^0.5.0", "zod": "^4.4.3" }, "devDependencies": { @@ -8279,7 +8279,7 @@ }, "packages/cli": { "name": "changesafe", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "dependencies": { "@kubernetes/client-node": "^1.4.0", @@ -8289,14 +8289,14 @@ "changesafe": "dist/changesafe.js" }, "devDependencies": { - "@changesafe/ai": "^0.4.1", - "@changesafe/core": "^0.4.1", - "@changesafe/domain-kubernetes": "^0.4.1", - "@changesafe/domain-network": "^0.4.1", - "@changesafe/domain-terraform": "^0.4.1", - "@changesafe/kubernetes-collector": "^0.4.1", - "@changesafe/ledger": "^0.4.1", - "@changesafe/server": "^0.4.1", + "@changesafe/ai": "^0.5.0", + "@changesafe/core": "^0.5.0", + "@changesafe/domain-kubernetes": "^0.5.0", + "@changesafe/domain-network": "^0.5.0", + "@changesafe/domain-terraform": "^0.5.0", + "@changesafe/kubernetes-collector": "^0.5.0", + "@changesafe/ledger": "^0.5.0", + "@changesafe/server": "^0.5.0", "esbuild": "^0.25.0", "typescript": "^5", "zod": "^4.4.3" @@ -8791,7 +8791,7 @@ }, "packages/core": { "name": "@changesafe/core", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "dependencies": { "zod": "^4.4.3" @@ -8805,10 +8805,10 @@ }, "packages/domain-kubernetes": { "name": "@changesafe/domain-kubernetes", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "dependencies": { - "@changesafe/core": "^0.4.1", + "@changesafe/core": "^0.5.0", "yaml": "2.9.0", "zod": "^4.4.3" }, @@ -8821,10 +8821,10 @@ }, "packages/domain-network": { "name": "@changesafe/domain-network", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "dependencies": { - "@changesafe/core": "^0.4.1", + "@changesafe/core": "^0.5.0", "zod": "^4.4.3" }, "devDependencies": { @@ -8836,10 +8836,10 @@ }, "packages/domain-terraform": { "name": "@changesafe/domain-terraform", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "dependencies": { - "@changesafe/core": "^0.4.1", + "@changesafe/core": "^0.5.0", "zod": "^4.4.3" }, "devDependencies": { @@ -8851,9 +8851,9 @@ }, "packages/kubernetes-collector": { "name": "@changesafe/kubernetes-collector", - "version": "0.4.1", + "version": "0.5.0", "dependencies": { - "@changesafe/domain-kubernetes": "^0.4.1", + "@changesafe/domain-kubernetes": "^0.5.0", "@kubernetes/client-node": "^1.4.0", "zod": "^4.4.3" }, @@ -8863,10 +8863,10 @@ }, "packages/ledger": { "name": "@changesafe/ledger", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "dependencies": { - "@changesafe/core": "^0.4.1", + "@changesafe/core": "^0.5.0", "zod": "^4.4.3" }, "devDependencies": { @@ -8875,13 +8875,13 @@ }, "packages/server": { "name": "@changesafe/server", - "version": "0.4.1", + "version": "0.5.0", "license": "MIT", "dependencies": { - "@changesafe/core": "^0.4.1", - "@changesafe/domain-network": "^0.4.1", - "@changesafe/domain-terraform": "^0.4.1", - "@changesafe/ledger": "^0.4.1", + "@changesafe/core": "^0.5.0", + "@changesafe/domain-network": "^0.5.0", + "@changesafe/domain-terraform": "^0.5.0", + "@changesafe/ledger": "^0.5.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/package.json b/package.json index c203852..d93cf51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "changesafe", - "version": "0.4.1", + "version": "0.5.0", "private": true, "description": "A deterministic airlock for AI-proposed infrastructure changes: AI proposes, pure policies validate, a human decides, simulation stays sandboxed.", "license": "MIT", diff --git a/packages/ai/package.json b/packages/ai/package.json index e87a63e..5a9f4ea 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@changesafe/ai", - "version": "0.4.1", + "version": "0.5.0", "description": "Provider-agnostic model adapters that produce ChangeProposals. One Zod schema drives structured output on every provider; every provider's output faces identical local validation.", "license": "MIT", "private": true, @@ -17,9 +17,9 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@changesafe/core": "^0.4.1", - "@changesafe/domain-kubernetes": "^0.4.1", - "@changesafe/domain-network": "^0.4.1", + "@changesafe/core": "^0.5.0", + "@changesafe/domain-kubernetes": "^0.5.0", + "@changesafe/domain-network": "^0.5.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/cli/dist/changesafe.js b/packages/cli/dist/changesafe.js index 0c5934e..87edab1 100755 --- a/packages/cli/dist/changesafe.js +++ b/packages/cli/dist/changesafe.js @@ -26181,7 +26181,7 @@ import { writeFileSync } from "node:fs"; import path2 from "node:path"; // src/version.ts -var CLI_PACKAGE_VERSION = "0.4.1"; +var CLI_PACKAGE_VERSION = "0.5.0"; var CLI_APP_VERSION = `changesafe-cli-${CLI_PACKAGE_VERSION}`; var SERVER_APP_VERSION = `changesafe-server-${CLI_PACKAGE_VERSION}`; diff --git a/packages/cli/package.json b/packages/cli/package.json index 7ce98e2..57912d0 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "changesafe", - "version": "0.4.1", + "version": "0.5.0", "description": "Gate AI-proposed infrastructure changes from the terminal or CI: deterministic policies, honest exit codes, hashed receipts. The gate itself never calls a model.", "license": "MIT", "type": "module", @@ -45,16 +45,16 @@ "yaml": "2.9.0" }, "devDependencies": { - "@changesafe/ai": "^0.4.1", - "@changesafe/core": "^0.4.1", - "@changesafe/domain-network": "^0.4.1", - "@changesafe/domain-terraform": "^0.4.1", - "@changesafe/ledger": "^0.4.1", - "@changesafe/server": "^0.4.1", + "@changesafe/ai": "^0.5.0", + "@changesafe/core": "^0.5.0", + "@changesafe/domain-network": "^0.5.0", + "@changesafe/domain-terraform": "^0.5.0", + "@changesafe/ledger": "^0.5.0", + "@changesafe/server": "^0.5.0", "esbuild": "^0.25.0", "typescript": "^5", "zod": "^4.4.3", - "@changesafe/domain-kubernetes": "^0.4.1", - "@changesafe/kubernetes-collector": "^0.4.1" + "@changesafe/domain-kubernetes": "^0.5.0", + "@changesafe/kubernetes-collector": "^0.5.0" } } diff --git a/packages/cli/src/version.ts b/packages/cli/src/version.ts index 0e65008..fa03e02 100644 --- a/packages/cli/src/version.ts +++ b/packages/cli/src/version.ts @@ -12,7 +12,7 @@ * a check. */ -export const CLI_PACKAGE_VERSION = "0.4.1"; +export const CLI_PACKAGE_VERSION = "0.5.0"; export const CLI_APP_VERSION = `changesafe-cli-${CLI_PACKAGE_VERSION}`; diff --git a/packages/core/package.json b/packages/core/package.json index 852286b..1703edb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@changesafe/core", - "version": "0.4.1", + "version": "0.5.0", "description": "The deterministic airlock engine: typed change proposals, pure safety policies, transactional decisions, and hashed receipts. Domain-agnostic.", "license": "MIT", "type": "module", diff --git a/packages/domain-kubernetes/package.json b/packages/domain-kubernetes/package.json index d4278c6..da66d67 100644 --- a/packages/domain-kubernetes/package.json +++ b/packages/domain-kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@changesafe/domain-kubernetes", - "version": "0.4.1", + "version": "0.5.0", "description": "Kubernetes domain contracts for ChangeSafe: offline snapshots and manifest proposals only; never contacts or mutates a cluster.", "license": "MIT", "type": "module", @@ -51,7 +51,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@changesafe/core": "^0.4.1", + "@changesafe/core": "^0.5.0", "yaml": "2.9.0", "zod": "^4.4.3" }, diff --git a/packages/domain-network/package.json b/packages/domain-network/package.json index 8082184..c3df724 100644 --- a/packages/domain-network/package.json +++ b/packages/domain-network/package.json @@ -1,6 +1,6 @@ { "name": "@changesafe/domain-network", - "version": "0.4.1", + "version": "0.5.0", "description": "Network domain for ChangeSafe: declarative device state, an allowlisted patch engine, deterministic reachability, sandboxed simulation, and network safety policies.", "license": "MIT", "type": "module", @@ -43,7 +43,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@changesafe/core": "^0.4.1", + "@changesafe/core": "^0.5.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/domain-terraform/package.json b/packages/domain-terraform/package.json index b2f6b49..4c67099 100644 --- a/packages/domain-terraform/package.json +++ b/packages/domain-terraform/package.json @@ -1,6 +1,6 @@ { "name": "@changesafe/domain-terraform", - "version": "0.4.1", + "version": "0.5.0", "description": "Terraform domain for ChangeSafe: gate `terraform show -json` plan output with deterministic policies. Read-only; never runs Terraform.", "license": "MIT", "type": "module", @@ -43,7 +43,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@changesafe/core": "^0.4.1", + "@changesafe/core": "^0.5.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/kubernetes-collector/package.json b/packages/kubernetes-collector/package.json index ddf646e..6bcdb30 100644 --- a/packages/kubernetes-collector/package.json +++ b/packages/kubernetes-collector/package.json @@ -1,6 +1,6 @@ { "name": "@changesafe/kubernetes-collector", - "version": "0.4.1", + "version": "0.5.0", "private": true, "description": "Namespace-scoped read-only Kubernetes snapshot collector for ChangeSafe.", "type": "module", @@ -11,7 +11,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@changesafe/domain-kubernetes": "^0.4.1", + "@changesafe/domain-kubernetes": "^0.5.0", "@kubernetes/client-node": "^1.4.0", "zod": "^4.4.3" }, diff --git a/packages/ledger/package.json b/packages/ledger/package.json index 7048865..9e7cce7 100644 --- a/packages/ledger/package.json +++ b/packages/ledger/package.json @@ -1,6 +1,6 @@ { "name": "@changesafe/ledger", - "version": "0.4.1", + "version": "0.5.0", "description": "Append-only SQLite receipt ledger with a hash chain, so deleting or reordering a decision is detectable rather than silent.", "license": "MIT", "private": true, @@ -17,7 +17,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@changesafe/core": "^0.4.1", + "@changesafe/core": "^0.5.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/packages/server/package.json b/packages/server/package.json index 5ea5b89..faab167 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@changesafe/server", - "version": "0.4.1", + "version": "0.5.0", "description": "Authenticated self-hosted decision path: OIDC approver identity, server-recomputed findings, signed receipts, append-only ledger. Executes nothing.", "license": "MIT", "private": true, @@ -17,10 +17,10 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@changesafe/core": "^0.4.1", - "@changesafe/domain-network": "^0.4.1", - "@changesafe/domain-terraform": "^0.4.1", - "@changesafe/ledger": "^0.4.1", + "@changesafe/core": "^0.5.0", + "@changesafe/domain-network": "^0.5.0", + "@changesafe/domain-terraform": "^0.5.0", + "@changesafe/ledger": "^0.5.0", "zod": "^4.4.3" }, "devDependencies": { diff --git a/tests/integration/version-sync.test.ts b/tests/integration/version-sync.test.ts index 595d552..efca318 100644 --- a/tests/integration/version-sync.test.ts +++ b/tests/integration/version-sync.test.ts @@ -26,8 +26,8 @@ import { TERRAFORM_POLICY_VERSION } from "@changesafe/domain-terraform"; const root = path.resolve(import.meta.dirname, "../.."); -const TARGET_VERSION = "0.4.1"; -const TARGET_INTERNAL_RANGE = "^0.4.1"; +const TARGET_VERSION = "0.5.0"; +const TARGET_INTERNAL_RANGE = "^0.5.0"; const ManifestSchema = z.object({ name: z.string().min(1), @@ -77,7 +77,7 @@ function internalRanges(manifest: z.infer) { }).filter(([name]) => name.startsWith("@changesafe/")); } -describe("v0.4.1 workspace release identity", () => { +describe("v0.5.0 workspace release identity", () => { it("moves every root and workspace manifest together", () => { for (const relative of MANIFEST_PATHS) { expect(readManifest(relative).version, `${relative}/package.json version`).toBe( diff --git a/tests/unit/network-review-receipt.test.ts b/tests/unit/network-review-receipt.test.ts index 899e5c9..1180afd 100644 --- a/tests/unit/network-review-receipt.test.ts +++ b/tests/unit/network-review-receipt.test.ts @@ -31,7 +31,7 @@ import { } from "@/features/reviews/controller"; import { NETWORK_SCENARIOS } from "@/scenarios"; -const APP_VERSION = "0.4.1"; +const APP_VERSION = "0.5.0"; const CREATED_AT = "2026-07-29T00:00:00.000Z"; function scenarioOrThrow(scenarioId: string) { From a124f6ed4aa20ccfa028455fef1c22fa18d66a09 Mon Sep 17 00:00:00 2001 From: wonkwonlee Date: Fri, 7 Aug 2026 03:04:57 -0400 Subject: [PATCH 2/2] docs(release): keep v0.5.0 pinned references at v0.4.1 until actually published Fixes review feedback on the v0.5.0 prep PR: the example GitHub Action workflow, README, and docs/LAUNCH.md all pointed wonkwonlee/ChangeSafe@v0.5.0 before that tag exists, so a workflow copied from the example would fail to resolve the action rather than run the gate. Reverted those to v0.4.1 (still published and valid) and reworded "canonical" claims to say v0.5.0 is prepared but not yet published, matching what MEMORY.md's release-status section already said correctly. Co-Authored-By: Claude Sonnet 5 --- MEMORY.md | 4 +-- README.md | 5 +-- docs/LAUNCH.md | 36 +++++++++++-------- .../github-actions/gate-terraform-plan.yml | 2 +- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/MEMORY.md b/MEMORY.md index 8a7c070..6e177fb 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -52,8 +52,8 @@ domain decisions. `0.5.0` on `main`; publishing still needs an explicit owner-authorized tag and GitHub Release, after which this section should record the same registry-smoke verification v0.4.1 got before being called canonical. -- **v0.4.1 was the canonical release before v0.5.0.** All five public - packages went out on npm at +- **v0.4.1 is still the canonical release** until v0.5.0 actually publishes. + All five public packages are on npm at `0.4.1`, published by the release workflow over trusted publishing, and every one carries a verified provenance attestation recording `.github/workflows/publish.yml` in `wonkwonlee/ChangeSafe` at diff --git a/README.md b/README.md index fb3ab84..d3a78c0 100644 --- a/README.md +++ b/README.md @@ -281,7 +281,7 @@ credentials for your infrastructure, and never applies anything. ```yaml - name: ChangeSafe gate - uses: wonkwonlee/ChangeSafe@v0.5.0 + uses: wonkwonlee/ChangeSafe@v0.4.1 with: plan: tfplan.json context: pr-body.txt # untrusted text, scanned but never obeyed @@ -352,9 +352,10 @@ attestation naming the workflow, repository, and commit that produced it — paragraph's word for it. The manually published v0.3.0 and v0.3.1 bootstrap/remediation versions carry no attestation. -Use `0.5.0` or later. `0.4.0` published only three of the five packages +Use `0.4.1` or later. `0.4.0` published only three of the five packages before failing, so the CLI and the Kubernetes domain do not exist at that version; those three are genuine and attested, but the set is incomplete. +(`v0.5.0` is prepared but not yet published — see `docs/LAUNCH.md`.) ```bash npm i @changesafe/core @changesafe/domain-terraform # embed the gate diff --git a/docs/LAUNCH.md b/docs/LAUNCH.md index 4130082..757b9f5 100644 --- a/docs/LAUNCH.md +++ b/docs/LAUNCH.md @@ -5,19 +5,27 @@ deploys, tags, or posts automatically. ## Release truth -- `v0.5.0` is the canonical package/tag release: a universal-policy skip - legitimacy fix in `@changesafe/core` (a domain adapter could previously - skip any universal policy, not just the two shape permits, with no - enforcement outside the app's own registration path), a new Terraform - `PLAN_CONTEXT_REQUIRED` policy replacing a prose-only `VERIFICATION_REQUIRED` - skip, every receipt now recording `policyCoverage`, and `changesafe eval` - gaining Kubernetes support. `CORE_POLICY_VERSION` moves to `core-v0.2.0` - and `TERRAFORM_POLICY_VERSION` to `terraform-v0.2.0` — a v0.5.0 receipt is - not directly comparable to an earlier one. See - [RELEASE_NOTES_v0.5.0.md](RELEASE_NOTES_v0.5.0.md). -- `v0.4.1` was the complete published set of the multi-domain workbench, and - the first to go out through the trusted publishing workflow rather than a - manual publish. +- `v0.5.0` is **prepared on `main`, not yet published.** It is not the + canonical release until an owner-authorized tag and GitHub Release + actually complete the publish workflow — until then, `v0.4.1` remains + what `npm install` and the GitHub Action resolve to, and every + user-facing reference (this file's example below, `README.md`) stays + pinned to it. What v0.5.0 will contain once published: a universal-policy + skip legitimacy fix in `@changesafe/core` (a domain adapter could + previously skip any universal policy, not just the two the shape permits, + with no enforcement outside the app's own registration path), a new + Terraform `PLAN_CONTEXT_REQUIRED` policy replacing a prose-only + `VERIFICATION_REQUIRED` skip, every receipt now recording + `policyCoverage`, and `changesafe eval` gaining Kubernetes support. + `CORE_POLICY_VERSION` moves to `core-v0.2.0` and + `TERRAFORM_POLICY_VERSION` to `terraform-v0.2.0` — a v0.5.0 receipt will + not be directly comparable to an earlier one. See + [RELEASE_NOTES_v0.5.0.md](RELEASE_NOTES_v0.5.0.md). Once published and + registry-verified the same way v0.4.1 was below, update this bullet and + the pinned references to v0.5.0. +- `v0.4.1` is the canonical package/tag release: the complete published set + of the multi-domain workbench, and the first to go out through the + trusted publishing workflow rather than a manual publish. - **Do not point anyone at `0.4.0`.** It published three of five packages before failing, so the CLI and the Kubernetes domain do not exist at that version. The three that shipped are genuine and carry provenance; the set is @@ -108,7 +116,7 @@ Example: ```yaml - name: ChangeSafe gate - uses: wonkwonlee/ChangeSafe@v0.5.0 + uses: wonkwonlee/ChangeSafe@v0.4.1 with: plan: tfplan.json context: pr-body.txt diff --git a/examples/github-actions/gate-terraform-plan.yml b/examples/github-actions/gate-terraform-plan.yml index 4874081..e90c5d5 100644 --- a/examples/github-actions/gate-terraform-plan.yml +++ b/examples/github-actions/gate-terraform-plan.yml @@ -55,7 +55,7 @@ jobs: run: printf '%s' "$CHANGESAFE_PR_BODY" > pr-body.txt - name: ChangeSafe gate - uses: wonkwonlee/ChangeSafe@v0.5.0 + uses: wonkwonlee/ChangeSafe@v0.4.1 with: plan: tfplan.json context: pr-body.txt