From 131e2db10cc931afb7e49f95f6d28aa5e3a4dc9b Mon Sep 17 00:00:00 2001
From: Dom <39115308+djm81@users.noreply.github.com>
Date: Wed, 21 Oct 2026 12:38:16 +0200
Subject: [PATCH 1/6] fix(ci): enforce scoped Requirements admission
---
.../dependency-hotfix-admission.md | 34 ++
.github/workflows/requirements-evidence.yml | 262 ++++++++++++-
.../TDD_EVIDENCE.md | 56 +++
.../design.md | 55 ++-
.../proposal.md | 27 +-
.../requirements-evidence.yaml | 36 ++
.../requirements-proof/review-evidence.json | 10 +-
.../specs/isolated-scanner-runtime/spec.md | 30 ++
.../isolate-semgrep-security-runtime/tasks.md | 12 +
.../test_requirements_late_cycle_workflow.py | 347 ++++++++++++++++++
10 files changed, 856 insertions(+), 13 deletions(-)
create mode 100644 .github/review-notes/dependency-hotfix-admission.md
create mode 100644 tests/unit/scripts/test_requirements_late_cycle_workflow.py
diff --git a/.github/review-notes/dependency-hotfix-admission.md b/.github/review-notes/dependency-hotfix-admission.md
new file mode 100644
index 00000000..2c23747b
--- /dev/null
+++ b/.github/review-notes/dependency-hotfix-admission.md
@@ -0,0 +1,34 @@
+# Scoped dependency hotfix admission follow-up
+
+This follow-up implements the Requirements admission requested after
+[nold-ai/specfact-cli#782](https://github.com/nold-ai/specfact-cli/pull/782)
+merged into dev. Its merge commit is
+`3cf72e848a2b5a233d3f599898d6983319d375de`. The original three Requirements
+checks remain failed. Security Audit passed with the exact temporary MCP
+advisory exception. A merge does not convert the failed provenance into proof.
+
+The companion protected-policy proposal is
+[nold-ai/.github#4](https://github.com/nold-ai/.github/pull/4).
+The implementation evaluates a separately approved HumanApprovedLateCycle
+state at all three core CI boundaries. The original native report remains
+failed and unchanged. Every boundary independently checks the exact source,
+head/tree, policy revision, evidence, live human permission and expiry.
+A previous decision sidecar grants no authority to another run.
+
+The original eleven dependency cases remain required. Three new workflow
+control cases cover native report preservation, enforced policy failures,
+scoped routing and an isolated CI-only native evidence attester. Their real
+pre-code workflow refusal is retained in the change's TDD evidence. The
+admission implementation has independent technical review and local verification.
+Protected deployment and live CI attestation verification remain pending.
+
+This follow-up changes CI controls and their specification and tests. It does
+not change application runtime, dependency pins, core version or signed module
+assets. It does not supply historical native RED, suppress other findings or
+authorize a protected deployment or merge. Signing is CI/CD-only. The owner
+will review the exact deployment and grant payloads after implementation.
+
+A later dependency-only package hotfix still starts from published v0.55.4
+commit `d579970565530c3fd7b98bad4de90cf874c2a99d`, with a release payload check
+that excludes unrelated unreleased dev runtime changes. This CI follow-up is
+not a package release or a promotion of all dev commits.
diff --git a/.github/workflows/requirements-evidence.yml b/.github/workflows/requirements-evidence.yml
index 2fa337b2..9be8688c 100644
--- a/.github/workflows/requirements-evidence.yml
+++ b/.github/workflows/requirements-evidence.yml
@@ -11,6 +11,9 @@ permissions:
contents: read
issues: read
+env:
+ LATE_CYCLE_POLICY_REVISION: ea0509fc5261e394cbc8de918b85ea02ac5f4126
+
jobs:
requirements-evidence-producer:
name: Requirements evidence producer
@@ -19,6 +22,7 @@ jobs:
checks: read
contents: read
issues: read
+ pull-requests: read
outputs:
artifact-id: ${{ steps.upload-requirements-evidence.outputs.artifact-id }}
prior-red-run-id: ${{ steps.prior-red-run.outputs.run-id }}
@@ -926,6 +930,9 @@ jobs:
"${reconciliation_arguments[@]}"
exit_code=$?
set -e
+ if [[ "$run_stage" == "final" ]]; then
+ printf 'native-reconciled=true\nselected-change=%s\n' "$selected_change" >> "$GITHUB_OUTPUT"
+ fi
failure_diagnostic="Requirements proof reconciliation exited with status $exit_code before writing reports."
if [[ "$run_stage" == "red" && "$exit_code" -eq 0 ]]; then
if ! binding_error="$("${isolated_python[@]}" scripts/requirements_proof_provenance.py \
@@ -955,6 +962,36 @@ jobs:
fi
exit "$exit_code"
+ - name: Evaluate scoped late-cycle admission
+ id: late-cycle-admission
+ continue-on-error: true
+ if: >-
+ always() && steps.run-evidence.outcome == 'failure' &&
+ steps.run-evidence.outputs.native-reconciled == 'true' &&
+ steps.run-evidence.outputs.selected-change == 'isolate-semgrep-security-runtime' &&
+ github.repository == 'nold-ai/specfact-cli' &&
+ github.event_name == 'pull_request' && github.event.pull_request.number > 0 &&
+ github.event.pull_request.head.repo.full_name == github.repository &&
+ github.base_ref == 'dev' && github.head_ref == 'codex/dependency-hotfix-admission'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ NATIVE_REPORT: artifacts/requirements-evidence/requirements-evidence.json
+ NATIVE_PLAN: artifacts/requirements-evidence/requirements-evidence-plan.json
+ NATIVE_JUNIT: artifacts/requirements-evidence/requirements-proof.xml
+ ADMISSION_AUDIT: artifacts/requirements-evidence/late-cycle-admission.json
+ shell: bash
+ run: |
+ set -euo pipefail
+ [[ "$LATE_CYCLE_POLICY_REVISION" =~ ^[0-9a-f]{40}$ ]]
+ LATE_CYCLE_POLICY_ROOT="$(mktemp -d "${RUNNER_TEMP}/late-cycle-policy.XXXXXX")"
+ mkdir -p "$LATE_CYCLE_POLICY_ROOT/.github/scripts"
+ curl --fail --silent --show-error --max-filesize 1048576 --connect-timeout 10 --max-time 60 --proto '=https' --tlsv1.2 \
+ "https://raw.githubusercontent.com/nold-ai/.github/${LATE_CYCLE_POLICY_REVISION}/.github/scripts/late_cycle_admission.py" \
+ --output "$LATE_CYCLE_POLICY_ROOT/.github/scripts/late_cycle_admission.py"
+ python3 -I -S "$LATE_CYCLE_POLICY_ROOT/.github/scripts/late_cycle_admission.py" decide \
+ --event "$GITHUB_EVENT_PATH" --report "$NATIVE_REPORT" --plan "$NATIVE_PLAN" --junit "$NATIVE_JUNIT" \
+ --policy-revision "$LATE_CYCLE_POLICY_REVISION" --output "$ADMISSION_AUDIT"
+
- name: Publish Requirements evidence summary
if: always()
run: |
@@ -978,9 +1015,105 @@ jobs:
artifacts/requirements-evidence/legacy-tdd-evidence.json
if-no-files-found: warn
- - name: Enforce Requirements evidence producer verdict
- if: steps.run-evidence.outcome == 'failure'
- run: exit 1
+ - name: Upload producer admission audit
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
+ with:
+ name: requirements-late-cycle-producer-audit
+ path: artifacts/requirements-evidence/late-cycle-admission.json
+ if-no-files-found: ignore
+
+ - name: Enforce native or admitted Requirements verdict
+ if: always()
+ env:
+ NATIVE_OUTCOME: ${{ steps.run-evidence.outcome }}
+ ADMISSION_OUTCOME: ${{ steps.late-cycle-admission.outcome }}
+ shell: bash
+ run: |
+ [[ "$NATIVE_OUTCOME" == "success" || "$ADMISSION_OUTCOME" == "success" ]]
+
+ requirements-native-origin:
+ name: Native Requirements Origin
+ needs: [requirements-evidence-producer]
+ if: always() && needs.requirements-evidence-producer.outputs.artifact-id != ''
+ runs-on: ubuntu-latest
+ permissions:
+ actions: read
+ contents: read
+ id-token: write
+ attestations: write
+ steps:
+ - name: Bind native artifact to executed workflow
+ env:
+ GH_TOKEN: ${{ github.token }}
+ NATIVE_ARTIFACT_ID: ${{ needs.requirements-evidence-producer.outputs.artifact-id }}
+ NATIVE_HEAD_COMMIT: ${{ github.event.pull_request.head.sha }}
+ NATIVE_WORKFLOW_SHA: ${{ github.workflow_sha }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ [[ "$GITHUB_REPOSITORY" == "nold-ai/specfact-cli" ]]
+ [[ "$NATIVE_ARTIFACT_ID" =~ ^[0-9]+$ ]]
+ [[ "$NATIVE_HEAD_COMMIT" =~ ^[0-9a-f]{40}$ ]]
+ [[ "$NATIVE_WORKFLOW_SHA" =~ ^[0-9a-f]{40}$ ]]
+ origin_root="${RUNNER_TEMP}/requirements-native-origin"
+ mkdir -p "$origin_root"
+ gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${NATIVE_ARTIFACT_ID}" > "$origin_root/artifact.json"
+ gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${NATIVE_ARTIFACT_ID}/zip" > "$origin_root/native.zip"
+ python3 -I -S - "$origin_root" <<'PY'
+ import hashlib
+ import json
+ import os
+ import sys
+ from pathlib import Path
+
+ root = Path(sys.argv[1])
+ metadata = json.loads((root / "artifact.json").read_text())
+ digest = "sha256:" + hashlib.sha256((root / "native.zip").read_bytes()).hexdigest()
+ artifact_id = int(os.environ["NATIVE_ARTIFACT_ID"])
+ run_id = int(os.environ["GITHUB_RUN_ID"])
+ if (metadata["id"] != artifact_id or metadata["name"] != "requirements-evidence"
+ or metadata["expired"] or metadata["workflow_run"]["id"] != run_id
+ or metadata["workflow_run"]["head_sha"] != os.environ["NATIVE_HEAD_COMMIT"]
+ or metadata["digest"] != digest):
+ raise SystemExit("Native artifact association or ZIP digest mismatch")
+ manifest = {
+ "schema_version": 1,
+ "repository": os.environ["GITHUB_REPOSITORY"],
+ "run_id": run_id,
+ "run_attempt": int(os.environ["GITHUB_RUN_ATTEMPT"]),
+ "artifact_id": artifact_id,
+ "artifact_digest": digest,
+ "head_commit": os.environ["NATIVE_HEAD_COMMIT"],
+ "workflow_path": ".github/workflows/requirements-evidence.yml",
+ "workflow_sha": os.environ["NATIVE_WORKFLOW_SHA"],
+ }
+ (root / "origin.json").write_text(json.dumps(manifest, sort_keys=True, separators=(",", ":")))
+ PY
+
+ - name: Attest native origin manifest
+ id: attest-native-origin
+ uses: actions/attest@7a37fbbab9669658a566371acd1cf2f8c7a40d32
+ with:
+ subject-path: ${{ runner.temp }}/requirements-native-origin/origin.json
+
+ - name: Retain native origin verification bundle
+ env:
+ ORIGIN_BUNDLE_PATH: ${{ steps.attest-native-origin.outputs.bundle-path }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ test -s "$ORIGIN_BUNDLE_PATH"
+ cp "$ORIGIN_BUNDLE_PATH" "${RUNNER_TEMP}/requirements-native-origin/origin.bundle.jsonl"
+
+ - name: Upload authenticated native origin
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
+ with:
+ name: requirements-native-origin-${{ github.run_id }}-${{ github.run_attempt }}
+ path: |
+ ${{ runner.temp }}/requirements-native-origin/origin.json
+ ${{ runner.temp }}/requirements-native-origin/origin.bundle.jsonl
+ if-no-files-found: error
requirements-evidence:
name: Requirements evidence execution
@@ -991,6 +1124,7 @@ jobs:
checks: read
contents: read
issues: read
+ pull-requests: read
outputs:
artifact-id: ${{ steps.upload-fresh-requirements-evidence.outputs.artifact-id }}
artifact-digest: ${{ steps.upload-fresh-requirements-evidence.outputs.artifact-digest }}
@@ -1598,8 +1732,12 @@ jobs:
run-id: ${{ needs.requirements-evidence-producer.outputs.prior-red-run-id }}
- name: Reconcile Requirements evidence on fresh runner
+ id: run-native-evidence
+ continue-on-error: true
timeout-minutes: 12
env:
+ EVIDENCE_PULL_REQUEST: ${{ github.event.pull_request.number }}
+ EVIDENCE_HEAD_BRANCH: ${{ github.head_ref }}
EVIDENCE_PROMOTION_REUSE: ${{ steps.execution-promotion.outputs.active }}
LATE_RED_ACTIVE: ${{ github.event.pull_request.number == 717 && github.head_ref == 'bugfix/692-promotion-final-context' && hashFiles('openspec/changes/fix-release-promotion-requirements-parity/requirements-proof/late-red-evidence.json') != '' }}
LATE_RED_CYCLE_BASE: ${{ steps.late-red.outputs.cycle-base-ref }}
@@ -1929,13 +2067,59 @@ jobs:
.plan_digest == $plan_digest' \
"$legacy_tdd_evidence" > /dev/null
reconciliation_arguments+=(--legacy-tdd-evidence "$legacy_tdd_evidence")
+ elif [[ "$GITHUB_REPOSITORY" == "nold-ai/specfact-cli" && "$EVIDENCE_PULL_REQUEST" =~ ^[1-9][0-9]*$ && "$EVIDENCE_HEAD_BRANCH" == "codex/dependency-hotfix-admission" && "$selected_change" == "isolate-semgrep-security-runtime" ]]; then
+ :
else
printf 'Fresh final reconciliation requires authenticated red or legacy evidence.\n' >&2
exit 1
fi
- "${reconciliation_arguments[@]}"
+ native_exit=0
+ "${reconciliation_arguments[@]}" || native_exit=$?
+ printf 'native-reconciled=true\nselected-change=%s\n' "$selected_change" >> "$GITHUB_OUTPUT"
+ if [[ "$native_exit" -ne 0 ]]; then
+ exit "$native_exit"
+ fi
+ jq -e '.gate_decision == "pass" and .verdict == "passed"' "$consumer_report" > /dev/null
fi
+ - name: Evaluate scoped late-cycle admission
+ id: late-cycle-admission
+ continue-on-error: true
+ if: >-
+ always() && steps.run-native-evidence.outcome == 'failure' &&
+ steps.run-native-evidence.outputs.native-reconciled == 'true' &&
+ steps.run-native-evidence.outputs.selected-change == 'isolate-semgrep-security-runtime' &&
+ github.repository == 'nold-ai/specfact-cli' &&
+ github.event_name == 'pull_request' && github.event.pull_request.number > 0 &&
+ github.event.pull_request.head.repo.full_name == github.repository &&
+ github.base_ref == 'dev' && github.head_ref == 'codex/dependency-hotfix-admission'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ NATIVE_REPORT: artifacts/requirements-evidence/requirements-evidence-consumer.json
+ NATIVE_PLAN: artifacts/requirements-evidence/requirements-evidence-consumer-plan.json
+ NATIVE_JUNIT: ${{ runner.temp }}/requirements-proof-consumer.xml
+ ADMISSION_AUDIT: artifacts/requirements-evidence/late-cycle-admission-consumer.json
+ shell: bash
+ run: |
+ set -euo pipefail
+ [[ "$LATE_CYCLE_POLICY_REVISION" =~ ^[0-9a-f]{40}$ ]]
+ LATE_CYCLE_POLICY_ROOT="$(mktemp -d "${RUNNER_TEMP}/late-cycle-policy.XXXXXX")"
+ mkdir -p "$LATE_CYCLE_POLICY_ROOT/.github/scripts"
+ curl --fail --silent --show-error --max-filesize 1048576 --connect-timeout 10 --max-time 60 --proto '=https' --tlsv1.2 \
+ "https://raw.githubusercontent.com/nold-ai/.github/${LATE_CYCLE_POLICY_REVISION}/.github/scripts/late_cycle_admission.py" \
+ --output "$LATE_CYCLE_POLICY_ROOT/.github/scripts/late_cycle_admission.py"
+ python3 -I -S "$LATE_CYCLE_POLICY_ROOT/.github/scripts/late_cycle_admission.py" decide \
+ --event "$GITHUB_EVENT_PATH" --report "$NATIVE_REPORT" --plan "$NATIVE_PLAN" --junit "$NATIVE_JUNIT" \
+ --policy-revision "$LATE_CYCLE_POLICY_REVISION" --output "$ADMISSION_AUDIT"
+
+ - name: Upload execution admission audit
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
+ with:
+ name: requirements-late-cycle-execution-audit
+ path: artifacts/requirements-evidence/late-cycle-admission-consumer.json
+ if-no-files-found: ignore
+
- name: Prepare fresh Requirements execution artifact
if: always()
env:
@@ -2024,6 +2208,15 @@ jobs:
path: artifacts/requirements-evidence/code-review.json
if-no-files-found: warn
+ - name: Enforce native or admitted Requirements verdict
+ if: always()
+ env:
+ NATIVE_OUTCOME: ${{ steps.run-native-evidence.outcome }}
+ ADMISSION_OUTCOME: ${{ steps.late-cycle-admission.outcome }}
+ shell: bash
+ run: |
+ [[ "$NATIVE_OUTCOME" == "success" || "$ADMISSION_OUTCOME" == "success" ]]
+
- name: Enforce requirements evidence verdict
if: steps.run-code-review.outcome == 'failure'
run: exit 1
@@ -2037,6 +2230,7 @@ jobs:
checks: read
contents: read
issues: read
+ pull-requests: read
runs-on: ubuntu-latest
steps:
- name: Enforce trusted Requirements execution success
@@ -2568,7 +2762,11 @@ jobs:
run-id: ${{ needs.requirements-evidence.outputs.prior-red-run-id }}
- name: Reconcile final Requirements verdict on fresh runner
+ id: run-final-native-evidence
+ continue-on-error: true
env:
+ EVIDENCE_PULL_REQUEST: ${{ github.event.pull_request.number }}
+ EVIDENCE_HEAD_BRANCH: ${{ github.head_ref }}
EVIDENCE_PROMOTION_REUSE: ${{ steps.final-promotion.outputs.active }}
LATE_RED_ACTIVE: ${{ github.event.pull_request.number == 717 && github.head_ref == 'bugfix/692-promotion-final-context' && hashFiles('openspec/changes/fix-release-promotion-requirements-parity/requirements-proof/late-red-evidence.json') != '' }}
PRIOR_RED_HEAD_SHA: ${{ steps.final-retained-red.outputs.head-sha }}
@@ -2783,14 +2981,59 @@ jobs:
.plan_digest == $plan_digest' \
"$legacy_tdd_evidence" > /dev/null
reconciliation_arguments+=(--legacy-tdd-evidence "$legacy_tdd_evidence")
+ elif [[ "$GITHUB_REPOSITORY" == "nold-ai/specfact-cli" && "$EVIDENCE_PULL_REQUEST" =~ ^[1-9][0-9]*$ && "$EVIDENCE_HEAD_BRANCH" == "codex/dependency-hotfix-admission" && "$selected_change" == "isolate-semgrep-security-runtime" ]]; then
+ :
else
printf 'Final reconciliation requires authenticated red or legacy evidence.\n' >&2
exit 1
fi
- "${reconciliation_arguments[@]}"
+ native_exit=0
+ "${reconciliation_arguments[@]}" || native_exit=$?
+ printf 'native-reconciled=true\nselected-change=%s\n' "$selected_change" >> "$GITHUB_OUTPUT"
+ if [[ "$native_exit" -ne 0 ]]; then
+ exit "$native_exit"
+ fi
jq -e '.gate_decision == "pass" and .verdict == "passed"' "$final_report" > /dev/null
cat "$final_summary" >> "$GITHUB_STEP_SUMMARY"
+ - name: Evaluate scoped late-cycle admission
+ id: late-cycle-admission
+ continue-on-error: true
+ if: >-
+ always() && steps.run-final-native-evidence.outcome == 'failure' &&
+ steps.run-final-native-evidence.outputs.native-reconciled == 'true' &&
+ steps.run-final-native-evidence.outputs.selected-change == 'isolate-semgrep-security-runtime' &&
+ github.repository == 'nold-ai/specfact-cli' &&
+ github.event_name == 'pull_request' && github.event.pull_request.number > 0 &&
+ github.event.pull_request.head.repo.full_name == github.repository &&
+ github.base_ref == 'dev' && github.head_ref == 'codex/dependency-hotfix-admission'
+ env:
+ GH_TOKEN: ${{ github.token }}
+ NATIVE_REPORT: ${{ runner.temp }}/final-requirements.json
+ NATIVE_PLAN: ${{ runner.temp }}/final-requirements-plan.json
+ NATIVE_JUNIT: ${{ runner.temp }}/requirements-execution/requirements-proof.xml
+ ADMISSION_AUDIT: ${{ runner.temp }}/final-late-cycle-admission.json
+ shell: bash
+ run: |
+ set -euo pipefail
+ [[ "$LATE_CYCLE_POLICY_REVISION" =~ ^[0-9a-f]{40}$ ]]
+ LATE_CYCLE_POLICY_ROOT="$(mktemp -d "${RUNNER_TEMP}/late-cycle-policy.XXXXXX")"
+ mkdir -p "$LATE_CYCLE_POLICY_ROOT/.github/scripts"
+ curl --fail --silent --show-error --max-filesize 1048576 --connect-timeout 10 --max-time 60 --proto '=https' --tlsv1.2 \
+ "https://raw.githubusercontent.com/nold-ai/.github/${LATE_CYCLE_POLICY_REVISION}/.github/scripts/late_cycle_admission.py" \
+ --output "$LATE_CYCLE_POLICY_ROOT/.github/scripts/late_cycle_admission.py"
+ python3 -I -S "$LATE_CYCLE_POLICY_ROOT/.github/scripts/late_cycle_admission.py" decide \
+ --event "$GITHUB_EVENT_PATH" --report "$NATIVE_REPORT" --plan "$NATIVE_PLAN" --junit "$NATIVE_JUNIT" \
+ --policy-revision "$LATE_CYCLE_POLICY_REVISION" --output "$ADMISSION_AUDIT"
+
+ - name: Upload final admission audit
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
+ with:
+ name: requirements-late-cycle-final-audit
+ path: ${{ runner.temp }}/final-late-cycle-admission.json
+ if-no-files-found: ignore
+
- name: Set up reviewed Code Review Node runtime for final verdict
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e
with:
@@ -2890,6 +3133,15 @@ jobs:
path: ${{ runner.temp }}/final-code-review.json
if-no-files-found: error
+ - name: Enforce native or admitted Requirements verdict
+ if: always()
+ env:
+ NATIVE_OUTCOME: ${{ steps.run-final-native-evidence.outcome }}
+ ADMISSION_OUTCOME: ${{ steps.late-cycle-admission.outcome }}
+ shell: bash
+ run: |
+ [[ "$NATIVE_OUTCOME" == "success" || "$ADMISSION_OUTCOME" == "success" ]]
+
- name: Enforce final Code Review verdict
if: steps.run-final-code-review.outcome == 'failure'
run: exit 1
diff --git a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
index 4491ffc1..df63e752 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
@@ -143,3 +143,59 @@ nor technical mapping acceptance fabricates those protected approvals.
Direct yamllint on the changed mapping passes. The repository-wide wrapper
prints unrelated baseline errors while returning success, already tracked by
issue #781. No wrapper behavior is changed in this dependency hotfix.
+
+## Owner-authorized scoped admission implementation, 2026-10-21
+
+The owner authorized actual implementation in policy PR #4 and core PR #782,
+with an explicit pause before protected deployment or merging. This supersedes
+the previous documentation-only policy checkpoint. Native historical proof
+remains missing. No original failed artifact is relabelled RED or modified.
+
+The new workflow wiring test was authored after the updated specification and
+before implementation. It extracts and executes the actual final reconciliation
+shell from requirements-evidence.yml with a synthetic policy-boundary adapter.
+The adapter isolates workflow routing; it does not claim authentic GitHub
+approval or signing. The raw failed native packet must remain byte-identical.
+
+The pre-code command was `python -m pytest -q
+ tests/unit/scripts/test_requirements_late_cycle_workflow.py` in the existing
+full tools environment. Actual result was 1 failed. The behavioral disagreement
+was `assert 1 == 0` with stderr:
+
+```text
+Final reconciliation requires authenticated red or legacy evidence.
+```
+
+This is evidence of the real early-refusal branch, not a missing-module/import
+failure. The complete retained log is
+`/private/tmp/specfact-dependency-audit-20261020/official-only/implementation/workflow-red.txt`.
+The protected policy baseline independently passes all 66 existing tests before
+new implementation. Hostile-input policy tests and all three workflow paths are
+separate required verification. Final CI and new exact-head grants remain open.
+
+## Separate follow-up verification, 2026-10-21
+
+After PR #782 merged, the owner authorized a separate CI-only core follow-up
+with policy PR #4. The fourteen exact mapped selectors pass, including the
+actual frozen certifi artifact test. The protected policy suite passes all
+179 tests. Its historical-attempt regression first failed against the old
+collector and then passed after explicit approved-attempt binding.
+
+The shell tests execute each actual producer, fresh and final boundary using
+transport and policy doubles. They prove preserved raw native bytes, ordinary
+success, enforced policy refusal, scope guards and isolated CI origin wiring.
+They do not prove authentic GitHub attestation or human approval. The final
+clean-code review of the refactored test reports zero findings. A fresh
+independent reviewer accepted the final fourteen-case mapping under its own
+identity in requirements-proof/review-evidence.json.
+
+All three strict frozen audits pass with only the owner-approved MCP exception.
+The required contract command exits zero with no modified source detected.
+This is a CI-only change; that command does not establish contract execution.
+Actual mapped test and policy results are retained separately. Full smart-test,
+normal hooks, final native CI origin verification, protected deployment and
+new exact-head human approvals remain separate gates.
+
+Policy revision pinned in the workflow is `ea0509fc5261e394cbc8de918b85ea02ac5f4126`.
+It remains inactive until separately authorized protected deployment.
+Native failed reports remain immutable and missing historical RED stays missing.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/design.md b/openspec/changes/isolate-semgrep-security-runtime/design.md
index 00eb855b..b0c37dcd 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/design.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/design.md
@@ -2,8 +2,9 @@
## Decision
-Reuse the existing exact advisory register. Do not alter the audit engine,
-workflow requirements, package identities or resolver behavior. Delete the
+Reuse the existing exact advisory register. Do not alter the audit engine, package identities or resolver behavior.
+The owner separately authorized narrowly scoped Requirements admission and
+core CI integration on 2026-10-21. Delete the
rejected fork rather than maintain inactive producer infrastructure.
## Data
@@ -40,3 +41,53 @@ Earlier native replacement planning and the rejected producer remain in Git
history. Their tests, mappings, candidate attestations and publication approvals
do not establish acceptance for this scope. Companion-module migration is only
needed if a future change replaces/removes the official integration.
+
+## Scoped CI admission
+
+Policy PR #4 owns one live evaluator in immutable protected policy code. Core
+invokes it at the producer, fresh and final Requirements reconciliation
+boundaries. Its output is audit data only. Every boundary independently reads
+the live exact-head grant, permission, current subject and retained evidence.
+No consumer trusts an earlier sidecar as a bearer credential.
+
+The domain has distinct NativeProvenance, HumanApprovedLateCycle and Refused
+outcomes. Admission preserves the byte-identical failed native report, including
+incomplete maturity and missing historical RED. Code Review consumes that real
+failed report through its existing signed schema-2 interface. Its own failures
+remain blocking. No signed module changes are required.
+
+The canonical separate grant binds repository, the live numeric direct follow-up PR, issue780, change, dev
+base, head branch/commit/tree, immutable policy revision, exact eleven dependency selectors and three admission controls,
+source/aggregate mapping and plan identities, native run/attempt/artifact and
+raw hashes, authenticated executed producer source, explicitly missing native
+RED, the exact allowed provenance finding and expiry. It is unedited, unique,
+issued by a currently authorized write/admin human, and at most seven days old.
+The current execution independently passes every selector and matches semantic
+plan/mapping identities. Historical and current byte hashes remain separate.
+
+All JSON, archive, XML and network inputs are bounded. Duplicate/unknown fields,
+unsafe archive members, malformed or skipped tests, extra findings, changed
+subject/policy, stale/revoked authority, network uncertainty and expiry refuse.
+No API run head SHA is claimed to prove executed workflow revision. Use verified
+GitHub CI provenance when the API does not authenticate that identity. Signing
+and attestation generation occur only in CI/CD. No local signing is permitted.
+
+The implementation is prepared and reviewed before protected activation. It
+must refuse before owner-approved deployment and the distinct final human grant.
+Rollback disables the scoped protected policy and restores refusal while keeping
+all native reports and ordinary CI gates. The generic exact-head authority and
+the MCP advisory register remain separate capabilities.
+
+Implementation sequence is updated specification, behavioral failing workflow
+fixture, policy/core implementation, passing hostile-input and native-path
+controls, independent review, normal hooks and CI. No immutable failed artifact
+is relabelled RED or rewritten. The final deployment packet records exact code
+revisions, new evidence, approval payloads, rollback and remaining owner actions.
+
+The owner merged dependency PR #782 before this CI implementation was ready,
+then authorized a separate follow-up. Current subject is the unique open dev PR
+on `codex/dependency-hotfix-admission`, independently obtained through GitHub.
+Its numeric PR identity is an exact canonical grant field. It inherits no old
+head/tree authority. The still-open issue #780 and this active change retain
+scope and tracking. The merged dependency payload remains separate from the
+CI control continuation.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/proposal.md b/openspec/changes/isolate-semgrep-security-runtime/proposal.md
index ae86eb3d..74e8a2af 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/proposal.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/proposal.md
@@ -21,6 +21,15 @@ prevents a compatible upgrade. Official Semgrep 1.182.0 still requires
release exists. Expiry, unrelated advisories and other versions fail closed.
- Preserve strict audits of all three committed requirement graphs, independent
SAST, module signatures, protected authority and CI/CD-only signing.
+- Implement the owner-requested scoped late-cycle admission in policy PR #4 and
+ its CI-only core integration. Keep the failed native report intact. A separate
+ exact-head human grant may admit only missing historical failing-first proof
+ for the eleven dependency scenarios and three new admission control cases. It grants no dependency-audit waiver,
+ native RED claim, merge, release or local signing.
+- Authenticate the retained native evidence and execute the same immutable
+ protected evaluator at producer, fresh and final reconciliation. Every
+ consumer independently verifies live scope, approval, permission and expiry.
+ Policy activation, protected deployment and merging require owner approval.
## Capabilities
@@ -31,7 +40,8 @@ prevents a compatible upgrade. Official Semgrep 1.182.0 still requires
### Modified Capabilities
-None.
+- CI Requirements admission for only issue #780 and the direct admission follow-up to merged PR #782. Application runtime
+ and the ordinary native-provenance path retain their existing contracts.
## Impact
@@ -63,6 +73,21 @@ promotion is authorized. Final commit/tree authority remains separate.
- Only the exact reviewed MCP finding is waived before expiry. Every other
advisory, audit error, skipped dependency or expired record remains blocking.
- Required quality, review, CI and human authority gates remain enforced.
+- Scoped admission is distinct from native proof, refuses all extra findings and
+ cannot transfer across heads, trees, policies, mappings or expired approvals.
+- Genuine pre-code failures and adversarial controls verify the new admission
+ implementation separately from the dependency scenarios being admitted.
Sources checked 2026-10-21: [official Semgrep metadata](https://pypi.org/pypi/semgrep/json),
[upstream MCP advisory](https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-84m7-p3x7-pcfv).
+
+## Direct follow-up after dependency merge
+
+On 2026-10-21 the owner merged dependency PR #782 as dev commit
+`3cf72e848a2b5a233d3f599898d6983319d375de`, then explicitly authorized a
+separate core admission follow-up alongside policy PR #4. This continuation
+uses branch `codex/dependency-hotfix-admission`, the same still-open issue #780,
+and the exact numeric follow-up PR from live GitHub metadata. Its separate
+human grant must bind that PR's own head/tree and authentic evidence. The old
+PR #782 grant cannot transfer. The follow-up changes CI controls and their
+verification, without changing merged dependency pins or application runtime.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/requirements-evidence.yaml b/openspec/changes/isolate-semgrep-security-runtime/requirements-evidence.yaml
index 86be19fb..b3fa1028 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/requirements-evidence.yaml
+++ b/openspec/changes/isolate-semgrep-security-runtime/requirements-evidence.yaml
@@ -125,3 +125,39 @@ requirements:
selector:
runner: pytest
node_id: tests/unit/scripts/test_security_audit_gate.py::test_committed_mcp_exception_is_exact_and_time_bounded[future-review]
+ openspec:isolate-semgrep-security-runtime:isolated-scanner-runtime:admit-only-the-explicitly-approved-late-cycle-provenance-gap:
+ rationale: Enforce the owner-authorized CI-only admission independently from the dependency advisory exception and native proof.
+ stakeholder_refs:
+ - nold-ai/specfact-cli#780
+ touchpoints:
+ - id: requirements-workflow
+ kind: config_file
+ locator: .github/workflows/requirements-evidence.yml
+ - id: scoped-admission-controls
+ kind: test
+ locator: tests/unit/scripts/test_requirements_late_cycle_workflow.py
+ verification_cases:
+ - case_id: ADMISSION-NATIVE-BYTES-AND-ENFORCEMENT
+ scenario_id: admit-only-the-explicitly-approved-late-cycle-provenance-gap
+ method: test
+ intent: Exercise all three real reconciliation boundaries, preserve raw native bytes and enforce policy exit codes.
+ observable: Native success remains independent and scoped failure proceeds only through the enforced separate decision.
+ selector:
+ runner: pytest
+ node_id: tests/unit/scripts/test_requirements_late_cycle_workflow.py::test_all_three_lanes_preserve_native_bytes_and_enforce_policy_exit
+ - case_id: ADMISSION-SCOPE-AND-PROOF-GUARDS
+ scenario_id: admit-only-the-explicitly-approved-late-cycle-provenance-gap
+ method: test
+ intent: Reject invalid PR contexts and other branches and preserve invalid native RED refusal.
+ observable: Unapproved scope and invalid native proof cannot silently acquire admission.
+ selector:
+ runner: pytest
+ node_id: tests/unit/scripts/test_requirements_late_cycle_workflow.py::test_fresh_and_final_preserve_scope_and_invalid_red_guards
+ - case_id: ADMISSION-ISOLATED-NATIVE-ORIGIN
+ scenario_id: admit-only-the-explicitly-approved-late-cycle-provenance-gap
+ method: test
+ intent: Require an isolated CI-only origin attester with immutable official actions and no candidate execution.
+ observable: The manifest validates native artifact metadata and CI wires an isolated attestation step after producer failure.
+ selector:
+ runner: pytest
+ node_id: tests/unit/scripts/test_requirements_late_cycle_workflow.py::test_origin_attestation_survives_producer_failure
diff --git a/openspec/changes/isolate-semgrep-security-runtime/requirements-proof/review-evidence.json b/openspec/changes/isolate-semgrep-security-runtime/requirements-proof/review-evidence.json
index d4514037..1f6bb66f 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/requirements-proof/review-evidence.json
+++ b/openspec/changes/isolate-semgrep-security-runtime/requirements-proof/review-evidence.json
@@ -1,9 +1,9 @@
{
"schema_version": "1",
"decision": "accepted",
- "reviewer_id": "Codex:/root/expanded_selector_map_review",
- "reviewer_role": "independent AI code reviewer",
- "recorded_at": "2026-10-21T08:58:05.627991+00:00",
- "reference": "https://github.com/nold-ai/specfact-cli/pull/782",
- "mapping_digest": "sha256:9be3a10b220655e55c299b9b3d69c2be0c0b029b1db1c0942abfea1ea6c4f86d"
+ "reviewer_id": "Codex:/root/admission_final_mapping_acceptance",
+ "reviewer_role": "independent AI technical mapping reviewer",
+ "recorded_at": "2026-10-21T10:34:11.586358+00:00",
+ "reference": "https://github.com/nold-ai/specfact-cli/issues/780",
+ "mapping_digest": "sha256:c3fd8ebfa8b872f5ba12eee146ae2bdf9fa60a4c9c9a318450d528c4b4215318"
}
diff --git a/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md b/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
index d80fd1aa..4f03b2d5 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
@@ -34,3 +34,33 @@ or expiry. No Semgrep fork or conflicting dependency override SHALL be adopted.
- **GIVEN** the reviewed record with a different version, advisory, skipped subject or date after expiry
- **WHEN** the existing audit gate evaluates that input
- **THEN** it returns a failure instead of suppressing the finding.
+
+### Requirement: Admit only the explicitly approved late-cycle provenance gap
+
+For only issue #780 and the owner-authorized direct follow-up to merged PR #782
+on branch `codex/dependency-hotfix-admission`, CI MAY
+admit missing historical failing-first proof through a separately approved
+HumanApprovedLateCycle decision from immutable protected policy code. Every
+producer, fresh and final boundary SHALL independently authenticate its inputs,
+exact subject and policy, live human permission and unedited bounded approval.
+The native report SHALL remain failed and byte-identical. Admission SHALL NOT
+claim native RED, verified provenance, waive other findings, alter application
+runtime, bypass Code Review, or authorize protected deployment or merge.
+
+#### Scenario: Exact scoped admission retains the failed native report
+
+- **GIVEN** authenticated exact-head evidence, all eleven dependency selectors and three admission controls passing, and a separate unexpired human grant
+- **WHEN** the immutable protected evaluator runs independently at each CI boundary
+- **THEN** it emits HumanApprovedLateCycle with the raw failed native report unchanged and keeps the independent Code Review gate enforced.
+
+#### Scenario: Substitution or expired authority remains blocking
+
+- **GIVEN** missing or changed policy, subject, evidence, selectors, permission, grant, expiry, or an additional failed obligation
+- **WHEN** any CI boundary evaluates admission
+- **THEN** it refuses and preserves the original report and every required check.
+
+#### Scenario: Ordinary native proof retains its existing gate
+
+- **GIVEN** valid complete native failing-first proof outside the scoped exception
+- **WHEN** Requirements reconciles it
+- **THEN** the existing native success path remains independent of late-cycle approval.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/tasks.md b/openspec/changes/isolate-semgrep-security-runtime/tasks.md
index a6b7da36..97465a1f 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/tasks.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/tasks.md
@@ -29,3 +29,15 @@
- [ ] Prepare any later dependency-only release from published v0.55.4.
- [ ] Link verified coverage before closing related dependency PRs.
- [ ] Archive through OpenSpec only after completion and merge.
+
+## 5. Implement owner-authorized scoped admission
+
+- [x] Ground all three core consumers and compare distinct implementation designs.
+- [x] Specify independent live policy evaluation and distinct admission semantics.
+- [x] Preserve genuine behavioral failing-before workflow evidence.
+- [x] Implement bounded policy, exact human grant and authenticated native origin.
+- [x] Integrate the immutable evaluator into producer, fresh and final CI paths.
+- [x] Prove hostile input refusal, unchanged native success and pinned Code Review compatibility.
+- [ ] Complete independent review, normal hooks and pre-deployment CI.
+- [ ] Present exact protected deployment and new authority requests for owner approval.
+- [ ] After approval only, activate and verify all required CI without merging.
diff --git a/tests/unit/scripts/test_requirements_late_cycle_workflow.py b/tests/unit/scripts/test_requirements_late_cycle_workflow.py
new file mode 100644
index 00000000..5d00cc11
--- /dev/null
+++ b/tests/unit/scripts/test_requirements_late_cycle_workflow.py
@@ -0,0 +1,347 @@
+from __future__ import annotations
+
+import hashlib
+import json
+import os
+import subprocess
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, NamedTuple
+
+import yaml
+
+
+ROOT = Path(__file__).resolve().parents[3]
+WORKFLOW = ROOT / ".github/workflows/requirements-evidence.yml"
+LANES = {
+ "final": "Reconcile final Requirements verdict on fresh runner",
+ "fresh": "Reconcile Requirements evidence on fresh runner",
+ "producer": "Run Requirements evidence gate",
+}
+
+
+def _boundary(lane: str) -> str:
+ workflow = yaml.safe_load(WORKFLOW.read_text())
+ shell = next(
+ step["run"] for job in workflow["jobs"].values() for step in job["steps"] if step.get("name") == LANES[lane]
+ )
+ if lane == "producer":
+ start = shell.index('set +e\n "${reconciliation_arguments[@]}"')
+ end = shell.index('failure_diagnostic="Requirements proof reconciliation exited with status $exit_code', start)
+ return shell[start:end] + '\nexit "$exit_code"'
+ if lane == "fresh":
+ return shell[shell.index(' test -s "$consumer_junit"') :].rsplit("\nfi", 1)[0]
+ return shell[shell.index('test -s "$execution_junit"') :].split('cat "$final_summary"')[0]
+
+
+@dataclass(frozen=True)
+class WorkflowProofCase:
+ lane: str
+ policy_exit: int
+ native_exit: int
+ scope: str = "scoped"
+ extra_failure: bool = False
+
+
+class BoundaryArtifacts(NamedTuple):
+ report: Path
+ junit: Path
+ plan: Path
+ summary: Path
+ policy: Path
+ called: Path
+ bin_dir: Path
+ event: Path
+ original: bytes
+
+
+def _write_policy_double(policy: Path, policy_exit: int) -> None:
+ policy.write_text(
+ "import argparse, json, os, sys\nfrom pathlib import Path\n"
+ "p=argparse.ArgumentParser(); p.add_argument('operation'); "
+ "p.add_argument('--output'); p.add_argument('--report'); p.add_argument('--plan'); "
+ "p.add_argument('--junit'); p.add_argument('--event'); p.add_argument('--policy-revision'); a=p.parse_args()\n"
+ "assert a.operation == 'decide'\n"
+ "assert Path(a.report).read_bytes() == Path(os.environ['EXPECTED_REPORT']).read_bytes()\n"
+ "assert Path(a.plan).is_file() and Path(a.junit).is_file() and Path(a.event).is_file()\n"
+ "Path(os.environ['CALLED']).write_text(json.dumps(vars(a)))\n"
+ "Path(a.output).write_text(json.dumps({'outcome':'HumanApprovedLateCycle'}))\n"
+ f"sys.exit({policy_exit})\n"
+ )
+
+
+def _native_artifacts(tmp_path: Path, case: WorkflowProofCase) -> BoundaryArtifacts:
+ artifact = tmp_path / "artifacts/requirements-evidence"
+ artifact.mkdir(parents=True)
+ report = artifact / "requirements-evidence.json"
+ findings = ["prior-red-proof-missing"] if case.native_exit else []
+ if case.extra_failure:
+ findings.append("failed-test")
+ raw = {
+ "schema_version": "2",
+ "gate_decision": "fail" if case.native_exit else "pass",
+ "verdict": "failed" if case.native_exit else "passed",
+ "findings": findings,
+ }
+ original = json.dumps(raw, sort_keys=True).encode()
+ report.write_bytes(original)
+ junit = artifact / "requirements-proof.xml"
+ junit.write_text('')
+ plan = artifact / "requirements-evidence-plan.json"
+ plan.write_text("{}")
+ summary = tmp_path / "summary.md"
+ summary.write_text("Native result.\n")
+ policy = tmp_path / "policy.py"
+ called = tmp_path / "policy-called.json"
+ _write_policy_double(policy, case.policy_exit)
+ bin_dir = tmp_path / "bin"
+ bin_dir.mkdir()
+ curl = bin_dir / "curl"
+ curl.write_text(
+ '#!/usr/bin/env bash\nset -eu\nwhile [[ "$1" != --output ]]; do shift; done\ncp "$POLICY_DOUBLE" "$2"\n'
+ )
+ curl.chmod(0o755)
+ event = tmp_path / "event.json"
+ event.write_text("{}")
+ return BoundaryArtifacts(report, junit, plan, summary, policy, called, bin_dir, event, original)
+
+
+def _native_prelude(case: WorkflowProofCase, paths: BoundaryArtifacts) -> str:
+ report, junit, plan, summary = paths[:4]
+ return "\n".join(
+ [
+ "set -euo pipefail",
+ f'isolated_specfact=(bash -c "exit {case.native_exit}" native)',
+ f'reconciliation_arguments=(bash -c "exit {case.native_exit}" native)',
+ f'execution_junit="{junit}"',
+ f'consumer_junit="{junit}"',
+ f'final_plan="{plan}"',
+ f'consumer_plan="{plan}"',
+ f'final_report="{report}"',
+ f'consumer_report="{report}"',
+ f'final_summary="{summary}"',
+ f'consumer_summary="{summary}"',
+ 'source_ref="0123456789abcdef0123456789abcdef01234567"',
+ 'LATE_RED_PROVENANCE="${RUNNER_TEMP}/late-red-trusted-provenance/scripts/requirements_proof_provenance.py"',
+ 'FINAL_LATE_RED_PROVENANCE="$LATE_RED_PROVENANCE"',
+ 'LATE_RED_CYCLE_BASE="0123456789abcdef0123456789abcdef01234567"',
+ f"LATE_RED_ACTIVE={'true' if case.scope == 'invalid-proof' else 'false'}",
+ 'PRIOR_RED_HEAD_SHA=""',
+ "run_stage=final",
+ "selected_change=isolate-semgrep-security-runtime",
+ 'evidence_base_commit="0123456789abcdef0123456789abcdef01234567"',
+ f"EVIDENCE_PULL_REQUEST={0 if case.scope == 'invalid-pr' else 900}",
+ f"EVIDENCE_HEAD_BRANCH={'other-branch' if case.scope == 'other-branch' else 'codex/dependency-hotfix-admission'}",
+ ]
+ )
+
+
+def _boundary_environment(tmp_path: Path, paths: BoundaryArtifacts) -> dict[str, str]:
+ report, _, _, _, policy, called, bin_dir, event, _ = paths
+ return {
+ **os.environ,
+ "PATH": f"{bin_dir}:{os.environ['PATH']}",
+ "RUNNER_TEMP": str(tmp_path),
+ "GITHUB_WORKSPACE": str(tmp_path),
+ "GITHUB_EVENT_PATH": str(event),
+ "GITHUB_REPOSITORY": "nold-ai/specfact-cli",
+ "LATE_CYCLE_POLICY_REVISION": "0123456789abcdef0123456789abcdef01234567",
+ "POLICY_DOUBLE": str(policy),
+ "CALLED": str(called),
+ "EXPECTED_REPORT": str(report),
+ }
+
+
+def _admission_steps(case: WorkflowProofCase) -> tuple[dict[str, Any], dict[str, Any]]:
+ workflow = yaml.safe_load(WORKFLOW.read_text())
+ job = next(
+ job for job in workflow["jobs"].values() if any(step.get("name") == LANES[case.lane] for step in job["steps"])
+ )
+ steps = job["steps"]
+ native = next(step for step in steps if step.get("name") == LANES[case.lane])
+ admission = next(step for step in steps if step.get("id") == "late-cycle-admission")
+ enforcement = next(step for step in steps if step.get("name") == "Enforce native or admitted Requirements verdict")
+ _assert_admission_isolation(steps, native, admission, enforcement)
+ return admission, enforcement
+
+
+def _assert_admission_isolation(
+ steps: list[dict[str, Any]], native: dict[str, Any], admission: dict[str, Any], enforcement: dict[str, Any]
+) -> None:
+ assert native["continue-on-error"] is True
+ assert "GH_TOKEN" not in native.get("env", {})
+ assert "curl " not in native["run"]
+ assert admission["env"]["GH_TOKEN"] == "${{ github.token }}"
+ assert steps.index(native) < steps.index(admission) < steps.index(enforcement)
+ assert "native-reconciled == 'true'" in admission["if"]
+ assert "outcome == 'failure'" in admission["if"]
+ assert "selected-change == 'isolate-semgrep-security-runtime'" in admission["if"]
+ assert "codex/dependency-hotfix-admission" in admission["if"]
+
+
+def _run_admission(
+ tmp_path: Path,
+ case: WorkflowProofCase,
+ admission: dict[str, Any],
+ environment: dict[str, str],
+ native: tuple[subprocess.CompletedProcess[str], bool],
+) -> str:
+ result, marker = native
+ admission_outcome = "skipped"
+ if not (result.returncode and marker and case.scope == "scoped"):
+ return admission_outcome
+ admission_environment = {**environment, "GH_TOKEN": "transport-double-token"}
+ paths = {
+ "NATIVE_REPORT": environment["EXPECTED_REPORT"],
+ "NATIVE_PLAN": str(tmp_path / "artifacts/requirements-evidence/requirements-evidence-plan.json"),
+ "NATIVE_JUNIT": str(tmp_path / "artifacts/requirements-evidence/requirements-proof.xml"),
+ "ADMISSION_AUDIT": str(tmp_path / "admission.json"),
+ }
+ admission_result = subprocess.run(
+ ["bash", "-c", admission["run"]],
+ cwd=tmp_path,
+ env={**admission_environment, **paths},
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ return "success" if admission_result.returncode == 0 else "failure"
+
+
+def _run_boundary(tmp_path: Path, case: WorkflowProofCase) -> None:
+ paths = _native_artifacts(tmp_path, case)
+ report, _, _, summary, _, called, _, _, original = paths
+ prelude = _native_prelude(case, paths)
+ environment = _boundary_environment(tmp_path, paths)
+ admission, enforcement = _admission_steps(case)
+ output = tmp_path / "native-output"
+ environment["GITHUB_OUTPUT"] = str(output)
+ environment["GITHUB_STEP_SUMMARY"] = str(summary)
+ environment.pop("GH_TOKEN", None)
+ environment.pop("GITHUB_TOKEN", None)
+ result = subprocess.run(
+ ["bash", "-c", prelude + "\n" + _boundary(case.lane)],
+ cwd=tmp_path,
+ env=environment,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ marker = output.exists() and "native-reconciled=true" in output.read_text()
+ assert marker == (case.scope != "invalid-proof" and (case.lane == "producer" or case.scope == "scoped"))
+ admission_outcome = _run_admission(tmp_path, case, admission, environment, (result, marker))
+ verdict = subprocess.run(
+ ["bash", "-c", enforcement["run"]],
+ cwd=tmp_path,
+ env={
+ **environment,
+ "NATIVE_OUTCOME": "failure" if result.returncode else "success",
+ "ADMISSION_OUTCOME": admission_outcome,
+ },
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ expected_success = case.scope == "scoped" and (case.native_exit == 0 or case.policy_exit == 0)
+ assert (verdict.returncode == 0) == expected_success, result.stderr
+ assert report.read_bytes() == original
+ assert called.exists() == (bool(case.native_exit) and case.scope == "scoped")
+
+
+def test_all_three_lanes_preserve_native_bytes_and_enforce_policy_exit(tmp_path: Path) -> None:
+ workflow = yaml.safe_load(WORKFLOW.read_text())
+ for name in ("requirements-evidence-producer", "requirements-evidence", "requirements-evidence-final"):
+ assert workflow["jobs"][name]["permissions"].get("pull-requests") == "read"
+ for lane in LANES:
+ for policy_exit in (0, 1, 7):
+ for native_exit in (0, 1):
+ _run_boundary(
+ tmp_path / f"{lane}-{policy_exit}-{native_exit}", WorkflowProofCase(lane, policy_exit, native_exit)
+ )
+ _run_boundary(tmp_path / f"{lane}-additional-native-failure", WorkflowProofCase(lane, 1, 1, extra_failure=True))
+
+
+def test_fresh_and_final_preserve_scope_and_invalid_red_guards(tmp_path: Path) -> None:
+ for lane in ("fresh", "final"):
+ for scope in ("invalid-pr", "other-branch", "invalid-proof"):
+ _run_boundary(tmp_path / f"{lane}-{scope}", WorkflowProofCase(lane, 0, 1, scope=scope))
+
+
+def _origin_fixture(tmp_path: Path, mismatch: str | None) -> tuple[Path, dict[str, Any], dict[str, str]]:
+ native_zip = b"inert native archive, never executed"
+ head = "1" * 40
+ workflow_sha = "2" * 40
+ root = tmp_path / str(mismatch)
+ root.mkdir()
+ (root / "native.zip").write_bytes(native_zip)
+ metadata = {
+ "id": 123,
+ "name": "requirements-evidence",
+ "expired": False,
+ "workflow_run": {"id": 457 if mismatch == "run" else 456, "head_sha": head},
+ "digest": "sha256:" + ("0" * 64 if mismatch == "digest" else hashlib.sha256(native_zip).hexdigest()),
+ }
+ (root / "artifact.json").write_text(json.dumps(metadata))
+ gh = root / "gh"
+ gh.write_text(
+ '#!/usr/bin/env bash\nset -eu\ncase "$2" in */zip) cat "$FIXTURE/native.zip";; *) cat "$FIXTURE/artifact.json";; esac\n'
+ )
+ gh.chmod(0o755)
+ environment = {
+ **os.environ,
+ "PATH": f"{root}:{os.environ['PATH']}",
+ "FIXTURE": str(root),
+ "GITHUB_REPOSITORY": "nold-ai/specfact-cli",
+ "RUNNER_TEMP": str(root),
+ "GITHUB_RUN_ID": "456",
+ "GITHUB_RUN_ATTEMPT": "3",
+ "NATIVE_ARTIFACT_ID": "123",
+ "NATIVE_HEAD_COMMIT": head,
+ "NATIVE_WORKFLOW_SHA": workflow_sha,
+ }
+ return root, metadata, environment
+
+
+def _assert_origin_attestation(origin: dict[str, Any]) -> None:
+ assert "always()" in origin["if"]
+ assert origin["permissions"]["id-token"] == "write"
+ assert all("checkout" not in step.get("uses", "") for step in origin["steps"])
+ attest = next(step for step in origin["steps"] if step.get("uses", "").startswith("actions/attest@"))
+ assert attest["with"]["subject-path"].endswith("/origin.json")
+ assert len(attest["uses"].split("@")[1]) == 40
+
+
+def _origin_binding() -> dict[str, Any]:
+ workflow = yaml.safe_load(WORKFLOW.read_text())
+ jobs = list(workflow["jobs"].values())
+ origins = [job for job in jobs if job.get("name") == "Native Requirements Origin"]
+ assert len(origins) == 1, "A failed producer must still generate independently authenticated origin metadata"
+ origin = origins[0]
+ _assert_origin_attestation(origin)
+ return next(step for step in origin["steps"] if step.get("name") == "Bind native artifact to executed workflow")
+
+
+def test_origin_attestation_survives_producer_failure(tmp_path: Path) -> None:
+ binding = _origin_binding()
+ assert binding["env"]["NATIVE_WORKFLOW_SHA"] == "${{ github.workflow_sha }}"
+ for mismatch in (None, "digest", "run"):
+ root, metadata, environment = _origin_fixture(tmp_path, mismatch)
+ result = subprocess.run(
+ ["bash", "-c", binding["run"]], env=environment, capture_output=True, text=True, check=False
+ )
+ assert (result.returncode == 0) == (mismatch is None), result.stderr
+ manifest = root / "requirements-native-origin/origin.json"
+ assert manifest.exists() == (mismatch is None)
+ if mismatch is None:
+ actual = json.loads(manifest.read_bytes())
+ assert actual == {
+ "schema_version": 1,
+ "repository": "nold-ai/specfact-cli",
+ "run_id": 456,
+ "run_attempt": 3,
+ "artifact_id": 123,
+ "artifact_digest": metadata["digest"],
+ "head_commit": "1" * 40,
+ "workflow_path": ".github/workflows/requirements-evidence.yml",
+ "workflow_sha": "2" * 40,
+ }
From 89bf62e8108b00b3ed8118466e6f984cb9a1048e Mon Sep 17 00:00:00 2001
From: Dom <39115308+djm81@users.noreply.github.com>
Date: Wed, 21 Oct 2026 12:45:22 +0200
Subject: [PATCH 2/6] test(ci): retain admission enforcement contracts
---
.../TDD_EVIDENCE.md | 31 +++++
...requirements_evidence_delivery_workflow.py | 125 +++++++++++++-----
.../test_requirements_planning_permissions.py | 8 +-
3 files changed, 131 insertions(+), 33 deletions(-)
diff --git a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
index df63e752..f8626313 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
@@ -199,3 +199,34 @@ new exact-head human approvals remain separate gates.
Policy revision pinned in the workflow is `ea0509fc5261e394cbc8de918b85ea02ac5f4126`.
It remains inactive until separately authorized protected deployment.
Native failed reports remain immutable and missing historical RED stays missing.
+
+### Normal-hook and existing contract corrections
+
+The normal commit hook first rejected job.workflow_sha because the pinned
+workflow linter does not recognize that new context. This origin job is directly
+defined in the workflow. GitHub's context reference documents the equivalent
+github.workflow_sha for this case. An added assertion first failed on the old
+context, then all three shell controls passed with the supported declaration.
+The final normal commit hooks passed, including zero findings for the new test.
+
+The first full follow-up smart suite recorded 7 failed, 3447 passed and 10
+skipped. Every failure was an existing workflow contract tied to the replaced
+enforcement name or previous exact read-permission set. The two existing test
+files now preserve their original mapped parameter IDs, exercise native/admission
+exit combinations in all three lanes, retain upload ordering and independently
+require both Code Review failure guards. The targeted suite passes all 30 cases.
+
+Narrow baseline quality exception: reviewing the two existing test files reports
+four pytest.mark/parametrize unknown-member typing warnings and four fixture-length
+informational findings. An actual unchanged-HEAD helper run records the same
+severity, rule and message for every finding. No new warning or information
+finding remains. The unchanged expanded fixture builders express authenticated
+artifact/run contracts and remain outside this focused CI admission correction.
+No suppression or tool configuration change is introduced. This exception covers
+only these eight baseline findings, not future regressions.
+
+Evidence is retained in the implementation audit as origin-context-red.log,
+origin-context-green.log, core-commit-hooks-final.log, followup-smart-final.log,
+existing-workflow-tests-red.log, existing-workflow-tests-green.log and the actual
+baseline/current review JSON files. The final full-suite rerun remains separately
+required; the targeted result does not turn the earlier failed run into a pass.
diff --git a/tests/unit/workflows/test_requirements_evidence_delivery_workflow.py b/tests/unit/workflows/test_requirements_evidence_delivery_workflow.py
index 9530a3d5..d64ba237 100644
--- a/tests/unit/workflows/test_requirements_evidence_delivery_workflow.py
+++ b/tests/unit/workflows/test_requirements_evidence_delivery_workflow.py
@@ -90,9 +90,15 @@ def _workflow_steps(workflow: dict[str, object]) -> list[tuple[str, int, dict[st
return collected
-def _step_location(workflow: dict[str, object], name: str) -> tuple[str, int, dict[str, object]]:
+def _step_location(
+ workflow: dict[str, object], name: str, *, job_name: str | None = None
+) -> tuple[str, int, dict[str, object]]:
"""Return the unique job, index, and step for a workflow step name."""
- matches = [location for location in _workflow_steps(workflow) if location[2].get("name") == name]
+ matches = [
+ location
+ for location in _workflow_steps(workflow)
+ if location[2].get("name") == name and (job_name is None or location[0] == job_name)
+ ]
assert len(matches) == 1, name
return matches[0]
@@ -101,10 +107,10 @@ def _step_by_name(workflow: dict[str, object], name: str) -> dict[str, object]:
return _step_location(workflow, name)[2]
-def _assert_step_order(workflow: dict[str, object], earlier: str, later: str) -> None:
+def _assert_step_order(workflow: dict[str, object], earlier: str, later: str, *, job_name: str | None = None) -> None:
"""Assert two uniquely named steps run in that order within one job."""
- earlier_job, earlier_index, _ = _step_location(workflow, earlier)
- later_job, later_index, _ = _step_location(workflow, later)
+ earlier_job, earlier_index, _ = _step_location(workflow, earlier, job_name=job_name)
+ later_job, later_index, _ = _step_location(workflow, later, job_name=job_name)
assert earlier_job == later_job
assert earlier_index < later_index
@@ -455,7 +461,9 @@ def _assert_retention_contract(workflow: dict[str, object]) -> None:
publish = _step_by_name(workflow, "Publish Requirements evidence summary")
upload = _step_by_name(workflow, "Persist Requirements evidence before Code Review")
upload_review = _step_by_name(workflow, "Upload Code Review evidence artifact")
- producer_enforce = _step_by_name(workflow, "Enforce Requirements evidence producer verdict")
+ _, _, producer_enforce = _step_location(
+ workflow, "Enforce native or admitted Requirements verdict", job_name="requirements-evidence-producer"
+ )
review_enforce = _step_by_name(workflow, "Enforce requirements evidence verdict")
assert publish["if"] == "always()" # type: ignore[index]
assert "GITHUB_STEP_SUMMARY" in publish["run"] # type: ignore[index]
@@ -471,15 +479,20 @@ def _assert_retention_contract(workflow: dict[str, object]) -> None:
"artifacts/requirements-evidence/legacy-tdd-evidence.json",
]
assert upload_review["with"]["path"] == "artifacts/requirements-evidence/code-review.json" # type: ignore[index]
- assert producer_enforce["if"] == "steps.run-evidence.outcome == 'failure'"
- assert producer_enforce["run"] == "exit 1"
+ assert producer_enforce["if"] == "always()"
assert review_enforce["if"] == "steps.run-code-review.outcome == 'failure'"
assert review_enforce["run"] == "exit 1"
_assert_step_order(
- workflow, "Publish Requirements evidence summary", "Enforce Requirements evidence producer verdict"
+ workflow,
+ "Publish Requirements evidence summary",
+ "Enforce native or admitted Requirements verdict",
+ job_name="requirements-evidence-producer",
)
_assert_step_order(
- workflow, "Persist Requirements evidence before Code Review", "Enforce Requirements evidence producer verdict"
+ workflow,
+ "Persist Requirements evidence before Code Review",
+ "Enforce native or admitted Requirements verdict",
+ job_name="requirements-evidence-producer",
)
_assert_step_order(workflow, "Upload Code Review evidence artifact", "Enforce requirements evidence verdict")
@@ -662,7 +675,13 @@ def _assert_fresh_consumer_order(
workflow: dict[str, object], outputs: dict[str, str], permissions: dict[str, str]
) -> None:
assert outputs["prior-red-run-id"] == "${{ steps.prior-red-run.outputs.run-id }}"
- assert permissions == {"actions": "read", "contents": "read", "issues": "read", "checks": "read"}
+ assert permissions == {
+ "actions": "read",
+ "contents": "read",
+ "issues": "read",
+ "checks": "read",
+ "pull-requests": "read",
+ }
_assert_step_order(
workflow, "Restore Requirements evidence for Code Review", "Download retained red proof for validation"
)
@@ -1603,18 +1622,60 @@ def test_same_named_fork_does_not_enter_promotion_path() -> None:
assert "pull_request.head.repo.full_name == github.event.repository.full_name" in condition
-def _review_and_enforcement_steps() -> tuple[
- dict[str, object], dict[str, object], dict[str, object], dict[str, object]
-]:
- """Load the proof and review steps that independently govern final PR status."""
- workflow = REPO_ROOT / ".github" / "workflows" / "requirements-evidence.yml"
- parsed = yaml.load(workflow.read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
- return (
- _step_by_name(parsed, "Run Code Review with finalized Requirements context"),
- _step_by_name(parsed, "Enforce Requirements evidence producer verdict"),
- _step_by_name(parsed, "Enforce Requirements evidence producer success"),
- _step_by_name(parsed, "Enforce requirements evidence verdict"),
+def _assert_independent_review_guards(workflow: dict[str, object]) -> None:
+ """Admission cannot override either Code Review failure or producer failure."""
+ producer_guard = _step_by_name(workflow, "Enforce Requirements evidence producer success")
+ assert producer_guard["if"] == "needs.requirements-evidence-producer.result != 'success'"
+ assert producer_guard["run"] == "exit 1"
+ for review_name, enforce_name, review_id in (
+ (
+ "Run Code Review with finalized Requirements context",
+ "Enforce requirements evidence verdict",
+ "run-code-review",
+ ),
+ (
+ "Run Code Review with trusted final Requirements context",
+ "Enforce final Code Review verdict",
+ "run-final-code-review",
+ ),
+ ):
+ review = _step_by_name(workflow, review_name)
+ review_enforce = _step_by_name(workflow, enforce_name)
+ assert review["continue-on-error"] == "true"
+ assert review_enforce["if"] == f"steps.{review_id}.outcome == 'failure'"
+ assert review_enforce["run"] == "exit 1"
+
+
+def _assert_lane_verdict(workflow: dict[str, object], job_name: str, native_step: str) -> None:
+ """Execute the lane's actual shell verdict for successful and denied outcomes."""
+ _, enforce_index, enforce = _step_location(
+ workflow, "Enforce native or admitted Requirements verdict", job_name=job_name
)
+ assert enforce["if"] == "always()"
+ assert enforce["shell"] == "bash"
+ assert enforce["env"] == {
+ "NATIVE_OUTCOME": "${{ steps." + native_step + ".outcome }}",
+ "ADMISSION_OUTCOME": "${{ steps.late-cycle-admission.outcome }}",
+ }
+ for native_outcome, admission_outcome, expected_exit in (
+ ("success", "skipped", 0),
+ ("failure", "success", 0),
+ ("failure", "failure", 1),
+ ("failure", "skipped", 1),
+ ("skipped", "skipped", 1),
+ ("", "", 1),
+ ):
+ result = subprocess.run(
+ ["bash", "-c", cast(str, enforce["run"])],
+ env={"NATIVE_OUTCOME": native_outcome, "ADMISSION_OUTCOME": admission_outcome},
+ check=False,
+ capture_output=True,
+ text=True,
+ )
+ assert result.returncode == expected_exit, (job_name, native_outcome, admission_outcome, result.stderr)
+ for owner, index, step in _workflow_steps(workflow):
+ if owner == job_name and str(step.get("uses", "")).startswith("actions/upload-artifact@"):
+ assert index < enforce_index
@pytest.mark.parametrize(
@@ -1623,18 +1684,18 @@ def _review_and_enforcement_steps() -> tuple[
ids=["requirements-failure", "code-review-failure"],
)
def test_requirements_evidence_workflow_blocks_each_final_verdict(verdict: str) -> None:
- """Requirements and Code Review failures remain independently terminal."""
- review, producer_enforce, producer_guard, review_enforce = _review_and_enforcement_steps()
-
+ """Every lane needs native success or admission, and review failure still blocks."""
+ workflow = REPO_ROOT / ".github" / "workflows" / "requirements-evidence.yml"
+ parsed = yaml.load(workflow.read_text(encoding="utf-8"), Loader=yaml.BaseLoader)
if verdict == "requirements-failure":
- assert producer_enforce["if"] == "steps.run-evidence.outcome == 'failure'"
- assert producer_enforce["run"] == "exit 1"
- assert producer_guard["if"] == "needs.requirements-evidence-producer.result != 'success'"
- assert producer_guard["run"] == "exit 1"
+ for job_name, native_step in (
+ ("requirements-evidence-producer", "run-evidence"),
+ ("requirements-evidence", "run-native-evidence"),
+ ("requirements-evidence-final", "run-final-native-evidence"),
+ ):
+ _assert_lane_verdict(parsed, job_name, native_step)
else:
- assert review["continue-on-error"] == "true"
- assert review_enforce["if"] == "steps.run-code-review.outcome == 'failure'"
- assert review_enforce["run"] == "exit 1"
+ _assert_independent_review_guards(parsed)
def test_all_proof_consumers_independently_authenticate_live_stack_context() -> None:
diff --git a/tests/unit/workflows/test_requirements_planning_permissions.py b/tests/unit/workflows/test_requirements_planning_permissions.py
index b9b5c69b..521531ae 100644
--- a/tests/unit/workflows/test_requirements_planning_permissions.py
+++ b/tests/unit/workflows/test_requirements_planning_permissions.py
@@ -17,4 +17,10 @@ def test_each_proof_consumer_has_explicit_read_only_checks_permission(job_name:
"""Authenticated check-run reads require permission without check-writing authority."""
workflow = yaml.safe_load((REPO_ROOT / ".github/workflows/requirements-evidence.yml").read_text())
permissions = workflow["jobs"][job_name]["permissions"]
- assert permissions == {"actions": "read", "contents": "read", "issues": "read", "checks": "read"}
+ assert permissions == {
+ "actions": "read",
+ "contents": "read",
+ "issues": "read",
+ "checks": "read",
+ "pull-requests": "read",
+ }
From 53d4b7b4907ab8ef05bc339efca594adaa69f860 Mon Sep 17 00:00:00 2001
From: Dom <39115308+djm81@users.noreply.github.com>
Date: Wed, 21 Oct 2026 12:54:10 +0200
Subject: [PATCH 3/6] fix(ci): scope native origin to admission subject
---
.github/workflows/requirements-evidence.yml | 7 ++++++-
.../TDD_EVIDENCE.md | 19 +++++++++++++++++++
.../design.md | 5 +++++
.../specs/isolated-scanner-runtime/spec.md | 6 ++++++
.../test_requirements_late_cycle_workflow.py | 10 ++++++++++
5 files changed, 46 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/requirements-evidence.yml b/.github/workflows/requirements-evidence.yml
index 9be8688c..1fbedee1 100644
--- a/.github/workflows/requirements-evidence.yml
+++ b/.github/workflows/requirements-evidence.yml
@@ -1035,7 +1035,12 @@ jobs:
requirements-native-origin:
name: Native Requirements Origin
needs: [requirements-evidence-producer]
- if: always() && needs.requirements-evidence-producer.outputs.artifact-id != ''
+ if: >-
+ always() && needs.requirements-evidence-producer.outputs.artifact-id != '' &&
+ github.repository == 'nold-ai/specfact-cli' &&
+ github.event_name == 'pull_request' && github.event.pull_request.number > 0 &&
+ github.event.pull_request.head.repo.full_name == github.repository &&
+ github.base_ref == 'dev' && github.head_ref == 'codex/dependency-hotfix-admission'
runs-on: ubuntu-latest
permissions:
actions: read
diff --git a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
index f8626313..023c800e 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
@@ -230,3 +230,22 @@ origin-context-green.log, core-commit-hooks-final.log, followup-smart-final.log,
existing-workflow-tests-red.log, existing-workflow-tests-green.log and the actual
baseline/current review JSON files. The final full-suite rerun remains separately
required; the targeted result does not turn the earlier failed run into a pass.
+
+### Full-suite pass and fork-attester review fix
+
+At core head 89bf62e8, the corrected full smart suite exited zero with 3455
+passed, 9 skipped and 2 dependency-deprecation warnings in 251.58 seconds.
+The final-head native origin job also succeeded. The actual protected-policy
+historical collector and official gh verifier authenticated its origin bundle,
+exact workflow revision, native ZIP and fourteen passing selectors. The only
+native finding is prior-red-proof-missing. This authenticates the observed
+failed report; it does not supply historical RED or a human grant.
+
+PR #784 review identified that the new attester could start on fork PRs without
+the required write permissions. The exact scoped-job contract first failed
+because its same-repository, pull-request, dev-base and branch guards were absent.
+The job now requires the same exact subject as late-cycle admission. It remains
+always() after producer failure but skips forks and unrelated subjects. No
+permission, CI check or artifact mutation is used to address the finding.
+The focused new and existing workflow suite passes all 33 tests after the fix.
+Evidence is retained in origin-scope-red.log and origin-scope-green.log.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/design.md b/openspec/changes/isolate-semgrep-security-runtime/design.md
index b0c37dcd..057ba61d 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/design.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/design.md
@@ -91,3 +91,8 @@ Its numeric PR identity is an exact canonical grant field. It inherits no old
head/tree authority. The still-open issue #780 and this active change retain
scope and tracking. The merged dependency payload remains separate from the
CI control continuation.
+
+The isolated origin attester is restricted to the same-repository dev follow-up
+on the exact admission branch. Forks and unrelated subjects skip that extra job
+and retain their ordinary native Requirements checks. GitHub does not grant
+fork pull-request jobs the attestation write permissions required by this job.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md b/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
index 4f03b2d5..88fec7ec 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
@@ -64,3 +64,9 @@ runtime, bypass Code Review, or authorize protected deployment or merge.
- **GIVEN** valid complete native failing-first proof outside the scoped exception
- **WHEN** Requirements reconciles it
- **THEN** the existing native success path remains independent of late-cycle approval.
+
+#### Scenario: Unrelated and fork pull requests do not start the attester
+
+- **GIVEN** a fork, another repository, base or branch, or an absent native artifact
+- **WHEN** CI evaluates the origin attester job
+- **THEN** it skips that job and retains ordinary Requirements enforcement without requesting signing permissions for that subject.
diff --git a/tests/unit/scripts/test_requirements_late_cycle_workflow.py b/tests/unit/scripts/test_requirements_late_cycle_workflow.py
index 5d00cc11..e256fb05 100644
--- a/tests/unit/scripts/test_requirements_late_cycle_workflow.py
+++ b/tests/unit/scripts/test_requirements_late_cycle_workflow.py
@@ -317,6 +317,16 @@ def _origin_binding() -> dict[str, Any]:
origins = [job for job in jobs if job.get("name") == "Native Requirements Origin"]
assert len(origins) == 1, "A failed producer must still generate independently authenticated origin metadata"
origin = origins[0]
+ assert {term.strip() for term in origin["if"].split("&&")} == {
+ "always()",
+ "needs.requirements-evidence-producer.outputs.artifact-id != ''",
+ "github.repository == 'nold-ai/specfact-cli'",
+ "github.event_name == 'pull_request'",
+ "github.event.pull_request.number > 0",
+ "github.event.pull_request.head.repo.full_name == github.repository",
+ "github.base_ref == 'dev'",
+ "github.head_ref == 'codex/dependency-hotfix-admission'",
+ }
_assert_origin_attestation(origin)
return next(step for step in origin["steps"] if step.get("name") == "Bind native artifact to executed workflow")
From d88310de977bfc5d080e7e03c75256434ed937ea Mon Sep 17 00:00:00 2001
From: Dom <39115308+djm81@users.noreply.github.com>
Date: Wed, 21 Oct 2026 13:09:32 +0200
Subject: [PATCH 4/6] docs(ci): clarify attester and rerun evidence
---
.../TDD_EVIDENCE.md | 25 ++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
index 023c800e..a9b7acd7 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
@@ -246,6 +246,29 @@ the required write permissions. The exact scoped-job contract first failed
because its same-repository, pull-request, dev-base and branch guards were absent.
The job now requires the same exact subject as late-cycle admission. It remains
always() after producer failure but skips forks and unrelated subjects. No
-permission, CI check or artifact mutation is used to address the finding.
+permission change, separate CI status check or artifact mutation is used to
+address the finding.
The focused new and existing workflow suite passes all 33 tests after the fix.
Evidence is retained in origin-scope-red.log and origin-scope-green.log.
+
+### Actual rerun retention check
+
+Review claimed that fixed artifact names conflict across workflow reruns.
+The exact locked artifact client documents reruns as a supported duplicate-name
+case. A controlled retry of Requirements run 39588065523 on 21 October 2026
+confirmed the actual backend behavior. Attempt 2 successfully uploaded the same
+requirements-evidence name as new artifact 12341235127 and the same producer
+audit name as new artifact 12341410036. The original native artifact 12339828168
+and origin artifact 12340872780 remained accessible and unexpired.
+
+After the run advanced to attempt 2, the actual reviewed historical collector
+and official gh verifier again authenticated attempt 1's original signed origin
+and native ZIP. Its bytes, fourteen selectors and sole missing-RED finding were
+unchanged. The existing consumers bind explicit artifact IDs. No overwrite,
+deletion or artifact naming change is required. The retry remains refused for
+the absent distinct human grant. Successful uploads do not admit provenance.
+
+The exact retry job is
+.
+Evidence is retained in pr784-after-rerun-artifacts.json,
+pr784-rerun-producer.log and pr784-retained-attempt1-verification.log.
From 4b25241ef5548d29b6ad78be6cd2d26dda6afa77 Mon Sep 17 00:00:00 2001
From: Dom <39115308+djm81@users.noreply.github.com>
Date: Wed, 21 Oct 2026 13:55:34 +0200
Subject: [PATCH 5/6] fix(ci): restore evidence by exact artifact ID
---
.github/workflows/requirements-evidence.yml | 324 ++++++++++++-
.../TDD_EVIDENCE.md | 116 +++++
.../design.md | 33 ++
.../specs/isolated-scanner-runtime/spec.md | 12 +
.../isolate-semgrep-security-runtime/tasks.md | 14 +-
.../test_release_promotion_security_gates.py | 2 +-
.../workflows/test_exact_artifact_transfer.py | 443 ++++++++++++++++++
...requirements_evidence_delivery_workflow.py | 9 +-
8 files changed, 934 insertions(+), 19 deletions(-)
create mode 100644 tests/unit/workflows/test_exact_artifact_transfer.py
diff --git a/.github/workflows/requirements-evidence.yml b/.github/workflows/requirements-evidence.yml
index 1fbedee1..cec43c7f 100644
--- a/.github/workflows/requirements-evidence.yml
+++ b/.github/workflows/requirements-evidence.yml
@@ -1422,10 +1422,296 @@ jobs:
echo "PYLINT_WRAPPER=$pylint_wrapper" >> "$GITHUB_ENV"
- name: Restore Requirements evidence for Code Review
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
- with:
- artifact-ids: ${{ needs.requirements-evidence-producer.outputs.artifact-id }}
- path: artifacts/requirements-evidence
+ env:
+ GH_TOKEN: ${{ github.token }}
+ ARTIFACT_ID: ${{ needs.requirements-evidence-producer.outputs.artifact-id }}
+ ARTIFACT_NAME: requirements-evidence
+ ARTIFACT_REPOSITORY: ${{ github.repository }}
+ ARTIFACT_RUN_ID: ${{ github.run_id }}
+ ARTIFACT_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
+ ARTIFACT_EXPECTED_SHA256: ''
+ ARTIFACT_DESTINATION: ${{ github.workspace }}/artifacts/requirements-evidence
+ shell: bash
+ run: &restore_exact_requirements_artifact |
+ set -euo pipefail
+ /usr/bin/python3 -I -S - <<'PYTHON'
+ import hashlib
+ import io
+ import json
+ import os
+ import re
+ import secrets
+ import signal
+ import stat
+ import time
+ import urllib.error
+ import urllib.parse
+ import urllib.request
+ import zipfile
+ from dataclasses import dataclass
+ from pathlib import Path
+
+ MAX_ARCHIVE = 16 * 1024 * 1024
+ MAX_FILE = 16 * 1024 * 1024
+ MAX_EXPANDED = 64 * 1024 * 1024
+ DIRECTORY_FLAGS = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW
+
+
+ @dataclass(frozen=True)
+ class ArtifactSubject:
+ artifact_id: int
+ name: str
+ repository: str
+ repository_id: int
+ run_id: int
+ head_sha: str
+ expected_sha256: str
+ destination: Path
+ allow_existing: bool
+
+
+ def positive_id(value):
+ if not re.fullmatch(r"[1-9][0-9]*", value):
+ raise ValueError("Invalid subject ID")
+ return int(value)
+
+
+ def parse_subject():
+ env = os.environ
+ name = env["ARTIFACT_NAME"]
+ digest = env["ARTIFACT_EXPECTED_SHA256"].removeprefix("sha256:")
+ repository = env["ARTIFACT_REPOSITORY"]
+ head = env["ARTIFACT_HEAD_SHA"]
+ if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository):
+ raise ValueError("Invalid repository")
+ if not re.fullmatch(r"[0-9a-f]{40}", head):
+ raise ValueError("Invalid head SHA")
+ if digest and not re.fullmatch(r"[0-9a-f]{64}", digest):
+ raise ValueError("Invalid independent digest")
+ if name not in ("requirements-evidence", "requirements-evidence-execution"):
+ raise ValueError("Invalid artifact name")
+ if name == "requirements-evidence-execution" and not digest:
+ raise ValueError("Execution upload digest is required")
+ workspace = Path(env["GITHUB_WORKSPACE"])
+ runner = Path(env["RUNNER_TEMP"])
+ destination = Path(env["ARTIFACT_DESTINATION"])
+ paths = (workspace, runner, destination)
+ if any(not p.is_absolute() or ".." in p.parts for p in paths):
+ raise ValueError("Invalid runner path")
+ review = workspace / "artifacts/requirements-evidence"
+ allowed = (runner / "requirements-execution",) if name.endswith("-execution") else (review, runner / "requirements-producer")
+ if destination not in allowed:
+ raise ValueError("Unexpected artifact destination")
+ return ArtifactSubject(
+ positive_id(env["ARTIFACT_ID"]), name, repository,
+ positive_id(env["GITHUB_REPOSITORY_ID"]), positive_id(env["ARTIFACT_RUN_ID"]),
+ head, digest, destination, destination == review,
+ )
+
+
+ class NoRedirect(urllib.request.HTTPRedirectHandler):
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
+ return None
+
+
+ def read_bounded(response, limit, deadline):
+ declared = response.headers.get("Content-Length")
+ if declared is not None and (not declared.isdecimal() or int(declared) > limit):
+ raise ValueError("Oversized or malformed response length")
+ chunks = bytearray()
+ while True:
+ if time.monotonic() >= deadline:
+ raise TimeoutError("Artifact transfer deadline exceeded")
+ chunk = response.read(min(65536, limit + 1 - len(chunks)))
+ if not chunk:
+ break
+ chunks.extend(chunk)
+ if len(chunks) > limit:
+ raise ValueError("Response exceeds size limit")
+ if declared is not None and len(chunks) != int(declared):
+ raise ValueError("Truncated response")
+ return bytes(chunks)
+
+
+ def fetch(url, token, limit, deadline, redirects=False):
+ opener = urllib.request.build_opener(NoRedirect())
+ for hop in range(5):
+ parsed = urllib.parse.urlsplit(url)
+ if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password or parsed.fragment:
+ raise ValueError("Unsafe artifact redirect")
+ headers = {"Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
+ if hop == 0:
+ if parsed.netloc != "api.github.com":
+ raise ValueError("Credentials require the fixed GitHub API")
+ headers["Authorization"] = "Bearer " + token
+ remaining = deadline - time.monotonic()
+ if remaining <= 0:
+ raise TimeoutError("Artifact transfer deadline exceeded")
+ request = urllib.request.Request(url, headers=headers)
+ try:
+ response = opener.open(request, timeout=min(15, remaining))
+ except urllib.error.HTTPError as error:
+ response = error
+ with response:
+ if response.status in (301, 302, 303, 307, 308) and redirects:
+ location = response.headers.get("Location")
+ if not location:
+ raise ValueError("Missing artifact redirect")
+ url = urllib.parse.urljoin(url, location)
+ continue
+ if response.status != 200:
+ raise ValueError("Artifact HTTP request refused")
+ return read_bounded(response, limit, deadline)
+ raise ValueError("Too many artifact redirects")
+
+
+ def unique_object(pairs):
+ result = {}
+ for key, value in pairs:
+ if key in result:
+ raise ValueError("Duplicate metadata field")
+ result[key] = value
+ return result
+
+
+ def authenticate_metadata(raw, subject):
+ value = json.loads(raw, object_pairs_hook=unique_object)
+ if not isinstance(value, dict):
+ raise ValueError("Invalid artifact metadata")
+ run = value.get("workflow_run")
+ if not isinstance(run, dict):
+ raise ValueError("Missing workflow association")
+ for actual, expected in (
+ (value.get("id"), subject.artifact_id), (run.get("id"), subject.run_id),
+ (run.get("repository_id"), subject.repository_id),
+ ):
+ if type(actual) is not int or actual != expected:
+ raise ValueError("Artifact identity mismatch")
+ if value.get("name") != subject.name or value.get("expired") is not False or run.get("head_sha") != subject.head_sha:
+ raise ValueError("Artifact association mismatch")
+ digest = value.get("digest")
+ size = value.get("size_in_bytes")
+ if not isinstance(digest, str) or not re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
+ raise ValueError("Missing authenticated SHA-256")
+ if type(size) is not int or not 0 < size <= MAX_ARCHIVE:
+ raise ValueError("Invalid artifact size")
+ if subject.expected_sha256 and subject.expected_sha256 != digest[7:]:
+ raise ValueError("Independent upload digest mismatch")
+ return digest[7:], size
+
+
+ def validated_members(archive):
+ members = archive.infolist()
+ if not 0 < len(members) <= 128:
+ raise ValueError("Invalid archive member count")
+ names = set()
+ expanded = 0
+ for member in members:
+ name = member.orig_filename
+ kind = stat.S_IFMT(member.external_attr >> 16)
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,239}", name) or name != member.filename:
+ raise ValueError("Unsafe archive filename")
+ if name.casefold() in names or member.is_dir() or member.external_attr & 0x10 or member.flag_bits & 1:
+ raise ValueError("Duplicate, directory or encrypted archive member")
+ if kind not in (0, stat.S_IFREG) or member.compress_type not in (zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED):
+ raise ValueError("Unsupported archive member")
+ if not 0 <= member.file_size <= MAX_FILE:
+ raise ValueError("Expanded member exceeds limit")
+ expanded += member.file_size
+ if expanded > MAX_EXPANDED:
+ raise ValueError("Expanded archive exceeds limit")
+ names.add(name.casefold())
+ return members
+
+
+ def open_destination(subject):
+ descriptor = os.open("/", DIRECTORY_FLAGS)
+ try:
+ for index, part in enumerate(subject.destination.parts[1:]):
+ final = index == len(subject.destination.parts) - 2
+ try:
+ os.mkdir(part, 0o700, dir_fd=descriptor)
+ except FileExistsError:
+ if final and not subject.allow_existing:
+ raise ValueError("Fresh artifact destination already exists") from None
+ child = os.open(part, DIRECTORY_FLAGS, dir_fd=descriptor)
+ os.close(descriptor)
+ descriptor = child
+ return descriptor
+ except BaseException:
+ os.close(descriptor)
+ raise
+
+
+ def publish_archive(payload, subject):
+ with zipfile.ZipFile(io.BytesIO(payload)) as archive:
+ members = validated_members(archive)
+ destination = open_destination(subject)
+ staging_name = ".artifact-stage-" + secrets.token_hex(16)
+ staging = None
+ try:
+ existing = {name.casefold() for name in os.listdir(destination)}
+ if any(member.filename.casefold() in existing for member in members):
+ raise ValueError("Artifact destination collision")
+ os.mkdir(staging_name, 0o700, dir_fd=destination)
+ staging = os.open(staging_name, DIRECTORY_FLAGS, dir_fd=destination)
+ expanded = 0
+ for member in members:
+ descriptor = os.open(member.filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=staging)
+ with os.fdopen(descriptor, "wb") as output, archive.open(member) as source:
+ size = 0
+ while chunk := source.read(65536):
+ size += len(chunk)
+ expanded += len(chunk)
+ if size > MAX_FILE or expanded > MAX_EXPANDED or size > member.file_size:
+ raise ValueError("Actual expansion exceeds limit")
+ output.write(chunk)
+ if size != member.file_size:
+ raise ValueError("Truncated archive member")
+ for member in members:
+ os.link(member.filename, member.filename, src_dir_fd=staging, dst_dir_fd=destination, follow_symlinks=False)
+ return len(members), expanded
+ finally:
+ if staging is not None:
+ for name in os.listdir(staging):
+ os.unlink(name, dir_fd=staging)
+ os.close(staging)
+ os.rmdir(staging_name, dir_fd=destination)
+ os.close(destination)
+
+
+ def deadline_expired(signum, frame):
+ raise TimeoutError("Artifact transfer deadline exceeded")
+
+
+ def main():
+ subject = parse_subject()
+ token = os.environ["GH_TOKEN"]
+ if not token:
+ raise ValueError("Missing artifact read credential")
+ previous = signal.signal(signal.SIGALRM, deadline_expired)
+ signal.alarm(90)
+ try:
+ deadline = time.monotonic() + 90
+ url = f"https://api.github.com/repos/{subject.repository}/actions/artifacts/{subject.artifact_id}"
+ digest, size = authenticate_metadata(fetch(url, token, 1024 * 1024, deadline), subject)
+ payload = fetch(url + "/zip", token, size, deadline, redirects=True)
+ if len(payload) != size or hashlib.sha256(payload).hexdigest() != digest:
+ raise ValueError("Artifact ZIP size or SHA-256 mismatch")
+ count, expanded = publish_archive(payload, subject)
+ print(f"Restored artifact {subject.artifact_id}: sha256:{digest}, {size} bytes, {count} files, {expanded} expanded bytes")
+ finally:
+ signal.alarm(0)
+ signal.signal(signal.SIGALRM, previous)
+
+
+ if __name__ == "__main__":
+ try:
+ main()
+ except Exception:
+ # Network exceptions can contain signed URLs; keep failure output inert.
+ raise SystemExit("Exact artifact restore refused; identity, transport or archive validation failed") from None
+ PYTHON
- name: Checkout trusted promotion authority policy for fresh execution
if: >-
@@ -2525,10 +2811,17 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: Restore fresh Requirements execution artifact
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
- with:
- artifact-ids: ${{ needs.requirements-evidence.outputs.artifact-id }}
- path: ${{ runner.temp }}/requirements-execution
+ env:
+ GH_TOKEN: ${{ github.token }}
+ ARTIFACT_ID: ${{ needs.requirements-evidence.outputs.artifact-id }}
+ ARTIFACT_NAME: requirements-evidence-execution
+ ARTIFACT_REPOSITORY: ${{ github.repository }}
+ ARTIFACT_RUN_ID: ${{ github.run_id }}
+ ARTIFACT_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
+ ARTIFACT_EXPECTED_SHA256: ${{ needs.requirements-evidence.outputs.artifact-digest }}
+ ARTIFACT_DESTINATION: ${{ runner.temp }}/requirements-execution
+ shell: bash
+ run: *restore_exact_requirements_artifact
- name: Checkout trusted promotion authority policy for final verdict
if: >-
@@ -2662,10 +2955,17 @@ jobs:
printf 'active=true\n' >> "$GITHUB_OUTPUT"
- name: Restore producer Requirements evidence for final verdict
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
- with:
- artifact-ids: ${{ needs.requirements-evidence.outputs.producer-artifact-id }}
- path: ${{ runner.temp }}/requirements-producer
+ env:
+ GH_TOKEN: ${{ github.token }}
+ ARTIFACT_ID: ${{ needs.requirements-evidence.outputs.producer-artifact-id }}
+ ARTIFACT_NAME: requirements-evidence
+ ARTIFACT_REPOSITORY: ${{ github.repository }}
+ ARTIFACT_RUN_ID: ${{ github.run_id }}
+ ARTIFACT_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
+ ARTIFACT_EXPECTED_SHA256: ''
+ ARTIFACT_DESTINATION: ${{ runner.temp }}/requirements-producer
+ shell: bash
+ run: *restore_exact_requirements_artifact
- name: Download exact late RED artifact for final verification
if: github.event.pull_request.number == 717 && github.head_ref == 'bugfix/692-promotion-final-context' && hashFiles('openspec/changes/fix-release-promotion-requirements-parity/requirements-proof/late-red-evidence.json') != ''
diff --git a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
index a9b7acd7..e2352e86 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
@@ -272,3 +272,119 @@ The exact retry job is
.
Evidence is retained in pr784-after-rerun-artifacts.json,
pr784-rerun-producer.log and pr784-retained-attempt1-verification.log.
+
+### Exact-ID artifact transfer correction, 2026-10-21 (Europe/Berlin)
+
+Specification was amended before the local behavioral probe and implementation.
+The actual pinned download-action runtime at
+`3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c` applies `filterLatest` before explicit
+ID filtering. A Node probe executed that function verbatim against the six-record
+authenticated REST fixture for run `39589983181`. Requested producer artifact
+`12341352657` exists, but older same-name `12341630570` has the larger numeric ID.
+The explicit-ID assertion failed with selected `[]`, `0 !== 1`, and exit 1 before
+production edits at `d88310de977bfc5d080e7e03c75256434ed937ea`.
+This retains genuine selection failure, not a missing-helper or empty-step error.
+
+Retained local evidence under
+`/private/tmp/specfact-dependency-audit-20261020/official-only/implementation/`:
+`artifact-design/reproduce-selection-red.js`, `selection-red.log` and
+`selection-red-report.md` (the latter two in the same artifact-design directory),
+and `review/artifact-download-rerun-sources/` with the source and REST fixture.
+Source SHA-256:
+`f4a6b7046eb834bed98bb687f6d08feb0bfe6fb32570e9da7de0707884c08b31`.
+Fixture SHA-256:
+`91bf7c28202053d63a64feb5562486b27f2507e0b110bd49308b8aa916ca55b3`.
+The real producer ZIP digest is
+`2d857d60225b8e0e1073488936d7fc62cee43a58283072ae67ae07c78f05b927`.
+
+The correction uses one YAML scalar with two aliases, invoked through
+`/usr/bin/python3 -I -S`. Tests execute that actual resolved Python body with
+transport-only doubles. The three names/destinations and the fourteen-case
+mapping remain unchanged. Existing workflow tests now inspect the equivalent
+exact-ID environment binding instead of obsolete action inputs.
+
+Passing command (Python 3.13.14, pytest 9.1.1, established run-hatch environment):
+
+```sh
+sh /private/tmp/specfact-dependency-audit-20261020/official-only/run-hatch.sh python -m pytest -o addopts='' -q -p no:cacheprovider tests/unit/workflows/test_exact_artifact_transfer.py tests/unit/workflows/test_requirements_evidence_delivery_workflow.py tests/unit/security/test_release_promotion_security_gates.py tests/unit/scripts/test_requirements_late_cycle_workflow.py
+```
+
+Result: **114 passed**, with no warnings, retained in
+`artifact-design/focused-tests.log`. Controls cover both exact requested IDs,
+independent execution digest, metadata association/type/expiry/size/digest,
+duplicate JSON, refusal statuses, truncation, HTTPS redirects and credential
+stripping across redirect chains, total deadline, digest-before-ZIP ordering,
+flat paths, raw NUL, encryption, links/devices/directories, CRC failure, member and
+expansion limits, existing and concurrent collisions, symlink parents, fresh
+exclusive destinations and preservation of unrelated context. Ruff checks pass
+for the three touched test files. Strict OpenSpec validation passes.
+
+The parent independently executed the real workflow program with local
+`/usr/bin/python3` 3.9.6 against authenticated live GitHub endpoints, verifying
+producer `12341352657` in both producer roles (6,582 ZIP bytes; four files;
+70,093 expanded bytes). Retained execution artifact `12302080806` from run
+`39502448590`, head `66ee3d58959e7798a6a7a5a1ed5c41d1db37fbc0`, restored two flat
+files (1,913 ZIP bytes; 4,102 expanded bytes) with independent digest
+`0e20e44875b9a43c4653940ca90a788d541e9ed085fa5c613648f1c41ae9abe5`.
+Byte comparisons and unrelated context preservation passed. Evidence is
+`live-exact-transfer-proof.json` and `.log`; the parent refreshes it if the
+workflow body changes. Historical execution proves transport/layout, not
+current-head native success. Existing grants remain historical. Fresh grants,
+normal hooks, independent review and current-head CI remain delivery gates.
+
+The program keeps verified compressed bytes in bounded memory rather than a
+private ZIP temporary file; extraction still stages every validated member on
+the destination filesystem before exclusive publication. Each file publishes
+atomically; whole-set atomicity is not claimed. Interruption or a late collision
+may leave partial new evidence, and retry refuses it. Existing evidence is never
+overwritten. Rollback is reverting the workflow correction; no artifact deletion,
+protected-policy change, signing or dependency changes are required.
+
+### Final transport review and local gates
+
+Independent reviewer `Codex:/root/artifact_transfer_security_review` returned
+PASS for exact program SHA-256
+`f9a09798fed949615bbd8845fca26044ca53a16fe95c85853ad04590f67e6b8a`.
+It independently passed 114 focused tests, eight additional hostile probe groups,
+compared all workflow YAML outside the three changed steps with the base, and
+compared the live restored files with genuine ZIPs. The review and evidence are
+retained in `review/artifact-transfer-security.md` and adjacent machine records
+under the local evidence root above. No grant, deployment or merge authority is
+implied by that technical review.
+
+The new test file has zero SpecFact findings. The full affected-file review has
+one warning and six informational findings in unchanged existing functions.
+The same independent reviewer explicitly accepted a narrow exception for exactly
+those baseline diagnostics. It compared both complete source and AST against
+`git show d88310de:`, independently of the parent's comparison. The warning
+is `banned-generic-public-names` on
+`test_invalid_utf8_authority_is_stable_metadata_failure`. The six
+`ai-bloat.loc-vs-complexity` findings concern that same function,
+`test_fresh_consumer_reexecutes_trusted_plan`, `_write_late_red_artifact`,
+`_write_late_red_metadata`, `_late_red_fixture`, and
+`test_workflow_revalidates_promotion_reuse_in_all_stages`. No rule, threshold,
+suppression or function body was changed to accept them. The explicit exception
+is limited to these unchanged symbols for this correction; it permits no new
+finding or changed body. Evidence is `artifact-transfer-self-review.json`,
+`artifact-transfer-baseline-findings.json`, and the reviewer's independent
+`review/artifact-transfer-security-baseline.json`.
+
+The review used the committed pinned BasedPyright runner with the actual Hatch
+Python path. The global pyenv shim previously could not resolve pytest and
+produced false unknown-member diagnostics. The isolated executable adapter
+preserves every caller argument and adds only the correct `--pythonpath`; it
+changes no repository configuration or check. The required authoritative full
+project type run reports zero errors and 1,456 warnings, exactly the baseline.
+
+Required formatter passed with all 1,128 files unchanged. Full lint, YAML lint,
+actual pinned actionlint 1.7.11, strict OpenSpec validation and stable fourteen-case
+mapping/plan identity checks passed. The smart-test gate passed its selected 111
+workflow/security tests; its unit-scope coverage notice is not a full-suite
+coverage result. The contract gate exited zero with no modified source scope,
+using its cached result. Fresh complete CI remains required at the new head.
+
+The comment review's two private docstrings and changed-line type suppression
+were removed. Its only kept comment explains redaction of signed URLs emitted
+by the external urllib exception formatter. No production change followed the
+final live transfer proof or independent security verdict. The core dependency,
+version and signed-module inputs remain unchanged. Signing stays in CI/CD.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/design.md b/openspec/changes/isolate-semgrep-security-runtime/design.md
index 057ba61d..4f73e972 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/design.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/design.md
@@ -96,3 +96,36 @@ The isolated origin attester is restricted to the same-repository dev follow-up
on the exact admission branch. Forks and unrelated subjects skip that extra job
and retain their ordinary native Requirements checks. GitHub does not grant
fork pull-request jobs the attestation write permissions required by this job.
+
+## Exact-ID artifact transport correction (2026-10-21)
+
+The pinned download action applies greatest-ID-per-name filtering before explicit
+ID selection. Authenticated run 39589983181 has fresh producer ID 12341352657
+below old same-name ID 12341630570. One YAML-anchored inline standard-library
+Python program replaces the three current-run transfers. It runs through
+`/usr/bin/python3 -I -S`, never imports candidate checkout code, and preserves
+artifact names, destinations, native consumers and the fourteen-case mapping.
+
+A typed artifact subject binds repository name and numeric ID, run, head, name,
+ID, destination and optional upload digest. REST metadata and exact ZIP are
+fetched directly by ID. Execution artifacts require the existing independent
+upload digest; producer artifacts require authenticated REST SHA-256. Transport
+has HTTPS-only bounded redirects with no forwarded credentials, size bounds,
+read timeouts and a total deadline. Digest verification precedes ZIP parsing.
+
+Only portable flat regular files enter private staging; compressed bytes are
+limited to 16 MiB, each file to 16 MiB, total expansion to 64 MiB, and membership
+to 128 files. These exceed the observed producer's 6,582-byte ZIP and 70,093-byte
+expansion; retained execution is 1,913 compressed bytes and 4,102 expanded bytes.
+The verified ZIP stays in bounded memory (at most 16 MiB of archive bytes); this
+avoids another filesystem lifecycle before extraction. Directory
+file descriptors and no-follow opens protect parents. All members validate
+before exclusive hard-link publication; existing unrelated context survives.
+Publication is atomic per file, not for the whole set. An interrupted run may
+leave partial evidence and retries then refuse collisions. Fresh jobs are the
+recovery path. No new helper, dependency, protected policy or permission exists.
+
+Tests execute the resolved YAML body with test-only HTTP doubles. Local RED
+replays the actual pinned filter over authenticated REST records. Final live
+exact-ID verification, independent review, normal hooks and fresh exact-head
+authority remain required before CI acceptance. Parent owns internal wiki sync.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md b/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
index 88fec7ec..d827596e 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
@@ -70,3 +70,15 @@ runtime, bypass Code Review, or authorize protected deployment or merge.
- **GIVEN** a fork, another repository, base or branch, or an absent native artifact
- **WHEN** CI evaluates the origin attester job
- **THEN** it skips that job and retains ordinary Requirements enforcement without requesting signing permissions for that subject.
+
+#### Scenario: Exact artifact IDs survive same-name reruns
+
+- **GIVEN** the bound producer or execution artifact ID and another artifact with the same name and a greater numeric ID
+- **WHEN** any of the three current-run evidence transfers restores its artifact
+- **THEN** it retrieves the requested immutable ID directly, authenticates repository numeric identity, run, head, name, expiry, SHA-256 and size, and requires the independent execution upload digest.
+
+#### Scenario: Artifact bytes enter only as bounded inert evidence
+
+- **GIVEN** authenticated exact-ID metadata and ZIP bytes
+- **WHEN** the shared isolated workflow-owned program restores evidence
+- **THEN** credentialed transport remains on the fixed GitHub API, HTTPS redirects never forward the token, digest verification precedes bounded flat regular-file extraction, all files validate in staging before exclusive publication, and unsafe paths, links, collisions or uncertain inputs fail closed while preserving unrelated context.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/tasks.md b/openspec/changes/isolate-semgrep-security-runtime/tasks.md
index 97465a1f..656a77a3 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/tasks.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/tasks.md
@@ -39,5 +39,15 @@
- [x] Integrate the immutable evaluator into producer, fresh and final CI paths.
- [x] Prove hostile input refusal, unchanged native success and pinned Code Review compatibility.
- [ ] Complete independent review, normal hooks and pre-deployment CI.
-- [ ] Present exact protected deployment and new authority requests for owner approval.
-- [ ] After approval only, activate and verify all required CI without merging.
+- [x] Present exact protected deployment and new authority requests; owner approved the original d883 head through comments 6314609869 and 6314563334, and approved policy merge b328ace4 at reviewed 78c2.
+- [x] Activate the approved immutable policy; attempt 2 producer and origin passed.
+- [ ] Verify all required CI after the exact-ID transport correction and renewed head grants, without merging core.
+
+## 6. Correct exact-ID artifact transport
+
+- [x] Specify one workflow-owned transfer program and retain mapping identities.
+- [x] Retain authentic pinned-action failing selection before production edits.
+- [x] Exercise actual YAML-resolved code against valid metadata and hostile inputs.
+- [x] Replace all three current-run transfers without changing names or destinations.
+- [x] Verify genuine producer and retained execution layouts, independent execution digest and live exact-ID transfer; current-head CI remains pending.
+- [ ] Complete independent review, normal hooks, wiki alignment and fresh exact-head grants before CI acceptance.
diff --git a/tests/unit/security/test_release_promotion_security_gates.py b/tests/unit/security/test_release_promotion_security_gates.py
index f211aedf..e1f0aac2 100644
--- a/tests/unit/security/test_release_promotion_security_gates.py
+++ b/tests/unit/security/test_release_promotion_security_gates.py
@@ -164,7 +164,7 @@ def _assert_review_tool_and_artifact_order(review: dict[str, Any]) -> None:
"Restore Requirements evidence for Code Review"
)
restore = _named_step(review, "Restore Requirements evidence for Code Review")
- assert restore["with"]["artifact-ids"] == "${{ needs.requirements-evidence-producer.outputs.artifact-id }}" # type: ignore[index]
+ assert restore["env"]["ARTIFACT_ID"] == "${{ needs.requirements-evidence-producer.outputs.artifact-id }}"
def _assert_review_diff_is_fail_closed(review: dict[str, Any]) -> None:
diff --git a/tests/unit/workflows/test_exact_artifact_transfer.py b/tests/unit/workflows/test_exact_artifact_transfer.py
new file mode 100644
index 00000000..a0f9ffd2
--- /dev/null
+++ b/tests/unit/workflows/test_exact_artifact_transfer.py
@@ -0,0 +1,443 @@
+from __future__ import annotations
+
+import hashlib
+import importlib.util
+import io
+import json
+import os
+import stat
+import sys
+import types
+import zipfile
+from collections.abc import Callable
+from contextlib import nullcontext
+from pathlib import Path
+from typing import Any, NamedTuple
+from unittest.mock import patch
+from urllib.request import Request
+
+import pytest
+import yaml
+
+
+ROOT = Path(__file__).resolve().parents[3]
+STEP_NAMES = (
+ "Restore Requirements evidence for Code Review",
+ "Restore fresh Requirements execution artifact",
+ "Restore producer Requirements evidence for final verdict",
+)
+
+
+def transfer_steps() -> list[dict[str, Any]]:
+ workflow = yaml.safe_load((ROOT / ".github/workflows/requirements-evidence.yml").read_text())
+ return [next(s for j in workflow["jobs"].values() for s in j["steps"] if s.get("name") == n) for n in STEP_NAMES]
+
+
+def archive_bytes(entries: list[tuple[str | zipfile.ZipInfo, bytes]] | None = None) -> bytes:
+ output = io.BytesIO()
+ with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive:
+ for name, content in entries or [
+ ("requirements-evidence.json", b'{"native":"failed"}'),
+ ("proof.xml", b""),
+ ]:
+ archive.writestr(name, content)
+ return output.getvalue()
+
+
+class Response(io.BytesIO):
+ def __init__(self, body: bytes = b"", status: int = 200, headers: dict[str, str] | None = None) -> None:
+ super().__init__(body)
+ self.status = status
+ self.headers = headers or {}
+
+
+class TransferHarness(NamedTuple):
+ run: Callable[..., Path]
+ destination: Path
+ runner: Path
+ requests: list[Request]
+ module: types.ModuleType
+
+
+@pytest.fixture
+def transfer(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TransferHarness:
+ root = tmp_path.resolve()
+ workspace, runner = root / "workspace", root / "runner"
+ workspace.mkdir()
+ runner.mkdir()
+ destination = workspace / "artifacts/requirements-evidence"
+ env = {
+ "GH_TOKEN": "test-secret-never-forward",
+ "GITHUB_WORKSPACE": str(workspace),
+ "RUNNER_TEMP": str(runner),
+ "GITHUB_REPOSITORY_ID": "1085706003",
+ "ARTIFACT_REPOSITORY": "nold-ai/specfact-cli",
+ "ARTIFACT_ID": "12341352657",
+ "ARTIFACT_NAME": "requirements-evidence",
+ "ARTIFACT_RUN_ID": "39589983181",
+ "ARTIFACT_HEAD_SHA": "d88310de977bfc5d080e7e03c75256434ed937ea",
+ "ARTIFACT_EXPECTED_SHA256": "",
+ "ARTIFACT_DESTINATION": str(destination),
+ }
+ body = transfer_steps()[0]["run"].split("<<'PYTHON'\n", 1)[1].rsplit("\nPYTHON", 1)[0]
+ program = root / "workflow_artifact_transfer.py"
+ program.write_text(body)
+ spec = importlib.util.spec_from_file_location("workflow_artifact_transfer", program)
+ assert spec is not None and spec.loader is not None
+ module = importlib.util.module_from_spec(spec)
+ monkeypatch.setitem(sys.modules, module.__name__, module)
+ spec.loader.exec_module(module)
+ requests: list[Request] = []
+
+ def run(
+ payload: bytes | None = None,
+ metadata_update: dict[str, Any] | None = None,
+ raw_metadata: bytes | None = None,
+ responses: list[Response | Exception] | None = None,
+ changes: dict[str, str] | None = None,
+ ) -> Path:
+ payload = archive_bytes() if payload is None else payload
+ selected = dict(env, **(changes or {}))
+ metadata: dict[str, Any] = {
+ "id": int(selected["ARTIFACT_ID"]),
+ "name": selected["ARTIFACT_NAME"],
+ "expired": False,
+ "size_in_bytes": len(payload),
+ "digest": "sha256:" + hashlib.sha256(payload).hexdigest(),
+ "workflow_run": {"id": 39589983181, "repository_id": 1085706003, "head_sha": env["ARTIFACT_HEAD_SHA"]},
+ }
+ metadata.update(metadata_update or {})
+ queue: list[Response | Exception] = [Response(raw_metadata or json.dumps(metadata).encode())]
+ queue += (
+ responses
+ if responses is not None
+ else [
+ Response(status=302, headers={"Location": "https://storage.example/artifact?sig=secret"}),
+ Response(payload),
+ ]
+ )
+
+ def open_response(_opener: Any, request: Request, timeout: float) -> Response:
+ requests.append(request)
+ assert timeout > 0
+ assert queue, "Unexpected request, possibly a listing or retry"
+ result = queue.pop(0)
+ if isinstance(result, Exception):
+ raise result
+ return result
+
+ with patch.dict(os.environ, selected, clear=True), patch("urllib.request.OpenerDirector.open", open_response):
+ module.main()
+ return Path(selected["ARTIFACT_DESTINATION"])
+
+ return TransferHarness(run, destination, runner, requests, module)
+
+
+def test_three_transfers_share_program_and_fixed_subjects() -> None:
+ steps = transfer_steps()
+ assert len({step["run"] for step in steps}) == 1
+ assert all("/usr/bin/python3 -I -S" in step["run"] for step in steps)
+ assert [s["env"]["ARTIFACT_NAME"] for s in steps] == [
+ "requirements-evidence",
+ "requirements-evidence-execution",
+ "requirements-evidence",
+ ]
+ assert steps[1]["env"]["ARTIFACT_EXPECTED_SHA256"] == "${{ needs.requirements-evidence.outputs.artifact-digest }}"
+
+
+@pytest.mark.parametrize("artifact_id", ["12341352657", "12341630570"])
+def test_exact_id_restores_inert_files_without_listing_or_token_forwarding(
+ transfer: TransferHarness, artifact_id: str
+) -> None:
+ run, destination, _, requests, _ = transfer
+ destination.mkdir(parents=True)
+ (destination / "unrelated-context.json").write_bytes(b"keep")
+ run(changes={"ARTIFACT_ID": artifact_id})
+ assert (destination / "requirements-evidence.json").read_bytes() == b'{"native":"failed"}'
+ assert (destination / "unrelated-context.json").read_bytes() == b"keep"
+ assert all(
+ stat.S_IMODE(p.stat().st_mode) == 0o600 for p in destination.iterdir() if p.name != "unrelated-context.json"
+ )
+ assert [r.full_url for r in requests[:2]] == [
+ f"https://api.github.com/repos/nold-ai/specfact-cli/actions/artifacts/{artifact_id}",
+ f"https://api.github.com/repos/nold-ai/specfact-cli/actions/artifacts/{artifact_id}/zip",
+ ]
+ assert all(r.get_header("Authorization") for r in requests[:2])
+ assert requests[2].get_header("Authorization") is None
+
+
+def test_execution_layout_requires_independent_digest(transfer: TransferHarness) -> None:
+ run, _, runner, _, _ = transfer
+ payload = archive_bytes(
+ [
+ ("requirements-evidence.json", b"{}"),
+ ("requirements-proof.xml", b""),
+ ("requirements-evidence-plan.json", b"{}"),
+ ]
+ )
+ changes = {
+ "ARTIFACT_NAME": "requirements-evidence-execution",
+ "ARTIFACT_DESTINATION": str(runner / "requirements-execution"),
+ }
+ with pytest.raises(ValueError):
+ run(payload, changes=changes)
+ changes["ARTIFACT_EXPECTED_SHA256"] = hashlib.sha256(payload).hexdigest()
+ destination = run(payload, changes=changes)
+ assert sorted(p.name for p in destination.iterdir()) == [
+ "requirements-evidence-plan.json",
+ "requirements-evidence.json",
+ "requirements-proof.xml",
+ ]
+
+
+@pytest.mark.parametrize(
+ "update",
+ [
+ {"id": 1},
+ {"id": True},
+ {"name": "wrong"},
+ {"expired": True},
+ {"expired": 0},
+ {"digest": ""},
+ {"digest": "md5:" + "a" * 32},
+ {"digest": "sha256:" + "0" * 64},
+ {"size_in_bytes": 0},
+ {"size_in_bytes": True},
+ {"size_in_bytes": 16777217},
+ {"size_in_bytes": 1},
+ {
+ "workflow_run": {
+ "id": 1,
+ "repository_id": 1085706003,
+ "head_sha": "d88310de977bfc5d080e7e03c75256434ed937ea",
+ }
+ },
+ {
+ "workflow_run": {
+ "id": 39589983181,
+ "repository_id": 1,
+ "head_sha": "d88310de977bfc5d080e7e03c75256434ed937ea",
+ }
+ },
+ {"workflow_run": {"id": 39589983181, "repository_id": 1085706003, "head_sha": "0" * 40}},
+ ],
+)
+def test_artifact_association_mismatch_never_publishes(transfer: TransferHarness, update: dict[str, Any]) -> None:
+ run, destination, _, _, _ = transfer
+ with pytest.raises((ValueError, OSError)):
+ run(metadata_update=update)
+ assert not destination.exists()
+
+
+@pytest.mark.parametrize(
+ "raw", [b'{"id":1,"id":2}', b"{", b"[]", b"x" * 1048577], ids=["duplicate", "truncated", "wrong-shape", "oversized"]
+)
+def test_invalid_rest_json_fails_closed(transfer: TransferHarness, raw: bytes) -> None:
+ run, destination, _, _, _ = transfer
+ with pytest.raises(ValueError):
+ run(raw_metadata=raw)
+ assert not destination.exists()
+
+
+@pytest.mark.parametrize(
+ "url",
+ [
+ "http://storage.example/a",
+ "https://user:pass@storage.example/a",
+ "file:///tmp/a",
+ "https://storage.example:443/a#fragment",
+ ],
+)
+def test_unsafe_redirect_never_receives_credentials(transfer: TransferHarness, url: str) -> None:
+ run, destination, _, requests, _ = transfer
+ with pytest.raises(ValueError):
+ run(responses=[Response(status=302, headers={"Location": url})])
+ assert len(requests) == 2
+ assert not destination.exists()
+
+
+@pytest.mark.parametrize(
+ "responses",
+ [
+ [Response(status=404)],
+ [Response(status=403)],
+ [Response(status=410)],
+ [Response(status=302)],
+ [Response(b"truncated")],
+ [Response(status=302, headers={"Location": "https://storage.example/a"}) for _ in range(6)],
+ [TimeoutError("timeout")],
+ [Response(b"", headers={"Content-Length": "999999999"})],
+ ],
+)
+def test_transport_failure_never_publishes(transfer: TransferHarness, responses: list[Response | Exception]) -> None:
+ run, destination, _, _, _ = transfer
+ with pytest.raises((ValueError, OSError)):
+ run(responses=responses)
+ assert not destination.exists()
+
+
+@pytest.mark.parametrize(
+ "name", ["../escape", "/absolute", "C:drive", "a\\b", "nested/a", ".", "..", "folder/", "bad\nname"]
+)
+def test_unsafe_archive_paths_are_inert(transfer: TransferHarness, name: str) -> None:
+ run, destination, _, _, _ = transfer
+ with pytest.raises(ValueError):
+ run(archive_bytes([("good.json", b"ok"), (name, b"bad")]))
+ assert not destination.exists()
+
+
+@pytest.mark.parametrize("kind", [stat.S_IFLNK, stat.S_IFIFO, stat.S_IFCHR, stat.S_IFDIR, stat.S_IFSOCK])
+def test_nonregular_members_never_publish(transfer: TransferHarness, kind: int) -> None:
+ run, destination, _, _, _ = transfer
+ member = zipfile.ZipInfo("evil")
+ member.create_system = 3
+ member.external_attr = (kind | 0o777) << 16
+ with pytest.raises(ValueError):
+ run(archive_bytes([("good.json", b"ok"), (member, b"outside")]))
+ assert not destination.exists()
+
+
+@pytest.mark.parametrize("names", [["same", "same"], ["Same", "same"]])
+def test_archive_name_collisions_never_publish(transfer: TransferHarness, names: list[str]) -> None:
+ run, destination, _, _, _ = transfer
+ expected_warning = pytest.warns(UserWarning, match="Duplicate name") if names[0] == names[1] else nullcontext()
+ with expected_warning:
+ payload = archive_bytes([(n, b"data") for n in names])
+ with pytest.raises(ValueError):
+ run(payload)
+ assert not destination.exists()
+
+
+def test_corrupt_member_does_not_publish_earlier_valid_member(transfer: TransferHarness) -> None:
+ run, destination, _, _, _ = transfer
+ payload = archive_bytes([("first.json", b"first"), ("second.json", b"second")])
+ broken = bytearray(payload)
+ index = broken.index(b"PK\x01\x02")
+ broken[index + 16] ^= 1
+ with pytest.raises(zipfile.BadZipFile):
+ run(bytes(broken))
+ assert not destination.exists() or not list(destination.iterdir())
+
+
+@pytest.mark.parametrize("existing", ["file", "symlink", "parent-symlink"])
+def test_existing_evidence_and_symlink_parents_are_preserved(
+ transfer: TransferHarness, existing: str, tmp_path: Path
+) -> None:
+ run, destination, _, _, _ = transfer
+ outside = tmp_path / "outside"
+ outside.mkdir()
+ sentinel = outside / "sentinel"
+ sentinel.write_bytes(b"unchanged")
+ if existing == "parent-symlink":
+ destination.parent.symlink_to(outside, target_is_directory=True)
+ else:
+ destination.mkdir(parents=True)
+ target = destination / "requirements-evidence.json"
+ target.symlink_to(outside / "missing") if existing == "symlink" else target.write_bytes(b"old")
+ with pytest.raises((ValueError, OSError)):
+ run()
+ assert sentinel.read_bytes() == b"unchanged"
+ if existing == "file":
+ assert (destination / "requirements-evidence.json").read_bytes() == b"old"
+
+
+def test_member_count_and_expansion_are_bounded(transfer: TransferHarness) -> None:
+ run, destination, _, _, _ = transfer
+ with pytest.raises(ValueError):
+ run(archive_bytes([(f"report-{n}.json", b"x") for n in range(129)]))
+ with pytest.raises(ValueError):
+ run(archive_bytes([("large.json", b"x" * (16 * 1024 * 1024 + 1))]))
+ assert not destination.exists()
+
+
+def test_raw_nul_and_encrypted_members_are_rejected(transfer: TransferHarness) -> None:
+ run, destination, _, _, _ = transfer
+ payload = archive_bytes([("unsafe_name", b"inert")])
+ with pytest.raises(ValueError):
+ run(payload.replace(b"unsafe_name", b"unsafe\x00name"))
+ encrypted = bytearray(payload)
+ encrypted[6] |= 1
+ encrypted[encrypted.index(b"PK\x01\x02") + 8] |= 1
+ with pytest.raises(ValueError):
+ run(bytes(encrypted))
+ assert not destination.exists()
+
+
+def test_digest_mismatch_precedes_zip_interpretation(transfer: TransferHarness) -> None:
+ run, destination, _, _, module = transfer
+ payload = archive_bytes()
+ with (
+ patch.object(module.zipfile, "ZipFile", side_effect=AssertionError("Unverified ZIP interpreted")),
+ pytest.raises(ValueError),
+ ):
+ run(payload, metadata_update={"digest": "sha256:" + "0" * 64})
+ assert not destination.exists()
+
+
+def test_independent_digest_mismatch_fails_before_download(transfer: TransferHarness) -> None:
+ run, destination, _, requests, _ = transfer
+ with pytest.raises(ValueError):
+ run(changes={"ARTIFACT_EXPECTED_SHA256": "f" * 64})
+ assert len(requests) == 1
+ assert not destination.exists()
+
+
+def test_redirect_chain_never_recovers_authorization(transfer: TransferHarness) -> None:
+ run, _, _, requests, _ = transfer
+ payload = archive_bytes()
+ run(
+ payload,
+ responses=[
+ Response(status=302, headers={"Location": "https://storage.example/a"}),
+ Response(status=307, headers={"Location": "https://api.github.com/redirected"}),
+ Response(payload),
+ ],
+ )
+ assert all(r.get_header("Authorization") is None for r in requests[2:])
+
+
+def test_publication_collision_preserves_concurrent_file(transfer: TransferHarness) -> None:
+ run, destination, _, _, module = transfer
+ link = module.os.link
+
+ def collide(source: str, target: str, **kwargs: Any) -> None:
+ assert (destination / "requirements-evidence.json").exists() is False
+ (destination / target).write_bytes(b"concurrent evidence")
+ link(source, target, **kwargs)
+
+ with patch.object(module.os, "link", collide), pytest.raises(FileExistsError):
+ run()
+ assert (destination / "requirements-evidence.json").read_bytes() == b"concurrent evidence"
+ assert not (destination / "proof.xml").exists()
+ assert not list(destination.glob(".artifact-stage-*"))
+
+
+def test_fresh_destinations_are_exclusive(transfer: TransferHarness) -> None:
+ run, _, runner, _, _ = transfer
+ destination = runner / "requirements-producer"
+ destination.mkdir()
+ with pytest.raises(ValueError):
+ run(changes={"ARTIFACT_DESTINATION": str(destination)})
+ assert not list(destination.iterdir())
+
+
+def test_destination_escape_is_rejected_before_network(transfer: TransferHarness) -> None:
+ run, _, runner, requests, _ = transfer
+ with pytest.raises(ValueError):
+ run(changes={"ARTIFACT_DESTINATION": str(runner / "elsewhere")})
+ assert requests == []
+
+
+def test_absolute_transfer_deadline_is_enforced(transfer: TransferHarness) -> None:
+ run, destination, _, _, module = transfer
+ times = iter([0, 0, 91])
+ with patch.object(module.time, "monotonic", lambda: next(times)), pytest.raises(TimeoutError):
+ run()
+ assert not destination.exists()
+
+
+def test_declared_total_expansion_limit_is_enforced(transfer: TransferHarness) -> None:
+ run, destination, _, _, module = transfer
+ with patch.object(module, "MAX_EXPANDED", 5), pytest.raises(ValueError):
+ run(archive_bytes([("first", b"123"), ("second", b"456")]))
+ assert not destination.exists()
diff --git a/tests/unit/workflows/test_requirements_evidence_delivery_workflow.py b/tests/unit/workflows/test_requirements_evidence_delivery_workflow.py
index d64ba237..c291e969 100644
--- a/tests/unit/workflows/test_requirements_evidence_delivery_workflow.py
+++ b/tests/unit/workflows/test_requirements_evidence_delivery_workflow.py
@@ -1278,10 +1278,11 @@ def _assert_review_artifact_binding(parsed: dict[str, object]) -> None:
restore = _step_by_name(parsed, "Restore Requirements evidence for Code Review")
verify_head = _step_by_name(parsed, "Verify exact head for Code Review")
assert upload["id"] == "upload-requirements-evidence"
- assert restore["with"] == {
- "artifact-ids": "${{ needs.requirements-evidence-producer.outputs.artifact-id }}",
- "path": "artifacts/requirements-evidence",
- }
+ environment = cast(dict[str, str], restore["env"])
+ assert environment["ARTIFACT_ID"] == "${{ needs.requirements-evidence-producer.outputs.artifact-id }}"
+ assert environment["ARTIFACT_NAME"] == "requirements-evidence"
+ assert environment["ARTIFACT_DESTINATION"] == "${{ github.workspace }}/artifacts/requirements-evidence"
+ assert "/usr/bin/python3 -I -S" in str(restore["run"])
assert verify_head["env"] == {"EXPECTED_HEAD": "${{ github.event.pull_request.head.sha || github.sha }}"}
assert 'test "$(git rev-parse HEAD)" = "$EXPECTED_HEAD"' in verify_head["run"] # type: ignore[operator]
From c73a9d6d3809ac763edb4eeacb7ad4e2d082e38d Mon Sep 17 00:00:00 2001
From: Dom <39115308+djm81@users.noreply.github.com>
Date: Wed, 21 Oct 2026 14:18:30 +0200
Subject: [PATCH 6/6] fix(ci): pin reviewed merge identity admission
---
.github/workflows/requirements-evidence.yml | 2 +-
.../TDD_EVIDENCE.md | 62 +++++++++++++++++++
.../design.md | 20 ++++++
.../specs/isolated-scanner-runtime/spec.md | 6 ++
.../isolate-semgrep-security-runtime/tasks.md | 9 +++
5 files changed, 98 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/requirements-evidence.yml b/.github/workflows/requirements-evidence.yml
index cec43c7f..b6cbaae9 100644
--- a/.github/workflows/requirements-evidence.yml
+++ b/.github/workflows/requirements-evidence.yml
@@ -12,7 +12,7 @@ permissions:
issues: read
env:
- LATE_CYCLE_POLICY_REVISION: ea0509fc5261e394cbc8de918b85ea02ac5f4126
+ LATE_CYCLE_POLICY_REVISION: 5cd61c45303bc5da174a39ff79945fafad77ea3b
jobs:
requirements-evidence-producer:
diff --git a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
index e2352e86..5f12440d 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md
@@ -388,3 +388,65 @@ were removed. Its only kept comment explains redaction of signed URLs emitted
by the external urllib exception formatter. No production change followed the
final live transfer proof or independent security verdict. The core dependency,
version and signed-module inputs remain unchanged. Signing stays in CI/CD.
+
+### Equivalent signed synthetic merge origin, 2026-10-21 (Europe/Berlin)
+
+Normal-hook commit `4b25241ef5548d29b6ad78be6cd2d26dda6afa77` published the
+reviewed exact-ID transfer. All ordinary CI checks passed at that head. The
+new native artifact `12344301975` and signed origin `12342973880` in run
+`39595683488`, attempt 1, retain fourteen passing cases and the sole missing-RED
+finding. The canonical request operation refused with `origin-source` before
+any new human request was issued.
+
+Authenticated GitHub commit records show actual attested workflow revision
+`21cca82b7d32c567bbea6f496184cbdf633cab13` and current PR merge revision
+`6636108be140545670ab348e503187102dded6e7`. Both have the exact reviewed tree
+`c131fc747fa4cd827a06ffd6a1ec164e146a986c`, the ordered parents
+`3cf72e848a2b5a233d3f599898d6983319d375de` and the current head, and valid
+signatures. Their signed payloads encode author/committer time zones differently.
+The deployed literal source-ID guard rejects this equivalent signed merge.
+No origin or artifact was rewritten to work around that refusal.
+
+The specification preceded the scoped policy regression. The new test
+`test_historical_equivalent_signed_merge_preserves_attested_source` first failed
+with `late_cycle_admission.Refused: origin-source`. Two subsequent ordered-parent
+negative controls first failed with `DID NOT RAISE Refused` before the candidate
+was tightened to a distinct first parent and exact current-head second parent.
+The final policy suite passed 264 tests, independently repeated by
+`Codex:/root/policy_identity_security_review`. Its exact source SHA-256 is
+`bdd671842d0f0c1d252a759a7aa8230b2b94d6d97e44bebc760b9b13e7c967f0`.
+Formatting/lint, comment review and diff checks passed. The independent technical
+verdict permits normal commit/publication, not protected deployment or grants.
+
+The candidate accepts a different signed merge only when both authenticated
+full identities match their requests, both signatures are valid, both trees
+match the exact reviewed head tree, and both ordered two-parent lists agree
+as distinct base/current-head. It retains workflow-byte equality and verifies
+the actual original signer/source digest through official gh. All native,
+selector, immutable-artifact, live subject/permission/expiry and deployment checks
+remain unchanged.
+
+Parent technical verification called the real candidate historical collector
+against the actual API and signed origin, without replacing its official gh
+verifier. It accepted the genuine native ZIP and sole missing-RED finding, while
+retaining the original workflow revision. This was evidence verification only;
+no request, grant or decision was issued. The unchanged `deployed()` restriction
+continues to refuse an unmerged candidate.
+
+The policy correction is proposed as
+, exact commit
+`5cd61c45303bc5da174a39ff79945fafad77ea3b`. Core now pins that immutable revision
+for all three existing evaluator calls. This pin is inactive for admission until
+the owner explicitly approves protected deployment and the revision is reachable
+from policy main. The earlier policy4 deployment approval does not apply to this
+new change. No policy5 deployment, new human grant, core merge or local signing
+has occurred. Fresh core evidence and separate exact owner grants remain required
+following approved deployment. Reverting the pin and policy correction restores
+the prior refusal without altering retained evidence.
+
+Local evidence is under the previously identified implementation directory,
+`policy-identity/`: `actual-deployed-red.log`, `regression-red.log`,
+`ordered-parent-red.log`, `final-tests.log`, `security-review.md`,
+`candidate-live-origin-proof.json` and its producer script in the parent directory.
+The original authenticated synthetic commit responses and origin manifest are
+retained adjacent to those records.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/design.md b/openspec/changes/isolate-semgrep-security-runtime/design.md
index 4f73e972..dec8e70c 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/design.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/design.md
@@ -129,3 +129,23 @@ Tests execute the resolved YAML body with test-only HTTP doubles. Local RED
replays the actual pinned filter over authenticated REST records. Final live
exact-ID verification, independent review, normal hooks and fresh exact-head
authority remain required before CI acceptance. Parent owns internal wiki sync.
+
+## Attested synthetic merge identity correction (2026-10-21)
+
+At core head `4b25241e`, authenticated native origin records workflow revision
+`21cca82b`, while the live PR merge revision is `6636108b`. Both have the exact
+head, tree and ordered base/head parents, and valid GitHub signatures. Their
+unsigned signed-payload text differs only in the author/committer time-zone
+encoding, producing different commit IDs. The deployed policy rejects this at
+`origin-source`, before a new grant can even be prepared.
+
+Prepare a separately reviewed protected-policy correction. The equivalent-merge
+path must bind both authenticated commit IDs, valid signatures, the exact
+reviewed full tree and ordered two-parent ancestry containing the current head.
+It retains actual attested signer/source digest and unchanged workflow bytes.
+It never rewrites origin metadata, substitutes workflow SHA or weakens the human
+subject binding. Strict negative controls cover wrong parents, tree, head,
+signature, metadata and workflow bytes. Genuine current CI refusal is retained
+before code. A new immutable core policy pin follows review, and owner approval
+is required before protected deployment. The original deployed revision remains
+unchanged until that approval.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md b/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
index d827596e..338c7011 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md
@@ -82,3 +82,9 @@ runtime, bypass Code Review, or authorize protected deployment or merge.
- **GIVEN** authenticated exact-ID metadata and ZIP bytes
- **WHEN** the shared isolated workflow-owned program restores evidence
- **THEN** credentialed transport remains on the fixed GitHub API, HTTPS redirects never forward the token, digest verification precedes bounded flat regular-file extraction, all files validate in staging before exclusive publication, and unsafe paths, links, collisions or uncertain inputs fail closed while preserving unrelated context.
+
+#### Scenario: Authenticated equivalent synthetic merge revisions
+
+- **GIVEN** an attested workflow revision and current GitHub PR merge revision have different IDs but the same exact reviewed full tree and ordered two-parent ancestry
+- **WHEN** the scoped policy verifies historical native origin for the unchanged head
+- **THEN** it may accept that equivalent signed merge only after authenticated commit identity, valid signatures, exact head/tree/parents, unchanged workflow bytes and the existing exact source-digest attestation verification pass; mismatched ancestry, content, signatures or source still refuse.
diff --git a/openspec/changes/isolate-semgrep-security-runtime/tasks.md b/openspec/changes/isolate-semgrep-security-runtime/tasks.md
index 656a77a3..b1cdf716 100644
--- a/openspec/changes/isolate-semgrep-security-runtime/tasks.md
+++ b/openspec/changes/isolate-semgrep-security-runtime/tasks.md
@@ -51,3 +51,12 @@
- [x] Replace all three current-run transfers without changing names or destinations.
- [x] Verify genuine producer and retained execution layouts, independent execution digest and live exact-ID transfer; current-head CI remains pending.
- [ ] Complete independent review, normal hooks, wiki alignment and fresh exact-head grants before CI acceptance.
+
+## 7. Preserve authenticated synthetic merge equivalence
+
+- [x] Retain exact `origin-source` refusal and both authenticated signed commit records.
+- [x] Specify strict equivalent-merge admission with unchanged attestation and human authority.
+- [x] Add meaningful failing-before regression and hostile substitution controls.
+- [x] Implement and independently review the scoped protected-policy correction.
+- [ ] Update the immutable core pin through normal hooks and verify fresh CI.
+- [ ] Request exact owner approval before protected deployment, then prepare fresh head-bound human grants.