diff --git a/.agents/skills/manage-ci/references/current-inventory.md b/.agents/skills/manage-ci/references/current-inventory.md index 74a253fc5..55349599e 100644 --- a/.agents/skills/manage-ci/references/current-inventory.md +++ b/.agents/skills/manage-ci/references/current-inventory.md @@ -170,9 +170,17 @@ successful manual canary does not prove PR isolation. Fork PR validation remains hosted and is the no-Depot-authority half of the sentinel acceptance evidence; only the exact same-repository sentinel ref may exercise the diagnostic Depot job. All three sentinel cache phases attest the -provider-injected `ACTIONS_CACHE_URL`/`ACTIONS_RESULTS_URL` structure and -require a runtime token before invoking `actions/cache`; the non-loopback -check includes all IPv4 `127/8` and IPv4-mapped IPv6 loopback spellings. +provider-injected `ACTIONS_CACHE_URL`/`ACTIONS_RESULTS_URL` structure before +invoking pinned `actions/cache` restore/save actions. The shell attestation +does not require ambient `ACTIONS_RUNTIME_TOKEN`: GitHub's +`NodeScriptActionHandler` injects that credential into the cache actions, while +the shell `ScriptHandler` does not. Successful full restore/save is the +credential/token proof. The non-loopback check includes all IPv4 `127/8` +and IPv4-mapped IPv6 loopback spellings. +The protected PR probe clears and fully restores its saved poison key, requires +a cache hit and exact marker bytes before the trusted-seed gate, and thereby +proves the same-job Node token/write path; main verify's poison miss remains +the cross-scope proof. Bracketed IPv6 authorities use the fixed runner's Python 3.8+ stdlib `ipaddress` classifier; parser absence/version/invalidity fails closed. Attestation reports only value-free variable/reason classes and fails closed diff --git a/.github/workflows/ci-quality-slice.yml b/.github/workflows/ci-quality-slice.yml index d987ca112..7f981b0e2 100644 --- a/.github/workflows/ci-quality-slice.yml +++ b/.github/workflows/ci-quality-slice.yml @@ -400,10 +400,6 @@ jobs: } attest_cache_endpoint ACTIONS_CACHE_URL attest_cache_endpoint ACTIONS_RESULTS_URL - if [[ -z "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then - echo "cache backend attestation failed (variable=ACTIONS_RUNTIME_TOKEN reason=missing)" >&2 - exit 1 - fi - name: Restore trusted authority marker id: restore_seed @@ -451,6 +447,40 @@ jobs: path: .depot-authority-sentinel key: ${{ steps.validate.outputs.poison_key }} + - name: Clear local poison marker before proof restore + shell: bash + run: rm -rf -- .depot-authority-sentinel + + - name: Restore saved PR poison marker + id: restore_poison + uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0 + with: + path: .depot-authority-sentinel + key: ${{ steps.validate.outputs.poison_key }} + fail-on-cache-miss: true + + - name: Validate saved PR poison marker content + shell: bash + env: + SENTINEL_ID: ${{ steps.validate.outputs.sentinel_id }} + PR_NUMBER: ${{ steps.validate.outputs.pr_number }} + CACHE_HIT: ${{ steps.restore_poison.outputs.cache-hit }} + run: | + set -euo pipefail + if [[ "$CACHE_HIT" != "true" ]]; then + echo "saved PR poison marker restore did not report a cache hit" >&2 + exit 1 + fi + expected_file="$(mktemp)" + trap 'rm -f "$expected_file"' EXIT + printf 'mesh-llm-depot-authority-pr-marker-v1\nsentinel_id=%s\npr_number=%s\n' \ + "$SENTINEL_ID" "$PR_NUMBER" > "$expected_file" + if [[ ! -f .depot-authority-sentinel/marker ]] || + ! cmp -s "$expected_file" .depot-authority-sentinel/marker; then + echo "saved PR poison marker content mismatch" >&2 + exit 1 + fi + - name: Require trusted seed isolation after poison publication shell: bash env: diff --git a/.github/workflows/depot-canary.yml b/.github/workflows/depot-canary.yml index b3be8de82..1f47a6ae5 100644 --- a/.github/workflows/depot-canary.yml +++ b/.github/workflows/depot-canary.yml @@ -428,10 +428,6 @@ jobs: } attest_cache_endpoint ACTIONS_CACHE_URL attest_cache_endpoint ACTIONS_RESULTS_URL - if [[ -z "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then - echo "cache backend attestation failed (variable=ACTIONS_RUNTIME_TOKEN reason=missing)" >&2 - exit 1 - fi - name: Prepare deterministic non-secret marker shell: bash @@ -632,10 +628,6 @@ jobs: } attest_cache_endpoint ACTIONS_CACHE_URL attest_cache_endpoint ACTIONS_RESULTS_URL - if [[ -z "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then - echo "cache backend attestation failed (variable=ACTIONS_RUNTIME_TOKEN reason=missing)" >&2 - exit 1 - fi - name: Clear local poison marker before trusted restore shell: bash diff --git a/.omo/specs/pr-ci-optimization.md b/.omo/specs/pr-ci-optimization.md index 7d2fd90e2..6d87e5447 100644 --- a/.omo/specs/pr-ci-optimization.md +++ b/.omo/specs/pr-ci-optimization.md @@ -268,17 +268,23 @@ permissions, no checkout, no secrets and only the fixed provider-injected `ACTIONS_CACHE_URL` and `ACTIONS_RESULTS_URL` to be present and structurally attested as HTTP endpoints with a nonempty, non-GitHub, non-loopback authority (including all IPv4 `127/8` and IPv4-mapped IPv6 -loopback spellings), numeric port and explicit path, plus a nonempty -`ACTIONS_RUNTIME_TOKEN`; it reports only variable/reason classes and never -endpoint values. Manual seed/verify inputs bind to the configured sentinel ID -and use the fixed runner's Python 3.8+ stdlib `ipaddress` classifier for every -bracketed IPv6 spelling; parser absence/version/invalidity fails closed. -and exact merge ref. The PR probe restores the trusted seed (not lookup-only), +loopback spellings), numeric port and explicit path; it reports only +variable/reason classes and never endpoint values. The shell attestation does +not require ambient `ACTIONS_RUNTIME_TOKEN`: GitHub's +`NodeScriptActionHandler` injects that credential into the pinned cache +actions, while the shell `ScriptHandler` does not. Their successful full +restore/save calls provide the credential/token proof. Manual seed/verify +inputs bind to both the configured sentinel ID and the exact merge ref. Every +attestation uses the fixed runner's Python 3.8+ stdlib `ipaddress` classifier +for each bracketed IPv6 spelling; parser absence/version/invalidity fails +closed. The PR probe restores the trusted seed (not lookup-only), validates exact seed marker content on a hit, replaces it with a deterministic -non-secret poison marker, saves the exact Stage 1 poison key, then fails only -after saving if the seed was readable. A seed miss passes with the trusted-main -`verify-pr-write` phase pending; main verify fully restores and validates exact -poison content before its expected-miss failure. Fork PRs remain hosted and +non-secret poison marker, saves the exact Stage 1 poison key, clears and fully +restores that key, and requires a cache hit and exact marker bytes before the +seed decision; only then does it fail if the seed was readable. This same-job +restore/save proves the PR Node token/write path. A seed miss passes with the +trusted-main `verify-pr-write` phase pending; main verify's poison miss remains +the cross-scope proof. Fork PRs remain hosted and provide the no-Depot-authority evidence; only the exact same-repository sentinel ref exercises the Depot diagnostic. diff --git a/ci/DEPOT_MIGRATION.md b/ci/DEPOT_MIGRATION.md index 2ecbde462..5b27db2a0 100644 --- a/ci/DEPOT_MIGRATION.md +++ b/ci/DEPOT_MIGRATION.md @@ -299,7 +299,11 @@ any cache action, all three cache phases attest that the provider-injected `ACTIONS_CACHE_URL` and `ACTIONS_RESULTS_URL` are present, value-free structurally validated HTTP endpoints with a non-GitHub/non-loopback authority (including all IPv4 `127/8` and IPv4-mapped IPv6 loopback spellings), numeric -port and explicit path, and that `ACTIONS_RUNTIME_TOKEN` is present. +port and explicit path. The shell attestation intentionally does not require +ambient `ACTIONS_RUNTIME_TOKEN`: GitHub's `NodeScriptActionHandler` injects +that credential into the pinned Actions-cache restore/save actions, while the +shell `ScriptHandler` does not. Successful full restore/save calls provide the +credential/token proof. Bracketed authorities use the fixed runner's Python 3.8+ stdlib `ipaddress` classifier for every IPv6 spelling; missing, too-old or invalid parser state fails closed with only a `parser` reason class. @@ -308,10 +312,13 @@ remote backend. Seed and verify inputs are bound to the configured sentinel ID and exact merge ref before cache access. Seed clears its local marker, performs a full restore and validates the exact marker content; verify performs a full restore and validates exact poison content before its expected-miss check. The -protected PR probe uses that same path and exact marker grammar so cache-version -metadata cannot vary across jobs. After the protected PR probe publishes the -bounded poison key, dispatch `mode=verify-pr-write` with the same validated -inputs; a hit fails because trusted main saw a PR publication. The existing +protected PR probe uses that same path and exact marker grammar, then clears +and fully restores its saved poison key, requiring a cache hit and exact bytes +before the trusted-seed gate; this same-job restore/save proves the PR Node +token/write path, while main verify's poison miss remains the cross-scope proof. +Cache-version metadata cannot vary across jobs. After the protected PR probe +publishes the bounded poison key, dispatch `mode=verify-pr-write` with the same +validated inputs; a hit fails because trusted main saw a PR publication. The existing default `mode=audit` remains the negative resource/credential audit across the full Depot matrix and is unchanged. @@ -347,15 +354,20 @@ record the result without executing PR-controlled code. The job has fixed `.depot-authority-sentinel` path. Before the pinned cache restore it attests the provider-injected remote backend using the same structural non-GitHub/non-loopback HTTP contract (including all IPv4 `127/8` and -IPv4-mapped IPv6 loopback spellings) and requires a nonempty runtime token; no -endpoint, host, path, port or token value is printed. It validates the +IPv4-mapped IPv6 loopback spellings); no endpoint, host, path, port or token +value is printed. The shell attestation does not require ambient +`ACTIONS_RUNTIME_TOKEN`; the pinned Actions-cache restore/save actions are Node +actions, whose `NodeScriptActionHandler` receives that credential while the +shell `ScriptHandler` does not. Successful full restore/save calls prove the +credential/token path. It validates the repository variable and the actual `github.event.pull_request.number`, restores the trusted seed (not lookup-only), validates exact seed marker content on a hit, replaces the path with a deterministic non-secret poison -marker, saves the exact Stage 1 poison key, then fails only if the trusted seed +marker, saves the exact Stage 1 poison key, clears and fully restores that key, +requires a cache hit and exact marker bytes, then fails only if the trusted seed was readable. A seed miss passes with a pending trusted-main -`verify-pr-write` check; a seed hit is reported only after the poison save has -completed. +`verify-pr-write` check; a seed hit is reported only after the poison +restore/save proof has completed. Fork PRs remain on the hosted path and provide the no-Depot-authority half of the acceptance evidence; only the same-repository exact sentinel ref can diff --git a/ci/ci.md b/ci/ci.md index 3bb3cc231..f57c7aed1 100644 --- a/ci/ci.md +++ b/ci/ci.md @@ -324,13 +324,21 @@ at `.depot-authority-sentinel`, and gates only after publication. Before any cache action, it attests the provider-injected `ACTIONS_CACHE_URL` and `ACTIONS_RESULTS_URL` as value-free structural HTTP endpoints with a nonempty non-GitHub/non-loopback authority (including all IPv4 `127/8` and IPv4-mapped -IPv6 loopback spellings), numeric port and explicit path, and requires -`ACTIONS_RUNTIME_TOKEN`; malformed/missing inputs fail closed without printing -endpoint, host, path, port or token values. Endpoint authorities are classified +IPv6 loopback spellings), numeric port and explicit path; malformed/missing +inputs fail closed without printing endpoint, host, path, port or token values. +The shell attestation intentionally does not inspect ambient +`ACTIONS_RUNTIME_TOKEN`: pinned `actions/cache` restore/save actions run as +Node actions; GitHub's `NodeScriptActionHandler` injects the runtime +credential, while the shell `ScriptHandler` does not. Successful full +restore/save calls are the credential/token proof. Endpoint authorities are +classified with the fixed runner's Python 3.8+ stdlib `ipaddress` parser for all bracketed IPv6 spellings; parser absence/version/invalidity fails closed. Seed and poison -markers are validated byte-for-byte on cache hits. It is outside planner -slices and does +markers are validated byte-for-byte on cache hits. After saving the PR poison, +the no-checkout job clears and fully restores that exact key, requires a cache +hit and exact bytes before the trusted-seed gate, and proves the same-job Node +token/write path; main verify's poison miss remains the cross-scope proof. It +is outside planner slices and does not add build commands, matrices, artifacts, or producer/consumer edges; the existing Quality lane summary still gates on its normal `quality` and `runner_contract` jobs. Fork PRs remain hosted and provide the diff --git a/scripts/tests/test_depot_authority_sentinel.py b/scripts/tests/test_depot_authority_sentinel.py index bc3f0803f..75da1f47e 100644 --- a/scripts/tests/test_depot_authority_sentinel.py +++ b/scripts/tests/test_depot_authority_sentinel.py @@ -379,6 +379,18 @@ def test_cache_probe_restores_then_publishes_before_gate(self) -> None: ) self.assertLess( positions["Save PR poison marker"], + positions["Clear local poison marker before proof restore"], + ) + self.assertLess( + positions["Clear local poison marker before proof restore"], + positions["Restore saved PR poison marker"], + ) + self.assertLess( + positions["Restore saved PR poison marker"], + positions["Validate saved PR poison marker content"], + ) + self.assertLess( + positions["Validate saved PR poison marker content"], positions["Require trusted seed isolation after poison publication"], ) restore = self._step_block(self.sentinel, "Restore trusted authority marker") @@ -388,7 +400,33 @@ def test_cache_probe_restores_then_publishes_before_gate(self) -> None: "uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25", self._step_block(self.sentinel, "Save PR poison marker"), ) - self.assertIn("rm -rf -- .depot-authority-sentinel", self.sentinel) + poison_restore = self._step_block( + self.sentinel, "Restore saved PR poison marker" + ) + self.assertIn( + "uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25", + poison_restore, + ) + self.assertIn( + "key: ${{ steps.validate.outputs.poison_key }}", + poison_restore, + ) + self.assertIn("fail-on-cache-miss: true", poison_restore) + poison_validation = self._step_block( + self.sentinel, "Validate saved PR poison marker content" + ) + self.assertIn( + 'CACHE_HIT: ${{ steps.restore_poison.outputs.cache-hit }}', + poison_validation, + ) + self.assertIn("mesh-llm-depot-authority-pr-marker-v1", poison_validation) + self.assertIn("cmp -s", poison_validation) + self.assertIn( + "rm -rf -- .depot-authority-sentinel", + self._step_block( + self.sentinel, "Clear local poison marker before proof restore" + ), + ) self.assertIn( "Trusted seed was not readable; pending trusted-main verify-pr-write.", self.sentinel, @@ -461,12 +499,9 @@ def test_authority_backend_attestation_is_value_free_and_fail_closed(self) -> No valid_results, runtime_token="", ) - self.assertNotEqual(missing_token.returncode, 0) - self.assertIn( - "cache backend attestation failed (variable=ACTIONS_RUNTIME_TOKEN reason=missing)", - missing_token.stderr, - ) - self.assertNotIn("non-secret-runtime-token", missing_token.stderr) + self.assertEqual(missing_token.returncode, 0, missing_token.stderr) + self.assertEqual(missing_token.stdout, "") + self.assertEqual(missing_token.stderr, "") with tempfile.TemporaryDirectory() as bin_dir: tr_path = shutil.which("tr") @@ -527,9 +562,9 @@ def test_authority_attestation_copies_are_identical(self) -> None: "python_status == 3", "ACTIONS_CACHE_URL", "ACTIONS_RESULTS_URL", - "ACTIONS_RUNTIME_TOKEN", ): self.assertIn(required, quality_script) + self.assertNotIn("ACTIONS_RUNTIME_TOKEN", quality_script) def test_five_pr_entrypoints_and_existing_build_shape_are_unchanged(self) -> None: expected = { diff --git a/scripts/tests/test_depot_canary_workflow.py b/scripts/tests/test_depot_canary_workflow.py index 8eefe23b1..144b9c4f0 100644 --- a/scripts/tests/test_depot_canary_workflow.py +++ b/scripts/tests/test_depot_canary_workflow.py @@ -149,7 +149,7 @@ def test_canary_sentinel_modes_are_single_runner_and_cache_bounded(self) -> None self.assertIn("Attest provider-injected cache backend", block) self.assertIn("ACTIONS_CACHE_URL", block) self.assertIn("ACTIONS_RESULTS_URL", block) - self.assertIn("ACTIONS_RUNTIME_TOKEN", block) + self.assertNotIn("ACTIONS_RUNTIME_TOKEN", block) self.assertIn("cache backend attestation failed", block) self.assertNotIn("${endpoint,,}", block) @@ -339,12 +339,9 @@ def test_all_cache_phases_attest_non_github_remote_backend(self) -> None: valid_results, runtime_token="", ) - self.assertNotEqual(result.returncode, 0) - self.assertIn( - "cache backend attestation failed (variable=ACTIONS_RUNTIME_TOKEN reason=missing)", - result.stderr, - ) - self.assertNotIn("non-secret-runtime-token", result.stderr) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "") + self.assertEqual(result.stderr, "") def test_canary_fails_closed_on_cache_and_registry_injection(self) -> None: self.assertIn("forbidden_names=", self.workflow)