diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e020613a..ec87365a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install ".[dev]" + python -m venv --system-site-packages .venv - name: Verify kernel phase env: MATRIX_PYTHON: ${{ matrix.python-version }} diff --git a/docs/INDEX.md b/docs/INDEX.md index 4e1c63a1..aa75f9ab 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -212,6 +212,18 @@ runtime acceptance and does not authorize a command, launch, token refund, retry, restart, replacement, receipt, or downstream transition. +The additive versioned +[`lifecycle-terminal-failure-clean-commit-authority-v2`](decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json) +preserves both clean-commit v1 authority files byte-for-byte and closes the +clean-hosted-checkout environment seam exposed by PR #448. It binds the one +exact workflow change that creates the matrix interpreter's repository-local +`.venv` with `python -m venv --system-site-packages .venv` after the existing +tooling install and before verification. The v2 verifier admits only the exact +18-path authority delta or exact 18-plus-seven clean integrated transition; +missing, extra, partial, wrong-workflow, wrong-lineage, and wrong-candidate +states fail closed. It adds no network install, qualification invocation, +launch, refund, retry, receipt, runtime acceptance, or downstream authority. + The V11 candidate must construct and validate the exact overlay/cohort-bound receipt and non-null stdout/stderr/output evidence before its first durable `completed` finalization. Evidence read/hash/parse/validation/finalization diff --git a/docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json b/docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json new file mode 100644 index 00000000..1e5036f6 --- /dev/null +++ b/docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json @@ -0,0 +1,143 @@ +{ + "schema": "codex-usage-tracker.ck07r1-lifecycle-terminal-failure-clean-commit-authority.v2", + "version": 2, + "task": "CK-07R1", + "status": "permitted_not_accepted", + "authority_base_sha": "487e0b7138d638d7cfb1d91627e5a6ebda743699", + "authority_base_tree_sha": "2eef42c34cee001c40333e4e3d0af49e09bd7ec8", + "source_authority": [ + { + "path": "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v1.json", + "sha256": "badda5361f66944eb4972c061103435d55c6d59c2797ce7db8f34c70662d1e02" + }, + { + "path": "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v1.schema.json", + "sha256": "0819f90addbacccc2d45a467cce134a4a8112bd836cbfe2cbe9702b163ace03b" + } + ], + "superseded_immutable_paths": [ + { + "path": ".github/workflows/ci.yml", + "before_sha256": "f25ea89fb207b2d7a9ff1c18953865ef91cb73cfe23796f545d304a40cc8959c", + "sha256": "ce1dd9c0324c32e00298aeb90100539796d583272dad4059cd81c5564b13437e" + }, + { + "path": "scripts/ck07r1_prelaunch_recovery.py", + "before_sha256": "a3f6376f9f1328b5ccce3d8b16486b87cefe4d2b6c783a4e2a706e71142b1fc4", + "sha256": "36dbe7a7fc2aaf70a5458ebbb7c9e25a271662e3d6883631311f37ef2499a5d0" + }, + { + "path": "scripts/ck07r1_shared_successor_overlay.py", + "before_sha256": "f3745ec07bf47ee15f50969132f315aec61d407c745f6c63694e9910a88c5768", + "sha256": "05ddf028f9cfda30b78d8756e21940c7915796357cef79c7a632da31dba60168" + } + ], + "implementation_transition": { + "pull_request": 448, + "source_base_sha": "652f2166b58b9ee0d719348a769901577d11e6fd", + "source_head_sha": "927aa06f7c4c88319cc30247343c40db8e9b817e", + "candidate_scope_source": "bound_v1_clean_commit_authority", + "representations": { + "dirty_prepublication": { + "head": "exact_v2_authority_head", + "worktree_delta": "exact_candidate_scope", + "committed_delta": "exact_v2_authority_scope" + }, + "clean_integrated": { + "base": "exact_v2_authority_base", + "worktree_delta": "empty", + "committed_delta": "exact_v2_authority_scope_plus_candidate_scope", + "source_identity": "bound_v1_source_head_or_exact_tree_equivalent_squash" + } + } + }, + "ci_environment_transition": { + "workflow_path": ".github/workflows/ci.yml", + "before_sha256": "f25ea89fb207b2d7a9ff1c18953865ef91cb73cfe23796f545d304a40cc8959c", + "sha256": "ce1dd9c0324c32e00298aeb90100539796d583272dad4059cd81c5564b13437e", + "matrix_python": [ + "3.10", + "3.14" + ], + "install_step": "Install kernel tooling", + "command": [ + "python", + "-m", + "venv", + "--system-site-packages", + ".venv" + ], + "required_before_step": "Verify kernel phase", + "lexical_interpreter": ".venv/bin/python", + "matching_sys_prefix": ".venv", + "network_access_added": false, + "qualification_command_invocations": 0 + }, + "decision": { + "root_cause": "clean_hosted_checkout_lacked_bound_repository_venv", + "v1_authority_bytes_preserved": true, + "dirty_prepublication_remains_valid": true, + "clean_committed_transition_permitted": true, + "implementation_acceptance": "not_claimed", + "runtime_acceptance": "not_claimed", + "new_command_invocations_permitted": 0, + "launch_authorized": false, + "token_consumed": true, + "token_refund": false, + "retry": "none", + "restart": "none", + "replacement": "none", + "receipt_fabrication": "forbidden", + "post_single_run": "unavailable_without_complete_planner_valid_receipt", + "final_accepted": "unavailable" + }, + "scope": { + "authority_write_scope": [ + ".github/workflows/ci.yml", + "docs/INDEX.md", + "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json", + "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.schema.json", + "docs/roadmap/REMAINING_EXECUTION_PLAN.md", + "docs/roadmap/TASK_PACKETS.md", + "docs/roadmap/tasks/ck-07r1-correct-lifecycle-preparation-scale.md", + "scripts/check_kernel_scope.py", + "scripts/ck07r1_consuming_boundary.py", + "scripts/ck07r1_prelaunch_recovery.py", + "scripts/ck07r1_shared_successor_overlay.py", + "scripts/ck07r1_terminal_failure_correction.py", + "tests/kernel/test_ck07r1_consuming_boundary_authority.py", + "tests/kernel/test_ck07r1_prelaunch_recovery_authority.py", + "tests/kernel/test_ck07r1_shared_successor_overlay.py", + "tests/kernel/test_ck07r1_terminal_failure_correction_authority.py", + "tests/kernel/test_documentation_authority.py", + "tests/kernel/test_kernel_scope.py" + ], + "candidate_scope": [ + "output/ck07r1/lifecycle-requalification-v1.launch-token.json", + "output/ck07r1/lifecycle-requalification-v2.launch-token.json", + "output/ck07r1/lifecycle-requalification-v2.stderr.txt", + "output/ck07r1/lifecycle-requalification-v2.stdout.txt", + "scripts/benchmark_ck07r1_lifecycle_scale.py", + "src/codex_usage_tracker/agent_kernel/publication/preparation.py", + "tests/agent_kernel/publication/test_lifecycle_scale.py" + ], + "forbidden": [ + "mixed_partial_or_extra_candidate_delta", + "extra_workflow_or_authority_delta", + "wrong_v2_authority_base_or_tree", + "wrong_implementation_source_head_or_candidate_bytes", + "v1_authority_rewrite", + "terminal_evidence_mutation", + "qualification_command_invocation", + "child_or_fork", + "token_refund_or_new_invocation", + "retry_restart_or_replacement", + "receipt_fabrication", + "implementation_files_in_authority_pr", + "PR_394_mutation", + "live_or_real_data", + "downstream_dispatch", + "cleanup_or_witness_loss" + ] + } +} diff --git a/docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.schema.json b/docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.schema.json new file mode 100644 index 00000000..5e578fce --- /dev/null +++ b/docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.schema.json @@ -0,0 +1,188 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://local.codex/schemas/ck07r1-lifecycle-terminal-failure-clean-commit-authority-v2.json", + "title": "CK-07R1 lifecycle terminal failure clean committed CI transition authority v2", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "version", + "task", + "status", + "authority_base_sha", + "authority_base_tree_sha", + "source_authority", + "superseded_immutable_paths", + "implementation_transition", + "ci_environment_transition", + "decision", + "scope" + ], + "properties": { + "schema": { + "const": "codex-usage-tracker.ck07r1-lifecycle-terminal-failure-clean-commit-authority.v2" + }, + "version": { + "const": 2 + }, + "task": { + "const": "CK-07R1" + }, + "status": { + "const": "permitted_not_accepted" + }, + "authority_base_sha": { + "const": "487e0b7138d638d7cfb1d91627e5a6ebda743699" + }, + "authority_base_tree_sha": { + "const": "2eef42c34cee001c40333e4e3d0af49e09bd7ec8" + }, + "source_authority": { + "const": [ + { + "path": "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v1.json", + "sha256": "badda5361f66944eb4972c061103435d55c6d59c2797ce7db8f34c70662d1e02" + }, + { + "path": "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v1.schema.json", + "sha256": "0819f90addbacccc2d45a467cce134a4a8112bd836cbfe2cbe9702b163ace03b" + } + ] + }, + "superseded_immutable_paths": { + "const": [ + { + "path": ".github/workflows/ci.yml", + "before_sha256": "f25ea89fb207b2d7a9ff1c18953865ef91cb73cfe23796f545d304a40cc8959c", + "sha256": "ce1dd9c0324c32e00298aeb90100539796d583272dad4059cd81c5564b13437e" + }, + { + "path": "scripts/ck07r1_prelaunch_recovery.py", + "before_sha256": "a3f6376f9f1328b5ccce3d8b16486b87cefe4d2b6c783a4e2a706e71142b1fc4", + "sha256": "36dbe7a7fc2aaf70a5458ebbb7c9e25a271662e3d6883631311f37ef2499a5d0" + }, + { + "path": "scripts/ck07r1_shared_successor_overlay.py", + "before_sha256": "f3745ec07bf47ee15f50969132f315aec61d407c745f6c63694e9910a88c5768", + "sha256": "05ddf028f9cfda30b78d8756e21940c7915796357cef79c7a632da31dba60168" + } + ] + }, + "implementation_transition": { + "const": { + "pull_request": 448, + "source_base_sha": "652f2166b58b9ee0d719348a769901577d11e6fd", + "source_head_sha": "927aa06f7c4c88319cc30247343c40db8e9b817e", + "candidate_scope_source": "bound_v1_clean_commit_authority", + "representations": { + "dirty_prepublication": { + "head": "exact_v2_authority_head", + "worktree_delta": "exact_candidate_scope", + "committed_delta": "exact_v2_authority_scope" + }, + "clean_integrated": { + "base": "exact_v2_authority_base", + "worktree_delta": "empty", + "committed_delta": "exact_v2_authority_scope_plus_candidate_scope", + "source_identity": "bound_v1_source_head_or_exact_tree_equivalent_squash" + } + } + } + }, + "ci_environment_transition": { + "const": { + "workflow_path": ".github/workflows/ci.yml", + "before_sha256": "f25ea89fb207b2d7a9ff1c18953865ef91cb73cfe23796f545d304a40cc8959c", + "sha256": "ce1dd9c0324c32e00298aeb90100539796d583272dad4059cd81c5564b13437e", + "matrix_python": [ + "3.10", + "3.14" + ], + "install_step": "Install kernel tooling", + "command": [ + "python", + "-m", + "venv", + "--system-site-packages", + ".venv" + ], + "required_before_step": "Verify kernel phase", + "lexical_interpreter": ".venv/bin/python", + "matching_sys_prefix": ".venv", + "network_access_added": false, + "qualification_command_invocations": 0 + } + }, + "decision": { + "const": { + "root_cause": "clean_hosted_checkout_lacked_bound_repository_venv", + "v1_authority_bytes_preserved": true, + "dirty_prepublication_remains_valid": true, + "clean_committed_transition_permitted": true, + "implementation_acceptance": "not_claimed", + "runtime_acceptance": "not_claimed", + "new_command_invocations_permitted": 0, + "launch_authorized": false, + "token_consumed": true, + "token_refund": false, + "retry": "none", + "restart": "none", + "replacement": "none", + "receipt_fabrication": "forbidden", + "post_single_run": "unavailable_without_complete_planner_valid_receipt", + "final_accepted": "unavailable" + } + }, + "scope": { + "const": { + "authority_write_scope": [ + ".github/workflows/ci.yml", + "docs/INDEX.md", + "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json", + "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.schema.json", + "docs/roadmap/REMAINING_EXECUTION_PLAN.md", + "docs/roadmap/TASK_PACKETS.md", + "docs/roadmap/tasks/ck-07r1-correct-lifecycle-preparation-scale.md", + "scripts/check_kernel_scope.py", + "scripts/ck07r1_consuming_boundary.py", + "scripts/ck07r1_prelaunch_recovery.py", + "scripts/ck07r1_shared_successor_overlay.py", + "scripts/ck07r1_terminal_failure_correction.py", + "tests/kernel/test_ck07r1_consuming_boundary_authority.py", + "tests/kernel/test_ck07r1_prelaunch_recovery_authority.py", + "tests/kernel/test_ck07r1_shared_successor_overlay.py", + "tests/kernel/test_ck07r1_terminal_failure_correction_authority.py", + "tests/kernel/test_documentation_authority.py", + "tests/kernel/test_kernel_scope.py" + ], + "candidate_scope": [ + "output/ck07r1/lifecycle-requalification-v1.launch-token.json", + "output/ck07r1/lifecycle-requalification-v2.launch-token.json", + "output/ck07r1/lifecycle-requalification-v2.stderr.txt", + "output/ck07r1/lifecycle-requalification-v2.stdout.txt", + "scripts/benchmark_ck07r1_lifecycle_scale.py", + "src/codex_usage_tracker/agent_kernel/publication/preparation.py", + "tests/agent_kernel/publication/test_lifecycle_scale.py" + ], + "forbidden": [ + "mixed_partial_or_extra_candidate_delta", + "extra_workflow_or_authority_delta", + "wrong_v2_authority_base_or_tree", + "wrong_implementation_source_head_or_candidate_bytes", + "v1_authority_rewrite", + "terminal_evidence_mutation", + "qualification_command_invocation", + "child_or_fork", + "token_refund_or_new_invocation", + "retry_restart_or_replacement", + "receipt_fabrication", + "implementation_files_in_authority_pr", + "PR_394_mutation", + "live_or_real_data", + "downstream_dispatch", + "cleanup_or_witness_loss" + ] + } + } + } +} diff --git a/docs/roadmap/REMAINING_EXECUTION_PLAN.md b/docs/roadmap/REMAINING_EXECUTION_PLAN.md index 054f8229..f6fdde5e 100644 --- a/docs/roadmap/REMAINING_EXECUTION_PLAN.md +++ b/docs/roadmap/REMAINING_EXECUTION_PLAN.md @@ -215,6 +215,14 @@ authority-main tree or as a clean committed PR/integrated delta with exact base, scope, and hashes. Neither representation reopens the consumed run or changes the existing blocked state. +The additive [clean-committed CI authority v2](../decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json) +preserves the v1 bridge bytes and binds the clean hosted matrix's +repository-local `.venv` creation before verification. It recognizes only the +exact 18-path follow-up authority delta and exact 18-plus-seven integrated +PR #448 state. This deterministic environment correction does not authorize a +qualification command, another child, token action, retry, receipt, +implementation acceptance, or downstream readiness. + The exact V11 launcher contract constructs and validates the fully overlay/cohort-bound receipt and non-null stdout/stderr/output evidence before any first durable `completed` finalization. Evidence diff --git a/docs/roadmap/TASK_PACKETS.md b/docs/roadmap/TASK_PACKETS.md index c4e73180..4bb9e09e 100644 --- a/docs/roadmap/TASK_PACKETS.md +++ b/docs/roadmap/TASK_PACKETS.md @@ -124,6 +124,13 @@ locks are unchanged. - [ ] **CK-16-03 — Build once and qualify release candidate** · Blocked on docs/selected optional work · [packet](tasks/ck-16-03-build-once-qualify-release-candidate.md) - [ ] **CK-16-04 — Publish and verify public artifacts** · Blocked on CK-16-03 and approval · [packet](tasks/ck-16-04-publish-verify-public-artifacts.md) +### CK-07R1 hosted CI authority supplement + +The [clean-committed CI authority v2](../decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json) +binds the exact repository-local hosted `.venv` seam for PR #448 without +reopening the consumed run, authorizing a retry, or changing receipt-based +acceptance and downstream holds. + ## Critical path `CK-00 → CK-01 → CK-02 → CK-03 → CK-04 → CK-05 → CK-06 → CK-07 → CK-07B diff --git a/docs/roadmap/tasks/ck-07r1-correct-lifecycle-preparation-scale.md b/docs/roadmap/tasks/ck-07r1-correct-lifecycle-preparation-scale.md index 56ea94dc..cf038cd1 100644 --- a/docs/roadmap/tasks/ck-07r1-correct-lifecycle-preparation-scale.md +++ b/docs/roadmap/tasks/ck-07r1-correct-lifecycle-preparation-scale.md @@ -115,6 +115,16 @@ durable promotion, recovery, rollback, and prior-readability path. Tail limits, production planner/preparation behavior, accepted authority bytes, both terminal ledgers, and all run artifacts remain immutable. +The additive [clean-committed CI authority v2](../../decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json) +preserves both v1 bridge files byte-for-byte and binds the exact hosted workflow +correction required by accepted lexical `.venv/bin/python` and matching +`sys.prefix` consumers. It creates `.venv` from the already-selected Python +3.10 or 3.14 matrix interpreter after the existing development install and +before verification, adds no network dependency step, and admits only the exact +follow-up authority scope or exact follow-up-plus-seven integrated state. It +does not reopen the consumed token or authorize a command, launch, retry, +replacement, refund, receipt, runtime acceptance, or downstream work. + **Parallelism:** Resume only existing worker `019fbfe2-8fe4-7de2-9264-d58572366727` after the consuming-boundary authority merges and exact-main verifies, using frozen cwd diff --git a/scripts/check_kernel_scope.py b/scripts/check_kernel_scope.py index f80dfb06..1d05a0b1 100644 --- a/scripts/check_kernel_scope.py +++ b/scripts/check_kernel_scope.py @@ -894,6 +894,8 @@ { "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v1.json", "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v1.schema.json", + "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json", + "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.schema.json", } ) diff --git a/scripts/ck07r1_consuming_boundary.py b/scripts/ck07r1_consuming_boundary.py index 7851daeb..11adc349 100644 --- a/scripts/ck07r1_consuming_boundary.py +++ b/scripts/ck07r1_consuming_boundary.py @@ -18,6 +18,11 @@ from jsonschema import Draft202012Validator +from scripts.ck07r1_terminal_failure_correction import ( + TerminalCorrectionError, + bound_authority_digest_matches, +) + ROOT = Path(__file__).resolve().parents[1] AUTHORITY_PATH = ( "docs/decisions/evidence/ck07r1a0/" @@ -104,11 +109,12 @@ def verify_bound_authority_bytes( expected = record.get("sha256") if not isinstance(relative, str) or not isinstance(expected, str): raise ConsumingBoundaryError("immutable authority identity malformed") - actual = _sha256(root / relative) - if actual != expected: - raise ConsumingBoundaryError( - f"bound authority bytes drifted: {relative}" - ) + try: + matches = bound_authority_digest_matches(root, relative, expected) + except TerminalCorrectionError as exc: + raise ConsumingBoundaryError(str(exc)) from exc + if not matches: + raise ConsumingBoundaryError(f"bound authority bytes drifted: {relative}") def verify_candidate_cohort( diff --git a/scripts/ck07r1_prelaunch_recovery.py b/scripts/ck07r1_prelaunch_recovery.py index 54477200..e7b18e00 100644 --- a/scripts/ck07r1_prelaunch_recovery.py +++ b/scripts/ck07r1_prelaunch_recovery.py @@ -20,6 +20,11 @@ from jsonschema import Draft202012Validator +from scripts.ck07r1_terminal_failure_correction import ( + TerminalCorrectionError, + bound_authority_digest_matches, +) + AUTHORITY_PATH = Path( "docs/decisions/evidence/ck07r1a0/lifecycle-prelaunch-recovery-authority-v1.json" ) @@ -77,9 +82,14 @@ def verify_bound_authority_bytes(authority: Mapping[str, Any], root: Path) -> No expected = record.get("sha256") if not isinstance(relative, str) or not isinstance(expected, str): raise PrelaunchRecoveryError("immutable authority identity is malformed") - path = root / relative - if not path.is_file() or _sha256(path) != expected: - raise PrelaunchRecoveryError(f"immutable authority byte identity mismatch: {relative}") + try: + matches = bound_authority_digest_matches(root, relative, expected) + except TerminalCorrectionError as exc: + raise PrelaunchRecoveryError(str(exc)) from exc + if not matches: + raise PrelaunchRecoveryError( + f"immutable authority byte identity mismatch: {relative}" + ) def _git(root: Path, *args: str) -> str: diff --git a/scripts/ck07r1_shared_successor_overlay.py b/scripts/ck07r1_shared_successor_overlay.py index 39d1d855..e715164f 100644 --- a/scripts/ck07r1_shared_successor_overlay.py +++ b/scripts/ck07r1_shared_successor_overlay.py @@ -12,6 +12,14 @@ from jsonschema import Draft202012Validator +from scripts.ck07r1_terminal_failure_correction import ( + CLEAN_COMMIT_CI_AUTHORITY_PATH, + TerminalCorrectionError, + bound_authority_digest_matches, + load_clean_commit_ci_authority, + verify_clean_commit_ci_authority_bytes, +) + ROOT = Path(__file__).resolve().parents[1] AUTHORITY_PATH = "docs/decisions/evidence/ck07r1a0/shared-successor-overlay-authority-v1.json" SCHEMA_PATH = AUTHORITY_PATH.removesuffix(".json") + ".schema.json" @@ -78,7 +86,11 @@ def load_consuming_boundary(root: Path = ROOT) -> dict[str, Any] | None: raise SharedSuccessorOverlayError( "consuming-boundary identity malformed" ) - if sha256_path(root, relative) != expected: + try: + matches = bound_authority_digest_matches(root, relative, expected) + except TerminalCorrectionError as exc: + raise SharedSuccessorOverlayError(str(exc)) from exc + if not matches: raise SharedSuccessorOverlayError( f"consuming-boundary bound bytes drifted: {relative}" ) @@ -593,6 +605,14 @@ def overlay_changed_path_allowance( allowed = set(authority_paths) allowed.update(_consuming_authority_scope(load_consuming_boundary(root))) + clean_commit_ci_path = root / CLEAN_COMMIT_CI_AUTHORITY_PATH + if clean_commit_ci_path.is_file(): + try: + clean_commit_ci = load_clean_commit_ci_authority(root) + verify_clean_commit_ci_authority_bytes(clean_commit_ci, root) + except TerminalCorrectionError as exc: + raise SharedSuccessorOverlayError(str(exc)) from exc + allowed.update(clean_commit_ci["scope"]["authority_write_scope"]) if state == "worker_prequalification": allowed.update(expected_worktree_delta(authority, state)) elif state != "authority_main": diff --git a/scripts/ck07r1_terminal_failure_correction.py b/scripts/ck07r1_terminal_failure_correction.py index 00da4dfe..e364b83f 100644 --- a/scripts/ck07r1_terminal_failure_correction.py +++ b/scripts/ck07r1_terminal_failure_correction.py @@ -28,6 +28,14 @@ "docs/decisions/evidence/ck07r1a0/" "lifecycle-terminal-failure-clean-commit-authority-v1.schema.json" ) +CLEAN_COMMIT_CI_AUTHORITY_PATH = Path( + "docs/decisions/evidence/ck07r1a0/" + "lifecycle-terminal-failure-clean-commit-authority-v2.json" +) +CLEAN_COMMIT_CI_SCHEMA_PATH = Path( + "docs/decisions/evidence/ck07r1a0/" + "lifecycle-terminal-failure-clean-commit-authority-v2.schema.json" +) class TerminalCorrectionError(RuntimeError): @@ -78,6 +86,19 @@ def load_clean_commit_authority(root: Path) -> dict[str, Any]: return authority +def load_clean_commit_ci_authority(root: Path) -> dict[str, Any]: + authority = _load_json(root / CLEAN_COMMIT_CI_AUTHORITY_PATH) + schema = _load_json(root / CLEAN_COMMIT_CI_SCHEMA_PATH) + try: + Draft202012Validator.check_schema(schema) + Draft202012Validator(schema).validate(authority) + except Exception as exc: + raise TerminalCorrectionError( + f"clean-commit CI authority/schema validation failed: {exc}" + ) from exc + return authority + + def _git(root: Path, *args: str) -> str: result = subprocess.run( ("git", *args), @@ -133,6 +154,198 @@ def _is_ancestor(root: Path, ancestor: str, descendant: str = "HEAD") -> bool: return result.returncode == 0 +def _git_blob_sha256(root: Path, revision: str, relative: str) -> str: + result = subprocess.run( + ("git", "show", f"{revision}:{relative}"), + cwd=root, + check=False, + capture_output=True, + ) + if result.returncode != 0: + raise TerminalCorrectionError( + f"cannot read exact Git blob {revision}:{relative}" + ) + return hashlib.sha256(result.stdout).hexdigest() + + +def verify_clean_commit_ci_authority_bytes( + authority: Mapping[str, Any], root: Path +) -> None: + for record in authority["source_authority"]: + path = root / record["path"] + if not path.is_file() or _sha256(path) != record["sha256"]: + raise TerminalCorrectionError( + f"clean-commit CI source authority byte identity mismatch: " + f"{record['path']}" + ) + for record in authority["superseded_immutable_paths"]: + relative = str(record["path"]) + path = root / relative + if not path.is_file() or _sha256(path) != record["sha256"]: + raise TerminalCorrectionError( + f"clean-commit CI successor byte identity mismatch: {relative}" + ) + if ( + _git_blob_sha256( + root, str(authority["authority_base_sha"]), relative + ) + != record["before_sha256"] + ): + raise TerminalCorrectionError( + f"clean-commit CI predecessor byte identity mismatch: {relative}" + ) + transition = authority["ci_environment_transition"] + workflow_path = str(transition["workflow_path"]) + workflow = root / workflow_path + if not workflow.is_file() or _sha256(workflow) != transition["sha256"]: + raise TerminalCorrectionError("clean-commit CI workflow byte identity mismatch") + if ( + _git_blob_sha256( + root, str(authority["authority_base_sha"]), workflow_path + ) + != transition["before_sha256"] + ): + raise TerminalCorrectionError( + "clean-commit CI predecessor workflow byte identity mismatch" + ) + text = workflow.read_text(encoding="utf-8") + command = "python -m venv --system-site-packages .venv" + if text.count(command) != 1: + raise TerminalCorrectionError("clean-commit CI venv command must be exact") + if not ( + text.index('python -m pip install ".[dev]"') + < text.index(command) + < text.index("- name: Verify kernel phase") + ): + raise TerminalCorrectionError( + "clean-commit CI venv command must precede verification" + ) + + +def bound_authority_digest_matches( + root: Path, relative: str, expected: str +) -> bool: + path = root / relative + if not path.is_file(): + return False + actual = _sha256(path) + if actual == expected: + return True + if not (root / CLEAN_COMMIT_CI_AUTHORITY_PATH).is_file(): + return False + authority = load_clean_commit_ci_authority(root) + verify_clean_commit_ci_authority_bytes(authority, root) + return any( + record["path"] == relative + and record["before_sha256"] == expected + and record["sha256"] == actual + for record in authority["superseded_immutable_paths"] + ) + + +def verify_clean_commit_ci_authority_delta( + authority: Mapping[str, Any], + root: Path, + *, + include_candidate: bool = False, + observed_committed: set[str] | None = None, + observed_worktree: set[str] | None = None, + base_is_ancestor: bool | None = None, +) -> None: + base = str(authority["authority_base_sha"]) + if base_is_ancestor is None: + base_is_ancestor = _is_ancestor(root, base) + if not base_is_ancestor: + raise TerminalCorrectionError( + "clean-commit CI authority base is not an ancestor of HEAD" + ) + authority_scope = set(authority["scope"]["authority_write_scope"]) + candidate_scope = set(authority["scope"]["candidate_scope"]) + expected_committed = authority_scope | ( + candidate_scope if include_candidate else set() + ) + if observed_committed is None: + head = _git(root, "rev-parse", "HEAD") + worktree = _status_paths(root) + if head == base: + committed = set() + expected_worktree = expected_committed + expected_committed_actual = set() + else: + committed = { + line + for line in _git( + root, "diff", "--name-only", f"{base}..{head}", "--" + ).splitlines() + if line + } + expected_worktree = set() + expected_committed_actual = expected_committed + else: + committed = observed_committed + worktree = observed_worktree or set() + expected_worktree = set() + expected_committed_actual = expected_committed + if committed != expected_committed_actual: + raise TerminalCorrectionError( + "clean-commit CI committed delta must be exact: " + f"expected={sorted(expected_committed)} actual={sorted(committed)}" + ) + if worktree != expected_worktree: + raise TerminalCorrectionError( + "clean-commit CI worktree delta must be exact: " + f"expected={sorted(expected_worktree)} actual={sorted(worktree)}" + ) + + +def verify_clean_commit_ci_transition( + authority: Mapping[str, Any], + v1_authority: Mapping[str, Any], + root: Path, + *, + observed_committed: set[str] | None = None, + observed_worktree: set[str] | None = None, + base_is_ancestor: bool | None = None, + verify_bytes: bool = True, +) -> str: + if verify_bytes: + verify_clean_candidate_bytes(v1_authority, root) + base = str(authority["authority_base_sha"]) + if base_is_ancestor is None: + base_is_ancestor = _is_ancestor(root, base) + if not base_is_ancestor: + raise TerminalCorrectionError( + "clean-commit CI candidate base is not an ancestor of HEAD" + ) + authority_scope = set(authority["scope"]["authority_write_scope"]) + candidate_scope = set(authority["scope"]["candidate_scope"]) + if observed_committed is None: + head = _git(root, "rev-parse", "HEAD") + committed = { + line + for line in _git( + root, "diff", "--name-only", f"{base}..{head}", "--" + ).splitlines() + if line + } + worktree = _status_paths(root) + else: + committed = observed_committed + worktree = observed_worktree or set() + if committed == authority_scope and worktree == candidate_scope: + return "dirty_prepublication" + if committed == authority_scope | candidate_scope and not worktree: + return "clean_integrated" + raise TerminalCorrectionError( + "clean-commit CI candidate lineage/delta must be exact: " + f"expected_dirty_committed={sorted(authority_scope)} " + f"expected_dirty_worktree={sorted(candidate_scope)} " + f"expected_integrated={sorted(authority_scope | candidate_scope)} " + f"actual_committed={sorted(committed)} " + f"actual_worktree={sorted(worktree)}" + ) + + def verify_clean_commit_authority_bytes( authority: Mapping[str, Any], root: Path ) -> None: @@ -282,6 +495,26 @@ def verify_exact_authority_delta( allowed_worktree_delta: set[str] | None = None, base_is_ancestor: bool | None = None, ) -> None: + if ( + observed is None + and allowed_worktree_delta is None + and (root / CLEAN_COMMIT_CI_AUTHORITY_PATH).is_file() + ): + clean_commit_ci = load_clean_commit_ci_authority(root) + clean_commit_v1 = load_clean_commit_authority(root) + verify_clean_commit_ci_authority_bytes(clean_commit_ci, root) + include_committed_candidate = False + if _clean_candidate_bytes_exact(clean_commit_v1, root): + representation = verify_clean_commit_ci_transition( + clean_commit_ci, clean_commit_v1, root + ) + include_committed_candidate = representation == "clean_integrated" + verify_clean_commit_ci_authority_delta( + clean_commit_ci, + root, + include_candidate=include_committed_candidate, + ) + return if ( observed is None and allowed_worktree_delta is None @@ -355,8 +588,9 @@ def verify_exact_candidate_delta( def verify_immutable_authority_bytes(authority: Mapping[str, Any], root: Path) -> None: for record in authority["immutable_authorities"]: - path = root / record["path"] - if not path.is_file() or _sha256(path) != record["sha256"]: + if not bound_authority_digest_matches( + root, str(record["path"]), str(record["sha256"]) + ): raise TerminalCorrectionError( f"immutable authority byte identity mismatch: {record['path']}" ) @@ -525,7 +759,28 @@ def verify_combined( ) -> dict[str, Any]: clean_commit_root = authority_root or root clean_commit_path = clean_commit_root / CLEAN_COMMIT_AUTHORITY_PATH - if clean_commit_path.is_file(): + clean_commit_ci_path = clean_commit_root / CLEAN_COMMIT_CI_AUTHORITY_PATH + if clean_commit_ci_path.is_file(): + clean_commit_ci = load_clean_commit_ci_authority(clean_commit_root) + clean_commit = load_clean_commit_authority(clean_commit_root) + verify_clean_commit_ci_authority_bytes(clean_commit_ci, clean_commit_root) + same_root = clean_commit_root == root + if same_root: + representation = verify_clean_commit_ci_transition( + clean_commit_ci, clean_commit, root + ) + verify_clean_commit_ci_authority_delta( + clean_commit_ci, + clean_commit_root, + include_candidate=representation == "clean_integrated", + ) + else: + verify_clean_commit_ci_authority_delta( + clean_commit_ci, clean_commit_root + ) + representation = verify_clean_candidate_transition(clean_commit, root) + verify_clean_candidate_bytes(clean_commit, root) + elif clean_commit_path.is_file(): clean_commit = load_clean_commit_authority(clean_commit_root) verify_clean_commit_authority_bytes(clean_commit, clean_commit_root) same_root = clean_commit_root == root @@ -571,11 +826,12 @@ def _main(argv: Sequence[str] | None = None) -> int: authority = load_authority(authority_root) verify_immutable_authority_bytes(authority, authority_root) verify_exact_authority_delta(authority, authority_root) - clean_commit = ( - load_clean_commit_authority(authority_root) - if (authority_root / CLEAN_COMMIT_AUTHORITY_PATH).is_file() - else None - ) + if (authority_root / CLEAN_COMMIT_CI_AUTHORITY_PATH).is_file(): + clean_commit = load_clean_commit_ci_authority(authority_root) + elif (authority_root / CLEAN_COMMIT_AUTHORITY_PATH).is_file(): + clean_commit = load_clean_commit_authority(authority_root) + else: + clean_commit = None result: dict[str, Any] = { "authority_paths": len( clean_commit["scope"]["authority_write_scope"] diff --git a/tests/kernel/test_ck07r1_consuming_boundary_authority.py b/tests/kernel/test_ck07r1_consuming_boundary_authority.py index 626ba6b0..1ea3d2e9 100644 --- a/tests/kernel/test_ck07r1_consuming_boundary_authority.py +++ b/tests/kernel/test_ck07r1_consuming_boundary_authority.py @@ -86,6 +86,18 @@ def test_consuming_boundary_preserves_every_bound_authority_byte() -> None: verify_bound_authority_bytes(_authority(), ROOT) +def test_consuming_boundary_accepts_only_versioned_ci_workflow_successor() -> None: + authority = _authority() + record = next( + item + for item in authority["immutable_authorities"] + if item["path"] == ".github/workflows/ci.yml" + ) + actual = hashlib.sha256((ROOT / record["path"]).read_bytes()).hexdigest() + assert actual != record["sha256"] + verify_bound_authority_bytes(authority, ROOT) + + def test_consuming_boundary_binds_bounded_console_browser_install() -> None: authority = _authority() workflow_path = ".github/workflows/ci.yml" diff --git a/tests/kernel/test_ck07r1_prelaunch_recovery_authority.py b/tests/kernel/test_ck07r1_prelaunch_recovery_authority.py index e87770e5..89df2b12 100644 --- a/tests/kernel/test_ck07r1_prelaunch_recovery_authority.py +++ b/tests/kernel/test_ck07r1_prelaunch_recovery_authority.py @@ -130,6 +130,18 @@ def test_recovery_authority_preserves_every_predecessor_byte() -> None: verify_bound_authority_bytes(_authority(), ROOT) +def test_recovery_accepts_only_versioned_shared_overlay_successor() -> None: + authority = _authority() + record = next( + item + for item in authority["immutable_authorities"] + if item["path"] == "scripts/ck07r1_shared_successor_overlay.py" + ) + actual = hashlib.sha256((ROOT / record["path"]).read_bytes()).hexdigest() + assert actual != record["sha256"] + verify_bound_authority_bytes(authority, ROOT) + + def test_recovery_authority_binds_exact_candidate_and_terminal_ledger( tmp_path: Path, ) -> None: diff --git a/tests/kernel/test_ck07r1_shared_successor_overlay.py b/tests/kernel/test_ck07r1_shared_successor_overlay.py index f03b4c70..d06afd36 100644 --- a/tests/kernel/test_ck07r1_shared_successor_overlay.py +++ b/tests/kernel/test_ck07r1_shared_successor_overlay.py @@ -16,6 +16,7 @@ ) from scripts.ck07r1_shared_successor_overlay import ( CONSUMING_AUTHORITY_PATH, + CONSUMING_SCHEMA_PATH, ROOT, SCHEMA_PATH, SharedSuccessorOverlayError, @@ -39,7 +40,9 @@ ) from scripts.ck07r1_terminal_failure_correction import ( CLEAN_COMMIT_AUTHORITY_PATH, + CLEAN_COMMIT_CI_AUTHORITY_PATH, load_clean_commit_authority, + load_clean_commit_ci_authority, verify_clean_candidate_transition, ) from scripts.ck07r1_terminal_failure_correction import ( @@ -159,6 +162,17 @@ def test_complete_consuming_boundary_is_the_only_additive_authority_delta() -> N verify_exact_worktree_delta(overlay, "authority_main", observed=changed) +def test_shared_overlay_accepts_only_versioned_ci_workflow_successor() -> None: + consuming = load_consuming_boundary() + assert consuming is not None + record = next( + item + for item in consuming["immutable_authorities"] + if item["path"] == ".github/workflows/ci.yml" + ) + assert sha256_path(ROOT, record["path"]) != record["sha256"] + + def test_partial_consuming_boundary_pair_fails_closed(tmp_path: Path) -> None: path = tmp_path / CONSUMING_AUTHORITY_PATH path.parent.mkdir(parents=True) @@ -167,6 +181,21 @@ def test_partial_consuming_boundary_pair_fails_closed(tmp_path: Path) -> None: load_consuming_boundary(tmp_path) +def test_consuming_boundary_rejects_drifted_bound_authority(tmp_path: Path) -> None: + for relative in (CONSUMING_AUTHORITY_PATH, CONSUMING_SCHEMA_PATH): + target = tmp_path / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes((ROOT / relative).read_bytes()) + agents = tmp_path / "AGENTS.md" + agents.write_bytes((ROOT / "AGENTS.md").read_bytes() + b"\n# drift\n") + + with pytest.raises( + SharedSuccessorOverlayError, + match="consuming-boundary bound bytes drifted: AGENTS.md", + ): + load_consuming_boundary(tmp_path) + + def test_consuming_activation_requires_exact_main_cwd_capacity_and_cohort() -> None: overlay = load_overlay() consuming = load_consuming_boundary() @@ -486,10 +515,14 @@ def test_overlay_scope_and_launcher_contract_are_exact() -> None: predecessor = overlay_changed_path_allowance(authority, "authority_main") successor = overlay_changed_path_allowance(authority, "worker_prequalification") candidate = set(authority["scope"]["combined_preflight_candidate_scope"]) + clean_commit_ci = load_clean_commit_ci_authority(ROOT) + clean_commit_ci_scope = set(clean_commit_ci["scope"]["authority_write_scope"]) assert successor == predecessor | candidate + assert clean_commit_ci_scope <= predecessor assert candidate.isdisjoint(predecessor) assert "src/codex_usage_tracker/agent_kernel/publication/writer.py" not in successor + assert str(CLEAN_COMMIT_CI_AUTHORITY_PATH) in predecessor verify_launcher_safety_contract(authority) weakened = deepcopy(authority) diff --git a/tests/kernel/test_ck07r1_terminal_failure_correction_authority.py b/tests/kernel/test_ck07r1_terminal_failure_correction_authority.py index 401f252d..229472dc 100644 --- a/tests/kernel/test_ck07r1_terminal_failure_correction_authority.py +++ b/tests/kernel/test_ck07r1_terminal_failure_correction_authority.py @@ -13,14 +13,20 @@ from scripts.ck07r1_terminal_failure_correction import ( AUTHORITY_PATH, CLEAN_COMMIT_AUTHORITY_PATH, + CLEAN_COMMIT_CI_AUTHORITY_PATH, + CLEAN_COMMIT_CI_SCHEMA_PATH, CLEAN_COMMIT_SCHEMA_PATH, SCHEMA_PATH, TerminalCorrectionError, load_authority, load_clean_commit_authority, + load_clean_commit_ci_authority, verify_clean_candidate_transition, verify_clean_commit_authority_bytes, verify_clean_commit_authority_delta, + verify_clean_commit_ci_authority_bytes, + verify_clean_commit_ci_authority_delta, + verify_clean_commit_ci_transition, verify_corrected_cohort, verify_exact_authority_delta, verify_exact_candidate_delta, @@ -39,6 +45,10 @@ def _clean_commit_authority() -> dict[str, Any]: return load_clean_commit_authority(ROOT) +def _clean_commit_ci_authority() -> dict[str, Any]: + return load_clean_commit_ci_authority(ROOT) + + def _write(path: Path, value: bytes) -> str: path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(value) @@ -158,8 +168,14 @@ def test_exact_authority_delta_admits_only_exact_clean_integrated_candidate( monkeypatch: pytest.MonkeyPatch, ) -> None: authority = _authority() + clean_commit_ci = _clean_commit_ci_authority() clean_commit = _clean_commit_authority() calls: list[tuple[str, object]] = [] + monkeypatch.setattr( + terminal_module, + "load_clean_commit_ci_authority", + lambda _root: clean_commit_ci, + ) monkeypatch.setattr( terminal_module, "load_clean_commit_authority", @@ -167,7 +183,7 @@ def test_exact_authority_delta_admits_only_exact_clean_integrated_candidate( ) monkeypatch.setattr( terminal_module, - "verify_clean_commit_authority_bytes", + "verify_clean_commit_ci_authority_bytes", lambda _authority, _root: calls.append(("authority_bytes", None)), ) monkeypatch.setattr( @@ -177,14 +193,14 @@ def test_exact_authority_delta_admits_only_exact_clean_integrated_candidate( ) monkeypatch.setattr( terminal_module, - "verify_clean_candidate_transition", - lambda _authority, _root: "clean_integrated", + "verify_clean_commit_ci_transition", + lambda _authority, _v1, _root: "clean_integrated", ) monkeypatch.setattr( terminal_module, - "verify_clean_commit_authority_delta", + "verify_clean_commit_ci_authority_delta", lambda _authority, _root, **kwargs: calls.append( - ("authority_delta", kwargs["include_committed_candidate"]) + ("authority_delta", kwargs["include_candidate"]) ), ) terminal_module.verify_exact_authority_delta(authority, ROOT) @@ -370,9 +386,15 @@ def test_combined_verifies_authority_binding_before_candidate( monkeypatch: pytest.MonkeyPatch, ) -> None: authority = _authority() + clean_commit_ci = _clean_commit_ci_authority() clean_commit = _clean_commit_authority() calls: list[str] = [] + monkeypatch.setattr( + terminal_module, + "load_clean_commit_ci_authority", + lambda _root: clean_commit_ci, + ) monkeypatch.setattr( terminal_module, "load_clean_commit_authority", @@ -380,18 +402,18 @@ def test_combined_verifies_authority_binding_before_candidate( ) monkeypatch.setattr( terminal_module, - "verify_clean_commit_authority_bytes", + "verify_clean_commit_ci_authority_bytes", lambda _authority, _root: calls.append("authority_bytes"), ) monkeypatch.setattr( terminal_module, - "verify_clean_candidate_transition", - lambda _authority, _root: calls.append("candidate_transition") + "verify_clean_commit_ci_transition", + lambda _authority, _v1, _root: calls.append("candidate_transition") or "clean_integrated", ) monkeypatch.setattr( terminal_module, - "verify_clean_commit_authority_delta", + "verify_clean_commit_ci_authority_delta", lambda _authority, _root, **_kwargs: calls.append("authority_delta"), ) monkeypatch.setattr( @@ -524,3 +546,121 @@ def test_clean_commit_schema_rejects_lineage_scope_and_no_run_weakening() -> Non def test_clean_commit_authority_file_name_is_versioned() -> None: assert Path(CLEAN_COMMIT_AUTHORITY_PATH).name.endswith("-v1.json") + + +def test_clean_commit_ci_authority_is_versioned_strict_and_preserves_v1() -> None: + authority = _clean_commit_ci_authority() + schema = json.loads( + (ROOT / CLEAN_COMMIT_CI_SCHEMA_PATH).read_text(encoding="utf-8") + ) + Draft202012Validator.check_schema(schema) + Draft202012Validator(schema).validate(authority) + assert authority["schema"].endswith(".v2") + assert authority["authority_base_sha"] == ( + "487e0b7138d638d7cfb1d91627e5a6ebda743699" + ) + assert authority["implementation_transition"]["source_head_sha"] == ( + "927aa06f7c4c88319cc30247343c40db8e9b817e" + ) + assert authority["status"] == "permitted_not_accepted" + verify_clean_commit_ci_authority_bytes(authority, ROOT) + + +def test_clean_commit_ci_authority_delta_and_candidate_states_are_exact() -> None: + authority = _clean_commit_ci_authority() + v1 = _clean_commit_authority() + authority_scope = set(authority["scope"]["authority_write_scope"]) + candidate_scope = set(authority["scope"]["candidate_scope"]) + + verify_clean_commit_ci_authority_delta( + authority, + ROOT, + observed_committed=authority_scope, + base_is_ancestor=True, + ) + assert ( + verify_clean_commit_ci_transition( + authority, + v1, + ROOT, + observed_committed=authority_scope, + observed_worktree=candidate_scope, + base_is_ancestor=True, + verify_bytes=False, + ) + == "dirty_prepublication" + ) + assert ( + verify_clean_commit_ci_transition( + authority, + v1, + ROOT, + observed_committed=authority_scope | candidate_scope, + observed_worktree=set(), + base_is_ancestor=True, + verify_bytes=False, + ) + == "clean_integrated" + ) + + for committed, worktree in ( + (authority_scope - {".github/workflows/ci.yml"}, set()), + (authority_scope | {"extra.txt"}, set()), + (authority_scope | candidate_scope, {"extra.txt"}), + (authority_scope, candidate_scope - {next(iter(candidate_scope))}), + ): + with pytest.raises(TerminalCorrectionError): + verify_clean_commit_ci_transition( + authority, + v1, + ROOT, + observed_committed=committed, + observed_worktree=worktree, + base_is_ancestor=True, + verify_bytes=False, + ) + with pytest.raises(TerminalCorrectionError, match="not an ancestor"): + verify_clean_commit_ci_transition( + authority, + v1, + ROOT, + observed_committed=authority_scope | candidate_scope, + base_is_ancestor=False, + verify_bytes=False, + ) + + +def test_clean_commit_ci_schema_rejects_scope_and_no_run_weakening() -> None: + authority = _clean_commit_ci_authority() + schema = json.loads( + (ROOT / CLEAN_COMMIT_CI_SCHEMA_PATH).read_text(encoding="utf-8") + ) + mutations = ( + lambda value: value["source_authority"][0].__setitem__("sha256", "0" * 64), + lambda value: value["ci_environment_transition"].__setitem__( + "command", ["python", "-m", "venv", ".venv"] + ), + lambda value: value["ci_environment_transition"].__setitem__( + "sha256", "1" * 64 + ), + lambda value: value["scope"]["authority_write_scope"].remove( + ".github/workflows/ci.yml" + ), + lambda value: value["scope"]["candidate_scope"].pop(), + lambda value: value["decision"].__setitem__( + "new_command_invocations_permitted", 1 + ), + lambda value: value["decision"].__setitem__("launch_authorized", True), + lambda value: value["decision"].__setitem__("token_consumed", False), + lambda value: value["decision"].__setitem__( + "runtime_acceptance", "claimed" + ), + ) + for mutate in mutations: + changed = deepcopy(authority) + mutate(changed) + assert list(Draft202012Validator(schema).iter_errors(changed)) + + +def test_clean_commit_ci_authority_file_name_is_versioned() -> None: + assert Path(CLEAN_COMMIT_CI_AUTHORITY_PATH).name.endswith("-v2.json") diff --git a/tests/kernel/test_documentation_authority.py b/tests/kernel/test_documentation_authority.py index d60841a4..eb47e249 100644 --- a/tests/kernel/test_documentation_authority.py +++ b/tests/kernel/test_documentation_authority.py @@ -1373,3 +1373,24 @@ def test_ck07r1_terminal_clean_commit_bridge_is_documented_fail_closed() -> None assert authority["decision"]["new_command_invocations_permitted"] == 0 assert authority["decision"]["launch_authorized"] is False assert authority["decision"]["token_consumed"] is True + + +def test_ck07r1_terminal_clean_commit_ci_v2_is_documented_fail_closed() -> None: + bodies = ( + _read("docs/INDEX.md"), + _read("docs/roadmap/REMAINING_EXECUTION_PLAN.md"), + _read("docs/roadmap/TASK_PACKETS.md"), + _read("docs/roadmap/tasks/ck-07r1-correct-lifecycle-preparation-scale.md"), + ) + authority = _json( + "docs/decisions/evidence/ck07r1a0/" + "lifecycle-terminal-failure-clean-commit-authority-v2.json" + ) + for body in bodies: + assert "lifecycle-terminal-failure-clean-commit-authority-v2" in body + assert "PR #448" in body + assert ".venv" in body + assert authority["decision"]["v1_authority_bytes_preserved"] is True + assert authority["decision"]["new_command_invocations_permitted"] == 0 + assert authority["decision"]["launch_authorized"] is False + assert authority["decision"]["token_consumed"] is True diff --git a/tests/kernel/test_kernel_scope.py b/tests/kernel/test_kernel_scope.py index d35454ce..440f73ed 100644 --- a/tests/kernel/test_kernel_scope.py +++ b/tests/kernel/test_kernel_scope.py @@ -847,6 +847,8 @@ def test_ck07r1_terminal_clean_commit_additions_are_explicit_and_bounded() -> No assert { "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v1.json", "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v1.schema.json", + "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.json", + "docs/decisions/evidence/ck07r1a0/lifecycle-terminal-failure-clean-commit-authority-v2.schema.json", } == CK07R1_TERMINAL_CLEAN_COMMIT_AUTHORITY_ADDITIONS