Skip to content

fix(core): enforce universal-policy skip legitimacy, record it on receipts - #61

Merged
wonkwonlee merged 2 commits into
mainfrom
wonkwonlee/next-phase-planning
Aug 7, 2026
Merged

fix(core): enforce universal-policy skip legitimacy, record it on receipts#61
wonkwonlee merged 2 commits into
mainfrom
wonkwonlee/next-phase-planning

Conversation

@wonkwonlee

Copy link
Copy Markdown
Owner

Summary

Code review found that skippedUniversalPolicies (packages/core/src/domain.ts) let a domain adapter skip any of the five universal policies, and the only legitimacy check lived in the app's runtime registration (features/domains/runtime.ts), not published core — so changesafe gate against a hand-written or third-party adapter got zero enforcement. A probe adapter that skipped all five universal policies passed through evaluatePolicies cleanly with 0 findings and LOW risk, despite the proposal containing an injection attempt, invented evidence, no rollback, and no verification. replacedBy was also a bare string, so nothing verified a skip's claimed replacement actually existed — Terraform's VERIFICATION_REQUIRED skip named "the pull request review", which is prose, not a policy.

  • SKIPPABLE_UNIVERSAL_POLICY_IDS — only ROLLBACK_COMPLETE and VERIFICATION_REQUIRED may ever be skipped; PATCH_SCHEMA, BLAST_RADIUS, UNTRUSTED_INSTRUCTION are structurally answerable by every domain and no adapter may skip them.
  • evaluatePolicies and policyOrder both call a shared validateSkips and throw for an illegitimate policyId, a duplicate skip, or a "domain-policy" replacedBy the adapter doesn't actually declare — enforced in core itself, so a third-party adapter reaching the gate directly gets the identical guarantee the app's registration path gets.
  • SkipReplacement is a typed discriminated union ({kind:"domain-policy",policyId} | {kind:"out-of-band",process}) instead of a bare string.
  • Every receipt now carries policyCoverage (computePolicyCoverage): the exact ordered policy ids that ran plus what was skipped and why, so a verifier reads this from the signed payload instead of trusting the adapter's source.
  • Terraform's VERIFICATION_REQUIRED skip now names a real domain policy, PLAN_CONTEXT_REQUIRED (WARNs when a destructive change carries no PR/commit context), replacing the "pull request review" prose.
  • CORE_POLICY_VERSIONcore-v0.2.0, TERRAFORM_POLICY_VERSIONterraform-v0.2.0.
  • 7 Terraform scenario expectations.json updated for the new policy; n/o/t got realistic PR context added to their fixtures to preserve their original MEDIUM/HIGH-risk narratives; docs/SCENARIOS.md regenerated.

Test plan

  • npm run lint
  • npm run typecheck
  • npm run build
  • npm run build:cli
  • npm test — 1173 passed, 4 skipped
  • node packages/cli/dist/changesafe.js scenario check
  • node packages/cli/dist/changesafe.js scenario gallery --check
  • New tests: skip-legitimacy enforcement (tests/unit/policies.test.ts), PLAN_CONTEXT_REQUIRED PASS/WARN behavior (packages/domain-terraform/tests/terraform.test.ts)

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
change-safe Ready Ready Preview Aug 7, 2026 6:34am

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed5aa84381

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/src/domain.ts Outdated
Comment thread packages/core/src/receipt.ts Outdated
Comment thread packages/core/src/policies/index.ts Outdated
wonkwonlee added a commit that referenced this pull request Aug 7, 2026
…parseable

Addresses PR #61 review feedback:

- policyCoverage is now optional on ChangeReceiptSchema. Making it required
  broke `changesafe verify` and ledger reads for every receipt issued before
  this field existed (v0.4.1 and earlier) with a schema-parse error before
  the hash/signature check ever ran. `createReceipt` still always sets it,
  and canonicalize drops undefined properties, so a legacy receipt's hash is
  unaffected by the field's absence.
- SkipReplacement drops the "out-of-band" variant entirely. A replacement
  that names a non-mechanical process produces no finding, so the gate would
  pass with a genuine verdict gap rather than merely an honestly-labeled one
  — every skip must now name a real domain policy that actually ran.
- validateSkips rejects a replacement whose policyId collides with a
  universal policy id: policyOrder's filter removes every occurrence of a
  skipped id, so a same-named domain policy would vanish from
  policyCoverage despite having actually evaluated and produced a finding.

Full gate green: lint, typecheck, build, build:cli, 1174 tests, scenario
check, gallery, and `changesafe verify` against the published v0.1.0 receipt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
wonkwonlee and others added 2 commits August 7, 2026 02:31
…eipts

An adapter could declare skippedUniversalPolicies for any of the five
universal policies, not just the two whose shape assumes a simulated-state
domain — and the only check was in the app's runtime registration, not
published core, so `changesafe gate` against a third-party adapter got no
enforcement at all. replacedBy was also a bare string, so a skip's stated
replacement was never verified to exist.

evaluatePolicies and policyOrder now both validate every skip: only
ROLLBACK_COMPLETE and VERIFICATION_REQUIRED may ever be skipped, never
twice each, and a "domain-policy" replacement must name a policy the
adapter actually declares. SkipReplacement is a typed union instead of
prose. Every receipt now carries policyCoverage (computePolicyCoverage),
so a verifier reads what ran and what was skipped from the signed payload
rather than trusting the adapter's source.

Terraform's VERIFICATION_REQUIRED skip now names a real domain policy
(PLAN_CONTEXT_REQUIRED — WARN when a destructive change carries no PR or
commit context) instead of "the pull request review" as prose.
CORE_POLICY_VERSION and TERRAFORM_POLICY_VERSION both bump accordingly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…parseable

Addresses PR #61 review feedback:

- policyCoverage is now optional on ChangeReceiptSchema. Making it required
  broke `changesafe verify` and ledger reads for every receipt issued before
  this field existed (v0.4.1 and earlier) with a schema-parse error before
  the hash/signature check ever ran. `createReceipt` still always sets it,
  and canonicalize drops undefined properties, so a legacy receipt's hash is
  unaffected by the field's absence.
- SkipReplacement drops the "out-of-band" variant entirely. A replacement
  that names a non-mechanical process produces no finding, so the gate would
  pass with a genuine verdict gap rather than merely an honestly-labeled one
  — every skip must now name a real domain policy that actually ran.
- validateSkips rejects a replacement whose policyId collides with a
  universal policy id: policyOrder's filter removes every occurrence of a
  skipped id, so a same-named domain policy would vanish from
  policyCoverage despite having actually evaluated and produced a finding.

Full gate green: lint, typecheck, build, build:cli, 1174 tests, scenario
check, gallery, and `changesafe verify` against the published v0.1.0 receipt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@wonkwonlee
wonkwonlee force-pushed the wonkwonlee/next-phase-planning branch from 9189e0c to 22af73a Compare August 7, 2026 06:33
@wonkwonlee
wonkwonlee merged commit 7dd93b0 into main Aug 7, 2026
9 checks passed
@wonkwonlee
wonkwonlee deleted the wonkwonlee/next-phase-planning branch August 7, 2026 06:37
@wonkwonlee wonkwonlee mentioned this pull request Aug 7, 2026
9 tasks
wonkwonlee added a commit that referenced this pull request Aug 7, 2026
* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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