diff --git a/.github/actions/setup-runtime-smoke-fixture/action.yml b/.github/actions/setup-runtime-smoke-fixture/action.yml new file mode 100644 index 000000000..d0fd30eb5 --- /dev/null +++ b/.github/actions/setup-runtime-smoke-fixture/action.yml @@ -0,0 +1,36 @@ +name: Reviewed portable runtime smoke fixture +description: Authenticate the signed portable module snapshot without changing Requirements authority. +runs: + using: composite + steps: + - name: Read portable fixture identity + shell: bash + run: | + set -euo pipefail + python - <<'PYCODE' + import json + from pathlib import Path + expected = { + "repository": "nold-ai/specfact-cli-modules", + "commit": "046d366c190af7a188d6f48953a96edb65e766db", + "tree": "c5b44db3477d1c9ea8dae1dceab2af276d265353", + } + actual = json.loads(Path("ci/runtime-smoke-fixture.lock.json").read_text()) + if actual != expected: + raise SystemExit("Unreviewed portable runtime fixture identity") + PYCODE + - name: Checkout portable runtime modules + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + with: + repository: nold-ai/specfact-cli-modules + ref: 046d366c190af7a188d6f48953a96edb65e766db + path: runtime-smoke-modules + persist-credentials: false + - name: Verify and export portable runtime fixture + shell: bash + run: | + set -euo pipefail + test "$(git -C runtime-smoke-modules rev-parse HEAD)" = "046d366c190af7a188d6f48953a96edb65e766db" + test "$(git -C runtime-smoke-modules rev-parse 'HEAD^{tree}')" = "c5b44db3477d1c9ea8dae1dceab2af276d265353" + test -z "$(git -C runtime-smoke-modules status --porcelain)" + printf 'SPECFACT_SMOKE_MODULES_REPO=%s/runtime-smoke-modules\n' "$GITHUB_WORKSPACE" >> "$GITHUB_ENV" diff --git a/.github/review-notes/dependency-hotfix-admission.md b/.github/review-notes/dependency-hotfix-admission.md new file mode 100644 index 000000000..2c23747bf --- /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/docs-dependency-checks.yml b/.github/workflows/docs-dependency-checks.yml new file mode 100644 index 000000000..79010014c --- /dev/null +++ b/.github/workflows/docs-dependency-checks.yml @@ -0,0 +1,66 @@ +name: Documentation Dependency Compatibility + +on: + pull_request: + paths: + - "docs/**" + - "scripts/check_jekyll_dependencies.rb" + - ".github/workflows/docs-dependency-checks.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + documentation-dependencies: + runs-on: ubuntu-latest + env: + BUNDLE_FROZEN: "true" + JEKYLL_ENV: production + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + - name: Setup frozen Ruby bundle + uses: ruby/setup-ruby@e81a8fa391b11a595c1b7ed84177cc0fe02faf83 + with: + ruby-version: "3.2" + bundler: "2.3.5" + bundler-cache: false + - name: Download hash-reviewed plugin artifacts + working-directory: docs + run: | + mkdir -p "${{ runner.temp }}/docs-bundle/ruby/3.2.0/cache" + cd "${{ runner.temp }}/docs-bundle/ruby/3.2.0/cache" + curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \ + 'https://rubygems.org/gems/jekyll-redirect-from-0.17.0.gem' -o jekyll-redirect-from-0.17.0.gem + curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \ + 'https://rubygems.org/gems/jekyll-feed-0.18.0.gem' -o jekyll-feed-0.18.0.gem + curl --fail --silent --show-error --location --proto '=https' --proto-redir '=https' \ + 'https://rubygems.org/gems/jekyll-relative-links-0.9.1.gem' -o jekyll-relative-links-0.9.1.gem + printf '%s\n' \ + '703dab0428b20bb5d550ab83d3c28b65bb5ecc0f734c5e61ee680de391b3e897 jekyll-redirect-from-0.17.0.gem' \ + '8e6829f455b8764a8fa1bbb198d0ebb3525128dacc6ffe9d15f4df7388bc3647 jekyll-feed-0.18.0.gem' \ + '6d5d70578c669ef9ee4c67f617d91497e0506fe5c1a059e484a900e18efb202e jekyll-relative-links-0.9.1.gem' \ + | sha256sum --check + - name: Install locked documentation dependencies + working-directory: docs + run: | + bundle config set --local path '${{ runner.temp }}/docs-bundle' + bundle install --jobs 1 --retry 3 + - name: Verify dependency security and compatibility + working-directory: docs + run: bundle exec ruby ../scripts/check_jekyll_dependencies.rb redirect feed_language feed_cdata relative + - name: Build root and prefixed documentation + working-directory: docs + run: | + bundle exec jekyll build --baseurl '' --destination '${{ runner.temp }}/docs-root' + bundle exec jekyll build --baseurl '/preview' --destination '${{ runner.temp }}/docs-preview' + - name: Retain built documentation and frozen inputs + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: documentation-dependency-evidence + path: | + docs/Gemfile + docs/Gemfile.lock + ${{ runner.temp }}/docs-root + ${{ runner.temp }}/docs-preview diff --git a/.github/workflows/pr-orchestrator.yml b/.github/workflows/pr-orchestrator.yml index cc57fb2c7..50af9e7b8 100644 --- a/.github/workflows/pr-orchestrator.yml +++ b/.github/workflows/pr-orchestrator.yml @@ -55,6 +55,7 @@ jobs: - 'requirements/ci/locked.txt' - 'requirements/code-review/requirements.in' - 'requirements/code-review/locked.txt' + - 'requirements/runtime-smoke/**' - 'modules/**/module-package.yaml' - 'src/specfact_cli/modules/**/module-package.yaml' - 'scripts/check_license_compliance.py' @@ -287,6 +288,10 @@ jobs: if: needs.changes.outputs.skip_expensive_tests_dev_to_main != 'true' uses: ./.github/actions/setup-frozen-python + - name: Set up reviewed portable runtime fixture + if: needs.changes.outputs.skip_expensive_tests_dev_to_main != 'true' + uses: ./.github/actions/setup-runtime-smoke-fixture + - name: Verify version strings are synchronized if: needs.changes.outputs.skip_expensive_tests_dev_to_main != 'true' shell: bash @@ -442,6 +447,9 @@ jobs: python-version: "3.11" - name: Set up frozen compatibility dependencies uses: ./.github/actions/setup-frozen-python + - name: Set up reviewed portable runtime fixture + uses: ./.github/actions/setup-runtime-smoke-fixture + - name: Run Python 3.11 compatibility tests shell: bash run: | @@ -921,11 +929,14 @@ jobs: - name: Set up frozen smoke dependencies uses: ./.github/actions/setup-frozen-python + - name: Set up reviewed portable runtime fixture + uses: ./.github/actions/setup-runtime-smoke-fixture + - name: Run macOS runtime discovery smoke shell: bash run: | mkdir -p logs/runtime-smoke/macos - python scripts/runtime_discovery_smoke.py --modules-repo specfact-cli-modules --launcher direct 2>&1 | tee logs/runtime-smoke/macos/runtime-discovery.log + python scripts/runtime_discovery_smoke.py --modules-repo "$SPECFACT_SMOKE_MODULES_REPO" --launcher direct 2>&1 | tee logs/runtime-smoke/macos/runtime-discovery.log - name: Upload macOS runtime-smoke logs if: always() @@ -980,11 +991,14 @@ jobs: - name: Set up frozen smoke dependencies uses: ./.github/actions/setup-frozen-python + - name: Set up reviewed portable runtime fixture + uses: ./.github/actions/setup-runtime-smoke-fixture + - name: Run Windows runtime discovery smoke shell: bash run: | mkdir -p logs/runtime-smoke/windows - python scripts/runtime_discovery_smoke.py --modules-repo specfact-cli-modules --launcher direct 2>&1 | tee logs/runtime-smoke/windows/runtime-discovery.log + python scripts/runtime_discovery_smoke.py --modules-repo "$SPECFACT_SMOKE_MODULES_REPO" --launcher direct 2>&1 | tee logs/runtime-smoke/windows/runtime-discovery.log - name: Upload Windows runtime-smoke logs if: always() @@ -1163,10 +1177,23 @@ jobs: -r requirements/code-review/locked.txt echo "REVIEW_TOOLS_PYTHON=${review_tools}/bin/python" >> "$GITHUB_ENV" + - name: Install frozen runtime smoke license scope + shell: bash + run: | + set -euo pipefail + python scripts/check_reproducible_delivery.py + smoke_tools="${RUNNER_TEMP}/specfact-runtime-smoke-license-scope" + uv venv --python "${pythonLocation}/bin/python" "$smoke_tools" + uv pip install \ + --python "${smoke_tools}/bin/python" \ + --require-hashes --no-deps --only-binary :all: \ + -r requirements/runtime-smoke/locked.txt + echo "SMOKE_TOOLS_PYTHON=${smoke_tools}/bin/python" >> "$GITHUB_ENV" + - name: Run license compliance gate run: | echo "🔍 Running license compliance gate..." - python scripts/check_license_compliance.py --code-review-python "$REVIEW_TOOLS_PYTHON" + python scripts/check_license_compliance.py --code-review-python "$REVIEW_TOOLS_PYTHON" --additional-python "$SMOKE_TOOLS_PYTHON" dependency-trust: name: Dependency Trust Gate @@ -1214,6 +1241,7 @@ jobs: echo "🔍 Running CVE security audit..." python scripts/security_audit_gate.py python scripts/security_audit_gate.py --requirement requirements/code-review/locked.txt + python scripts/security_audit_gate.py --requirement requirements/runtime-smoke/locked.txt reproducible-delivery: name: Reproducible Delivery Evidence diff --git a/.github/workflows/requirements-evidence.yml b/.github/workflows/requirements-evidence.yml index 5c490e063..b6cbaae9b 100644 --- a/.github/workflows/requirements-evidence.yml +++ b/.github/workflows/requirements-evidence.yml @@ -11,16 +11,22 @@ permissions: contents: read issues: read +env: + LATE_CYCLE_POLICY_REVISION: 5cd61c45303bc5da174a39ff79945fafad77ea3b + jobs: requirements-evidence-producer: name: Requirements evidence producer permissions: actions: read + 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 }} + stack-context-digest: ${{ steps.stack-context.outputs.digest }} runs-on: ubuntu-latest steps: - name: Checkout @@ -80,6 +86,60 @@ jobs: with: python-version: "3.12" + - name: Authenticate live native stack proof boundary + id: stack-context + env: + GH_TOKEN: ${{ github.token }} + EXPECTED_CONTEXT_DIGEST: "" + shell: bash + run: | + set -euo pipefail + policy_root="${RUNNER_TEMP}/trusted-stack-context" + mkdir -p "$policy_root" + context="${policy_root}/context.json" + policy_ref="origin/dev" + if git cat-file -e "${policy_ref}:scripts/requirements_stack_context.py" 2>/dev/null; then + if git cat-file -e "${policy_ref}:scripts/requirements_planning_context.py" 2>/dev/null; then + if git cat-file -e "${policy_ref}:scripts/requirements_review_context.py" 2>/dev/null; then + git archive "$policy_ref" -- scripts/requirements_stack_context.py scripts/requirements_planning_context.py scripts/requirements_review_context.py | tar -x -C "$policy_root" + else + git archive "$policy_ref" -- scripts/requirements_stack_context.py scripts/requirements_planning_context.py | tar -x -C "$policy_root" + fi + else + git archive "$policy_ref" -- scripts/requirements_stack_context.py | tar -x -C "$policy_root" + fi + python3 -I -S "$policy_root/scripts/requirements_stack_context.py" \ + --event "$GITHUB_EVENT_PATH" --output "$context" + else + # Bootstrap supports protected-base PRs only; candidate helper bytes never authorize a stack. + case "$GITHUB_BASE_REF" in dev|main) ;; *) exit 1 ;; esac + base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" + jq -S --arg base_commit "$base_commit" \ + '{proof_base_ref: .pull_request.base.ref, proof_base_commit: $base_commit, + proof_head_branch: .pull_request.head.ref, proof_head_commit: .pull_request.head.sha, + proof_pull_request: .pull_request.number}' "$GITHUB_EVENT_PATH" > "$context" + fi + proof_base="$(jq -er '.proof_base_commit | select(test("^[0-9a-f]{40}$"))' "$context")" + proof_head="$(jq -er '.proof_head_commit | select(test("^[0-9a-f]{40}$"))' "$context")" + git merge-base --is-ancestor "$proof_base" "$proof_head" + git merge-base --is-ancestor "$proof_head" HEAD + digest="$(sha256sum < "$context" | cut -d ' ' -f 1)" + if [[ -n "$EXPECTED_CONTEXT_DIGEST" ]]; then + test "$digest" = "$EXPECTED_CONTEXT_DIGEST" + fi + printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" + { + printf 'PROOF_BASE_COMMIT=%s\n' "$proof_base" + planning_base="$(jq -r '.planning_base_commit // empty' "$context")" + if [[ -n "$planning_base" ]]; then + [[ "$planning_base" =~ ^[0-9a-f]{40}$ ]] + test "$planning_base" = "$proof_head" + printf 'PLANNING_BASE_COMMIT=%s\n' "$planning_base" + fi + printf 'PROOF_HEAD_BRANCH=%s\n' "$(jq -er '.proof_head_branch' "$context")" + printf 'PROOF_PULL_REQUEST=%s\n' "$(jq -er '.proof_pull_request' "$context")" + } >> "$GITHUB_ENV" + - name: Checkout trusted Requirements authority policy if: github.event_name == 'pull_request' && 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') != '' uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 @@ -161,7 +221,7 @@ jobs: shell: bash run: | set -euo pipefail - base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" + base_commit="$PROOF_BASE_COMMIT" [[ "$base_commit" =~ ^[0-9a-f]{40}$ ]] trusted_root="${RUNNER_TEMP}/trusted-requirements" mkdir -p "$trusted_root" @@ -377,7 +437,7 @@ jobs: [[ -n "$run_id" && -n "$head_sha" ]] || continue [[ "$conclusion" == "failure" ]] || continue [[ "$head_sha" != "$current_head" ]] || continue - git merge-base --is-ancestor "origin/${GITHUB_BASE_REF}" "$head_sha" || continue + git merge-base --is-ancestor "$PROOF_BASE_COMMIT" "$head_sha" || continue git merge-base --is-ancestor "$head_sha" "$current_head" || continue candidate_dir="${RUNNER_TEMP}/prior-red-candidate" rm -rf "$candidate_dir" @@ -389,7 +449,7 @@ jobs: || ! "${isolated_python[@]}" scripts/requirements_proof_provenance.py \ --prior-red-proof "$candidate_dir/red.json" \ --repo-root . \ - --base-ref "origin/${GITHUB_BASE_REF}" \ + --base-ref "$PROOF_BASE_COMMIT" \ --final-ref "$current_head"; then continue fi @@ -398,7 +458,7 @@ jobs: done < <( gh api --method GET --paginate \ "repos/${GITHUB_REPOSITORY}/actions/workflows/requirements-evidence.yml/runs" \ - -f branch="$GITHUB_HEAD_REF" \ + -f branch="$PROOF_HEAD_BRANCH" \ -f event=pull_request \ -f status=completed \ -f per_page=100 \ @@ -492,8 +552,8 @@ jobs: printf 'Invalid evidence base branch: %s\n' "$EVIDENCE_BASE_BRANCH" >&2 exit 1 fi - if ! evidence_base_commit="$(git merge-base "origin/${EVIDENCE_BASE_BRANCH}" HEAD)" \ - || [[ ! "$evidence_base_commit" =~ ^[0-9a-f]{40}$ ]]; then + evidence_base_commit="${PLANNING_BASE_COMMIT:-${PROOF_BASE_COMMIT:-}}" + if [[ ! "$evidence_base_commit" =~ ^[0-9a-f]{40}$ ]]; then write_failure_reports "Unable to resolve immutable evidence base for $EVIDENCE_BASE_BRANCH" exit 1 fi @@ -668,7 +728,7 @@ jobs: ) prior_red_proof="" prior_red_junit="" - prior_red_base_ref="$evidence_base_commit" + prior_red_base_ref="$PROOF_BASE_COMMIT" prior_red_provenance="scripts/requirements_proof_provenance.py" prior_red_report_digest="" prior_red_junit_digest="" @@ -686,7 +746,7 @@ jobs: fi if [[ -n "$selected_change" ]]; then review_evidence="openspec/changes/${selected_change}/requirements-proof/review-evidence.json" - if [[ -f "$review_evidence" ]]; then + if [[ "$planning_maturity" != "planned" && -f "$review_evidence" ]]; then evidence_arguments+=(--review-evidence "$review_evidence") fi candidate_red_proof="${RUNNER_TEMP}/prior-red-proof/red.json" @@ -870,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 \ @@ -899,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: | @@ -922,9 +1015,110 @@ 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 != '' && + 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 + 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 @@ -932,13 +1126,16 @@ jobs: if: always() permissions: actions: read + 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 }} prior-red-run-id: ${{ needs.requirements-evidence-producer.outputs.prior-red-run-id }} producer-artifact-id: ${{ needs.requirements-evidence-producer.outputs.artifact-id }} + stack-context-digest: ${{ needs.requirements-evidence-producer.outputs.stack-context-digest }} runs-on: ubuntu-latest steps: - name: Enforce Requirements evidence producer success @@ -1009,11 +1206,65 @@ jobs: with: enable-cache: false + - name: Authenticate live native stack proof boundary + id: stack-context + env: + GH_TOKEN: ${{ github.token }} + EXPECTED_CONTEXT_DIGEST: ${{ needs.requirements-evidence-producer.outputs.stack-context-digest }} + shell: bash + run: | + set -euo pipefail + policy_root="${RUNNER_TEMP}/trusted-stack-context" + mkdir -p "$policy_root" + context="${policy_root}/context.json" + policy_ref="origin/dev" + if git cat-file -e "${policy_ref}:scripts/requirements_stack_context.py" 2>/dev/null; then + if git cat-file -e "${policy_ref}:scripts/requirements_planning_context.py" 2>/dev/null; then + if git cat-file -e "${policy_ref}:scripts/requirements_review_context.py" 2>/dev/null; then + git archive "$policy_ref" -- scripts/requirements_stack_context.py scripts/requirements_planning_context.py scripts/requirements_review_context.py | tar -x -C "$policy_root" + else + git archive "$policy_ref" -- scripts/requirements_stack_context.py scripts/requirements_planning_context.py | tar -x -C "$policy_root" + fi + else + git archive "$policy_ref" -- scripts/requirements_stack_context.py | tar -x -C "$policy_root" + fi + python3 -I -S "$policy_root/scripts/requirements_stack_context.py" \ + --event "$GITHUB_EVENT_PATH" --output "$context" + else + # Bootstrap supports protected-base PRs only; candidate helper bytes never authorize a stack. + case "$GITHUB_BASE_REF" in dev|main) ;; *) exit 1 ;; esac + base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" + jq -S --arg base_commit "$base_commit" \ + '{proof_base_ref: .pull_request.base.ref, proof_base_commit: $base_commit, + proof_head_branch: .pull_request.head.ref, proof_head_commit: .pull_request.head.sha, + proof_pull_request: .pull_request.number}' "$GITHUB_EVENT_PATH" > "$context" + fi + proof_base="$(jq -er '.proof_base_commit | select(test("^[0-9a-f]{40}$"))' "$context")" + proof_head="$(jq -er '.proof_head_commit | select(test("^[0-9a-f]{40}$"))' "$context")" + git merge-base --is-ancestor "$proof_base" "$proof_head" + git merge-base --is-ancestor "$proof_head" HEAD + digest="$(sha256sum < "$context" | cut -d ' ' -f 1)" + if [[ -n "$EXPECTED_CONTEXT_DIGEST" ]]; then + test "$digest" = "$EXPECTED_CONTEXT_DIGEST" + fi + printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" + { + printf 'PROOF_BASE_COMMIT=%s\n' "$proof_base" + planning_base="$(jq -r '.planning_base_commit // empty' "$context")" + if [[ -n "$planning_base" ]]; then + [[ "$planning_base" =~ ^[0-9a-f]{40}$ ]] + test "$planning_base" = "$proof_head" + printf 'PLANNING_BASE_COMMIT=%s\n' "$planning_base" + fi + printf 'PROOF_HEAD_BRANCH=%s\n' "$(jq -er '.proof_head_branch' "$context")" + printf 'PROOF_PULL_REQUEST=%s\n' "$(jq -er '.proof_pull_request' "$context")" + } >> "$GITHUB_ENV" + - name: Materialize trusted Requirements core shell: bash run: | set -euo pipefail - base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" + base_commit="$PROOF_BASE_COMMIT" [[ "$base_commit" =~ ^[0-9a-f]{40}$ ]] review_source="$base_commit" review_input_present=false @@ -1073,6 +1324,9 @@ jobs: requirements/code-review/requirements.in \ requirements/code-review/locked.txt \ | tar -x -C "$trusted_core_root" + if git cat-file -e "${base_commit}:requirements/runtime-smoke/locked.txt" 2>/dev/null; then + git archive "$base_commit" -- requirements/runtime-smoke | tar -x -C "$trusted_core_root" + fi test -f "$trusted_core_root/src/specfact_cli/cli.py" test -f "$trusted_core_root/scripts/check_reproducible_delivery.py" test -f "$trusted_core_root/scripts/requirements_proof_executor.py" @@ -1168,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: >- @@ -1434,7 +1974,8 @@ jobs: jq -e \ --argjson run_id "$PRIOR_RED_RUN_ID" \ --arg repository "$GITHUB_REPOSITORY" \ - --arg head_branch "$GITHUB_HEAD_REF" \ + --arg head_branch "$PROOF_HEAD_BRANCH" \ + --argjson proof_pull_request "$PROOF_PULL_REQUEST" \ '.id == $run_id and .repository.full_name == $repository and .name == "Requirements Evidence" and @@ -1443,10 +1984,11 @@ jobs: .status == "completed" and .conclusion == "failure" and .head_branch == $head_branch and - (.head_sha | test("^[0-9a-f]{40}$"))' \ + (.head_sha | test("^[0-9a-f]{40}$")) and + ([.pull_requests[].number] | index($proof_pull_request) != null)' \ "$metadata_root/run.json" > /dev/null red_head_sha="$(jq -er '.head_sha' "$metadata_root/run.json")" - git merge-base --is-ancestor "origin/${GITHUB_BASE_REF}" "$red_head_sha" + git merge-base --is-ancestor "$PROOF_BASE_COMMIT" "$red_head_sha" git merge-base --is-ancestor "$red_head_sha" HEAD gh api --method GET \ "repos/${GITHUB_REPOSITORY}/actions/runs/${PRIOR_RED_RUN_ID}/artifacts" \ @@ -1481,8 +2023,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 }} @@ -1493,11 +2039,14 @@ jobs: run: | set -euo pipefail source_ref="$(git rev-parse HEAD)" - evidence_base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" "$source_ref")" + evidence_base_commit="${PLANNING_BASE_COMMIT:-$PROOF_BASE_COMMIT}" review_base_commit="$(git merge-base "origin/${REVIEW_BASE_BRANCH}" HEAD)" [[ "$source_ref" =~ ^[0-9a-f]{40}$ ]] [[ "$evidence_base_commit" =~ ^[0-9a-f]{40}$ ]] - test "$review_base_commit" = "$evidence_base_commit" + [[ "$PROOF_BASE_COMMIT" =~ ^[0-9a-f]{40}$ ]] + [[ "$review_base_commit" =~ ^[0-9a-f]{40}$ ]] + git merge-base --is-ancestor "$PROOF_BASE_COMMIT" "$review_base_commit" + git merge-base --is-ancestor "$evidence_base_commit" "$source_ref" validator_site="${REQUIREMENTS_VALIDATOR_ROOT}/lib/python3.12/site-packages" isolated_python=("${REQUIREMENTS_VALIDATOR_ROOT}/bin/python" -I -S -c 'import runpy, sys; sys.path.append(sys.argv.pop(1)); script = sys.argv.pop(1); runpy.run_path(script, run_name="__main__")' "$validator_site") isolated_specfact=("${REQUIREMENTS_VALIDATOR_ROOT}/bin/python" -I -S -c 'import sys; site_packages, trusted_core = sys.argv[1:3]; del sys.argv[1:3]; sys.path.append(site_packages); sys.path.insert(0, trusted_core); from specfact_cli.cli import cli_main; cli_main()' "$validator_site" "$TRUSTED_REQUIREMENTS_CORE") @@ -1564,7 +2113,7 @@ jobs: esac done < "$consumer_change_paths_file" review_evidence_paths=() - if [[ "$EVIDENCE_PROMOTION_REUSE" != "true" ]]; then + if [[ "$EVIDENCE_PROMOTION_REUSE" != "true" && "$planning_maturity" != "planned" ]]; then if [[ "${#consumer_changed_change_ids[@]}" -eq 1 ]]; then for selected_change in "${!consumer_changed_change_ids[@]}"; do : @@ -1575,7 +2124,7 @@ jobs: exit 1 fi fi - if [[ "$EVIDENCE_PROMOTION_REUSE" != "true" ]]; then + if [[ "$EVIDENCE_PROMOTION_REUSE" != "true" && "$planning_maturity" != "planned" ]]; then if [[ "${#review_evidence_paths[@]}" -eq 0 ]]; then mapfile -t active_review_evidence < <( find openspec/changes -path 'openspec/changes/archive' -prune -o -path '*/requirements-proof/review-evidence.json' -type f -print | sort @@ -1809,13 +2358,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: @@ -1852,6 +2447,7 @@ jobs: id: run-code-review continue-on-error: true env: + GH_TOKEN: ${{ github.token }} REVIEW_BASE_BRANCH: ${{ github.base_ref }} run: | set -euo pipefail @@ -1859,6 +2455,12 @@ jobs: isolated_specfact=("${REQUIREMENTS_VALIDATOR_ROOT}/bin/python" -I -S -c 'import sys; site_packages, trusted_core = sys.argv[1:3]; del sys.argv[1:3]; sys.path.append(site_packages); sys.path.insert(0, trusted_core); from specfact_cli.cli import cli_main; cli_main()' "$validator_site" "$TRUSTED_REQUIREMENTS_CORE") requirements_context="artifacts/requirements-evidence/requirements-evidence-consumer.json" [[ -s "$requirements_context" ]] + if [[ -n "${PLANNING_BASE_COMMIT:-}" ]]; then + requirements_context="${RUNNER_TEMP}/authenticated-root-review-proof.json" + python3 -I -S "${RUNNER_TEMP}/trusted-stack-context/scripts/requirements_review_context.py" \ + --context "${RUNNER_TEMP}/trusted-stack-context/context.json" \ + --event "$GITHUB_EVENT_PATH" --output "$requirements_context" + fi if [[ ! "$REVIEW_BASE_BRANCH" =~ ^[A-Za-z0-9._/-]+$ ]]; then printf 'Invalid Code Review base branch: %s\n' "$REVIEW_BASE_BRANCH" >&2 exit 1 @@ -1897,6 +2499,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 @@ -1907,8 +2518,10 @@ jobs: if: always() permissions: actions: read + checks: read contents: read issues: read + pull-requests: read runs-on: ubuntu-latest steps: - name: Enforce trusted Requirements execution success @@ -1962,11 +2575,65 @@ jobs: with: enable-cache: false + - name: Authenticate live native stack proof boundary + id: stack-context + env: + GH_TOKEN: ${{ github.token }} + EXPECTED_CONTEXT_DIGEST: ${{ needs.requirements-evidence.outputs.stack-context-digest }} + shell: bash + run: | + set -euo pipefail + policy_root="${RUNNER_TEMP}/trusted-stack-context" + mkdir -p "$policy_root" + context="${policy_root}/context.json" + policy_ref="origin/dev" + if git cat-file -e "${policy_ref}:scripts/requirements_stack_context.py" 2>/dev/null; then + if git cat-file -e "${policy_ref}:scripts/requirements_planning_context.py" 2>/dev/null; then + if git cat-file -e "${policy_ref}:scripts/requirements_review_context.py" 2>/dev/null; then + git archive "$policy_ref" -- scripts/requirements_stack_context.py scripts/requirements_planning_context.py scripts/requirements_review_context.py | tar -x -C "$policy_root" + else + git archive "$policy_ref" -- scripts/requirements_stack_context.py scripts/requirements_planning_context.py | tar -x -C "$policy_root" + fi + else + git archive "$policy_ref" -- scripts/requirements_stack_context.py | tar -x -C "$policy_root" + fi + python3 -I -S "$policy_root/scripts/requirements_stack_context.py" \ + --event "$GITHUB_EVENT_PATH" --output "$context" + else + # Bootstrap supports protected-base PRs only; candidate helper bytes never authorize a stack. + case "$GITHUB_BASE_REF" in dev|main) ;; *) exit 1 ;; esac + base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" + jq -S --arg base_commit "$base_commit" \ + '{proof_base_ref: .pull_request.base.ref, proof_base_commit: $base_commit, + proof_head_branch: .pull_request.head.ref, proof_head_commit: .pull_request.head.sha, + proof_pull_request: .pull_request.number}' "$GITHUB_EVENT_PATH" > "$context" + fi + proof_base="$(jq -er '.proof_base_commit | select(test("^[0-9a-f]{40}$"))' "$context")" + proof_head="$(jq -er '.proof_head_commit | select(test("^[0-9a-f]{40}$"))' "$context")" + git merge-base --is-ancestor "$proof_base" "$proof_head" + git merge-base --is-ancestor "$proof_head" HEAD + digest="$(sha256sum < "$context" | cut -d ' ' -f 1)" + if [[ -n "$EXPECTED_CONTEXT_DIGEST" ]]; then + test "$digest" = "$EXPECTED_CONTEXT_DIGEST" + fi + printf 'digest=%s\n' "$digest" >> "$GITHUB_OUTPUT" + { + printf 'PROOF_BASE_COMMIT=%s\n' "$proof_base" + planning_base="$(jq -r '.planning_base_commit // empty' "$context")" + if [[ -n "$planning_base" ]]; then + [[ "$planning_base" =~ ^[0-9a-f]{40}$ ]] + test "$planning_base" = "$proof_head" + printf 'PLANNING_BASE_COMMIT=%s\n' "$planning_base" + fi + printf 'PROOF_HEAD_BRANCH=%s\n' "$(jq -er '.proof_head_branch' "$context")" + printf 'PROOF_PULL_REQUEST=%s\n' "$(jq -er '.proof_pull_request' "$context")" + } >> "$GITHUB_ENV" + - name: Materialize trusted final Requirements core shell: bash run: | set -euo pipefail - base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" + base_commit="$PROOF_BASE_COMMIT" [[ "$base_commit" =~ ^[0-9a-f]{40}$ ]] review_source="$base_commit" review_input_present=false @@ -2020,6 +2687,9 @@ jobs: requirements/code-review/requirements.in \ requirements/code-review/locked.txt \ | tar -x -C "$trusted_root" + if git cat-file -e "${base_commit}:requirements/runtime-smoke/locked.txt" 2>/dev/null; then + git archive "$base_commit" -- requirements/runtime-smoke | tar -x -C "$trusted_root" + fi test -f "$trusted_root/src/specfact_cli/cli.py" test -f "$trusted_root/ci/module-fixture.lock.json" test -f "$trusted_root/scripts/check_reproducible_delivery.py" @@ -2141,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: >- @@ -2278,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') != '' @@ -2336,7 +3020,8 @@ jobs: jq -e \ --argjson run_id "$PRIOR_RED_RUN_ID" \ --arg repository "$GITHUB_REPOSITORY" \ - --arg head_branch "$GITHUB_HEAD_REF" \ + --arg head_branch "$PROOF_HEAD_BRANCH" \ + --argjson proof_pull_request "$PROOF_PULL_REQUEST" \ '.id == $run_id and .repository.full_name == $repository and .name == "Requirements Evidence" and @@ -2345,7 +3030,8 @@ jobs: .status == "completed" and .conclusion == "failure" and .head_branch == $head_branch and - (.head_sha | test("^[0-9a-f]{40}$"))' \ + (.head_sha | test("^[0-9a-f]{40}$")) and + ([.pull_requests[].number] | index($proof_pull_request) != null)' \ "$metadata_root/run.json" > /dev/null red_head_sha="$(jq -er '.head_sha' "$metadata_root/run.json")" git merge-base --is-ancestor "$FINAL_BASE_COMMIT" "$red_head_sha" @@ -2381,7 +3067,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 }} @@ -2391,6 +3081,8 @@ jobs: source_ref="$(git rev-parse HEAD)" [[ "$source_ref" =~ ^[0-9a-f]{40}$ ]] [[ "$FINAL_BASE_COMMIT" =~ ^[0-9a-f]{40}$ ]] + evidence_base_commit="${PLANNING_BASE_COMMIT:-$FINAL_BASE_COMMIT}" + [[ "$evidence_base_commit" =~ ^[0-9a-f]{40}$ ]] validator_site="${FINAL_VERIFIER_ROOT}/lib/python3.12/site-packages" isolated_python=("${FINAL_VERIFIER_ROOT}/bin/python" -I -S -c 'import runpy, sys; sys.path.append(sys.argv.pop(1)); script = sys.argv.pop(1); runpy.run_path(script, run_name="__main__")' "$validator_site") isolated_specfact=("${FINAL_VERIFIER_ROOT}/bin/python" -I -S -c 'import sys; site_packages, trusted_core = sys.argv[1:3]; del sys.argv[1:3]; sys.path.append(site_packages); sys.path.insert(0, trusted_core); from specfact_cli.cli import cli_main; cli_main()' "$validator_site" "$FINAL_TRUSTED_CORE") @@ -2401,7 +3093,7 @@ jobs: required_maturity=planned final_status_file="${RUNNER_TEMP}/final-requirements-status.z" - git diff --name-status -z --find-renames=100% "$FINAL_BASE_COMMIT..$source_ref" > "$final_status_file" + git diff --name-status -z --find-renames=100% "$evidence_base_commit..$source_ref" > "$final_status_file" classify_path() { case "$1" in .github/*|ci/*|scripts/*|src/*|tools/*|requirements/*|pyproject.toml|setup.py|uv.lock|resources/templates/*|resources/schemas/*|resources/mappings/*|resources/keys/*|modules/bundle-mapper/*) @@ -2438,7 +3130,7 @@ jobs: fi final_change_paths_file="${RUNNER_TEMP}/final-change-paths.z" - git diff --name-only -z --diff-filter=ACMRD "$FINAL_BASE_COMMIT..$source_ref" -- openspec/changes/ \ + git diff --name-only -z --diff-filter=ACMRD "$evidence_base_commit..$source_ref" -- openspec/changes/ \ > "$final_change_paths_file" declare -A final_changed_change_ids=() while IFS= read -r -d '' changed_path; do @@ -2454,7 +3146,7 @@ jobs: esac done < "$final_change_paths_file" review_evidence_paths=() - if [[ "$EVIDENCE_PROMOTION_REUSE" != "true" ]]; then + if [[ "$EVIDENCE_PROMOTION_REUSE" != "true" && "$planning_maturity" != "planned" ]]; then if [[ "${#final_changed_change_ids[@]}" -eq 1 ]]; then for selected_change in "${!final_changed_change_ids[@]}"; do : @@ -2465,7 +3157,7 @@ jobs: exit 1 fi fi - if [[ "$EVIDENCE_PROMOTION_REUSE" != "true" ]]; then + if [[ "$EVIDENCE_PROMOTION_REUSE" != "true" && "$planning_maturity" != "planned" ]]; then if [[ "${#review_evidence_paths[@]}" -eq 0 ]]; then mapfile -t active_review_evidence < <( find openspec/changes -path 'openspec/changes/archive' -prune -o -path '*/requirements-proof/review-evidence.json' -type f -print | sort @@ -2493,7 +3185,7 @@ jobs: evidence_arguments=( "${isolated_specfact[@]}" requirements evidence --repo-root "$GITHUB_WORKSPACE" - --base-ref "$FINAL_BASE_COMMIT" + --base-ref "$evidence_base_commit" --required-maturity "$planning_maturity" --plan-output "$final_plan" --output "$final_planning_report" @@ -2558,7 +3250,7 @@ jobs: "${isolated_python[@]}" "$FINAL_TRUSTED_PROVENANCE" \ --prior-red-proof "$prior_red_proof" \ --repo-root "$GITHUB_WORKSPACE" \ - --base-ref "${FINAL_BASE_COMMIT}" \ + --base-ref "${evidence_base_commit}" \ --final-ref "$source_ref" reconciliation_arguments+=(--prior-red-proof "$prior_red_proof") elif [[ -s "$legacy_tdd_evidence" && -s "$legacy_tdd_ledger" ]]; then @@ -2594,14 +3286,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: @@ -2654,11 +3391,19 @@ jobs: id: run-final-code-review continue-on-error: true env: + GH_TOKEN: ${{ github.token }} REVIEW_BASE_BRANCH: ${{ github.base_ref }} shell: bash run: | set -euo pipefail printf 'review-required=false\n' >> "$GITHUB_OUTPUT" + requirements_context="${RUNNER_TEMP}/final-requirements.json" + if [[ -n "${PLANNING_BASE_COMMIT:-}" ]]; then + requirements_context="${RUNNER_TEMP}/authenticated-root-final-review-proof.json" + python3 -I -S "${RUNNER_TEMP}/trusted-stack-context/scripts/requirements_review_context.py" \ + --context "${RUNNER_TEMP}/trusted-stack-context/context.json" \ + --event "$GITHUB_EVENT_PATH" --output "$requirements_context" + fi [[ "$REVIEW_BASE_BRANCH" =~ ^[A-Za-z0-9._/-]+$ ]] review_base_commit="$(git merge-base "origin/${REVIEW_BASE_BRANCH}" HEAD)" [[ "$review_base_commit" =~ ^[0-9a-f]{40}$ ]] @@ -2683,7 +3428,7 @@ jobs: --enforcement full \ --json \ --out "${RUNNER_TEMP}/final-code-review.json" \ - --requirements-evidence "${RUNNER_TEMP}/final-requirements.json" + --requirements-evidence "$requirements_context" - name: Upload final Code Review evidence artifact if: always() && steps.run-final-code-review.outputs.review-required == 'true' @@ -2693,6 +3438,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/CHANGELOG.md b/CHANGELOG.md index 121ee1126..aaccd7b6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,115 @@ All notable changes to this project will be documented in this file. --- +## [0.55.11] - 2026-10-20 + +### Security + +- Require certifi >=2026.10.19 and refresh frozen dependencies to remove the compromised + emSign ECC Root CA - G3 from the delivered CA trust store (GHSA-6q48-6c7w-9gw2). +- Retain official Semgrep and record an owner-approved MCP 1.29.0 exception for + CVE-2026-59951 only, expiring 2026-11-04 while its upstream exact pin blocks + patched MCP. All strict security audits and CI checks remain enforced. + +--- + +## [0.55.10] - 2026-10-13 + +### Security + +- Reject malformed alternate project manifests during authenticated admission without aborting CLI startup. + +- Require authenticated project authority for indirect category mounts and shared member loaders, including bundle aliases and cross-category claims. + +- Bind privileged project registration metadata and executable inventory to authenticated artifact bytes; reproduce source caches before the final file check and recheck authority before registration effects. + +- Reject category-less grouped workspace root commands, external claims to permanent core commands and + unauthenticated workspace claims to official category roots before registry, bridge or import-source activation. +- Preserve resolved built-in authority, reject forged source labels and escaping symlinks, and prevent + rejected workspace sources from shadowing eligible package dependencies. +- Reject unsigned executable caches and omitted Python/native artifacts; authenticate captured signed source + bytes before reproducing compatible interpreter caches without executing candidate code. + +### Fixed + +- Select the first fully eligible copy of each module, reconsider dependency fallbacks against selected versions + and each candidate's own version, and refuse contradictory priority cycles before registration effects. +- Preserve categorized routing and supported unreserved legacy registration, including operator-configured + built-in discovery, while retaining the completed removal of flat bundle shims. +- Preserve all six official project-scoped bundle roots only after full artifact checksum and signature + verification against the core-bundled key; reject unsigned, tampered and workspace-key replacements. +- Preserve genuine installed bundles when a rejected project copy claims the same name, retain authenticated + project priority, and skip artifact payload verification for disabled module names. +- Synchronize bugfix version declarations, frozen project metadata and the promotion fixture. + +## [0.55.9] - 2026-10-13 + +### Security + +- Authenticate unchanged finalized root Requirements proof for inherited Code Review on closed native + planning children. Verify live root/context, artifact provenance, digest and bounded report identity. + Preserve full review against protected dev and reject forged, non-final or stale context. + +### Fixed + +- Keep child planning receipts separate from execution proof so independently validated planning layers + retain the immutable Code Review provider's finalized-proof requirement. + +## [0.55.8] - 2026-10-12 + +### Security + +- Independently validate protected-proof and immutable review-base identities before Requirements execution. + Reject unrelated trust, unrelated planning ancestry and evidence newer than the candidate without changing + root authentication, source authority, signatures, plan equality or required gates. + +### Fixed + +- Validate native-stack planning parents against candidate ancestry while retaining protected dev/main as + review and verifier trust. Planning parents newer than dev can reach independent evidence reconciliation. +- Synchronize bugfix declarations, frozen project metadata and the generated promotion fixture. + +## [0.55.7] - 2026-10-12 + +### Security + +- Authenticate signed native-stack roots and exact successful Requirements and member-authority checks before + separating deferred planning evidence. Protected validator sources, full implementation proof, child authority + and independent review remain mandatory; executable, symlink, runtime and inherited-source changes cannot + receive a planning exemption. +- Require inherited root proof workflow and local CI action bytes to match protected-base objects. Authenticate + complete check listings and immutable workflow repository, issuer, event, branch, SHA and run attempts. + +### Fixed + +- Validate planning-only native stack members at planned maturity without applying one implementation mapping + acceptance to unrelated proposals. All three consumers compare the same authenticated context and planning + plan while continuing to materialize validators from protected dev/main. + +## [0.55.6] - 2026-10-11 + +### Security + +- Update jekyll-feed to 0.18.0 to preserve XML language and CDATA escaping fixes. +- Update jekyll-redirect-from to 0.17.0 and retain its unsafe-target security fix. +- Require JSON >=2.21.2,<3 for documentation while retaining JSON 2.21.2 and + Jekyll 4.4.1; defer the incompatible JSON 3 migration. +- Raise the runtime GitPython floor and frozen graph to 3.2.0, including its + additional submodule containment security fixes. + +### Fixed + +- Update jekyll-relative-links to 0.9.1 so rebuilt links, included navigation + and configured base URLs use current targets. +- Update the frozen Hatchling backend to 1.32.4 and include the bundled module + manifest in source distributions so offline wheel rebuilding remains complete. +- Authenticate native GitHub stack ancestry in Requirements proof reuse while + preserving complete plan equality and original failing-first evidence. +- Isolate direct and console marketplace smoke installations from the running + pytest environment and use reviewed, hash-pinned module dependencies. + +--- + ## [0.55.4] - 2026-09-02 ### Security diff --git a/ci/runtime-smoke-fixture.lock.json b/ci/runtime-smoke-fixture.lock.json new file mode 100644 index 000000000..ba22bf1cc --- /dev/null +++ b/ci/runtime-smoke-fixture.lock.json @@ -0,0 +1,5 @@ +{ + "repository": "nold-ai/specfact-cli-modules", + "commit": "046d366c190af7a188d6f48953a96edb65e766db", + "tree": "c5b44db3477d1c9ea8dae1dceab2af276d265353" +} diff --git a/ci/vulnerability-audit-exceptions.json b/ci/vulnerability-audit-exceptions.json index b5aac17ff..e852ce09d 100644 --- a/ci/vulnerability-audit-exceptions.json +++ b/ci/vulnerability-audit-exceptions.json @@ -1,3 +1,19 @@ { - "exceptions": [] + "exceptions": [ + { + "package": "mcp", + "version": "1.29.0", + "vulnerability_ids": [ + "CVE-2026-59951", + "GHSA-84m7-p3x7-pcfv" + ], + "reviewed_on": "2026-10-21", + "expires_on": "2026-11-04", + "rationale": "Owner approved this temporary exception on 2026-10-21 for PR #782. Official Semgrep 1.179.0 and latest 1.182.0 require exactly mcp==1.29.0; normal resolution with patched MCP >=1.30.0,<2 fails. Keep official packages only and remove the exception as soon as a verified compatible upstream release is available. Path: specfact-cli[scanning/dev] -> semgrep==1.179.0 -> mcp==1.29.0.", + "mitigation": "Use ordinary CLI scanning/SAST only. Do not start or expose a stateful Streamable HTTP MCP server. Semgrep imports MCP during startup, so this is accepted temporary residual risk, not proof of unreachability or a fix. Keep all three strict frozen audits and all other CI checks enforced. Review upstream weekly and remove after verified normal installation, scanner compatibility and security checks, or explicitly renew review before expiry.", + "advisory_url": "https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-84m7-p3x7-pcfv", + "upstream_metadata_url": "https://pypi.org/pypi/semgrep/1.182.0/json", + "tracking_issue": "https://github.com/nold-ai/specfact-cli/issues/780" + } + ] } diff --git a/docs/Gemfile b/docs/Gemfile index a1f94d872..9ff6cb9c8 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -4,12 +4,13 @@ source "https://rubygems.org" gem "jekyll", "~> 4.3" gem "minima", "~> 2.5" -gem "jekyll-feed", "~> 0.12" -gem "jekyll-redirect-from", "~> 0.16" -gem "jekyll-relative-links", "~> 0.8" +gem "jekyll-feed", "~> 0.18.0" +gem "jekyll-redirect-from", "~> 0.17.0" +gem "jekyll-relative-links", "~> 0.9.1" gem "jekyll-sitemap", "~> 1.4" -# Keep the documentation build clear of GHSA-x2f5-4prf-w687 / CVE-2026-54696. -gem "json", ">= 2.19.9" +# Patched JSON 2.x; JSON 3 migration is deferred until Jekyll supports it. +# Includes GHSA-9hj4-r449-hfvc / CVE-2026-71847 and the earlier security floor. +gem "json", ">= 2.21.2", "< 3" platforms :mingw, :x64_mingw, :mswin, :jruby do gem "tzinfo", ">= 1", "< 3" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 17f8dcb00..bd849ecdd 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -39,11 +39,11 @@ GEM safe_yaml (~> 1.0) terminal-table (>= 1.8, < 4.0) webrick (~> 1.7) - jekyll-feed (0.17.0) + jekyll-feed (0.18.0) jekyll (>= 3.7, < 5.0) - jekyll-redirect-from (0.16.0) + jekyll-redirect-from (0.17.0) jekyll (>= 3.3, < 5.0) - jekyll-relative-links (0.8.0) + jekyll-relative-links (0.9.1) jekyll (>= 3.3, < 5.0) jekyll-sass-converter (3.1.0) sass-embedded (~> 1.75) @@ -91,11 +91,11 @@ PLATFORMS DEPENDENCIES jekyll (~> 4.3) - jekyll-feed (~> 0.12) - jekyll-redirect-from (~> 0.16) - jekyll-relative-links (~> 0.8) + jekyll-feed (~> 0.18.0) + jekyll-redirect-from (~> 0.17.0) + jekyll-relative-links (~> 0.9.1) jekyll-sitemap (~> 1.4) - json (>= 2.19.9) + json (>= 2.21.2, < 3) minima (~> 2.5) tzinfo (>= 1, < 3) tzinfo-data diff --git a/docs/reference/module-categories.md b/docs/reference/module-categories.md index a63b6f984..5b96ec6c9 100644 --- a/docs/reference/module-categories.md +++ b/docs/reference/module-categories.md @@ -5,7 +5,7 @@ nav_order: 35 permalink: /reference/module-categories/ doc_owner: specfact-cli tracks: [src/specfact_cli/registry/**, ../specfact-cli-modules/packages/**] -last_reviewed: 2026-07-10 +last_reviewed: 2026-10-11 exempt: false exempt_reason: "" --- @@ -91,6 +91,15 @@ Namespace mapping: Compatibility note: - Flat top-level command shims were removed. Use category groups (`project`, `backlog`, `code`, `spec`, `govern`, `requirements`). +- Category-less workspace modules are skipped in default grouped mode. Add category/bundle metadata to + use the supported grouped surface. Explicitly disabling grouping preserves unreserved legacy workspace + commands; category-less user, marketplace and custom sources retain their compatibility path. +- Permanent core commands (`init`, `module`, `upgrade`) are reserved for resolved built-in package paths. + Operator-selected built-in discovery remains supported. External packages cannot extend these roots + by declaring a core category, name or source label. Workspace packages also cannot supply official + category root command apps in either mode. Rejected packages contribute no active import roots, + schema extensions, service bridges or category mounts. Eligible categorized execution remains + intentional extension behavior; this guard is not a sandbox. - `specfact backlog auth ...` is provided by the backlog bundle, not by the permanent core command surface. - Prerelease `specfact-requirements` manifests that still declare category `project` with `bundle_group_command: requirements` are normalized to the diff --git a/docs/reference/module-security.md b/docs/reference/module-security.md index d5e28b0d0..efd2c0bb6 100644 --- a/docs/reference/module-security.md +++ b/docs/reference/module-security.md @@ -19,6 +19,34 @@ Module packages carry **publisher** and **integrity** metadata so installation, - **Publisher trust gate**: Non-official publishers require one-time explicit trust (interactive confirmation or `--trust-non-official` / `SPECFACT_TRUST_NON_OFFICIAL`). - **Denylist gate**: Modules listed in denylist are blocked before install/bootstrap regardless of source. +## Workspace command authority + +Default grouped registration rejects category-less project modules before registration or import effects. +Permanent `init`, `module` and `upgrade` commands require resolved built-in package paths in both modes. +Unverified workspace packages cannot supply the official `project`, `backlog`, `code`, `spec`, `govern` or +`requirements` roots. Supported official bundles installed with `--scope project` retain their canonical roots +only after complete filesystem checksum and signature verification, including untracked module source files, +against the public key bundled with the CLI and matching +signed official bundle metadata. Payload symlink members, standalone bytecode, uncovered executable source and missing keys fail closed. +Current-interpreter source caches remain compatible only when their compiled bytes match the signed source. +Verification compares those bytes without executing or deserializing candidate caches. Unsigned/test flags, installation receipts and +`SPECFACT_MODULE_PUBLIC_KEY_PEM` overrides cannot grant these project root commands. + +Categorized project packages can populate shared category-member loaders even when their commands do not name a +root. They therefore require authenticated official bundle authority before candidate selection and registration. +The same gate covers category mounts induced by explicit bundle metadata or namespaced bundle aliases. +Explicit legacy registration retains unreserved category-less project commands without an official mount claim; +non-project categorized routing retains its existing policy. These controls protect command admission and import activation, and do not sandbox intentionally +invoked module code. + +## Preparing project-scoped release artifacts + +Publish regular payload files rather than symlinks. Replace links with their intended files before signing; +remove standalone bytecode and executable files under directories excluded from signing. Regenerate ordinary +source caches with the target interpreter, or remove stale or transformed caches before project installation. +The strict project-root admission check independently verifies these conditions. Passing a companion repository +signing or release-verification command alone does not establish eligibility for privileged project roots. + ## Integrity flow 1. Discovery reads `module-package.yaml` and parses `integrity.checksum`. @@ -104,3 +132,9 @@ Module packages carry **publisher** and **integrity** metadata so installation, ## Versioned dependencies Manifest may declare versioned module and pip dependencies via `module_dependencies_versioned` and `pip_dependencies_versioned` (each entry: `name`, `version_specifier`). These are parsed and stored for installation-time resolution while keeping legacy `module_dependencies` / `pip_dependencies` lists backward compatible. + +Privileged project-root admission binds discovery fields to the same normalized manifest captured in the signed +payload. It also completes reproduction of any permitted source caches before checking a fresh executable inventory. +The final check compares membership, source/native digests and raw cache bodies without executing candidate code or +invoking compilation. Admission and its recheck precede schema, bridge, protocol and import-root registration effects. +These checks authenticate the admission snapshot; they do not make a mutable workspace immutable after admission. diff --git a/docs/technical/dependency-trust-security-review.md b/docs/technical/dependency-trust-security-review.md index d1c531dcb..e5becb979 100644 --- a/docs/technical/dependency-trust-security-review.md +++ b/docs/technical/dependency-trust-security-review.md @@ -99,6 +99,39 @@ These controls validate the checked-in lock and explicit exceptions. They do not independently establish an upstream package's source provenance, and they cannot prove absence of malicious behavior in any dependency. The remaining mitigations are reviewed lock changes, short exception expiry, dependency -alerts, reproducible builds, and human security review. The MCP exception is -no longer a residual risk because the reviewed Semgrep/MCP pair is fixed in the -frozen dependency set. +alerts, reproducible builds, and human security review. The earlier MCP exceptions covered three older advisories fixed by MCP 1.29.0. +A later advisory requires a separate review, below. + +## Temporary official MCP exception, 2026-10-21 + +The previous MCP upgrade remains effective for its three original advisories. +CVE-2026-59951, also GHSA-84m7-p3x7-pcfv, is a later session-memory exhaustion +advisory affecting stateful Streamable HTTP servers. MCP 1.30.0 fixes the 1.x +line. Official Semgrep 1.182.0 still requires exactly MCP 1.29.0. A fresh normal +resolver rejects Semgrep 1.182.0 with MCP >=1.30.0,<2. The frozen graph therefore +keeps official Semgrep 1.179.0 and MCP 1.29.0. + +On 2026-10-21 Europe/Berlin the owner approved an exception while this upstream +pin prevents a compatible upgrade. The register selects only MCP 1.29.0 and +this advisory, reviewed 2026-10-21 and expiring 2026-11-04 inclusive. All three +frozen requirement audits still run in strict mode. Other advisories, different +versions, skipped subjects and expired records remain blocking. + +The path is `specfact-cli[scanning/dev] -> semgrep==1.179.0 -> mcp==1.29.0`. +Ordinary base-wheel dependencies do not include MCP or Semgrep. Semgrep imports +MCP at CLI startup, even for normal scanning. Reviewed automation does not start +the MCP server. This is not proof of universal unreachability. While the +exception applies, use ordinary scanning commands and do not expose a stateful +Streamable HTTP MCP server. The vulnerable package remains installed for +opt-in scanner users, and this exception does not remediate that risk. + +Review official releases weekly. Remove the exception immediately when an +official compatible patched graph passes artifact, resolver, installation, +scanner and audit verification. The gate fails after expiry unless the package +is removed or the owner explicitly approves a renewed review. No fork, package +publication, local signing or required-check bypass is part of this change. + +Sources accessed 2026-10-21. + +- [Official Semgrep metadata](https://pypi.org/pypi/semgrep/json). +- [MCP maintainer advisory](https://github.com/modelcontextprotocol/python-sdk/security/advisories/GHSA-84m7-p3x7-pcfv). diff --git a/openspec/CHANGE_ORDER.md b/openspec/CHANGE_ORDER.md index e192a50f4..e69b71c83 100644 --- a/openspec/CHANGE_ORDER.md +++ b/openspec/CHANGE_ORDER.md @@ -4,11 +4,104 @@ This document is the **single source of truth for active work** in this repository. It lists what is in flight, what is paused, and the order in which active changes should be implemented. +## High-priority trusted local Code Review reset (2026-10-16) + +Planned only. The approved ordinary local sequence is +[modules #513](https://github.com/nold-ai/specfact-cli-modules/issues/513) (`code-review-17-project-runtime-bootstrap`) +-> [modules #514](https://github.com/nold-ai/specfact-cli-modules/issues/514) (`code-review-18-project-standards-and-checks`) +-> [modules #515](https://github.com/nold-ai/specfact-cli-modules/issues/515) (`code-review-19-local-report-and-cutover`) +-> [core #769](https://github.com/nold-ai/specfact-cli/issues/769) ([`code-review-local-report-adoption`](changes/code-review-local-report-adoption/proposal.md)) +-> [modules #516](https://github.com/nold-ai/specfact-cli-modules/issues/516) (`code-review-20-external-installation-acceptance`). +Core #769 has only modules #515 as its direct blocker and blocks only modules #516. + +This reset removes capsule infrastructure from ordinary trusted-local scope and plans report schema 2.0. +It makes no shipped-support claim. Older capsule/runtime references below remain historical or separate protected work, not current local reset acceptance. +Core protected #680 remains separate and cannot be satisfied by local schema 2.0. C15 core #679 and modules #417 retain owners, status, and acceptance. +Optional preflight, architecture, and Requirements addons may consume later without blanket blocker edges. +Independent security modules #511 and core #719/#718 and existing signing gates remain independent. + +## Authorized #747 dependency stack (2026-10-10) + +### Official dependency hotfix and temporary MCP exception (2026-10-21) + +[PR #782 / issue #780](changes/isolate-semgrep-security-runtime/proposal.md) retains +certifi 2026.10.19 and core 0.55.11. The owner rejected the Semgrep fork and +requested its removal. Use official packages only. Latest official Semgrep still +pins MCP 1.29.0; the owner authorized one exact, expiring advisory exception +through 2026-11-04 while upstream remains blocked. Keep every strict audit and +required CI check, independent scanner coverage and CI/CD-only signing. Actual +final authority and verified coverage precede readiness or related-PR closure. +A later dependency-only hotfix starts from published v0.55.4, not all dev runtime +commits. No merge, package publication or promotion is authorized here. + +The owner approved `dev <- #744 <- #727 <- #752 <- #754 <- #753`, retaining +all PR numbers and compatible JSON 2.21.2/Jekyll 4.4.1. After #758 reserved +0.55.5 on protected dev, the required version gate selects one 0.55.6 bugfix release. +The merged #756 evidence remains historical and unchanged. Reconcile conflicting +planning PR #748 and the internal wiki; broader inventory and JSON 3 migration +remain deferred. Keep #747 open. New native RED/final evidence and ordinary quality, +security, signature and exact authority gates remain required for this stack. + +## Focused #747 baseline repair (2026-10-09) + +The owner authorized dependency-update-consolidation's focused security slice and +versioning fixture repair to unblock #755. Original #748 remains broader planning. +This branch covers only the focused scenarios; existing gates, authentic RED and final +authority remain required. + +## Minimal-evidence rescope (2026-09-20) + +The paired `requirements-09-minimal-evidence` owns the default policy correction: +[core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). Implementation order is signed modules +current-run contract -> core adoption/pilot -> coordinated organization/repository +cutover. Existing corrected R07 is superseded planning, not an additional prerequisite. +R08 remains abandoned. Runtime and branch rules have not changed in this planning update. + +For every independently runnable change rescoped here, check effective governance +at implementation start. Lean evidence tasks apply after the governance migration +is merged or under an explicit owner-authorized exception; otherwise current +requirements still apply. A proposal does not bypass required checks. This +transition does not add a dependency on the complete R09 runtime rollout. + +Optional preflight keeps core #682 -> modules #431, then core #683 after both +modules #431 and independently delivered C14 #680 -> modules #432 -> core #684/modules #434. +Remove default dependencies C14 #680 <- #431, C15 #417 <- #432, generic skills +core #251 <- #434, and native execution #460 <- #434. Optional adapters #433 require +both #434 and core #253 explicitly. C14/C15 retain their actual producer, layout, +policy/profile/exception and release prerequisites. No full-chain graph or preflight +release blocks the new MEB stories. This section supersedes contradictory historical +ordering prose below; individual affected proposals carry the same scope amendment. + +Core #680/#679 and modules #417 were recovered from uncommitted planning-only feature worktrees into local `dev` and the R09 planning worktrees on 2026-09-20. Remote integration remains pending. All three remain planned; no implementation was imported. Modules C14 #416 shipped and is closed, but its existing proposal remains unarchived on dev; it is distinct from core C14 adoption #680. + +## Risk-first workflow amendment (2026-10-08) + +Extend the existing paired `workflow-01-turn-orchestration` stories, modules [#483](https://github.com/nold-ai/specfact-cli-modules/issues/483) and core [#742](https://github.com/nold-ai/specfact-cli/issues/742), preserving the owner-approved 2026-10-04 optional-context, producer trust/basis, structured-pytest identity and shared-budget amendment. Modules owns reusable guidance/runtime; core owns wrappers/projection. Deliver skills/projection -> deterministic verification -> bounded repair -> opt-in PR automation. Skills do not activate unavailable runtime commands. The signed modules #481 -> #483 -> core #742 runtime prerequisites remain; independent projection proceeds separately. Modules #483 also retains its existing blocking relationship to core #492. + +Use one or two early critical-assumption probes, evidence-based finding triage and compact one-writer repair handoffs within existing gates/budgets. No default panel, pstack installation, ledger, new interfaces or policy cutover is added. Review the initial skills/projection investment after two engineer-days. Bounded trials await the next subsequently authorized nontrivial change per repository; measurements reuse available records and do not establish causal savings. Planning creation completes no implementation/trial task and does not archive either change. + +## Bounded turn workflow (2026-09-20) + +`workflow-01-turn-orchestration` pairs [core #742](https://github.com/nold-ai/specfact-cli/issues/742) with [modules #483](https://github.com/nold-ai/specfact-cli-modules/issues/483). Both are planning/Todo. Core owns repository check-only adoption and canonical wrapper projection; modules owns the reusable runtime, producer adapters, local progress and bounded loops. + +Native release dependencies: modules #481 -> modules #483 -> core #742 runtime adoption. Repository projection and standalone adapter development can proceed independently. R09 #740 retains its own policy cutover; C15-specific consumption retains its own signed policy readiness. Optional preflight, full-chain graph, parked findings taxonomy and generic #251/#253 installation are related, not blanket prerequisites. No workflow receipt becomes mandatory historical delivery evidence or protected CI authority. + +Delivery slices: projection -> signed deterministic pre-validation/verification -> bounded local repair -> opt-in local PR automation. Verification preserves source/index contents and independent gate outcomes. Implementation uses effective governance or an explicit owner-authorized exception. Publish through normal reviewed integration and the canonical signed release path before stable downstream adoption. + +## Recovered planning proposals (2026-09-20) + +Recovered from uncommitted proposal-only feature worktrees into local `dev`; remote integration is still pending. These are planned changes, not active implementations. Optional preflight does not block them; signed runtime, policy/profile and exception prerequisites remain. + +| Change | Issue | Status / prerequisites | +|---|---|---| +| `code-review-14-protected-range-adoption` | [#680](https://github.com/nold-ai/specfact-cli/issues/680) | Planned core consumer; shipped signed C14 producer and runtime/layout compatibility | +| `cli-val-07-code-review-gate-adoption` | [#679](https://github.com/nold-ai/specfact-cli/issues/679) | Planned C15 consumer; core C14, signed modules C15, profile, policy and exceptions | + ## Status snapshot | Bucket | Count | Location | |---|---:|---| -| **Active-tree entries** | 35 | [`openspec/changes/`](changes/) | +| **Active-tree entries** | 48 | [`openspec/changes/`](changes/) | | **Parking-lot entries** | 21 | [`openspec/parking-lot/`](parking-lot/) | | **Archived** | 120 | [`openspec/changes/archive/`](changes/archive/) | @@ -54,6 +147,10 @@ brownfield delivery. The active roadmap should make that thesis stronger: ## Active tracks +### Planning integration prerequisite + +`fix-planning-evidence-multichange` ([#745](https://github.com/nold-ai/specfact-cli/issues/745), PR #746) corrects planning-only review selection in the existing Requirements workflow. It unblocks coordinated planning integration in PR #741 without activating R09 or relaxing implementation proof, trusted authority, or promotion verification. PR #746 merged into dev on 2026-09-21 after its evidence checks passed; integration into planning PR #741 is in progress. + The inventory includes five product tracks and scoped reliability and documentation follow-ups. Tracks can run in parallel; within a track, follow the order column and the explicit dependencies below. @@ -84,7 +181,7 @@ upstream intent-engineering product. | Order | Change | Issue | Positioning | Blocked by | |---:|---|---|---|---| -| 1 | `ai-integration-01-agent-skill` | [#251](https://github.com/nold-ai/specfact-cli/issues/251) | Discover, verify, install, update, uninstall, and canonically export module-owned skills under `.agents/skills`; no workflow-content ownership | signed modules checkpoint/conformance release [#434](https://github.com/nold-ai/specfact-cli-modules/issues/434) | +| 1 | `ai-integration-01-agent-skill` | [#251](https://github.com/nold-ai/specfact-cli/issues/251) | Discover, verify, install, update, uninstall, and canonically export module-owned skills under `.agents/skills`; no workflow-content ownership | bounded module-owned descriptor/asset fixtures; optional #434 assets only after their own release | | 2 | `ai-integration-03-instruction-files` | [#253](https://github.com/nold-ai/specfact-cli/issues/253) | Generate compact AGENTS/OpenSpec/Spec Kit and harness gate references; no validation logic or adapter packaging | ai-integration-01 | | 3 | `ai-integration-02-mcp-server` | [#252](https://github.com/nold-ai/specfact-cli/issues/252) | Later thin adapter with 2-3 validation tools only | CLI pull from ai-integration-01 | @@ -102,7 +199,7 @@ planning workflows. | 3 | `openspec-01-intent-trace` | [#350](https://github.com/nold-ai/specfact-cli/issues/350) | Import-first OpenSpec and Spec Kit requirement evidence with pass/fail gates (rescoped 2026-07-13) | requirements-01/02 | | 4 | `requirements-04-upstream-source-readiness` | [#648](https://github.com/nold-ai/specfact-cli/issues/648) | Reject incomplete or policy-invalid native OpenSpec and Spec Kit sources before requirement normalization | openspec-01; paired modules #346 | | 5 | `requirements-06-evidence-enforcement` | [#657](https://github.com/nold-ai/specfact-cli/issues/657) | Enforce released Requirements evidence reports in staged pre-commit and pull-request delivery gates | released modules #361 fixture | -| 6 | `requirements-07-runtime-proof-delivery` | [#662](https://github.com/nold-ai/specfact-cli/issues/662) | Execute exact scenario selectors and report current-run JUnit evidence independently from historical chronology | corrected modules R07 signed release | +| 6 | `requirements-07-runtime-proof-delivery` | [#662](https://github.com/nold-ai/specfact-cli/issues/662) | Reconcile superseded R07 after replacement delivery; no independent correction pipeline | core #740 and paired modules #481 | | 8 | `architecture-01-solution-layer` | [#240](https://github.com/nold-ai/specfact-cli/issues/240) | Architecture-boundary records and drift validation | requirements input contracts | | Parked | `requirements-03-backlog-sync` | [#244](https://github.com/nold-ai/specfact-cli/issues/244) | Read-first drift evidence; no write-back critical path. Deprioritized 2026-07-13 behind openspec-01 | requirements-02; modules `sync-01` | | Gated | `architecture-02-well-architected-review` | [#524](https://github.com/nold-ai/specfact-cli/issues/524) | Architecture-boundary review findings | architecture-01 shipped plus one usage cycle | @@ -144,15 +241,15 @@ separate authority class. | Order | Change | Issue | Positioning | Blocked by | |---:|---|---|---|---| | 1 | `preflight-01-design-contract-core` | [#682](https://github.com/nold-ai/specfact-cli/issues/682) | Design-contract, validation-result, canonical digest, approval-seal, and side-effect-free verifier interfaces | architecture/governance/traceability/OpenSpec import are upstream inputs, not reowned blockers | -| 2 | `preflight-03-dogfood-hardening-and-release` (core) | [#683](https://github.com/nold-ai/specfact-cli/issues/683) | Identity-bound C14 dogfood evidence and bounded readiness decision | core C14 [#680](https://github.com/nold-ai/specfact-cli/issues/680) | +| 2 | `preflight-03-dogfood-hardening-and-release` (core) | [#683](https://github.com/nold-ai/specfact-cli/issues/683) | Optional identity-bound C14 dogfood and readiness | core C14 [#680](https://github.com/nold-ai/specfact-cli/issues/680) and modules [#431](https://github.com/nold-ai/specfact-cli-modules/issues/431) | | 3 | `preflight-05-implementation-conformance` (core) | [#684](https://github.com/nold-ai/specfact-cli/issues/684) | Worktree/index/range snapshots, sealed-obligation mapping, local checkpoint/final conformance results, findings, authority, and pure verifier | signed modules hardening/publication [#432](https://github.com/nold-ai/specfact-cli-modules/issues/432) | -The cross-repository dependency sequence is: - -`core #682 -> modules #431 -> core C14 #680/#683 -> modules #432 -> core #684 -> modules #434 -> core #251 -> core #253 -> modules #433`. -Modules #432 also unblocks modules C15 #417, which remains upstream of core C15 -issue #679. Native GitHub relationships, not this prose alone, are authoritative -for readiness. +The optional assurance chain is core #682 -> modules #431; core #683 requires +both #431 and independently delivered C14 #680, then #683 -> modules #432 -> +core #684 -> modules #434. Optional adapters #433 require both #434 and +core #253. Generic #251 -> #253 and modules C15 #417 -> core C15 #679 do not depend +on that optional runtime chain. C15 retains its actual producer/profile/policy +prerequisites. Native GitHub relationships must match this scope at readiness. ## Modify queue before implementation @@ -178,16 +275,16 @@ Update each proposal first, then run strict OpenSpec validation. ## Implementation waves -### Preflight assurance sequence - mandatory dependency gate +### Optional preflight assurance sequence 1. Implement core `preflight-01-design-contract-core` [#682](https://github.com/nold-ai/specfact-cli/issues/682). 2. Implement the unpublished modules runtime `preflight-02-assurance-runtime` [#431](https://github.com/nold-ai/specfact-cli-modules/issues/431). -3. Complete core C14 adoption [#680](https://github.com/nold-ai/specfact-cli/issues/680). +3. Independently complete core C14 adoption [#680](https://github.com/nold-ai/specfact-cli/issues/680); it does not wait for preflight. 4. Run the exact preflight loop through core dogfood/readiness [#683](https://github.com/nold-ai/specfact-cli/issues/683). 5. Harden, sign, and publish the stable modules release [#432](https://github.com/nold-ai/specfact-cli-modules/issues/432) from accepted dogfood evidence only. 6. Implement core seal-bound checkpoint/conformance contracts [#684](https://github.com/nold-ai/specfact-cli/issues/684). 7. Implement, dogfood, sign, and publish modules checkpoint/conformance runtime [#434](https://github.com/nold-ai/specfact-cli-modules/issues/434). -8. Use that release for #251 -> #253 -> external adapters #433. Modules C15 #417 -> core C15 #679 may proceed from the stable #432 handoff independently. +8. Optional external adapters #433 consume both that release and independently delivered #253. Generic #251 -> #253 and C15 #417 -> #679 do not wait for #432/#434. Every implementation item starts in its own issue-linked worktree and dedicated session. Proposal-stage preflight review may refine another pending change only @@ -223,7 +320,7 @@ and requires a complete rerun. ### Wave 4 - AI IDE validation loop -- `ai-integration-01-agent-skill` after the signed preflight workflow exists. +- `ai-integration-01-agent-skill` uses bounded module-owned descriptor/asset fixtures; optional preflight assets are integrated only after their own signed release. - `ai-integration-03-instruction-files` after canonical installation/export is released. - Dogfooding slice: run review on a real repo, emit JSON evidence, identify AI-bloat findings, hand remediation packets to an AI IDE, rerun review, and @@ -241,9 +338,9 @@ and requires a complete rerun. `openspec-01`; it blocks the paired modules command/persistence patch. - `architecture-02`, `telemetry-01`, and `ai-integration-02` only after pull from the validation loop exists. -- core/modules `preflight-05` after the signed #432 handoff and before generic - installation/instructions; modules `preflight-04-harness-adapters` only after - #253 and the signed #434 checkpoint/conformance handoff. +- core/modules `preflight-05` after the signed #432 handoff, independently of + generic installation/instructions. Optional modules `preflight-04-harness-adapters` + require both #253 and the signed #434 checkpoint/conformance handoff. ## Wave exit gates @@ -260,7 +357,8 @@ A wave is complete only when all listed criteria are auditable: - **Wave 5**: External planning artifacts are consumed as inputs. They are not positioned as workflows SpecFact replaces. - **Preflight sequence**: Exact C14 dogfood evidence produces a bounded go/no-go - decision; stable publication and downstream installation stay blocked on no-go. + decision; no-go blocks that optional preflight publication and its assets/adapters, + never generic #251/#253 delivery, C14/C15 or native review. ## Ownership authority @@ -307,3 +405,40 @@ repo root. Do not move folders manually. - [`INTEGRATION.md`](INTEGRATION.md) - cross-change ownership contract - [`config.yaml`](config.yaml) - repo-wide OpenSpec rules and context - [`specfact-cli-modules/openspec/CHANGE_ORDER.md`](https://github.com/nold-ai/specfact-cli-modules/blob/main/openspec/CHANGE_ORDER.md) - module-side companion plan + +## Lean evidence delivery + +| Change | Issue | Dependency | +|---|---|---| +| `requirements-09-minimal-evidence` | [#740](https://github.com/nold-ai/specfact-cli/issues/740) | signed modules #481 for runtime adoption | + +## Security stack gate readiness (2026-10-11, Europe/Berlin) + +The owner resumed #747 to resolve stack #757 gates without bypasses. +Implement `security-stack-gate-readiness` as a reviewed prerequisite before +rebasing the approved five-layer `dependency-update-consolidation` slice. +Keep broader dependency work deferred and #747 open. + +## Planning stack proof boundary (2026-10-11, Europe/Berlin) + +Issue #760 / `requirements-planning-stack-boundary` is a protective prerequisite for stack #759. +Implement and review it on protected dev before restacking #719/#743/#751 and regenerating native proof. +Keep implemented root evidence and deferred planning maturity separate, with unchanged protected proof +workflow/local action bytes and independent exact-head member authority. Runtime, tests, delivery and +inherited source changes retain the full existing proof path. Owner mapping/grant/integration approval is +required for this separate prerequisite; stack membership does not infer it. The three stack PRs remain +unmerged, and future module-side work remains deferred under verified #481/#483 readiness. + +## Native planning consumer lineage correction (2026-10-12) + +Existing issue #760 / `fix-native-planning-lineage` corrects the independent execution preflight discovered +after merged #761. Preserve protected-base-to-review and planning-parent-to-candidate ancestry separately. +Reviewed protected integration precedes stack #759 restacking. No required proof, authority, review or +signature gate changes meaning. Leave #719/#743/#751 unmerged. + +### Protected planning review context follow-up + +Issue #760 / `fix-planning-review-proof-context` follows merged #767. Real #743 run 38303860784 +rejects planning receipts at the immutable finalized Code Review boundary. Preserve full protected-dev +review scope and supply independently authenticated genuine root final proof. Protected integration, +fresh root RED/GREEN and sequential planning-child validation remain prerequisites; no gate is weakened. diff --git a/openspec/INTEGRATION.md b/openspec/INTEGRATION.md index efe84f3d8..b4c5244e9 100644 --- a/openspec/INTEGRATION.md +++ b/openspec/INTEGRATION.md @@ -4,6 +4,15 @@ This document is the living ownership contract for active SpecFact OpenSpec changes. It exists to keep parallel roadmap work aligned without creating a separate product feature for integration governance. +## Ordinary workflow ownership + +- Paired `workflow-01-turn-orchestration` assigns reusable `specfact workflow` execution, bundled workflow skills, producer adapters and disposable local receipts to modules #483; core #742 owns repository settings, check-only gate adapters, thin wrapper projection and signed adoption/pilot. +- This unsealed ordinary path does not replace optional preflight/checkpoint/conformance. Seal-bound behavior stays with its existing owners and applies only on explicit assurance selection. +- R09 owns current-run Requirements claims; its module reconciler remains free of Git/test/network execution. Governance owns durable evidence envelopes, traceability owns artifact semantics, and C15 owns calibrated review policy. A workflow findings projection preserves original producer identities/outcomes and cannot fuse verdicts or silently extend ReviewReport. +- Standalone verify and ordinary CI require no prior local receipts. Local progress cannot authenticate itself for protected CI. Generic #251/#253 still owns customer installation/export and general generated references; repository wrapper projection is a separate development convenience. +- Release direction is modules #481 -> workflow modules #483 -> core #742 runtime adoption. Independent projection has no signed-runtime prerequisite. Related optional assurance/graph/findings work is not a blanket blocker. +- Runtime integration/adoption validates exact signed #481 schema-v3 current_execution and actual core/#483 compatibility before selecting the workflow combination; a signed workflow alone is insufficient. Projection remains independent. + ## Positioning SpecFact is the local validation and AI-bloat defense CLI. Active changes SHALL @@ -80,9 +89,11 @@ requires a new snapshot, full validation rerun, and explicit approval. Normal implementation snapshots remain separate evidence against that unchanged seal. General AGENTS.md, OpenSpec, Spec Kit, and command-harness instructions MUST -contain only the compact gate and installed workflow reference. The signed -module skill is the canonical workflow source; Python validators are the -canonical determinate checks. +include the compact preflight gate and installed preflight workflow reference +only when the assurance policy explicitly selects that installed capability. +Ordinary instructions retain applicable validation guidance without a preflight +prerequisite. Within selected preflight, the signed module skill is the canonical +workflow source and Python validators are the canonical determinate checks. ## Wave Gates diff --git a/openspec/changes/ai-integration-01-agent-skill/CHANGE_VALIDATION.md b/openspec/changes/ai-integration-01-agent-skill/CHANGE_VALIDATION.md index 1898a4419..89e8da66b 100644 --- a/openspec/changes/ai-integration-01-agent-skill/CHANGE_VALIDATION.md +++ b/openspec/changes/ai-integration-01-agent-skill/CHANGE_VALIDATION.md @@ -18,11 +18,19 @@ ## Dependency Review - Parent Feature: core [#372](https://github.com/nold-ai/specfact-cli/issues/372). -- Native blocker to be updated: signed modules checkpoint/conformance handoff [#434](https://github.com/nold-ai/specfact-cli-modules/issues/434). +- Generic discovery/install/export has no native #434 blocker. Optional checkpoint/conformance assets from [#434](https://github.com/nold-ai/specfact-cli-modules/issues/434) are integrated only after their signed publication. - Native downstream verified: core [#253](https://github.com/nold-ai/specfact-cli/issues/253). - GitHub readback verified the retained User Story parent #372, project `SpecFact CLI` / `Todo`, assignee `djm81`, and the required labels. -## Validation Record +## Current planning validation — 2026-09-20 + +Strict OpenSpec validation and scoped Markdown checks pass for the amended plan. +Proposal, design, tasks and this dependency review agree: #434 does not block +ordinary skill distribution; its optional assets require their signed publication. +This is planning validation, not implementation readiness or CI-gate success; +current Requirements/authority CI failures remain recorded on PR #741. + +## Historical validation — 2026-08-25 - `openspec status --change ai-integration-01-agent-skill --json`: PASS on 2026-08-25; all required proposal artifacts reported complete. - `openspec validate ai-integration-01-agent-skill --strict`: PASS on 2026-08-25. diff --git a/openspec/changes/ai-integration-01-agent-skill/design.md b/openspec/changes/ai-integration-01-agent-skill/design.md index cc51aadb0..a07296d2a 100644 --- a/openspec/changes/ai-integration-01-agent-skill/design.md +++ b/openspec/changes/ai-integration-01-agent-skill/design.md @@ -1,5 +1,11 @@ ## Context +## Scope rescope — 2026-09-20 + +Remove the prerequisite on modules #434 for generic skill discovery/install/export. Validate against a bounded module-owned fixture and signed assets where used. Install optional preflight/checkpoint assets only once their own signed release exists. Keep safe writes, identity, collision handling, and inventory; this issue does not author assurance workflow content. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + The original proposal mixed validation workflow content, sub-skills, and installation. The preflight architecture makes that ownership ambiguous: a signed module must own the canonical workflow it implements, while core can safely own the reusable distribution mechanism. This rescope retains issue #251 and its current Feature hierarchy while removing content ownership. ## Goals / Non-Goals @@ -35,9 +41,12 @@ Core records exactly which files it installed and their digests. Reinstall is id Two modules cannot silently claim the same canonical skill ID. Untrusted, unsigned where policy requires signing, incompatible, or digest-mismatched assets are not installed. Diagnostics identify the competing identities and remediation options. -### 5. First consumer follows checkpoint/conformance publication +### 5. Generic installation is independently testable -The signed modules #434 identity supplies `specfact-preflight` and the bounded implementation-check workflow. #251 discovers and exports them unchanged. This proves the generic mechanism but does not couple the installer to preflight or checkpoint semantics. +Use a bounded module-owned descriptor/asset fixture, signed where required, +to verify discovery and byte-preserving export. No preflight release blocks +the generic mechanism. Once published, optional #434 assets can use the same +contract unchanged; the installer does not own their workflow semantics. ## Risks / Trade-offs diff --git a/openspec/changes/ai-integration-01-agent-skill/proposal.md b/openspec/changes/ai-integration-01-agent-skill/proposal.md index 52a459792..d35ef539e 100644 --- a/openspec/changes/ai-integration-01-agent-skill/proposal.md +++ b/openspec/changes/ai-integration-01-agent-skill/proposal.md @@ -1,5 +1,11 @@ # Change: Shared Module-Owned Skill Discovery, Installation, and Export +## Scope rescope — 2026-09-20 + +Remove the prerequisite on modules #434 for generic skill discovery/install/export. Validate against a bounded module-owned fixture and signed assets where used. Install optional preflight/checkpoint assets only once their own signed release exists. Keep safe writes, identity, collision handling, and inventory; this issue does not author assurance workflow content. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + ## Why SpecFact modules can own focused agent workflows, but users need one core installation and export surface that discovers those assets, verifies their identity, and materializes them in a portable canonical layout. Without a shared distributor, each module or harness would copy skill content and drift independently. @@ -32,7 +38,7 @@ SpecFact modules can own focused agent workflows, but users need one core instal ## Dependencies - Retains parent Feature [#372](https://github.com/nold-ai/specfact-cli/issues/372), under Epic [#257](https://github.com/nold-ai/specfact-cli/issues/257). -- Blocked by the signed modules `preflight-05-implementation-conformance` handoff so the first installed identity includes both preflight and seal-bound implementation-check workflows. +- Generic installation has no dependency on modules #434. Validate the descriptor and safe installation contract with bounded module-owned fixtures and signed assets where required. Optional preflight/conformance assets are consumed only after their own signed publication. - Blocks `ai-integration-03-instruction-files` [#253](https://github.com/nold-ai/specfact-cli/issues/253). ## Explicit Non-Goals diff --git a/openspec/changes/ai-integration-01-agent-skill/specs/agent-skill-spec-intelligence/spec.md b/openspec/changes/ai-integration-01-agent-skill/specs/agent-skill-spec-intelligence/spec.md index 0a8e86229..517349b40 100644 --- a/openspec/changes/ai-integration-01-agent-skill/specs/agent-skill-spec-intelligence/spec.md +++ b/openspec/changes/ai-integration-01-agent-skill/specs/agent-skill-spec-intelligence/spec.md @@ -1,3 +1,9 @@ +## Scope rescope — 2026-09-20 + +Remove the prerequisite on modules #434 for generic skill discovery/install/export. Validate against a bounded module-owned fixture and signed assets where used. Install optional preflight/checkpoint assets only once their own signed release exists. Keep safe writes, identity, collision handling, and inventory; this issue does not author assurance workflow content. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + ## ADDED Requirements ### Requirement: Discover module-owned skills diff --git a/openspec/changes/ai-integration-01-agent-skill/tasks.md b/openspec/changes/ai-integration-01-agent-skill/tasks.md index b03acd57d..71c14abbf 100644 --- a/openspec/changes/ai-integration-01-agent-skill/tasks.md +++ b/openspec/changes/ai-integration-01-agent-skill/tasks.md @@ -1,24 +1,33 @@ # Tasks: ai-integration-01-agent-skill +## Scope rescope — 2026-09-20 + +Remove the prerequisite on modules #434 for generic skill discovery/install/export. Validate against a bounded module-owned fixture and signed assets where used. Install optional preflight/checkpoint assets only once their own signed release exists. Keep safe writes, identity, collision handling, and inventory; this issue does not author assurance workflow content. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + +Implementation discipline: use focused regression/reproduction and current-run results. Any task requiring immutable hosted RED, frozen test authoring or approval receipts merely to implement this feature is superseded; tests of an explicitly selected optional seal feature remain in scope. + All tasks below are future implementation work. This rescope completes none of them and creates no skill/export or `TDD_EVIDENCE.md`. ## 1. Dedicated session, worktree, and readiness - [ ] 1.1 In a dedicated issue-linked session, create `feature/ai-integration-01-agent-skill` from current `origin/dev` in a new core worktree before any implementation edit. -- [ ] 1.2 Refresh hierarchy metadata and verify #251 retains parent #372, complete labels/project/assignee, the signed modules #434 blocker, and no concurrent `In Progress` owner. -- [ ] 1.3 Verify the released module skill descriptor and core module discovery/safe-write contracts against current repository reality. +- [ ] 1.2 Refresh hierarchy metadata and verify #251 retains parent #372, complete labels/project/assignee, absence of a generic modules #434 prerequisite, and no concurrent `In Progress` owner. +- [ ] 1.3 Verify the bounded module-owned descriptor/asset fixture and core module discovery/safe-write contracts against current repository reality. +- [ ] 1.4 Check effective contributor governance at session start. The lean evidence task below applies after its governance migration or an explicit owner-authorized exception; otherwise follow current evidence requirements. Do not block this independent feature on the complete R09 runtime rollout. ## 2. Specification and failing-first evidence - [ ] 2.1 Finalize descriptor, discovery, trust, canonical `.agents/skills`, inventory, collision, update, and uninstall deltas without adding workflow content or adapters. - [ ] 2.2 Add tests mapped to all discovery/export/idempotency/drift/collision/uninstall scenarios. -- [ ] 2.3 Run targeted tests before production edits and record failing-first results in a newly created `TDD_EVIDENCE.md`. +- [ ] 2.3 Run relevant regression tests before production edits, summarize meaningful failures briefly, and reference ordinary CI artifacts where available; no authored TDD ledger is required. ## 3. Minimal distribution implementation - [ ] 3.1 Implement module-owned skill discovery and integrity/compatibility validation. - [ ] 3.2 Implement canonical `.agents/skills` installation/export and inventory-backed update/uninstall. -- [ ] 3.3 Verify `specfact-preflight` and the implementation-check workflow pass through from the signed module unchanged; do not add validator, checkpoint, or workflow semantics to core. +- [ ] 3.3 Verify fixture assets pass through unchanged, using signed assets where policy requires; add optional preflight/conformance assets only after their own signed publication; do not add validator, checkpoint, or workflow semantics to core. ## 4. Passing evidence and quality gates @@ -30,5 +39,5 @@ All tasks below are future implementation work. This rescope completes none of t ## 5. Delivery and post-merge cleanup - [ ] 5.1 Hand the exact installation/export contract to #253 and keep external adapter packaging downstream. -- [ ] 5.2 Open the implementation PR to `dev` as the final pre-merge task, linking #251 and the signed module dependency. +- [ ] 5.2 Open the implementation PR to `dev` as the final pre-merge task, linking #251 and the verified descriptor/asset contract. - [ ] 5.3 After merge, run `openspec archive ai-integration-01-agent-skill`, update ordering/wiki source state, and remove the dedicated worktree and merged branch. diff --git a/openspec/changes/ai-integration-03-instruction-files/design.md b/openspec/changes/ai-integration-03-instruction-files/design.md index 0e9be21a4..d0eae7b9e 100644 --- a/openspec/changes/ai-integration-03-instruction-files/design.md +++ b/openspec/changes/ai-integration-03-instruction-files/design.md @@ -1,5 +1,11 @@ ## Context +## Scope rescope — 2026-09-20 + +Generate lean validation references by default from the installed inventory. Emit approved-seal, preflight, checkpoint, or stop-on-stale instructions only for an explicitly selected assurance policy and installed optional capability. Do not inject those gates into every AGENTS/OpenSpec/Spec Kit workflow. Keep #251 as the prerequisite. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + The original change mixed platform setup with validation/remediation workflow content. The new boundary makes instruction files lightweight routing surfaces: the signed module owns workflow semantics, #251 owns verified installation/export, and #253 emits only managed references and gates. Research reviewed on 2026-08-25: @@ -27,7 +33,10 @@ Research reviewed on 2026-08-25: ### 1. Minimal gate contract -Every generated preflight reference conveys five rules: +Ordinary generated instructions select/validate the intended change and reference +relevant current tests and installed validation commands. They require neither +preflight nor an approved seal. Only an explicitly selected assurance policy +with the installed optional capability emits these additional rules: 1. select and validate the intended change before implementation; 2. invoke the installed canonical preflight workflow in the harness-native form; @@ -35,7 +44,7 @@ Every generated preflight reference conveys five rules: 4. stop on blocked, unknown, stale, ambiguous, or concurrent-work results; 5. return material refinements to the owning artifact and rerun before implementation. -Detailed phases, CLI flags, evidence interpretation, and refinement dialogue remain in the module-owned skill. +Detailed phases, CLI flags, evidence interpretation, and refinement dialogue remain in the module-owned skill. If explicitly selected policy requires an unavailable capability, report setup guidance instead of silently downgrading that policy. An absent optional capability does not block ordinary generation. ### 2. Managed sections, not full-file ownership @@ -43,11 +52,11 @@ Generated instructions use stable start/end markers and an inventory containing ### 3. OpenSpec ordering reference -For OpenSpec projects, the generated section places `specfact-preflight` after proposal/spec/design/tasks are ready and strict validation succeeds, and before `/opsx:apply`, `/openspec:apply`, or equivalent implementation. It does not modify OpenSpec's own generated command files. +When explicit assurance policy requires it, the generated OpenSpec section places `specfact-preflight` after proposal/spec/design/tasks are ready and strict validation succeeds, and before `/opsx:apply`, `/openspec:apply`, or equivalent implementation. It does not modify OpenSpec's own generated command files. ### 4. Spec Kit extension compatibility -For Spec Kit, generated content integrates through the enabled `agent-context` extension or another explicitly owned project section. The base Specify CLI is not assumed to manage AGENTS.md. The reference places preflight after the relevant clarify/checklist/analyze loop and before `/speckit.implement` or its harness-native equivalent. +For Spec Kit, generated content integrates through the enabled `agent-context` extension or another explicitly owned project section. The base Specify CLI is not assumed to manage AGENTS.md. Only explicit assurance policy adds preflight after the relevant clarify/checklist/analyze loop and before `/speckit.implement` or its harness-native equivalent. Ordinary instructions preserve upstream planning order without a seal gate. ### 5. Harness invocation comes from installed metadata diff --git a/openspec/changes/ai-integration-03-instruction-files/proposal.md b/openspec/changes/ai-integration-03-instruction-files/proposal.md index 1f308d951..599eb1c2c 100644 --- a/openspec/changes/ai-integration-03-instruction-files/proposal.md +++ b/openspec/changes/ai-integration-03-instruction-files/proposal.md @@ -1,5 +1,11 @@ # Change: Generated AGENTS, OpenSpec, Spec Kit, and Harness Instructions +## Scope rescope — 2026-09-20 + +Generate lean validation references by default from the installed inventory. Emit approved-seal, preflight, checkpoint, or stop-on-stale instructions only for an explicitly selected assurance policy and installed optional capability. Do not inject those gates into every AGENTS/OpenSpec/Spec Kit workflow. Keep #251 as the prerequisite. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + ## Why Once module-owned skills can be installed canonically, repositories still need small, deterministic instruction references that tell agents when the workflow is mandatory and how to invoke it in the active harness. Those references must preserve upstream OpenSpec/Spec Kit ownership and avoid copying the preflight loop or validator rules into every instruction file. @@ -7,10 +13,10 @@ Once module-owned skills can be installed canonically, repositories still need s ## What Changes - **NEW**: Generate bounded, idempotent managed sections for root AGENTS.md and supported harness instruction files. -- **NEW**: Generate OpenSpec-aware instructions that place preflight after proposal artifacts are ready and before any apply/implementation command. -- **NEW**: Generate Spec Kit-aware instructions that place preflight after clarification/plan/tasks/analyze quality work and before implementation, while respecting Spec Kit's opt-in agent-context ownership. +- **NEW**: Generate OpenSpec-aware change selection and validation references; include preflight before apply only when explicitly selected policy requires the installed optional workflow. +- **NEW**: Generate Spec Kit-aware planning/validation references, respecting opt-in agent-context ownership. Place preflight before implementation only under an explicitly selected assurance policy. - **NEW**: Resolve the installed canonical skill and emit the harness-native invocation reference plus stop conditions. -- **CLARIFY**: Instructions state the gate only: run preflight, require a current approved seal, stop on blocked/unknown/stale results, and obtain user approval for material refinement. +- **CLARIFY**: Ordinary instructions select/validate the change and reference current tests. Only explicitly selected assurance policy adds preflight, approved-seal, stale/unknown stop and refinement approval requirements; missing selected capabilities produce setup diagnostics. - **EXCLUDE**: Validation logic, canonical workflow content, skill installation, and Codex/ECC/hatch3r adapter packaging remain separately owned. ## Capabilities diff --git a/openspec/changes/ai-integration-03-instruction-files/specs/cross-platform-instructions/spec.md b/openspec/changes/ai-integration-03-instruction-files/specs/cross-platform-instructions/spec.md index f297eb9ea..2fbdf3206 100644 --- a/openspec/changes/ai-integration-03-instruction-files/specs/cross-platform-instructions/spec.md +++ b/openspec/changes/ai-integration-03-instruction-files/specs/cross-platform-instructions/spec.md @@ -1,12 +1,36 @@ +## Scope rescope — 2026-09-20 + +Generate lean validation references by default from the installed inventory. Emit approved-seal, preflight, checkpoint, or stop-on-stale instructions only for an explicitly selected assurance policy and installed optional capability. Do not inject those gates into every AGENTS/OpenSpec/Spec Kit workflow. Keep #251 as the prerequisite. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + ## ADDED Requirements ### Requirement: Compact preflight gate instructions -Generated instruction sections SHALL reference the installed canonical preflight workflow and SHALL state selection, approval, stale/unknown/blocking stop, owner-authorized refinement, and rerun requirements without embedding validator logic. +Generated sections SHALL select/validate the intended change and reference +current tests and available installed validation commands. They SHALL add +preflight, approved-seal, stale/unknown/blocking stops, owner-authorized +refinement and rerun requirements only when explicit assurance policy selects +the installed optional capability. They SHALL NOT embed validator logic. + +#### Scenario: Ordinary generation needs no optional assurance capability + +- **GIVEN** ordinary MEB policy and no preflight installation +- **WHEN** instructions are generated +- **THEN** they reference change validation and current tests without a seal gate +- **AND** the missing optional capability does not block generation. + +#### Scenario: Selected assurance capability is unavailable + +- **GIVEN** an explicit policy requires preflight and its capability is not installed +- **WHEN** instruction generation is requested +- **THEN** it reports the missing capability and setup path +- **AND** it does not silently downgrade the selected assurance policy. #### Scenario: AGENTS.md section is generated -- **GIVEN** a verified `specfact-preflight` skill installation and a supported AGENTS.md target +- **GIVEN** explicit assurance policy, a verified `specfact-preflight` installation and a supported AGENTS.md target - **WHEN** instruction generation is approved - **THEN** the managed section references the verified harness-native invocation - **AND** the section does not copy the full skill or Python validation rules. @@ -31,22 +55,22 @@ Instruction generation SHALL update only a bounded owned section identified by s ### Requirement: OpenSpec pre-apply ordering -For an OpenSpec project, generated instructions SHALL require proposal artifacts and strict validation followed by the installed preflight workflow before any apply/implementation command. +For an OpenSpec project, instructions SHALL require proposal artifacts and strict validation before implementation. Only explicitly selected assurance policy SHALL add the installed preflight workflow and seal approval before apply. #### Scenario: Agent prepares to run OpenSpec apply -- **GIVEN** proposal, specs, design, and tasks are ready +- **GIVEN** proposal, specs, design and tasks are ready and explicit assurance policy selects installed preflight - **WHEN** the agent reaches `/opsx:apply`, `/openspec:apply`, or the harness-equivalent implementation step - **THEN** it first requires a current approved preflight seal - **AND** blocked, unknown, stale, or unapproved results return to the owning planning artifact. ### Requirement: Spec Kit pre-implement ordering -For a Spec Kit project, generated instructions SHALL place the installed preflight workflow after the applicable specify/clarify/plan/checklist/tasks/analyze quality loop and before implementation. +For a Spec Kit project, instructions SHALL preserve the applicable specify/clarify/plan/checklist/tasks/analyze quality loop. Only explicitly selected assurance policy SHALL add installed preflight and approval before implementation. #### Scenario: Agent prepares to run Spec Kit implement -- **GIVEN** the selected feature has completed its required planning and analysis commands +- **GIVEN** explicit assurance policy selects installed preflight and the feature has completed its required planning and analysis commands - **WHEN** the agent reaches `/speckit.implement` or the harness-equivalent form - **THEN** it first requires current preflight approval - **AND** findings route back to specify/clarify, plan, or tasks according to the owning artifact. diff --git a/openspec/changes/ai-integration-03-instruction-files/tasks.md b/openspec/changes/ai-integration-03-instruction-files/tasks.md index 9e2365083..66785f7f7 100644 --- a/openspec/changes/ai-integration-03-instruction-files/tasks.md +++ b/openspec/changes/ai-integration-03-instruction-files/tasks.md @@ -1,5 +1,13 @@ # Tasks: ai-integration-03-instruction-files +## Scope rescope — 2026-09-20 + +Generate lean validation references by default from the installed inventory. Emit approved-seal, preflight, checkpoint, or stop-on-stale instructions only for an explicitly selected assurance policy and installed optional capability. Do not inject those gates into every AGENTS/OpenSpec/Spec Kit workflow. Keep #251 as the prerequisite. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + +Implementation discipline: use focused regression/reproduction and current-run results. Any task requiring immutable hosted RED, frozen test authoring or approval receipts merely to implement this feature is superseded; tests of an explicitly selected optional seal feature remain in scope. + All tasks below are future implementation work. This rescope completes none of them and creates no generated instruction or `TDD_EVIDENCE.md`. ## 1. Dedicated session, worktree, and readiness @@ -11,14 +19,14 @@ All tasks below are future implementation work. This rescope completes none of t ## 2. Specification and failing-first evidence - [ ] 2.1 Finalize gate fields, managed markers, inventories, OpenSpec ordering, Spec Kit extension compatibility, and invocation resolution without validator or adapter packaging scope. -- [ ] 2.2 Add tests mapped to idempotency, malformed markers, user-content preservation, OpenSpec/Spec Kit ordering, and harness-native invocation fixtures. -- [ ] 2.3 Run targeted tests before production edits and record failing-first results in a newly created `TDD_EVIDENCE.md`. +- [ ] 2.2 Add tests mapped to idempotency, malformed markers, user-content preservation, OpenSpec/Spec Kit ordering, harness-native invocation, ordinary no-seal defaults, selected assurance policy, and missing-selected-capability diagnostics. +- [ ] 2.3 Run relevant regression tests before production edits, summarize meaningful failures briefly, and reference ordinary CI artifacts where available; no authored TDD ledger is required. ## 3. Minimal instruction implementation - [ ] 3.1 Implement previewable managed-section generation for AGENTS.md and the approved core-owned target files. - [ ] 3.2 Implement installed-metadata invocation resolution and safe inventory-backed update/removal. -- [ ] 3.3 Implement OpenSpec and Spec Kit ordering references while respecting each upstream tool's context ownership. +- [ ] 3.3 Implement OpenSpec and Spec Kit ordering references while respecting upstream context ownership; emit assurance gates only for explicit policy and installed optional capability. - [ ] 3.4 Keep detailed preflight workflow content, validators, and external adapter packages out of core. ## 4. Passing evidence and quality gates diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/CHANGE_VALIDATION.md b/openspec/changes/cli-val-07-code-review-gate-adoption/CHANGE_VALIDATION.md new file mode 100644 index 000000000..bd91cc6ab --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/CHANGE_VALIDATION.md @@ -0,0 +1,30 @@ +## Recovery review — 2026-09-20 + +Recovered from uncommitted planning files on `feature/cli-val-07-code-review-gate-adoption`; the source worktree contains only this proposal and its change-order edit. Imported into local `dev` and the R09 planning worktree. No runtime changes were imported. Historical readiness and version identities below are dated context and must be revalidated before implementation. Recovery validation preserves existing staged-review scenarios and classifies previously nonexistent requirement headers as ADDED, avoiding invalid archive replacements. Canonical C14 follow-up reconciliation remains a prerequisite to final C15 specification promotion. + +# Change Validation + +## Repository reality + +- Current core consumes legacy code-review reports and has no released C14 + protected schema 1.6 verifier on `dev`. +- No signed C15 schema 1.7 module release exists. +- Therefore production consumer tests/code cannot truthfully be implemented or + validated against the planned interface yet. + +## Sequential specification promotion + +C15 modifies the existing pre-commit requirement and the protected-range and +green-check requirements introduced by C14. Apply these replacements only after prerequisite +C14 integration and native archival; do not archive C15 against today's missing +protected-range capability. The schema 1.7-only blocking policy then replaces, +rather than coexists with, C14's schema 1.6 acceptance rule. + +## Decision + +Validate and review the OpenSpec plan now. Keep production tests/code blocked +until the signed upstream contracts and public readiness tasks are complete. + +## Planning review follow-up — 2026-09-20 + +Reopened implementation-time worktree readiness and ordered reviewed integration/authenticated verifier publication before protected C15 activation; strict OpenSpec and scoped Markdown/whitespace pass. Existing canonical C14-predecessor archive notice remains expected until its native integration/archival. diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/TDD_EVIDENCE.md b/openspec/changes/cli-val-07-code-review-gate-adoption/TDD_EVIDENCE.md new file mode 100644 index 000000000..8590750e6 --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/TDD_EVIDENCE.md @@ -0,0 +1,3 @@ +# Validation status: cli-val-07-code-review-gate-adoption + +Recovered planning only on 2026-09-20; implementation has not started and no test result is claimed. Future implementation uses a short validation summary and existing CI artifacts under #740/#481. This inherited placeholder creates no committed transcript, hosted RED or frozen-checkpoint obligation. diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/design.md b/openspec/changes/cli-val-07-code-review-gate-adoption/design.md new file mode 100644 index 000000000..40854bbeb --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/design.md @@ -0,0 +1,47 @@ +# Design: Schema 1.7 Code Review Gate Adoption + +## Scope rescope — 2026-09-20 + +Keep the signed C14/C15, profile, policy and exception-authority prerequisites. Optional preflight is not an indirect prerequisite. Use meaningful regression reproduction, current test results and independent review; no immutable RED history or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## Trust Boundary + +The modules producer owns analyzer execution and code-review policy application. +Core owns local helper consumption and the protected workflow verifier. The +protected consumer accepts only the signed module/schema/profile identities +approved by the paired change and authenticates exceptions from trusted base +policy. It never treats candidate files or a report boolean as trust authority. + +## Local Pre-Commit + +The helper requests explicit changed enforcement and bug-hunt timeouts. It +loads the JSON report into a strict schema-aware projection, validates the +closed status/exit matrix, prints the evidence summary, and returns +`ci_exit_code`. Subprocess failure, missing report, invalid schema, contradictory +status/exit, or unsupported enforcing schema fails closed. + +For one migration release, schema 1.6 may be read only when effective mode is +shadow. The legacy error-count fallback is removed rather than retained as an +alternate authority. + +## Protected CI + +Extend the C14 verifier after that implementation exists. It requires schema +1.7, the approved signed module and calibration-profile digest, the C14 +verification envelope, and complete waiver evidence. A waived error is accepted +only when the verifier independently matches the directive to a trusted-base, +active governance exception for the same canonical rule/path/symbol. + +## Rollout + +1. Consume the released signed C15 module in candidate shadow tests and prove the consumer matrix, retaining existing protected enforcement. +2. Open and integrate the reviewed consumer implementation before deployment; publish/authenticate the integrated verifier through the normal trusted-source promotion path, including main promotion if required. +3. Run trusted-source shadow dogfood against that authenticated verifier. Candidate tests do not supply protected authority. +4. Activate changed local and protected range/full enforcement only after the modules measurement gate and trusted consumer evidence pass. +5. Roll back by returning to shadow; preserve the validated consumer and report. + +## Local reset coordination (2026-10-16) + +Planned ordinary local adoption is tracked separately by [core #769](https://github.com/nold-ai/specfact-cli/issues/769), after modules #515 and before modules #516. Its schema 2.0 trusted-local evidence cannot satisfy protected core #680 capsule acceptance. This additive note does not change this change's owners, status, tasks, or acceptance. C15 core #679 and modules #417 retain their own contracts. Older capsule references remain relevant to their separate protected contract, not ordinary local reset scope. diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/proposal.md b/openspec/changes/cli-val-07-code-review-gate-adoption/proposal.md new file mode 100644 index 000000000..6495cf621 --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/proposal.md @@ -0,0 +1,91 @@ +# Change: Adopt the Signal-Calibrated Code Review Gate + +## Scope rescope — 2026-09-20 + +Keep the signed C14/C15, profile, policy and exception-authority prerequisites. Optional preflight is not an indirect prerequisite. Use meaningful regression reproduction, current test results and independent review; no immutable RED history or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## Why + +The core pre-commit helper currently counts error findings itself and then +returns the nested process status. A report can therefore contain errors while +the helper returns zero when the producer's status and severity diverge. Core +also needs an authoritative schema 1.7 consumer before protected CI can trust +the modules-side calibrated gate and approved exception evidence. + +## Dependency and Readiness Status + +This change is paired with modules change +`code-review-15-signal-calibrated-blocking-gate`. It is downstream of the signed +C14 module release and C14 protected core adoption, then the signed C15 module +release. Production implementation SHALL NOT consume a feature-branch module +or infer schema 1.7 fields from older reports. + +The identifier uses `cli-val-07` because `cli-val-06` is already assigned to the +parked Copilot test-generation change. + +## What Changes + +- **NEW**: Core-owned schema 1.7 consumer contract for assurance status, + authoritative exit code, signed policy/profile identity, occurrence evidence, + and authenticated waiver evidence. +- **CHANGED**: `scripts/pre_commit_code_review.py` invokes review with explicit + `--enforcement changed --bug-hunt` and returns the validated report exit code. +- **CHANGED**: Contradictory or malformed schema 1.7 status/exit fields fail + closed instead of falling back to severity counting or subprocess status. +- **NEW**: Protected CI requires the approved signed module/schema/profile and + trusted-base exception identity before accepting a waived error. +- **COMPATIBILITY**: Schema 1.6 remains readable only in shadow compatibility + mode for one migration release; it cannot satisfy protected blocking policy. + +## Capabilities + +### New Capabilities + +- `code-review-gate-consumer`: authoritative local and protected consumption of + schema 1.7 code-review evidence. + +### Modified Capabilities + +- `pre-commit-review-gate`: replace C14's schema 1.6 blocking contract with the + authoritative schema 1.7 status/exit contract; preserve staged-only authority. +- `protected-code-review-range-assurance`: advance C14's accepted producer + schema to 1.7 without weakening its independent range and trust verification. +- `trustworthy-green-checks`: code-review green status is derived from validated + report truth rather than a fallback severity count. +- `ci-integration`: protected review enforcement pins the signed module, schema, + policy, and exception authority. + +## Impact + +- **Code**: pre-commit code-review helper and the C14 protected consumer/workflow + integration point after it exists. +- **Tests**: unit consumer matrix plus protected-CI acceptance/rejection fixtures. +- **Compatibility**: enforcing consumers require schema 1.7; shadow mode can + read 1.6 during the migration release. +- **Documentation**: contributor and CI-gate guidance on authoritative status, + approved waivers, rollout, and rollback. +- **Rollback**: set code-review policy to shadow while retaining schema 1.7 + evidence; do not restore severity-count fallback. + +## Non-Goals + +- Core does not implement analyzer rules, severity calibration, source + suppression parsing, finding deduplication, or module-side policy execution. +- Core does not replace C14 protected range verification. +- Candidate policy or exception files never become approval authority. + +## Source Tracking + + +- **GitHub Issue**: [#679](https://github.com/nold-ai/specfact-cli/issues/679) +- **Issue URL**: +- **Repository**: nold-ai/specfact-cli +- **Last Synced Status**: GitHub metadata verified; blocked on signed prerequisite releases +- **Parent Feature**: [#375](https://github.com/nold-ai/specfact-cli/issues/375) +- **Paired Modules Change**: [nold-ai/specfact-cli-modules#417](https://github.com/nold-ai/specfact-cli-modules/issues/417) / `code-review-15-signal-calibrated-blocking-gate` + +## Local reset coordination (2026-10-16) + +Planned ordinary local adoption is tracked separately by [core #769](https://github.com/nold-ai/specfact-cli/issues/769), after modules #515 and before modules #516. Its schema 2.0 trusted-local evidence cannot satisfy protected core #680 capsule acceptance. This additive note does not change this change's owners, status, tasks, or acceptance. C15 core #679 and modules #417 retain their own contracts. Older capsule references remain relevant to their separate protected contract, not ordinary local reset scope. diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/requirements-evidence.yaml b/openspec/changes/cli-val-07-code-review-gate-adoption/requirements-evidence.yaml new file mode 100644 index 000000000..d6dc9fdfb --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/requirements-evidence.yaml @@ -0,0 +1,86 @@ +schema_version: "2" +x-planning-context: &planning-context + rationale: "Make this requirement reviewable before implementation begins." + stakeholder_refs: ["nold-ai/specfact-cli#679"] +x-planning-inspection: &planning-inspection + method: inspection + observable: >- + Strict OpenSpec validation retains the requirement and its scenarios + without claiming implementation or test execution. +x-spec-1: &spec-1 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/cli-val-07-code-review-gate-adoption/specs/ci-integration/spec.md" +x-spec-2: &spec-2 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/cli-val-07-code-review-gate-adoption/specs/code-review-gate-consumer/spec.md" +x-spec-3: &spec-3 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/cli-val-07-code-review-gate-adoption/specs/pre-commit-review-gate/spec.md" +x-spec-4: &spec-4 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/cli-val-07-code-review-gate-adoption/specs/protected-code-review-range-assurance/spec.md" +x-spec-5: &spec-5 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/cli-val-07-code-review-gate-adoption/specs/trustworthy-green-checks/spec.md" +requirements: + openspec:cli-val-07-code-review-gate-adoption:ci-integration:protected-code-review-uses-approved-signed-evidence: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: C15-001 + scenario_id: protected-code-review-uses-approved-signed-evidence + intent: "Inspect the protected code review uses approved signed evidence planning contract and its scenarios." + openspec:cli-val-07-code-review-gate-adoption:code-review-gate-consumer:authoritative-schema-1-7-local-consumer: + <<: *planning-context + touchpoints: *spec-2 + verification_cases: + - <<: *planning-inspection + case_id: C15-002 + scenario_id: authoritative-schema-1-7-local-consumer + intent: "Inspect the authoritative schema 1.7 local consumer planning contract and its scenarios." + openspec:cli-val-07-code-review-gate-adoption:code-review-gate-consumer:protected-schema-1-7-waiver-verification: + <<: *planning-context + touchpoints: *spec-2 + verification_cases: + - <<: *planning-inspection + case_id: C15-003 + scenario_id: protected-schema-1-7-waiver-verification + intent: "Inspect the protected schema 1.7 waiver verification planning contract and its scenarios." + openspec:cli-val-07-code-review-gate-adoption:code-review-gate-consumer:trusted-integration-precedes-c15-activation: + <<: *planning-context + touchpoints: *spec-2 + verification_cases: + - <<: *planning-inspection + case_id: C15-004 + scenario_id: trusted-integration-precedes-c15-activation + intent: "Inspect the trusted integration precedes c15 activation planning contract and its scenarios." + openspec:cli-val-07-code-review-gate-adoption:pre-commit-review-gate:repository-pre-commit-review-gate: + <<: *planning-context + touchpoints: *spec-3 + verification_cases: + - <<: *planning-inspection + case_id: C15-005 + scenario_id: repository-pre-commit-review-gate + intent: "Inspect the repository pre-commit review gate planning contract and its scenarios." + openspec:cli-val-07-code-review-gate-adoption:protected-code-review-range-assurance:protected-consumer-shall-independently-verify-c14-range-evidence: + <<: *planning-context + touchpoints: *spec-4 + verification_cases: + - <<: *planning-inspection + case_id: C15-006 + scenario_id: protected-consumer-shall-independently-verify-c14-range-evidence + intent: "Inspect the protected consumer shall independently verify c14 range evidence planning contract and its scenarios." + openspec:cli-val-07-code-review-gate-adoption:trustworthy-green-checks:protected-code-review-green-shall-require-a-verified-c14-envelope: + <<: *planning-context + touchpoints: *spec-5 + verification_cases: + - <<: *planning-inspection + case_id: C15-007 + scenario_id: protected-code-review-green-shall-require-a-verified-c14-envelope + intent: "Inspect the protected code review green shall require a verified c14 envelope planning contract and its scenarios." diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/specs/ci-integration/spec.md b/openspec/changes/cli-val-07-code-review-gate-adoption/specs/ci-integration/spec.md new file mode 100644 index 000000000..758542996 --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/specs/ci-integration/spec.md @@ -0,0 +1,20 @@ +## Scope rescope — 2026-09-20 + +Keep the signed C14/C15, profile, policy and exception-authority prerequisites. Optional preflight is not an indirect prerequisite. Use meaningful regression reproduction, current test results and independent review; no immutable RED history or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## ADDED Requirements + +### Requirement: Protected Code Review Uses Approved Signed Evidence + +Protected CI SHALL consume only the released, approved schema 1.7 producer and +shall bind the C14 verification envelope, C15 policy profile, and trusted-base +exception authority. + +#### Scenario: Feature-branch or unapproved producer cannot enforce + +- **GIVEN** a code-review report comes from a feature-branch, unsigned, version-mismatched, or profile-mismatched module +- **WHEN** protected CI evaluates it +- **THEN** the report cannot satisfy required code-review assurance +- **AND** CI fails closed rather than falling back to legacy parsing. diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/specs/code-review-gate-consumer/spec.md b/openspec/changes/cli-val-07-code-review-gate-adoption/specs/code-review-gate-consumer/spec.md new file mode 100644 index 000000000..070524470 --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/specs/code-review-gate-consumer/spec.md @@ -0,0 +1,83 @@ +## Scope rescope — 2026-09-20 + +Keep the signed C14/C15, profile, policy and exception-authority prerequisites. Optional preflight is not an indirect prerequisite. Use meaningful regression reproduction, current test results and independent review; no immutable RED history or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## ADDED Requirements + +### Requirement: Authoritative Schema 1.7 Local Consumer + +The core pre-commit helper SHALL invoke explicit changed enforcement with +bug-hunt timeouts, validate the schema 1.7 status/exit contract, and return the +report's authoritative exit code. + +#### Scenario: Valid schema 1.7 result controls the hook + +- **GIVEN** the review command writes a valid schema 1.7 report +- **WHEN** the pre-commit helper consumes it +- **THEN** the helper returns the report ci_exit_code +- **AND** it does not recompute authority from finding counts, score, fixable, or subprocess status. + +#### Scenario: Contradictory report fails closed + +- **GIVEN** schema 1.7 reports FAIL or UNKNOWN with a non-shadow zero exit, or PASS/NOT_APPLICABLE with exit one +- **WHEN** the helper validates the report +- **THEN** the report is rejected and the hook exits non-zero. + +#### Scenario: Shadow preserves truthful status + +- **GIVEN** schema 1.7 reports FAIL or UNKNOWN under shadow mode with exit zero +- **WHEN** the helper validates it +- **THEN** the report is accepted as shadow evidence +- **AND** the printed summary retains the non-passing assurance status. + +#### Scenario: Legacy schema is shadow-only + +- **GIVEN** a schema 1.6 report +- **WHEN** protected or local blocking enforcement requires schema 1.7 +- **THEN** the older report cannot satisfy the gate +- **AND** it may be read only through the documented shadow compatibility path. + +### Requirement: Protected Schema 1.7 Waiver Verification + +Protected CI SHALL independently verify the approved module, profile, C14 +envelope, and trusted-base exception evidence before accepting a waived error. + +#### Scenario: Trusted approved waiver is accepted + +- **GIVEN** the report and C14 envelope match approved signed identities +- **AND** a retained directive matches an active trusted-base exception for the same canonical rule, path, and symbol +- **WHEN** protected verification runs +- **THEN** the waiver is accepted and retained as advisory evidence. + +#### Scenario: Candidate self-approval is rejected + +- **GIVEN** the candidate adds or changes the exception that would authorize its own error suppression +- **WHEN** protected verification runs +- **THEN** the candidate exception is not approval authority +- **AND** the error remains blocking. + +#### Scenario: Identity or expiry mismatch fails closed + +- **GIVEN** the module/profile digest, C14 envelope, rule/scope, approval, or expiry evidence is missing or mismatched +- **WHEN** protected verification runs +- **THEN** effective assurance is UNKNOWN or FAIL as applicable +- **AND** protected CI exits non-zero. + +### Requirement: Trusted Integration Precedes C15 Activation + +Protected C15 enforcement SHALL activate only after the reviewed consumer implementation is integrated and its schema 1.7 verifier, dependencies and trust roots are available from an authenticated source independent of the candidate. Candidate-only tests SHALL NOT authorize protected execution; existing protected policy SHALL remain until the reviewed activation. + +#### Scenario: Updated verifier exists only on the candidate branch + +- **GIVEN** schema 1.7 consumer tests pass but the authenticated source still provides only the C14 verifier +- **WHEN** C15 activation readiness is evaluated +- **THEN** activation remains incomplete until reviewed integration, trusted publication and trusted-source shadow validation finish. + +#### Scenario: Trusted enforcement is rolled back to shadow + +- **GIVEN** activated C15 enforcement and an authorized rollback through trusted policy +- **WHEN** maintainers return execution to shadow mode +- **THEN** the validated consumer and schema 1.7 report remain, including truthful non-passing outcomes +- **AND** rollback restores neither severity-count fallback nor candidate-controlled approval authority. diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/specs/pre-commit-review-gate/spec.md b/openspec/changes/cli-val-07-code-review-gate-adoption/specs/pre-commit-review-gate/spec.md new file mode 100644 index 000000000..7648dd253 --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/specs/pre-commit-review-gate/spec.md @@ -0,0 +1,78 @@ +## MODIFIED Requirements + +### Requirement: Repository Pre-Commit Review Gate + +The system SHALL integrate `specfact code review run` into this repository's +pre-commit workflow as an `explicit_files` staged-file check using explicit +changed enforcement and bug-hunt timeouts. After C15 adoption, blocking policy +SHALL require the signed schema 1.7 authoritative status/exit contract, replacing +C14's schema 1.6 contract. The hook SHALL validate and return `ci_exit_code`, +reject malformed or contradictory reports, and never derive approval from +finding counts, a legacy verdict, or subprocess success alone. Outside shadow, +`FAIL` and `UNKNOWN` SHALL block; valid `PASS` and `NOT_APPLICABLE` SHALL allow +progress. It SHALL NOT accept protected PR context or claim `range_candidate` +or `pr_range` assurance. + +#### Scenario: Pre-commit passes when review verdict is PASS + +- **GIVEN** relevant staged files produce a valid schema 1.7 `PASS` with exit zero +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the hook exits successfully and reports `explicit_files` assurance. + +#### Scenario: Pre-commit passes when review verdict is PASS_WITH_ADVISORY + +- **GIVEN** schema 1.7 has authoritative `PASS` or `NOT_APPLICABLE` and exit zero with advisory findings +- **WHEN** the hook presents any legacy `PASS_WITH_ADVISORY` summary +- **THEN** the commit may proceed based on the validated schema 1.7 fields, not that summary. + +#### Scenario: Pre-commit blocks commit when review verdict is FAIL + +- **GIVEN** non-shadow schema 1.7 reports `FAIL` or `UNKNOWN`, or has contradictory status/exit fields +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the hook exits non-zero with actionable diagnostics. + +#### Scenario: Review gate only targets relevant staged files + +- **GIVEN** a commit contains relevant staged source files and non-code staged files +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the command reviews only the relevant staged source files +- **AND** it passes no protected PR context and cannot emit effective `pr_range`. + +#### Scenario: Missing review command surfaces actionable setup guidance + +- **GIVEN** the local environment cannot run the review command or supply its valid report +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the hook exits non-zero with actionable setup or report diagnostics. + +#### Scenario: Shadow preserves truthful status + +- **GIVEN** valid schema 1.7 reports `FAIL` or `UNKNOWN` with an authorized shadow exit of zero +- **WHEN** the hook validates and displays the result +- **THEN** it retains the non-passing assurance status while returning the shadow exit code. + +#### Scenario: Schema 1.6 no longer satisfies blocking enforcement + +- **GIVEN** a schema 1.6 report after C15 adoption +- **WHEN** local blocking policy requires schema 1.7 +- **THEN** the old report cannot satisfy the gate +- **AND** it is readable only in the documented shadow path for one migration release. + +#### Scenario: Staged hook consumes a schema 1.6 PASS + +- **GIVEN** a schema 1.6 report claims `PASS` after C15 adoption +- **WHEN** the hook reads it during the one-release shadow compatibility window +- **THEN** it identifies the result as legacy shadow evidence +- **AND** the same report cannot satisfy schema 1.7 blocking enforcement. + +#### Scenario: Staged hook fails closed on schema 1.6 uncertainty + +- **GIVEN** a schema 1.6 report is uncertain or contradictory +- **WHEN** the hook requires schema 1.7 blocking enforcement +- **THEN** it exits non-zero rather than inferring missing fields or a passing status. + +#### Scenario: Staged files cannot claim PR authority + +- **GIVEN** the local hook receives staged positional files +- **WHEN** it builds and executes the review command +- **THEN** it does not pass protected PR context or range-assurance arguments +- **AND** it cannot emit effective `pr_range`. diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/specs/protected-code-review-range-assurance/spec.md b/openspec/changes/cli-val-07-code-review-gate-adoption/specs/protected-code-review-range-assurance/spec.md new file mode 100644 index 000000000..178dd9ffb --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/specs/protected-code-review-range-assurance/spec.md @@ -0,0 +1,49 @@ +## MODIFIED Requirements + +### Requirement: Protected consumer SHALL independently verify C14 range evidence + +The protected core consumer SHALL accept only an approved signed schema 1.7 producer +report whose `assurance_kind` is `range_candidate`. It SHALL independently +derive the unique merge base, complete governed selection, changed-line, Git +status/deletion/rename/object-type/mode manifests, target-tip policy/config and +project-runtime identities, approved suppression-catalog digest, signed +producer/package/profile/toolchain/checkpoint identities, and protected +workflow/artifact provenance. It SHALL compare those values to the producer +report without treating producer-derived expected values as authority. This +replaces the C14 schema 1.6-only acceptance rule after C15 adoption; the remaining +C14 authority, trusted-context and provenance requirements remain unchanged. +Schema 1.6 MAY be read only in the documented shadow compatibility path for one +migration release and SHALL NOT satisfy protected blocking enforcement. + +#### Scenario: Complete trusted candidate is promoted + +- **GIVEN** the approved signed schema 1.7 producer report declares `range_candidate` +- **AND** every independently derived identity and manifest exactly matches +- **WHEN** the protected core verifier evaluates the report +- **THEN** it emits a separate verifier envelope with + `effective_assurance_kind=pr_range` +- **AND** the envelope binds every compared identity, manifest digest, producer + report digest, protected-context digest, decision, and reason code. + +#### Scenario: Merge base is ambiguous or mismatched + +- **GIVEN** the protected verifier derives zero or multiple best merge bases, + or its unique merge base differs from the producer claim +- **WHEN** it evaluates the report +- **THEN** the effective assurance status is `UNKNOWN` +- **AND** it does not emit effective `pr_range`. + +#### Scenario: Governed input evidence is incomplete or mismatched + +- **GIVEN** governed selection, changed lines, diff, rename, deletion, Git + object type, or Git mode evidence is omitted or differs +- **WHEN** the protected verifier compares the manifests +- **THEN** it records the specific mismatch as `UNKNOWN` +- **AND** the protected check cannot report success in enforce mode. + +#### Scenario: Older schema cannot satisfy protected C15 enforcement + +- **GIVEN** a schema 1.6 report with otherwise complete C14 provenance +- **WHEN** protected blocking policy requires schema 1.7 after C15 adoption +- **THEN** it cannot satisfy the required check or be inferred to contain missing C15 fields +- **AND** any permitted shadow read retains its actual schema and non-enforcing status. diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/specs/trustworthy-green-checks/spec.md b/openspec/changes/cli-val-07-code-review-gate-adoption/specs/trustworthy-green-checks/spec.md new file mode 100644 index 000000000..e160601f0 --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/specs/trustworthy-green-checks/spec.md @@ -0,0 +1,44 @@ +## Scope rescope — 2026-09-20 + +Keep the signed C14/C15, profile, policy and exception-authority prerequisites. Optional preflight is not an indirect prerequisite. Use meaningful regression reproduction, current test results and independent review; no immutable RED history or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## MODIFIED Requirements + +### Requirement: Protected Code Review green SHALL require a verified C14 envelope + +A protected Code Review required check SHALL report success only when the +core-owned verifier envelope binds the approved signed schema 1.7 producer +report, verifies the applicable C15 policy/exception identities, and records +effective `pr_range` with authoritative PASS. Producer status alone SHALL NOT +constitute protected-range authority. After C15 adoption this replaces the +C14 producer-version acceptance rule while preserving C14 range verification +and trusted-context provenance. Required local and protected checks SHALL use +their authoritative schema contract and fail closed when required evidence is +unavailable or invalid. + +#### Scenario: Producer passes but protected verification is unknown + +- **GIVEN** a producer report records PASS with `range_candidate` +- **AND** the core verifier records an omitted or mismatched required identity +- **WHEN** the protected Code Review check runs in enforce mode +- **THEN** the emitted check fails +- **AND** it reports `UNKNOWN` rather than a green PR-range result. + +#### Scenario: Verified C14 envelope passes + +- **GIVEN** the approved schema 1.7 producer report and core verifier envelope are complete, + signed, mutually consistent, and bind the protected workflow context +- **AND** the envelope records effective `pr_range` with PASS +- **WHEN** the protected check completes in enforce mode +- **THEN** the check reports success +- **AND** retains the producer report and verifier envelope as distinct + artifacts. + +#### Scenario: Score and finding recount cannot manufacture green + +- **GIVEN** the authoritative report is failing, unknown, missing, or invalid +- **WHEN** a required check evaluates code-review evidence +- **THEN** a local score threshold, raw severity recount, or nested process zero cannot produce green +- **AND** the required check exits non-zero outside shadow mode. diff --git a/openspec/changes/cli-val-07-code-review-gate-adoption/tasks.md b/openspec/changes/cli-val-07-code-review-gate-adoption/tasks.md new file mode 100644 index 000000000..7739c7389 --- /dev/null +++ b/openspec/changes/cli-val-07-code-review-gate-adoption/tasks.md @@ -0,0 +1,51 @@ +# Tasks: cli-val-07-code-review-gate-adoption + +## Scope rescope — 2026-09-20 + +Keep the signed C14/C15, profile, policy and exception-authority prerequisites. Optional preflight is not an indirect prerequisite. Use meaningful regression reproduction, current test results and independent review; no immutable RED history or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## 1. Worktree and readiness + +- [ ] 1.1 At implementation start, create or verify a dedicated issue-linked worktree from current `origin/dev`; refresh/rebase a retained worktree after checking ownership. The recovered planning branch is not completed implementation setup. +- [ ] 1.2 At implementation start, refresh and read back existing core [#679](https://github.com/nold-ai/specfact-cli/issues/679): parent #375, labels, assignee, User Story/project status, ownership and current blocked-by relationships. Reconcile them against the retained signed C14/C15, profile, policy and exception prerequisites; do not preserve the historical blocker count or restore optional preflight dependencies. Resolve any concurrent core ownership before proceeding. +- [ ] 1.3 Verify C14 protected adoption and the signed C15 schema 1.7 module release are available. +- [ ] 1.4 Verify the paired modules issue is not concurrently in progress elsewhere. +- [ ] 1.5 Stop production implementation if any readiness item is incomplete. + +## 2. Spec and compatibility freeze + +- [x] 2.1 Add proposal, design, tasks, and schema/consumer delta specs. +- [x] 2.2 Cross-link both repository change orders and the paired source tracking. +- [ ] 2.3 Import the released producer/consumer matrix and pin signed module, schema, profile, and exception identities. +- [x] 2.4 Update the internal wiki source page and rebuild the graph. +- [x] 2.5 Run `openspec validate cli-val-07-code-review-gate-adoption --strict`. +- [ ] 2.6 After prerequisite C14 integration and native archival, reconcile the MODIFIED local, protected-range and green-check requirements against the canonical C14 contract before implementation. Preserve its trust and staged/range boundaries while replacing schema 1.6-only blocking semantics; revalidate before C15 archival. + +## 3. Tests first + +- [ ] 3.1 Add failing helper tests for explicit changed/bug-hunt invocation and authoritative report exit propagation. +- [ ] 3.2 Add failing tests for PASS, FAIL, UNKNOWN, NOT_APPLICABLE, shadow exit, malformed fields, and contradictory status/exit pairs. +- [ ] 3.3 Add failing tests proving schema 1.6 is shadow-only and cannot satisfy protected enforcement. +- [ ] 3.4 Add failing protected-consumer tests for approved trusted-base waiver, candidate self-approval, expiry, scope mismatch, and profile/module digest mismatch. +- [ ] 3.5 Observe meaningful regression failures before production edits and summarize the command and result briefly; no committed output transcript is required. + +## 4. Implementation + +- [ ] 4.1 Implement strict schema 1.7 report projection and invariant validation. +- [ ] 4.2 Update the pre-commit command and remove severity-count/subprocess-status fallback authority. +- [ ] 4.3 Extend the released C14 protected verifier to validate C15 policy and waiver identities. +- [ ] 4.4 Prepare shadow-first workflow/config changes and activation controls; retain existing protected enforcement while testing the candidate. Do not activate protected C15 before reviewed integration and trusted verifier publication in phase 5. +- [ ] 4.5 Re-run focused tests and record passing evidence. + +## 5. Validation and delivery + +- [ ] 5.1 Dogfood the released signed module on core in shadow; remediate or obtain approved time-bound exceptions for every error. +- [ ] 5.2 Run format, type-check, lint, yaml, contracts, smart/full tests, independent static analysis, and fresh SpecFact review evidence. +- [ ] 5.3 Run strict OpenSpec validation and the complete schema compatibility matrix. +- [ ] 5.4 Update contributor/CI documentation and changelog/version surfaces required by release policy. +- [ ] 5.5 Open the implementation PR to dev once reviewable and integrate only after modules measurement acceptance, consumer fixtures and normal required checks pass. Candidate verifier tests are not deployed protected authority. +- [ ] 5.6 Publish/authenticate the integrated schema 1.7 verifier and its dependencies/trust roots through the normal trusted-source release/promotion path; if that path requires main, complete reviewed dev-to-main promotion first. +- [ ] 5.7 Run the trusted-source shadow pilot, verify the selected authenticated verifier identity, and only then activate changed local/protected C15 enforcement after measurement acceptance. Never execute the candidate verifier as protected authority. +- [ ] 5.8 After paired delivery and merge complete, run `openspec archive cli-val-07-code-review-gate-adoption` from the repository root, then clean the implementation worktree; never manually move archive files. Record rollout/rollback validation in the delivery PR. diff --git a/openspec/changes/code-review-14-protected-range-adoption/.openspec.yaml b/openspec/changes/code-review-14-protected-range-adoption/.openspec.yaml new file mode 100644 index 000000000..44f55ffee --- /dev/null +++ b/openspec/changes/code-review-14-protected-range-adoption/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-23 diff --git a/openspec/changes/code-review-14-protected-range-adoption/CHANGE_VALIDATION.md b/openspec/changes/code-review-14-protected-range-adoption/CHANGE_VALIDATION.md new file mode 100644 index 000000000..2739785ec --- /dev/null +++ b/openspec/changes/code-review-14-protected-range-adoption/CHANGE_VALIDATION.md @@ -0,0 +1,71 @@ +## Recovery review — 2026-09-20 + +Recovered from uncommitted planning files on `feature/code-review-14-protected-range-adoption`; the source worktree contains only this proposal and its change-order edit. Imported into local `dev` and the R09 planning worktree. No runtime changes were imported. Historical readiness and version identities below are dated context and must be revalidated before implementation. Recovery validation preserves existing staged-review scenarios and classifies previously nonexistent requirement headers as ADDED, avoiding invalid archive replacements. Canonical C14 follow-up reconciliation remains a prerequisite to final C15 specification promotion. + +Current review correction: the old `===0.55.1` handoff does not establish +compatibility with current core. Readiness now requires a compatible signed +publication and independently authenticated verifier source before enforcement. +The report below records the 2026-08-23 review only; its pass and original +allowlist are not current implementation authorization. + +# Change Validation Report: code-review-14-protected-range-adoption + +**Validation Date**: 2026-08-23 +**Change Proposal**: [proposal.md](./proposal.md) +**Validation Method**: repository inspection at core `origin/dev` +`e3a20f20`, signed modules publication `6a0d0b31`, live GitHub issue and +hierarchy readback, and strict OpenSpec validation. + +## Executive Summary + +- Breaking Changes: 0 existing public core CLI signatures changed by proposal +- Dependency Boundary: modules C14 producer complete; core C14 consumer missing +- Impact Level: High trust impact; deliberately small core edit surface +- Validation Result: Pass for proposal/readiness; implementation not started + +## Existing-Change and Issue Check + +- No C14 protected-consumer OpenSpec change exists on core `origin/dev`. +- Core #679 is C15/schema 1.7 adoption and is not a substitute for this change. +- Core #680 now tracks this C14 adoption as a User Story under #375, on the + `SpecFact CLI` project in `Todo`, assigned to `djm81`, with the expected + labels and a native blocking relationship to #679. + +## Frozen Upstream Handoff + +- Modules implementation PR: #418 +- Signed publication PR: #419 +- Modules publication commit/tree: + `6a0d0b31` / `0531325d68ae21f75abda01ce8968f92ba1b6d06` +- Module/version: `specfact-code-review` `0.49.46` +- Strict core compatibility: `===0.55.1` +- Core tag/commit/tree: `v0.55.1` / + `b1e517e60e669eaba15a18ecfa83ef5a9df65276` / + `47984be5434d7ae65ed6908bf525a32053290337` + +All complete signature and checksum fields remain an implementation-readiness +gate and must be read directly from the immutable signed publication before +tests or fixture edits. + +## Scope Validation + +The proposal limits production work to one new verifier, one existing workflow, +one immutable fixture, and schema 1.6 parsing in one staged helper. It expressly +excludes producer/analyzer logic, generic module trust refactors, C15/schema +1.7, Requirements, waivers, scoring, and unrelated CI hardening. Additional +production files require a named failing test plus a spec amendment and strict +revalidation. + +## OpenSpec Validation + +- **Status**: Pass +- **Command**: `openspec validate code-review-14-protected-range-adoption --strict` +- **Issues Found/Fixed**: 0 + +## Stop Conditions Before Implementation + +- Any signed publication identity differs from the frozen handoff. +- Core `origin/dev` or the module fixture moves without revalidation. +- Issue #680 leaves `Todo` because another implementation session started. +- Parent, labels, project, assignee, or native dependency metadata drifts. +- A required behavior cannot be expressed inside the normative scope boundary. diff --git a/openspec/changes/code-review-14-protected-range-adoption/design.md b/openspec/changes/code-review-14-protected-range-adoption/design.md new file mode 100644 index 000000000..49f42900f --- /dev/null +++ b/openspec/changes/code-review-14-protected-range-adoption/design.md @@ -0,0 +1,191 @@ +## Context + +## Scope rescope — 2026-09-20 + +Optional preflight, approval seals and development checkpoints are not prerequisites. Keep independent protected-range verification, complete scope, authenticated runtime and signed producer compatibility. Use focused regression reproduction and current-run results; do not require immutable RED history, frozen selector inventories or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +Modules C14 deliberately stops at a signed producer/report boundary. The +producer can emit `range_candidate`, but it cannot attest to the protected +workflow event or promote itself to `pr_range`. Core owns that separate trust +decision because core controls the protected checkout, event payload, workflow +identity, immutable modules fixture, and required check. + +The historical handoff was the signed modules publication merged by +modules PR #419 at commit `6a0d0b31` (tree +`0531325d68ae21f75abda01ce8968f92ba1b6d06`), containing +`specfact-code-review` `0.49.46` with strict core compatibility `===0.55.1`. +The module was developed and smoked against core tag `v0.55.1`, commit +`b1e517e60e669eaba15a18ecfa83ef5a9df65276`, tree +`47984be5434d7ae65ed6908bf525a32053290337`. + +For current adoption, select and authenticate a signed C14-capable publication +whose declared compatibility includes the actual core candidate, then verify +the exact packaged pair. The historical strict pin must not be reused on a +different core version. Request a fresh signed publication if necessary. + +## Goals / Non-Goals + +### Goals + +- Independently verify every finite identity and manifest named by the C14 + consumer handoff before claiming effective `pr_range`. +- Keep protected workflow context outside candidate-controlled paths and bind + it to full immutable base/head identities. +- Fail closed as `UNKNOWN` for missing, ambiguous, incomplete, or mismatched + required evidence. +- Preserve the local staged hook as truthful `explicit_files` assurance. +- Make rollout and rollback explicit and release-scoped. + +### Non-Goals + +- Producing or changing Code Review findings. +- Designing a reusable attestation framework or refactoring module trust + primitives. +- Implementing C15/schema-1.7 severity, profile, policy, waiver, or broader + enforcement behavior. +- Changing analyzer rules, runtime capsules, project-runtime semantics, + Requirements, or test-selection policy. + +## Decisions + +### Decision 1: Isolate the verifier from the module producer + +Core adds one verifier entry point that reads the immutable producer report, +the freshly written protected context, the signed module/profile identities, +and independently derived repository facts. It does not import producer +selection helpers or accept producer-derived expected values as authority. + +Protected workflow policy selects an authenticated immutable base revision or +approved release for the verifier, its imports/dependencies, configuration and +trust roots. Execute it outside the candidate checkout/import path in a +separate trusted job that consumes bounded candidate artifacts as data. +Candidate workflow edits, verifier edits and `ci/module-fixture.lock.json` +cannot choose approval authority. Do not run candidate code with repository +write or signing credentials. Runner-temp context alone is not isolation. + +Publish and authenticate the verifier in the trusted source before enforcement. +If that revision lacks the verifier, report `UNKNOWN`; never fall back to the +candidate copy. The existing organization-required workflow invocation policy +may need a bounded coordinated change to enforce this source selection. + +The verifier emits a separate core-owned envelope. It never edits or replaces +the producer report. + +### Decision 2: Verify a closed C14 manifest set + +The verifier independently derives and compares: + +1. exactly one best merge base and its commit/tree; +2. full base/head refs and the authenticated target-tip commit/tree; +3. complete governed path selection and changed-line manifest; +4. Git status, deletion, exact-rename, object-type, and mode manifests; +5. target-tip policy/config selection and declared project-runtime + source-lock paths/blobs plus applicable dependency/build/artifact + attestation; +6. approved suppression-catalog digest obtained from the signed module/profile + policy, never from candidate input; +7. signed producer module/version/manifest/archive/signature, schema 1.6, + profile, toolchain, checkpoint, package, and report identities; and +8. protected workflow, job, event, checkout, and artifact provenance. + +An omitted governed input, multiple/no best merge bases, unapproved producer, +candidate-supplied trust identity, or any mismatch yields `UNKNOWN`. + +### Decision 3: Promotion is envelope-only + +The producer must report `assurance_kind=range_candidate`. Only the core +verifier envelope can state `effective_assurance_kind=pr_range`. The envelope +binds all compared identities, manifest digests, producer report digest, +context digest, decision, and reason codes. + +`FAIL` producer evidence remains `FAIL`; infrastructure or reconciliation +uncertainty remains `UNKNOWN`. The verifier never upgrades +`range_preview`, `worktree`, `index`, `explicit_files`, or an unsigned report. + +### Decision 4: Keep local and protected consumers separate + +`scripts/pre_commit_code_review.py` continues to pass only staged positional +files. It may parse schema 1.6 `assurance_status` and authoritative exit +semantics for truthful summaries, but it cannot accept PR context, emit a +verifier envelope, or claim `pr_range`. + +Each job independently derives the same canonical context from authenticated +protected event/run data and immutable base/head identities. The producer job +writes its local copy under `${RUNNER_TEMP}` immediately before invocation and +passes full refs plus `--pr-context-file`. The trusted verifier regenerates its +own copy; it never trusts or expects access to the producer runner's file. +Canonical serialization and digest cover the same event/run and revision fields +in both jobs; runner paths and per-job identities are not shared context fields. +Authenticate producer-job/artifact provenance separately and bind verifier-job +provenance in the envelope. Compare the producer report's context digest with the trusted +regenerated digest and bind that digest to verifier input and output envelope. +Missing protected data or a mismatch yields `UNKNOWN`. Retain producer report +and envelope separately; no new context-transfer artifact is required. + +### Decision 5: Stage enforcement without weakening evidence + +The verifier contract is identical in all phases: + +- **shadow** records the authoritative result but is not a required gate; +- **warning** reports non-green results visibly while migration remains + non-blocking; and +- **enforce** makes `FAIL` and `UNKNOWN` non-zero required-check outcomes. + +Changing phase never changes manifest completeness or turns uncertainty into +PASS. Rollback changes only the enforcement phase and retains artifacts. + +### Decision 6: File and review allowlist is normative + +The proposal's named production, test, documentation, and release paths are the +default maximum implementation surface. Any additional path requires an +explicit proposal update, a relevant acceptance case, strict revalidation, and review +of whether the work belongs to C15 or another follow-up. Generic cleanup and +valid-but-unrelated review findings do not expand C14 automatically. Each finding +receives a fix, reasoned rejection, or individually approved exception with +impact and a linked follow-up; there is no blanket deferral. + +## Risks / Trade-offs + +- **Identity drift**: a later modules publication or core release can invalidate + the frozen matrix. Mitigation: pin commit/tree/checksums and rerun immutable + smoke before release; update through a reviewed spec amendment. +- **Workflow context spoofing**: candidate-controlled files could impersonate + event data or replace the verifier. Mitigation: independently select trusted + verifier code and roots, isolate execution from candidate code, and bind + canonical context and protected provenance in the envelope. +- **Verifier/producer correlated logic**: importing producer helpers could make + equality checks circular. Mitigation: independently derive the closed + manifests with core-owned code and compare only serialized evidence. +- **Scope creep**: general trust-framework refactors could make review + unbounded. Mitigation: use the file/test allowlist and individually triage unmatched + findings under the exception policy. + +## Migration Plan + +1. Pin and verify the signed modules C14 publication and exact compatibility + matrix. +2. Add focused verifier/workflow regressions, including candidate edits to the + verifier, lock and workflow, and briefly summarize observed failures. +3. Implement the isolated verifier, protected context flow, staged-hook schema + handling, and shadow envelope. +4. Reference passing candidate CI results, integrate the reviewed implementation + into `dev`, and promote through a reviewed PR to protected `main` under the + effective gates. The unpublished verifier cannot authorize its own integration. +5. After final signed module/core immutable smoke, use the existing main release + workflow to publish the core release and select/authenticate the integrated + verifier from its approved immutable base or release. +6. Only then promote trusted shadow to warning and enforcement through reviewed + rollout decisions. Hand the released C14 contract to #679; no historical + development checkpoints or candidate-authorized bootstrap are required. + +## Open Questions + +None for scope. Branch-protection selection of the final emitted check remains +an operational promotion step after warning evidence is accepted; it does not +change this software contract. + +## Local reset coordination (2026-10-16) + +Planned ordinary local adoption is tracked separately by [core #769](https://github.com/nold-ai/specfact-cli/issues/769), after modules #515 and before modules #516. Its schema 2.0 trusted-local evidence cannot satisfy protected core #680 capsule acceptance. This additive note does not change this change's owners, status, tasks, or acceptance. C15 core #679 and modules #417 retain their own contracts. Older capsule references remain relevant to their separate protected contract, not ordinary local reset scope. diff --git a/openspec/changes/code-review-14-protected-range-adoption/proposal.md b/openspec/changes/code-review-14-protected-range-adoption/proposal.md new file mode 100644 index 000000000..29766674b --- /dev/null +++ b/openspec/changes/code-review-14-protected-range-adoption/proposal.md @@ -0,0 +1,128 @@ +# Change: Adopt C14 Protected PR-Range Assurance + +## Scope rescope — 2026-09-20 + +Optional preflight, approval seals and development checkpoints are not prerequisites. Keep independent protected-range verification, complete scope, authenticated runtime and signed producer compatibility. Use focused regression reproduction and current-run results; do not require immutable RED history, frozen selector inventories or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## Why + +The signed Code Review C14 producer now emits truthful schema 1.6 +`range_candidate` evidence, but core has no protected consumer that can +independently verify that evidence and promote it to effective `pr_range`. +Core issue #679 is the later C15/schema-1.7 policy adoption and cannot absorb +this missing trust-boundary work without obscuring both changes. + +Without a dedicated core C14 change, a clean pull-request checkout can run the +module producer but cannot make an authoritative protected-range claim or ship +a core release that fully supports C14. + +## What Changes + +- **NEW**: A protected core verifier independently recomputes the unique merge + base, governed selection, Git object/mode, diff/rename, target-tip + policy/config, suppression-catalog, producer, artifact, and workflow + identities required by the signed C14 handoff. +- **NEW**: A verifier-bound envelope promotes an accepted producer + `range_candidate` to effective `pr_range`; every omitted or mismatched + required input produces `UNKNOWN` and cannot become a green protected check. +- **NEW**: The protected PR workflow writes canonical event context under the + runner temporary directory immediately before invocation, passes full + base/head refs plus `--pr-context-file`, and rolls out through explicit + shadow, warning, and enforcement phases. +- **NEW**: Execute the verifier, its imports/dependencies, policy and trust roots + from an authenticated immutable base revision or approved release selected by + protected workflow policy. Candidate files, workflow edits and fixture locks + cannot select or replace that authority. +- **UPDATE**: Select a signed C14-capable publication compatible with the actual + core candidate, verify its packaged identities, and pin the tested pair. + The original `6a0d0b31` / module `0.49.46` / core `===0.55.1` handoff is + historical, not a pin for current core. If no compatible signed publication + exists, obtain one before adoption; never relax compatibility to install it. +- **UPDATE**: The existing staged pre-commit helper remains + `explicit_files` only and consumes schema 1.6 authoritative assurance + status/exit semantics without claiming PR authority. + +## Capabilities + +### New Capabilities + +- `protected-code-review-range-assurance`: Protected verification and + promotion of signed C14 `range_candidate` reports to effective `pr_range`. + +### Modified Capabilities + +- `pre-commit-review-gate`: Consume schema 1.6 authoritative status/exit while + remaining a staged explicit-file check. +- `trustworthy-green-checks`: Emit a protected Code Review check only from the + independently verified C14 envelope and fail closed on unknown evidence. + +## Scope Boundary + +Implementation is restricted to the following production surfaces unless a +named failing acceptance test proves one additional path is unavoidable: + +- `scripts/verify_code_review_range_assurance.py` (new isolated verifier), +- `.github/workflows/pr-orchestrator.yml` (protected context and invocation), +- the existing organization-required workflow invocation policy in `nold-ai/.github` + where needed to select the trusted verifier independently of the candidate, +- `ci/module-fixture.lock.json` (immutable signed publication identity), and +- `scripts/pre_commit_code_review.py` (schema 1.6 status/exit consumption only). + +Tests are restricted to +`tests/unit/scripts/test_verify_code_review_range_assurance.py` (new), +`tests/unit/scripts/test_pre_commit_code_review.py`, and +`tests/unit/workflows/test_code_review_14_protected_range.py` (new). Tests SHALL +build temporary repositories in-process rather than adding a new shared fixture +framework. Documentation is restricted to optional +`docs/guides/code-review-protected-range-assurance.md` plus +`docs/_data/nav.yml`. Release metadata is restricted to `CHANGELOG.md`, +`pyproject.toml`, `setup.py`, `src/__init__.py`, and +`src/specfact_cli/__init__.py` when the release policy requires a version bump; +those edits are not permission to widen runtime scope. + +This change MUST NOT: + +- reimplement or alter module analyzers, finding classification, selection, + sandboxing, toolchain provisioning, or report production; +- introduce schema 1.7, severity calibration, profiles, policy promotion, + scoring, waiver/exception, autofix, or C15 behavior; +- refactor generic module discovery, installation, registry, signature, or + package-loading abstractions; +- add Requirements behavior, evidence-graph generalization, source-to-test + inference, new analyzer rules, or unrelated CI hardening; or +- treat review findings outside this finite C14 consumer contract as scope. + +Triage every finding individually. Fix introduced or relevant defects; reject +false positives with a reason. A real pre-existing or unrelated finding needs +an individually documented, approved exception with impact and a linked +follow-up before deferral. The scope boundary is not a blanket waiver and +does not automatically authorize unrelated implementation. + +## Impact + +- **Affected specs**: `protected-code-review-range-assurance`, + `pre-commit-review-gate`, `trustworthy-green-checks`. +- **Affected code**: one isolated verifier, the PR orchestrator, immutable + module fixture, narrow staged-hook parsing, and their exact tests. +- **Release impact**: a new core release is required after implementation and + final immutable compatibility smoke. +- **Rollback**: disable enforcement while retaining the signed C14 module, + verifier envelope, and shadow evidence for diagnosis; the staged + `explicit_files` hook remains available. + +## Source Tracking + + +- **GitHub Issue**: #680 +- **Issue URL**: +- **Repository**: nold-ai/specfact-cli +- **Modules implementation**: nold-ai/specfact-cli-modules#416 via PR #418 +- **Historical signed publication**: nold-ai/specfact-cli-modules#419 at `6a0d0b31` +- **Downstream C15 adoption**: nold-ai/specfact-cli#679 +- **Last Synced Status**: open; Todo; parent #375; blocks #679; current compatible signed pair and trusted verifier publication must be verified; implementation not started + +## Local reset coordination (2026-10-16) + +Planned ordinary local adoption is tracked separately by [core #769](https://github.com/nold-ai/specfact-cli/issues/769), after modules #515 and before modules #516. Its schema 2.0 trusted-local evidence cannot satisfy protected core #680 capsule acceptance. This additive note does not change this change's owners, status, tasks, or acceptance. C15 core #679 and modules #417 retain their own contracts. Older capsule references remain relevant to their separate protected contract, not ordinary local reset scope. diff --git a/openspec/changes/code-review-14-protected-range-adoption/requirements-evidence.yaml b/openspec/changes/code-review-14-protected-range-adoption/requirements-evidence.yaml new file mode 100644 index 000000000..d2a1d23d0 --- /dev/null +++ b/openspec/changes/code-review-14-protected-range-adoption/requirements-evidence.yaml @@ -0,0 +1,78 @@ +schema_version: "2" +x-planning-context: &planning-context + rationale: "Make this requirement reviewable before implementation begins." + stakeholder_refs: ["nold-ai/specfact-cli#680"] +x-planning-inspection: &planning-inspection + method: inspection + observable: >- + Strict OpenSpec validation retains the requirement and its scenarios + without claiming implementation or test execution. +x-spec-1: &spec-1 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/code-review-14-protected-range-adoption/specs/pre-commit-review-gate/spec.md" +x-spec-2: &spec-2 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/code-review-14-protected-range-adoption/specs/protected-code-review-range-assurance/spec.md" +x-spec-3: &spec-3 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/code-review-14-protected-range-adoption/specs/trustworthy-green-checks/spec.md" +requirements: + openspec:code-review-14-protected-range-adoption:pre-commit-review-gate:repository-pre-commit-review-gate: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: C14-001 + scenario_id: repository-pre-commit-review-gate + intent: "Inspect the repository pre-commit review gate planning contract and its scenarios." + openspec:code-review-14-protected-range-adoption:protected-code-review-range-assurance:protected-consumer-shall-independently-verify-c14-range-evidence: + <<: *planning-context + touchpoints: *spec-2 + verification_cases: + - <<: *planning-inspection + case_id: C14-002 + scenario_id: protected-consumer-shall-independently-verify-c14-range-evidence + intent: "Inspect the protected consumer shall independently verify c14 range evidence planning contract and its scenarios." + openspec:code-review-14-protected-range-adoption:protected-code-review-range-assurance:protected-consumer-shall-bind-policy-and-producer-trust: + <<: *planning-context + touchpoints: *spec-2 + verification_cases: + - <<: *planning-inspection + case_id: C14-003 + scenario_id: protected-consumer-shall-bind-policy-and-producer-trust + intent: "Inspect the protected consumer shall bind policy and producer trust planning contract and its scenarios." + openspec:code-review-14-protected-range-adoption:protected-code-review-range-assurance:verifier-execution-shall-use-independently-selected-trusted-code: + <<: *planning-context + touchpoints: *spec-2 + verification_cases: + - <<: *planning-inspection + case_id: C14-004 + scenario_id: verifier-execution-shall-use-independently-selected-trusted-code + intent: "Inspect the verifier execution shall use independently selected trusted code planning contract and its scenarios." + openspec:code-review-14-protected-range-adoption:protected-code-review-range-assurance:protected-workflow-shall-provide-canonical-trusted-context: + <<: *planning-context + touchpoints: *spec-2 + verification_cases: + - <<: *planning-inspection + case_id: C14-005 + scenario_id: protected-workflow-shall-provide-canonical-trusted-context + intent: "Inspect the protected workflow shall provide canonical trusted context planning contract and its scenarios." + openspec:code-review-14-protected-range-adoption:protected-code-review-range-assurance:c14-enforcement-shall-be-staged-and-reversible: + <<: *planning-context + touchpoints: *spec-2 + verification_cases: + - <<: *planning-inspection + case_id: C14-006 + scenario_id: c14-enforcement-shall-be-staged-and-reversible + intent: "Inspect the c14 enforcement shall be staged and reversible planning contract and its scenarios." + openspec:code-review-14-protected-range-adoption:trustworthy-green-checks:protected-code-review-green-shall-require-a-verified-c14-envelope: + <<: *planning-context + touchpoints: *spec-3 + verification_cases: + - <<: *planning-inspection + case_id: C14-007 + scenario_id: protected-code-review-green-shall-require-a-verified-c14-envelope + intent: "Inspect the protected code review green shall require a verified c14 envelope planning contract and its scenarios." diff --git a/openspec/changes/code-review-14-protected-range-adoption/specs/pre-commit-review-gate/spec.md b/openspec/changes/code-review-14-protected-range-adoption/specs/pre-commit-review-gate/spec.md new file mode 100644 index 000000000..89171185a --- /dev/null +++ b/openspec/changes/code-review-14-protected-range-adoption/specs/pre-commit-review-gate/spec.md @@ -0,0 +1,69 @@ +## Scope rescope — 2026-09-20 + +Optional preflight, approval seals and development checkpoints are not prerequisites. Keep independent protected-range verification, complete scope, authenticated runtime and signed producer compatibility. Use focused regression reproduction and current-run results; do not require immutable RED history, frozen selector inventories or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## MODIFIED Requirements + +### Requirement: Repository Pre-Commit Review Gate + +The system SHALL integrate `specfact code review run` into this repository's +pre-commit workflow as an `explicit_files` staged-file check. It SHALL consume +schema 1.6 authoritative `assurance_status` and exit semantics, block any +authoritative `FAIL` or `UNKNOWN`, and allow an authoritative `PASS` or +`NOT_APPLICABLE` according to the signed report contract. It SHALL NOT accept +protected PR context or claim `range_candidate` or `pr_range` assurance. + +#### Scenario: Staged hook consumes a schema 1.6 PASS + +- **GIVEN** the staged explicit-file review emits a valid schema 1.6 report + with authoritative `assurance_status=PASS` +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the hook exits successfully +- **AND** its summary identifies the assurance as `explicit_files`. + +#### Scenario: Staged hook fails closed on schema 1.6 uncertainty + +- **GIVEN** the staged explicit-file review emits authoritative + `assurance_status=UNKNOWN` or inconsistent status/exit evidence +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the hook exits non-zero with actionable diagnostics +- **AND** it does not reinterpret uncertainty through legacy finding severity. + +#### Scenario: Staged files cannot claim PR authority + +- **GIVEN** the local hook receives staged positional files +- **WHEN** it builds and executes the review command +- **THEN** it does not pass protected PR context or range-assurance arguments +- **AND** it cannot emit effective `pr_range`. + +#### Scenario: Pre-commit passes when review verdict is PASS + +- **GIVEN** staged repository files produce a valid schema 1.6 authoritative `PASS` with consistent legacy verdict and exit code +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the hook exits successfully and the commit may proceed + +#### Scenario: Pre-commit passes when review verdict is PASS_WITH_ADVISORY + +- **GIVEN** staged repository files produce a valid schema 1.6 report with authoritative `PASS` or `NOT_APPLICABLE` and a consistent `PASS_WITH_ADVISORY` projection +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the hook exits successfully and the commit may proceed + +#### Scenario: Pre-commit blocks commit when review verdict is FAIL + +- **GIVEN** staged repository files produce authoritative `FAIL` or `UNKNOWN` with a conservative legacy `FAIL` projection +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the hook exits non-zero and the commit is blocked + +#### Scenario: Review gate only targets relevant staged files + +- **GIVEN** a commit contains staged files and non-code staged files +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the command reviews only the relevant staged source files instead of the full repository + +#### Scenario: Missing review command surfaces actionable setup guidance + +- **GIVEN** the local environment cannot run `specfact code review run` +- **WHEN** the repository pre-commit workflow runs the review gate +- **THEN** the hook exits non-zero with setup guidance instead of failing silently diff --git a/openspec/changes/code-review-14-protected-range-adoption/specs/protected-code-review-range-assurance/spec.md b/openspec/changes/code-review-14-protected-range-adoption/specs/protected-code-review-range-assurance/spec.md new file mode 100644 index 000000000..fae563d06 --- /dev/null +++ b/openspec/changes/code-review-14-protected-range-adoption/specs/protected-code-review-range-assurance/spec.md @@ -0,0 +1,135 @@ +## Scope rescope — 2026-09-20 + +Optional preflight, approval seals and development checkpoints are not prerequisites. Keep independent protected-range verification, complete scope, authenticated runtime and signed producer compatibility. Use focused regression reproduction and current-run results; do not require immutable RED history, frozen selector inventories or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## ADDED Requirements + +### Requirement: Protected consumer SHALL independently verify C14 range evidence + +The protected core consumer SHALL accept only a signed schema 1.6 producer +report whose `assurance_kind` is `range_candidate`. It SHALL independently +derive the unique merge base, complete governed selection, changed-line, Git +status/deletion/rename/object-type/mode manifests, target-tip policy/config and +project-runtime identities, approved suppression-catalog digest, signed +producer/package/profile/toolchain/checkpoint identities, and protected +workflow/artifact provenance. It SHALL compare those values to the producer +report without treating producer-derived expected values as authority. + +#### Scenario: Complete trusted candidate is promoted + +- **GIVEN** the signed C14 producer report declares `range_candidate` +- **AND** every independently derived identity and manifest exactly matches +- **WHEN** the protected core verifier evaluates the report +- **THEN** it emits a separate verifier envelope with + `effective_assurance_kind=pr_range` +- **AND** the envelope binds every compared identity, manifest digest, producer + report digest, protected-context digest, decision, and reason code. + +#### Scenario: Merge base is ambiguous or mismatched + +- **GIVEN** the protected verifier derives zero or multiple best merge bases, + or its unique merge base differs from the producer claim +- **WHEN** it evaluates the report +- **THEN** the effective assurance status is `UNKNOWN` +- **AND** it does not emit effective `pr_range`. + +#### Scenario: Governed input evidence is incomplete or mismatched + +- **GIVEN** governed selection, changed lines, diff, rename, deletion, Git + object type, or Git mode evidence is omitted or differs +- **WHEN** the protected verifier compares the manifests +- **THEN** it records the specific mismatch as `UNKNOWN` +- **AND** the protected check cannot report success in enforce mode. + +### Requirement: Protected consumer SHALL bind policy and producer trust + +The protected consumer SHALL independently select authorized target-tip +policy/config, obtain the approved suppression-catalog digest from signed +module/profile policy, verify declared project-runtime source-lock paths/blobs +and applicable dependency/build/artifact attestation, and require approved +signed producer module, schema, profile, toolchain, checkpoint, package, +workflow, job, and artifact identities. + +#### Scenario: Target-tip project runtime is fully attested + +- **GIVEN** every declared target-tip source-lock path and blob and every + applicable runtime dependency/build/artifact identity is authorized +- **WHEN** the protected verifier evaluates the project-runtime evidence +- **THEN** that evidence may contribute to an effective `pr_range` decision. + +#### Scenario: Candidate or untrusted runtime attempts to authorize itself + +- **GIVEN** a candidate-controlled policy, catalog, runtime, producer, or + artifact identity differs from the independently approved identity +- **WHEN** the protected verifier evaluates the report +- **THEN** the effective assurance status is `UNKNOWN` +- **AND** candidate input cannot authorize or promote itself. + +### Requirement: Verifier execution SHALL use independently selected trusted code + +Protected workflow policy SHALL select the verifier, imports/dependencies, +configuration and trust roots from an authenticated immutable base revision or +approved release. It SHALL execute outside candidate-controlled paths in a +separate trusted job. Candidate code SHALL NOT run with signing or repository +write credentials. Candidate workflow and fixture-lock edits SHALL NOT select +or replace approval authority. A trusted verifier publication SHALL precede +enforcement; its absence SHALL produce `UNKNOWN`, not candidate fallback. + +#### Scenario: Candidate replaces verifier or its authority inputs + +- **GIVEN** a candidate edits the verifier, its imports, workflow or fixture lock +- **WHEN** protected verification runs +- **THEN** it uses the independently authorized code and trust roots +- **AND** candidate edits cannot turn rejected evidence into effective `pr_range`. + +#### Scenario: Trusted revision does not yet contain the verifier + +- **GIVEN** the independently selected trusted revision lacks the verifier +- **WHEN** protected verification is requested +- **THEN** it reports `UNKNOWN` and does not execute the candidate copy +- **AND** enforce mode cannot succeed. + +### Requirement: Protected workflow SHALL provide canonical trusted context + +The protected PR workflow SHALL write canonical context under the runner +temporary directory immediately before producer invocation, pass full base and +head refs plus `--pr-context-file`, preserve producer report and verifier +envelope as separate immutable artifacts, and bind protected event/workflow/job +provenance. The separate trusted verifier job SHALL independently regenerate +canonical context from authenticated protected event/run data and immutable +base/head identities, compare its digest with the producer report, and bind the +regenerated digest to verifier input and output envelope. It SHALL NOT trust +producer-local files or require a shared runner filesystem. Shared context +SHALL exclude runner paths and per-job identities; producer-job/artifact and +verifier-job provenance SHALL be authenticated and bound separately. + +#### Scenario: Producer and verifier use separate runners + +- **GIVEN** the producer runner's temporary context is unavailable to the verifier +- **WHEN** the trusted job independently regenerates context for the same event/run and revisions +- **THEN** matching canonical context digests permit the remaining verification +- **AND** missing protected data or a different event/run or revision yields `UNKNOWN`. + +#### Scenario: Candidate checkout contains a lookalike context file + +- **GIVEN** candidate-controlled repository content contains a file resembling + protected PR context +- **WHEN** the protected workflow invokes the producer and verifier +- **THEN** the producer uses freshly generated runner-temporary context and the verifier uses its independently regenerated context +- **AND** the candidate file cannot affect effective assurance. + +### Requirement: C14 enforcement SHALL be staged and reversible + +The same verifier contract SHALL operate in shadow, warning, and enforce +phases. Phase changes SHALL NOT reduce manifest completeness or reinterpret +`UNKNOWN` as PASS. In enforce mode, `FAIL` and `UNKNOWN` SHALL exit non-zero. + +#### Scenario: Enforcement is rolled back after an operational incident + +- **GIVEN** maintainers return the protected check from enforce to warning +- **WHEN** a report is `FAIL` or `UNKNOWN` +- **THEN** the producer report and verifier envelope remain available with the + same authoritative status and evidence +- **AND** only merge-blocking behavior changes. diff --git a/openspec/changes/code-review-14-protected-range-adoption/specs/trustworthy-green-checks/spec.md b/openspec/changes/code-review-14-protected-range-adoption/specs/trustworthy-green-checks/spec.md new file mode 100644 index 000000000..a4aded40d --- /dev/null +++ b/openspec/changes/code-review-14-protected-range-adoption/specs/trustworthy-green-checks/spec.md @@ -0,0 +1,32 @@ +## Scope rescope — 2026-09-20 + +Optional preflight, approval seals and development checkpoints are not prerequisites. Keep independent protected-range verification, complete scope, authenticated runtime and signed producer compatibility. Use focused regression reproduction and current-run results; do not require immutable RED history, frozen selector inventories or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## ADDED Requirements + +### Requirement: Protected Code Review green SHALL require a verified C14 envelope + +A protected Code Review required check SHALL report success only when the +core-owned verifier envelope binds the signed C14 producer report and records +effective `pr_range` with authoritative PASS. Producer status alone SHALL NOT +constitute protected-range authority. + +#### Scenario: Producer passes but protected verification is unknown + +- **GIVEN** a producer report records PASS with `range_candidate` +- **AND** the core verifier records an omitted or mismatched required identity +- **WHEN** the protected Code Review check runs in enforce mode +- **THEN** the emitted check fails +- **AND** it reports `UNKNOWN` rather than a green PR-range result. + +#### Scenario: Verified C14 envelope passes + +- **GIVEN** the producer report and core verifier envelope are complete, + signed, mutually consistent, and bind the protected workflow context +- **AND** the envelope records effective `pr_range` with PASS +- **WHEN** the protected check completes in enforce mode +- **THEN** the check reports success +- **AND** retains the producer report and verifier envelope as distinct + artifacts. diff --git a/openspec/changes/code-review-14-protected-range-adoption/tasks.md b/openspec/changes/code-review-14-protected-range-adoption/tasks.md new file mode 100644 index 000000000..3d83ae2ec --- /dev/null +++ b/openspec/changes/code-review-14-protected-range-adoption/tasks.md @@ -0,0 +1,65 @@ +# Tasks: code-review-14-protected-range-adoption + +## Scope rescope — 2026-09-20 + +Optional preflight, approval seals and development checkpoints are not prerequisites. Keep independent protected-range verification, complete scope, authenticated runtime and signed producer compatibility. Use focused regression reproduction and current-run results; do not require immutable RED history, frozen selector inventories or committed test transcripts. + +This owner-requested planning amendment supersedes conflicting development-workflow and dependency wording below. Runtime behavior is unchanged. Replacement policy: [core #740](https://github.com/nold-ai/specfact-cli/issues/740) and [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481). + +## 1. Readiness and compatible signed identity + +- [ ] 1.1 At implementation start, create or verify the dedicated issue-linked worktree against current `origin/dev`; recovery-era worktree creation does not establish current ownership. +- [ ] 1.2 Refresh and read back existing core #680 at implementation start: parent #375, User Story type, labels, assignee, project/status, actual prerequisites, downstream #679 relationship and current ownership. Resolve concurrent implementation before proceeding; retain signed producer/runtime prerequisites without restoring optional preflight #431. +- [ ] 1.3 Select a signed C14-capable publication compatible with current core; verify packaged commit/tree, archive/signature/checksums and schema matrix. Treat the original `0.49.46` / `===0.55.1` pair as historical. Obtain a fresh signed publication if no compatible one exists. +- [x] 1.4 Update the matching internal-wiki source, correct C14/C15 dependency links, and rebuild its graph. +- [ ] 1.5 Revalidate current scope and run `openspec validate code-review-14-protected-range-adoption --strict` before tests or production edits; the recovery validation is historical. + +## 2. Named regression tests + +- [ ] 2.1 Add exactly these verifier acceptance tests before implementation: + - [ ] `test_trusted_consumer_envelope_upgrades_range_candidate_to_pr_range` + - [ ] `test_trusted_consumer_rejects_merge_base_mismatch` + - [ ] `test_trusted_consumer_rejects_multiple_best_merge_bases` + - [ ] `test_trusted_consumer_rejects_incomplete_governed_selection` + - [ ] `test_trusted_consumer_rejects_diff_or_rename_manifest_mismatch` + - [ ] `test_trusted_consumer_rejects_governed_object_type_or_mode_mismatch` + - [ ] `test_trusted_consumer_rejects_policy_config_identity_mismatch` + - [ ] `test_trusted_consumer_rejects_suppression_catalog_identity_mismatch` + - [ ] `test_trusted_consumer_rejects_unapproved_producer_or_artifact_identity` + - [ ] `test_trusted_consumer_accepts_target_tip_project_runtime_attestation` + - [ ] `test_trusted_consumer_rejects_candidate_or_untrusted_project_runtime` + - [ ] `test_candidate_verifier_lock_and_workflow_cannot_replace_trusted_authority` + - [ ] `test_missing_trusted_verifier_is_unknown_without_candidate_fallback` +- [ ] 2.2 Add focused workflow contract tests in `tests/unit/workflows/test_code_review_14_protected_range.py` for trusted verifier/import/config/root selection and job isolation, independent context regeneration across separate runners, missing/mismatched context rejection, context digest binding, full refs, separate artifacts, compatible signed fixture, and shadow/warning/enforce behavior. +- [ ] 2.3 Add focused staged-hook tests in `tests/unit/scripts/test_pre_commit_code_review.py` proving schema 1.6 status/exit consumption and continued `explicit_files` assurance. +- [ ] 2.4 Select the focused regression cases needed to demonstrate the affected behavior; no frozen selector inventory or development checkpoint. +- [ ] 2.5 Observe relevant failures before production edits and summarize the reproduction briefly; retain detailed output in ordinary CI artifacts when applicable. + +## 3. Bounded implementation + +- [ ] 3.1 Pin `ci/module-fixture.lock.json` to the verified compatible signed C14-capable publication selected in 1.3 and verify commit/tree/archive/signature/manifest/matrix identities. +- [ ] 3.2 Implement `scripts/verify_code_review_range_assurance.py` to independently derive the closed C14 manifest set and emit a separate verifier-bound envelope. +- [ ] 3.3 Update the PR orchestrator and bounded organization invocation policy to select authenticated verifier code/dependencies/roots independently of candidate files. Isolate trusted execution; regenerate canonical event/run/revision context independently in each job, compare and bind its digest, pass full refs plus `--pr-context-file`, and retain separate producer/envelope artifacts. Never rely on sharing producer runner-temp files. Missing trusted verifier is `UNKNOWN`, never candidate fallback. +- [ ] 3.4 Update `scripts/pre_commit_code_review.py` only for schema 1.6 authoritative status/exit parsing; keep staged positional files and no PR authority. +- [ ] 3.5 Stop and amend/revalidate the proposal before touching any other production path. + +## 4. Candidate verification + +- [ ] 4.1 Set the intended core release version and prepare changelog/operator guidance and reviewed rollout/rollback configuration before candidate verification. Confirm the selected signed module's declared compatibility includes that resulting core version; the unpublished candidate verifier cannot authorize its own integration. +- [ ] 4.2 Run the named regression tests against the candidate and reference the passing CI results. +- [ ] 4.3 Run workflow policy tests, contracts, type, lint, focused/full tests as required, registry integrity, signature verification, and strict OpenSpec validation. +- [ ] 4.4 Run SpecFact Code Review over the explicit base/head range and triage every finding: fix introduced/relevant defects, explain false positives, and obtain an individual documented exception with impact and linked follow-up for any real deferred finding. +- [ ] 4.5 Run the compatibility matrix against the exact signed module and resulting candidate core build on supported Python versions. If the core version or signed module changes before publication, repeat the affected compatibility checks against the new identities. + +## 5. Reviewed integration, publication and rollout + +- [ ] 5.1 Commit, push, and open the implementation PR to `dev` with issue and OpenSpec references. +- [ ] 5.2 Integrate into `dev`, then promote through a reviewed PR to protected `main`, satisfying the effective required gates. Do not require the new verifier gate before its trusted source exists or substitute a candidate verifier for that source. +- [ ] 5.3 Run final immutable module/core smoke and publish the core release through the existing protected-main release workflow. Select/authenticate the integrated verifier from the approved immutable base or release before enabling enforcement. +- [ ] 5.4 Run trusted shadow, warning, then enforcement using current results and reviewed rollout decisions; preserve `FAIL`/`UNKNOWN` semantics throughout. +- [ ] 5.5 Unblock downstream C15 adoption #679 only after the compatible C14 core release and required protected-consumer handoff are available. + +## 6. Cleanup + +- [ ] 6.1 Archive with `openspec archive code-review-14-protected-range-adoption` after completed integration and release handoff. +- [ ] 6.2 Remove the worktree and prune the feature branch only after merge. diff --git a/openspec/changes/code-review-local-report-adoption/design.md b/openspec/changes/code-review-local-report-adoption/design.md new file mode 100644 index 000000000..2821134fb --- /dev/null +++ b/openspec/changes/code-review-local-report-adoption/design.md @@ -0,0 +1,37 @@ +# Planned local report consumption + +## Existing paths and ownership + +Inspected on 2026-10-16: `scripts/pre_commit_code_review.py` shells out to the CLI, writes `.specfact/code-review.json`, and defines a minimal `CodeReviewReport`. `src/specfact_cli/registry/module_installer.py` installs verified modules and invokes `install_resolved_pip_requirements`. `src/specfact_cli/registry/module_discovery.py` and `module_packages.py` own discovery and compatibility. These are implementation touchpoints to verify again before changing behavior, not evidence of schema 2.0 support. + +Reuse ordinary invocation and existing module verification first. Adapt their narrow report and installation boundaries. Custom runtime or registry infrastructure is unnecessary for this approved outcome. Modules own project runtime and standards resolution, analyzer execution, and serialized reports. Core owns ordinary hooks, workflow invocation, installation compatibility, loading, and discovery. + +## Report boundary + +Use a small validated, version-discriminated report input at existing consumers. Schema 2.0 dispatch selects the modules #515 contract. Historical 1.6 dispatch is restricted to consumers with supported 1.6 semantics. Consumers governed by C15 retain explicit schema 1.7 dispatch and its enforcement until a separately authorized C15 retirement. This adoption neither enables C15 nor changes its owners, prerequisites or acceptance. Reject unrecognized versions, including unsupported same-major schemas, and malformed required fields before summarizing success. Any current or future helper using a numeric `>=1.6` test must not classify 2.0 as 1.6. + +Read the producer's authoritative assurance status and completeness, preserving failure and uncertainty in the summary and machine output. Score, fixability, and missing findings cannot replace assurance. Shadow mode changes blocking policy only. It retains adverse evidence and does not print a successful assurance claim for an incomplete report. Reconcile process exit and report according to the exact reviewed producer contract; contradictory evidence cannot become success. + +No consumer promotes local evidence to protected `pr_range` or capsule attestation. The protected #680 contract remains separate. Preserve C15 #679 and modules #417 ownership and acceptance rather than folding those contracts into local schema 2.0. + +## Installation boundary + +The customer CLI may run in pipx, uv tool, or uvx while project Python differs. Preserve this separation. Do not inject analyzer or module dependencies into the customer project environment. A uvx tool environment may have no pip. Use supported external tooling or declared tool dependencies for the installed CLI environment instead of unmanaged mutation or an assumption that `python -m pip` exists. Verify the actual installer path against each supported installation before claiming compatibility. + +After provisioning, exercise ordinary local review with networking disabled and valid locally verified signed modules, the selected project environment and all selected check tools available. Loading must not require fresh registry or package-index access. Missing or invalid local prerequisites retain their normal nonaccepting diagnostics; this does not promise cold offline installation or dependency bootstrap. Modules #516 owns release-level offline acceptance as part of the installed workflow. + +`--project-runtime` denotes a descriptor contract. Do not reinterpret it as an interpreter path. Modules resolve project execution using their descriptor and project discovery contracts. Core forwards only supported declared inputs. Ordinary invocation must execute the project-aware local runner with capsule acquisition/runtime, sandbox brokers and the legacy host orchestrator absent; protected/C15 contracts remain separate. + +## Rollout and risks + +Consume the exact immutable signed modules #515 candidate and its compatibility fixtures without requiring stable canonical publication first. Core supplies its own reviewed candidate for paired preflight. Modules #516 owns the coordinated stable publication after this compatibility handoff and installed release acceptance, keeping the implementation dependency graph acyclic. + +First prove one real installed local review workflow. Then collect core consumer-compatibility preflight evidence for pipx, uv tool, and uvx with exact signed module/core identities and project interpreters. Modules #516 owns release-level installed acceptance and its full manager/platform matrix; core supplies the compatibility handoff rather than duplicating that release authority. Apply schema dispatch before replacing ordinary invocation. Retain security and signature gates, including independent modules #511 and core #719/#718 work. + +Schema confusion can falsely report success. Mitigate with unsupported-major and contradictory-report cases. Installation drift can hide dependency mistakes. Mitigate with clean external environments, including uvx without pip. Trusted project tools can execute repository code. Document that trust boundary and keep protected credentials and attestation claims outside this workflow. + +Rollback pins the previous immutable compatible released CLI/module pair and retains actual adverse results. Do not reduce acceptance requirements to hide an incompatible consumer or resurrect a second capsule backend in the replacement. Optional downstream consumers remain future work. + +## Planning and later implementation + +All acceptance remains planned. Follow spec, tests, observed failing evidence, production code, then passing evidence during implementation. Before RED, bind scenario-derived tests into requirements-evidence.yaml with executable selectors and stable requirement/case identities. Before GREEN, run the Requirements evidence gate against actual test results at implementation maturity; planning inspection alone cannot establish implementation evidence. The internal wiki source and C14/C15 compatibility notes were aligned through the owning checkout on 2026-10-16, and the native graph was rebuilt. Its strict health check reports 59 pre-existing errors, down from 62, with no new errors. Unrelated source pages were preserved. This records planning alignment only; all implementation tasks remain unchecked. diff --git a/openspec/changes/code-review-local-report-adoption/proposal.md b/openspec/changes/code-review-local-report-adoption/proposal.md new file mode 100644 index 000000000..6fb2ea759 --- /dev/null +++ b/openspec/changes/code-review-local-report-adoption/proposal.md @@ -0,0 +1,46 @@ +# Change: Adopt local Code Review reports in ordinary core workflows + +## Status + +Planned only, 2026-10-16. No runtime implementation, test execution, release support, or completed acceptance is claimed. + +- GitHub issue: [core #769](https://github.com/nold-ai/specfact-cli/issues/769). +- Parent: core #375 under epic #285. Project: SpecFact CLI. Status: Todo. Type: User Story. +- Direct blocker: [modules #515](https://github.com/nold-ai/specfact-cli-modules/issues/515), `code-review-19-local-report-and-cutover`. +- Blocks only [modules #516](https://github.com/nold-ai/specfact-cli-modules/issues/516), `code-review-20-external-installation-acceptance`. + +## Why + +A customer runs an externally installed SpecFact CLI against a trusted local Python repository. Ordinary hooks and workflows need truthful local evidence without making the project adopt SpecFact's interpreter or capsule infrastructure. Core must consume the new producer contract before external installation acceptance can prove the complete workflow. + +## What Changes + +- Plan explicit report schema 2.0 dispatch in ordinary core consumers. Keep a historical 1.6 reader only where its contract is supported. Preserve explicit schema 1.7 dispatch at consumers governed by C15 until its owner authorizes a separate retirement; this adoption does not enable C15. Reject unrecognized versions, including unsupported same-major schemas. +- Consume producer assurance and completeness. Do not derive success from score or fixability. Shadow rollout preserves adverse results. +- Keep ordinary hook and workflow invocation, module installation compatibility, loading, and discovery in core. Keep project runtime resolution, customer standards, analyzers, and report production in modules. +- Collect exact installed core and signed module compatibility preflight evidence for pipx, uv tool, and uvx, independent of project Python. Modules #516 retains release-level installation acceptance and the manager/platform matrix. + +## Capabilities + +### New capabilities + +- `local-review-consumer`: ordinary core consumption of trusted local schema 2.0 evidence. + +## Non-goals + +Protected `pr_range`, capsule attestation, analyzer changes, customer-environment dependency injection, broader registry rewrites, and C15 policy redesign are excluded. Local execution trusts the repository, including its project tooling. Optional preflight, architecture, and Requirements addons may consume reports later. They are not blanket blockers. + +Core #680 retains its separate protected capsule contract. Local schema 2.0 cannot satisfy it. Core #679 and modules #417 retain their owners, status, and acceptance. Independent security work modules #511 and core #719/#718 remains independent. Signing and security gates remain mandatory. + +## Impact + +Future work is bounded to existing ordinary consumers and necessary installed-module compatibility paths. Completion requires real installed customer workflow proof, explicit schema rejection, truthful adverse summaries, and normal quality/security/signature gates. Effort requires consumer changes and a multi-install acceptance matrix; no duration estimate is asserted. Rollback pins the previous immutable compatible released CLI/module pair while retaining adverse evidence. It must not silently reinterpret schema 2.0 through an older reader or reduce acceptance to hide incompatibility. + +## Source Tracking + + +- **GitHub Issue**: #769 +- **Issue URL**: +- **Last Synced Status**: open +- **Project Status**: Todo +- **Parent Feature**: [#375](https://github.com/nold-ai/specfact-cli/issues/375) diff --git a/openspec/changes/code-review-local-report-adoption/requirements-evidence.yaml b/openspec/changes/code-review-local-report-adoption/requirements-evidence.yaml new file mode 100644 index 000000000..08c4c190a --- /dev/null +++ b/openspec/changes/code-review-local-report-adoption/requirements-evidence.yaml @@ -0,0 +1,140 @@ +schema_version: '2' +x-maturity: planned +requirements: + openspec:code-review-local-report-adoption:local-review-consumer:explicit-report-schema-dispatch: + rationale: Plan ordinary trusted-local report adoption without claiming implementation. + stakeholder_refs: + - nold-ai/specfact-cli#769 + touchpoints: + - id: planned-spec + kind: config_file + locator: openspec/changes/code-review-local-report-adoption/specs/local-review-consumer/spec.md + verification_cases: + - case_id: LOCAL-SELECT-THE-SCHEMA-2.0-CONSUMER + scenario_id: explicit-report-schema-dispatch + method: inspection + intent: 'Inspect planned scenario: Select the schema 2.0 consumer. Given a valid local schema 2.0 report; + when an ordinary core consumer reads the report; then it selects the schema 2.0 contract and does not + apply historical 1.6 semantics.' + observable: The planned spec retains this scenario; no runtime or test maturity is claimed. + - case_id: LOCAL-BOUND-HISTORICAL-SUPPORT + scenario_id: explicit-report-schema-dispatch + method: inspection + intent: 'Inspect planned scenario: Bound historical support. Given a historical 1.6 report at a consumer + that supports it; when the consumer dispatches by version; then it uses only supported historical semantics.' + observable: The planned spec retains this scenario; no runtime or test maturity is claimed. + - case_id: LOCAL-REJECT-UNSUPPORTED-EVIDENCE + scenario_id: explicit-report-schema-dispatch + method: inspection + intent: 'Inspect planned scenario: Reject unsupported evidence. Given an unsupported major version or malformed + required report data; when the consumer parses the report; then it rejects the input without claiming + successful assurance.' + observable: The planned spec retains this scenario; no runtime or test maturity is claimed. + - case_id: LOCAL-PRESERVE-SEPARATE-C15-DISPATCH + scenario_id: explicit-report-schema-dispatch + method: inspection + intent: 'Inspect planned scenario: Preserve separate C15 dispatch. A consumer governed by C15 retains schema + 1.7 dispatch and enforcement until a separately authorized retirement; local adoption neither enables + C15 nor changes its acceptance.' + observable: The planned spec retains separate C15 dispatch and ownership; no runtime or test maturity is + claimed. + - case_id: LOCAL-REJECT-UNSUPPORTED-SAME-MAJOR-SCHEMAS + scenario_id: explicit-report-schema-dispatch + method: inspection + intent: 'Inspect planned scenario: Reject unsupported same-major schemas. An ordinary consumer without C15 + governance rejects schema 1.7 or any unrecognized 1.x/2.x version rather than falling back to historical + or local semantics.' + observable: The planned spec admits only explicitly supported contracts and versions; no runtime or test + maturity is claimed. + openspec:code-review-local-report-adoption:local-review-consumer:preserve-authoritative-local-truth: + rationale: Plan ordinary trusted-local report adoption without claiming implementation. + stakeholder_refs: + - nold-ai/specfact-cli#769 + touchpoints: + - id: planned-spec + kind: config_file + locator: openspec/changes/code-review-local-report-adoption/specs/local-review-consumer/spec.md + verification_cases: + - case_id: LOCAL-PRESERVE-INCOMPLETE-SHADOW-RESULTS + scenario_id: preserve-authoritative-local-truth + method: inspection + intent: 'Inspect planned scenario: Preserve incomplete shadow results. Given producer failure, uncertainty, + or incomplete evidence in shadow mode; when core renders the local result; then the summary and machine + evidence retain the adverse truth even when enforcement is non-blocking.' + observable: The planned spec retains this scenario; no runtime or test maturity is claimed. + - case_id: LOCAL-REJECT-SCORE-BASED-SUCCESS + scenario_id: preserve-authoritative-local-truth + method: inspection + intent: 'Inspect planned scenario: Reject score based success. Given a favorable score or fixability alongside + adverse assurance or contradictory process evidence; when core decides the local result; then it does + not replace producer assurance and completeness with success.' + observable: The planned spec retains this scenario; no runtime or test maturity is claimed. + openspec:code-review-local-report-adoption:local-review-consumer:separate-installed-cli-and-project-runtime: + rationale: Plan ordinary trusted-local report adoption without claiming implementation. + stakeholder_refs: + - nold-ai/specfact-cli#769 + touchpoints: + - id: planned-spec + kind: config_file + locator: openspec/changes/code-review-local-report-adoption/specs/local-review-consumer/spec.md + verification_cases: + - case_id: LOCAL-RUN-AN-EXTERNALLY-INSTALLED-CLI + scenario_id: separate-installed-cli-and-project-runtime + method: inspection + intent: 'Inspect planned scenario: Run an externally installed CLI. Given a supported signed core/module + pair installed by pipx, uv tool, or uvx and a different project Python; when ordinary local review runs; + then core loads the compatible module while modules resolve the trusted project runtime without injecting + dependencies into the project environment.' + observable: The planned spec retains this scenario; no runtime or test maturity is claimed. + - case_id: LOCAL-RUN-WITHOUT-CAPSULES + scenario_id: separate-installed-cli-and-project-runtime + method: inspection + intent: 'Inspect planned scenario: Run ordinary local review without capsules. The installed local workflow + executes selected checks without capsule provisioning, a sandbox broker or the legacy host orchestrator; + protected and C15 contracts remain separate.' + observable: The planned spec requires the ordinary local runner; no runtime or test maturity is claimed. + - case_id: LOCAL-ARTIFACTS-OFFLINE + scenario_id: separate-installed-cli-and-project-runtime + method: inspection + intent: 'Inspect planned scenario: Run from verified local artifacts offline. With valid verified signed + modules, a prepared project environment and selected tools present locally, disabled networking does + not prevent loading or local review; missing prerequisites remain honest preparation failures.' + observable: The planned spec retains provisioned offline execution; no runtime or test maturity is claimed. + - case_id: LOCAL-HANDLE-UVX-WITHOUT-PIP + scenario_id: separate-installed-cli-and-project-runtime + method: inspection + intent: 'Inspect planned scenario: Handle uvx without pip. Given a uvx tool environment without pip; when + installation compatibility provisions required tool dependencies; then it uses supported external tooling + or declarations rather than unmanaged mutation.' + observable: The planned spec retains this scenario; no runtime or test maturity is claimed. + - case_id: LOCAL-PRESERVE-THE-RUNTIME-DESCRIPTOR + scenario_id: separate-installed-cli-and-project-runtime + method: inspection + intent: 'Inspect planned scenario: Preserve the runtime descriptor. Given a supported --project-runtime + descriptor; when core invokes the module; then it forwards descriptor semantics without treating the value + as an interpreter path.' + observable: The planned spec retains this scenario; no runtime or test maturity is claimed. + openspec:code-review-local-report-adoption:local-review-consumer:keep-protected-contracts-and-independent-gates-separate: + rationale: Plan ordinary trusted-local report adoption without claiming implementation. + stakeholder_refs: + - nold-ai/specfact-cli#769 + touchpoints: + - id: planned-spec + kind: config_file + locator: openspec/changes/code-review-local-report-adoption/specs/local-review-consumer/spec.md + verification_cases: + - case_id: LOCAL-RETAIN-SEPARATE-PROTECTED-ACCEPTANCE + scenario_id: keep-protected-contracts-and-independent-gates-separate + method: inspection + intent: 'Inspect planned scenario: Retain separate protected acceptance. Given a local schema 2.0 report; + when a protected #680 or C15 #679 consumer is considered; then local evidence does not satisfy the protected + capsule contract or change C15 owners, status, or acceptance.' + observable: The planned spec retains this scenario; no runtime or test maturity is claimed. + - case_id: LOCAL-RETAIN-INDEPENDENT-SECURITY-AND-SIGNATURES + scenario_id: keep-protected-contracts-and-independent-gates-separate + method: inspection + intent: 'Inspect planned scenario: Retain independent security and signatures. Given ordinary local adoption + and optional downstream consumers; when implementation readiness is evaluated; then signing and independent + security gates remain while optional preflight, architecture, and Requirements addons create no blanket + blockers.' + observable: The planned spec retains this scenario; no runtime or test maturity is claimed. diff --git a/openspec/changes/code-review-local-report-adoption/specs/local-review-consumer/spec.md b/openspec/changes/code-review-local-report-adoption/specs/local-review-consumer/spec.md new file mode 100644 index 000000000..0e7e0994b --- /dev/null +++ b/openspec/changes/code-review-local-report-adoption/specs/local-review-consumer/spec.md @@ -0,0 +1,104 @@ +## ADDED Requirements + +### Requirement: Explicit report schema dispatch + +Core SHALL dispatch only through explicitly supported report contracts and versions, support historical 1.6 only where supported, and reject any unrecognized version, including an unsupported minor version within a known major. A numeric >=1.6 branch SHALL NOT classify schema 2.0 as 1.6. Consumers governed by the separate C15 contract SHALL retain explicit schema 1.7 dispatch and C15 enforcement until its owner authorizes a separate retirement cutover. This change SHALL NOT enable C15 or alter its acceptance. + +#### Scenario: Select the schema 2.0 consumer + +- **GIVEN** a valid local schema 2.0 report +- **WHEN** an ordinary core consumer reads the report +- **THEN** it selects the schema 2.0 contract and does not apply historical 1.6 semantics + +#### Scenario: Bound historical support + +- **GIVEN** a historical 1.6 report at a consumer that supports it +- **WHEN** the consumer dispatches by version +- **THEN** it uses only supported historical semantics + +#### Scenario: Preserve separate C15 dispatch + +- **GIVEN** a schema 1.7 report reaches a consumer governed by the C15 contract +- **WHEN** ordinary schema 2.0 adoption is evaluated before a separately authorized C15 retirement +- **THEN** the consumer retains schema 1.7 dispatch and C15 enforcement without treating the report as schema 1.6 or schema 2.0 evidence +- **AND** this local adoption does not enable C15 or change its owners, prerequisites, status or acceptance + +#### Scenario: Reject unsupported same-major schemas + +- **GIVEN** an ordinary consumer supports only the released local 2.0 and bounded historical 1.6 contracts, and is not governed by C15 +- **WHEN** it receives schema 1.7, an unknown 1.x version or an unrecognized 2.x version +- **THEN** it rejects that version without routing it through 1.6 or 2.0 semantics or claiming successful assurance + +#### Scenario: Reject unsupported evidence + +- **GIVEN** an unsupported major version or malformed required report data +- **WHEN** the consumer parses the report +- **THEN** it rejects the input without claiming successful assurance + +### Requirement: Preserve authoritative local truth + +Core SHALL consume producer assurance and completeness independently of score and fixability. Shadow mode SHALL preserve adverse truth and SHALL NOT claim protected PR authority. + +#### Scenario: Preserve incomplete shadow results + +- **GIVEN** producer failure, uncertainty, or incomplete evidence in shadow mode +- **WHEN** core renders the local result +- **THEN** the summary and machine evidence retain the adverse truth even when enforcement is non-blocking + +#### Scenario: Reject score based success + +- **GIVEN** a favorable score or fixability alongside adverse assurance or contradictory process evidence +- **WHEN** core decides the local result +- **THEN** it does not replace producer assurance and completeness with success + +### Requirement: Separate installed CLI and project runtime + +Core SHALL preserve externally installed CLI and project Python separation, supported installation tooling, signed compatibility, and project-runtime descriptor semantics. Ordinary local invocation SHALL use the project-aware local runner without capsule acquisition or execution. Once required signed artifacts and execution prerequisites are provisioned and locally verified, ordinary local review SHALL run without requiring network access. + +#### Scenario: Run an externally installed CLI + +- **GIVEN** a supported signed core/module pair installed by pipx, uv tool, or uvx and a different project Python +- **WHEN** ordinary local review runs +- **THEN** core loads the compatible module while modules resolve the trusted project runtime without injecting dependencies into the project environment + +#### Scenario: Run ordinary local review without capsules + +- **GIVEN** a supported externally installed signed CLI/module pair, a selected project environment and no capsule runtime, catalog or acquisition service +- **WHEN** the customer invokes ordinary trusted local review +- **THEN** core invokes the project-aware local runner and selected checks execute without capsule provisioning, a sandbox broker or a legacy host orchestrator +- **AND** protected and C15 consumers retain their separately governed contracts + +#### Scenario: Run from verified local artifacts offline + +- **GIVEN** an externally installed compatible CLI/module pair, valid locally verified signed modules, a prepared project environment and all selected check tools are available locally +- **WHEN** networking is disabled and ordinary local review runs +- **THEN** loading and invocation use those artifacts without requiring a registry or package-index lookup and the selected checks execute +- **AND** missing local prerequisites remain actionable preparation failures rather than claimed offline bootstrap support + +#### Scenario: Handle uvx without pip + +- **GIVEN** a uvx tool environment without pip +- **WHEN** installation compatibility provisions required tool dependencies +- **THEN** it uses supported external tooling or declarations rather than unmanaged mutation + +#### Scenario: Preserve the runtime descriptor + +- **GIVEN** a supported --project-runtime descriptor +- **WHEN** core invokes the module +- **THEN** it forwards descriptor semantics without treating the value as an interpreter path + +### Requirement: Keep protected contracts and independent gates separate + +Core SHALL retain protected #680 and C15 #679/modules #417 contracts and independent security/signature gates. Local adoption SHALL NOT introduce blanket optional-addon blockers. + +#### Scenario: Retain separate protected acceptance + +- **GIVEN** a local schema 2.0 report +- **WHEN** a protected #680 or C15 #679 consumer is considered +- **THEN** local evidence does not satisfy the protected capsule contract or change C15 owners, status, or acceptance + +#### Scenario: Retain independent security and signatures + +- **GIVEN** ordinary local adoption and optional downstream consumers +- **WHEN** implementation readiness is evaluated +- **THEN** signing and independent security gates remain while optional preflight, architecture, and Requirements addons create no blanket blockers diff --git a/openspec/changes/code-review-local-report-adoption/tasks.md b/openspec/changes/code-review-local-report-adoption/tasks.md new file mode 100644 index 000000000..7c9c08a6f --- /dev/null +++ b/openspec/changes/code-review-local-report-adoption/tasks.md @@ -0,0 +1,60 @@ +# Planned implementation tasks + +All tasks remain planned and unchecked. Planning validation is not implementation evidence. Before starting an +implementation unit estimated above two hours of active work, split it into additional unchecked tasks; external CI +waiting does not count as completed work. + +- [ ] 1. Create or verify a dedicated git worktree on an issue-linked implementation branch from the current approved + baseline; reject dev/main and the old planning checkout, then revalidate modules #515 delivery, GitHub + hierarchy/status, exact producer schema and ordinary consumer paths. +- [ ] 2. Confirm spec scenarios against the exact reviewed producer candidate contract and strictly validate OpenSpec + and requirement mappings before adding tests. +- [ ] 3. Add behavior tests for explicit supported-version dispatch, separate C15 schema 1.7 routing and retirement + authority, historical 1.6 boundaries, unsupported same-major and major versions, malformed evidence and + process/report disagreement. +- [ ] 4. Add behavior tests for producer assurance/completeness and truthful shadow summaries independent of + score/fixability. +- [ ] 5. Add scenario-derived installation and module loading/discovery tests for pipx, uv tool and repeated uvx + invocations, including uvx without pip, a different project Python, descriptor semantics, capsule-free ordinary + execution, provisioned offline execution and required failure cases before implementing compatibility; bind every + behavior scenario to executable test selectors in requirements-evidence.yaml before recording RED, preserving stable + requirement and case identities. +- [ ] 6. Record observed RED commands, timestamps, results and causes for the consumer and installation tests in + openspec/changes/code-review-local-report-adoption/TDD_EVIDENCE.md before production edits. +- [ ] 7. Implement narrow ordinary hook/workflow report consumption and invocation updates. +- [ ] 8. Implement only necessary module loading/discovery/install compatibility using supported external tooling or + declared dependencies; preserve descriptor semantics and customer environment isolation. +- [ ] 9. Run the consumer-compatibility preflight tests for exact installed core and signed modules across pipx, uv + tool and repeated uvx, including no pip, a different project Python and verified local artifacts with networking + disabled; hand preflight evidence to modules #516, which owns release-level installation acceptance and the + manager/platform matrix. +- [ ] 10. Record GREEN commands, timestamps and results in + openspec/changes/code-review-local-report-adoption/TDD_EVIDENCE.md only after all mapped tests and the + consumer-compatibility preflight matrix pass and the Requirements evidence gate verifies executable selectors and + implementation maturity against the actual test results, including one complete installed customer local review + workflow; rerun affected tests and refresh GREEN evidence after any later production correction. +- [ ] 11. Run required format, type-check, lint, YAML, contract, smart-test, independent security, clean-code review + and applicable signature/version gates; cover public API contract decorators where applicable and resolve findings. + Before PR publication, verify module signatures with `hatch run ./scripts/verify-modules-signature.py + --require-signature`; if module assets change, bump their versions, re-sign affected manifests and reverify until + green. +- [ ] 12. Identify and update affected docs/ reference, guides, adapters and getting-started content, README.md and + docs/index.md; preserve documentation frontmatter and update navigation if pages are added or moved. Align the + internal wiki source and rebuild its graph from that repository root. +- [ ] 13. Select the owner-approved semver change for the implemented behavior; synchronize pyproject.toml, setup.py, + src/__init__.py and src/specfact_cli/__init__.py, and add a dated CHANGELOG.md entry after quality gates and + documentation, before opening the implementation PR. +- [ ] 14. After documentation and version edits, run the final full SpecFact code review and affected quality gates, + verify .specfact/code-review.json is fresh for the final changed inputs, resolve all findings including warnings or + obtain an explicitly approved exception, and refresh affected test/GREEN evidence. +- [ ] 15. Revalidate the final spec and implementation evidence, create signed commits, push the branch and publish the + implementation PR to dev. +- [ ] 16. Complete required CI and review feedback in bounded repair passes; after the last substantive repair, rerun + the final full review and affected quality/test gates, resolve findings including warnings or obtain an explicitly + approved exception, and verify .specfact/code-review.json and affected GREEN evidence are fresh. Retain honest status + for external blockers. +- [ ] 17. Seek owner-authorized integration of the reviewed implementation through the normal merge process. +- [ ] 18. After actual completed implementation and reviewed merge, run `openspec archive + code-review-local-report-adoption` from the repository root; reconcile source tracking and project state without + marking incomplete acceptance complete. Publish archival changes through normal reviewed delivery, then clean up only + the clean, fully integrated implementation worktree and branch while retaining needed evidence and unrelated work. diff --git a/openspec/changes/dependency-update-consolidation/CHANGE_VALIDATION.md b/openspec/changes/dependency-update-consolidation/CHANGE_VALIDATION.md new file mode 100644 index 000000000..4628a822b --- /dev/null +++ b/openspec/changes/dependency-update-consolidation/CHANGE_VALIDATION.md @@ -0,0 +1,33 @@ +# Validation: dependency-update-consolidation + +Validated 2026-10-10 Europe/Berlin against dev +553b46f01c6581c96dc92133b6e77404c8df7cd9 and captured original PR heads. + +The human approved all five existing PRs and material scope reconciliation. +The active #747 metadata has parent #194, required labels, project assignment, +approved In Progress ownership and no native blocking edges. #756 is merged; +its original proof files remain historical rather than being replayed as new RED. + +No public Python API, analyzer contract, module manifest or immutable fixture +changes. GitPython's containment behavior is a security repair with a fresh +boundary test. Hatchling 1.32.4 restores its plugin interface and remains compatible +with supported Python versions. Jekyll 4.4.1 requires JSON 2.x; the JSON 3 proposal +is reworked into a compatible floor and the major migration remains deferred. +Selected plugin versions accept Jekyll 4.4.1 and Ruby 3.2 by published constraints. + +Validation decision: approved scope is implementable with executable checks still +pending. Pure lock changes require regenerated parity; behavior probes require +genuine failing evidence followed by final results. Current artifact screening, +consumer/matrix, required gates and fresh head-bound authority determine readiness. +No dependency safety, passing CI or release readiness is inferred from constraints. + +Rollback refs are retained under refs/codex-security-backup/20261010/pr-N. +Explicit leases prevent concurrent branch overwrites. Shared lock conflicts, +JSON incompatibility and rebased proof are mitigated by sequential regeneration, +the compatible JSON contract and fresh final-head verification. + +Revalidated 2026-10-11 Europe/Berlin after #3/#758 normal protected merges. +New root baseline: 30f98cc14e074802a9f21c6b84e69e743eee6442. +The complete mapping is unchanged and its test-authored provider passes with +no findings; fresh local RED is retained before candidate replay. Native +RED and final-head validation remain required. No broader scope is added. diff --git a/openspec/changes/dependency-update-consolidation/REVIEW_AND_PROMOTION.md b/openspec/changes/dependency-update-consolidation/REVIEW_AND_PROMOTION.md new file mode 100644 index 000000000..a8571581d --- /dev/null +++ b/openspec/changes/dependency-update-consolidation/REVIEW_AND_PROMOTION.md @@ -0,0 +1,176 @@ +# Security dependency stack: review and promotion + +Prepared on 10 October 2026 (Europe/Berlin). Confidence: Medium. Dependency +compatibility is exercised; a required local smart-test run and protected +Requirements authority remain unresolved. This document does not authorize merge, +release publication, a policy exception or unrelated dependency upgrades. + +## Review order and disposition + +GitHub native stack #757 preserves all five PR numbers: + +`dev ← #744 ← #727 ← #752 ← #754 ← #753` + +| PR | Disposition | Result | +| --- | --- | --- | +| [#744](https://github.com/nold-ai/specfact-cli/pull/744) | Python security/build and single 0.55.5 bugfix | GitPython 3.2.0; Hatchling 1.32.4; minimal Python lock/export; sdist includes unchanged bundle-mapper assets | +| [#727](https://github.com/nold-ai/specfact-cli/pull/727) | Compatible JSON security hardening | `>=2.21.2,<3`; locked JSON 2.21.2 and Jekyll 4.4.1 retained; JSON 3 deferred | +| [#752](https://github.com/nold-ai/specfact-cli/pull/752) | Redirect security update | redirect-from 0.17.0; unsafe-scheme/encoding probes | +| [#754](https://github.com/nold-ai/specfact-cli/pull/754) | Feed security update | feed 0.18.0; language and CDATA escaping probes | +| [#753](https://github.com/nold-ai/specfact-cli/pull/753) | Link compatibility update | relative-links 0.9.1; Markdown/navigation/baseurl/rebuild probes; frozen Ruby CI evidence | + +The user explicitly selected GitPython 3.2.0 after validation, superseding 3.1.62. +Original #756 evidence remains intact; its merged repair and subsequent #755 +baseline are preserved. [#748](https://github.com/nold-ai/specfact-cli/pull/748) +retains its dated inventory as a planning follow-up, with the current approved +scope inherited. [#747](https://github.com/nold-ai/specfact-cli/issues/747) stays open +for broader accepted work. No lower-version duplicate was found in the inspected +open dependency PRs; none is closed without verified replacement coverage. + +## Exact artifacts and source review + +Selected executable artifacts were downloaded inertly, checked against registry +SHA-256 metadata, and compared with their upstream release tags before local +candidate execution. Ruby CI downloads these selected gems into its local cache +and checks the same hashes before bundle installation. Other locked Ruby packages +and Linux platforms remain unchanged. Temporary macOS platform locks are local +validation artifacts and are not delivery inputs. + +| Artifact | SHA-256 | License | +| --- | --- | --- | +| GitPython 3.2.0 wheel | `bd70c5ec05cd2b797423e7eb312147d2458d3cca92085888fba2213f85905537` | BSD-3-Clause | +| GitPython 3.2.0 sdist | `fb92310af6844d96adc95ca066ed2e617c00e1dbd146a326626c81e72e18cc2e` | BSD-3-Clause | +| Hatchling 1.32.4 wheel | `08ecf7548fb48205e7f213d70c71e67b8271b7242093dc3f1da578b42c734a2c` | MIT | +| Hatchling 1.32.4 sdist | `c4468f73144c054d2aab4ef0f0378c43b9878bf07f8ffd6b79690e970d375f07` | MIT | +| redirect-from 0.17.0 gem | `703dab0428b20bb5d550ab83d3c28b65bb5ecc0f734c5e61ee680de391b3e897` | MIT | +| feed 0.18.0 gem | `8e6829f455b8764a8fa1bbb198d0ebb3525128dacc6ffe9d15f4df7388bc3647` | MIT | +| relative-links 0.9.1 gem | `6d5d70578c669ef9ee4c67f617d91497e0506fe5c1a059e484a900e18efb202e` | MIT | + +GitPython's authenticated upstream tag resolves to +`6a7180a9dfcb276755a8af99dd78155f775a6b14`; all 37 wheel Python files match apart +from the generated version substitution. Hatchling's 67 package Python files +match its release tag; PyPI trusted-publisher metadata is observed, without a +claim of local cryptographic attestation verification. The Ruby runtime files +match their respective tags (9, 6 and 6 files). + +The [GitPython tagged changelog](https://github.com/gitpython-developers/GitPython/blob/3.2.0/doc/source/changes.rst) +lists the additional security fixes. Redirect/feed release source records the +unsafe-target and escaping fixes. Some global advisory API records were unavailable; +that does not negate upstream fixes or establish exhaustive advisory coverage. +Both frozen Python advisory audits pass. Hatchling's Socket potential-vulnerability +alert concerns its configurable code-version plugin, byte-identical to 1.32.0; +this project uses static version metadata. Source triage is scoped to this build. +Protected Socket checks pass on the recorded Python implementation runs; no trust +exception was added. The new Ruby workflow pins action commits, including upstream +Ruby setup `e81a8fa391b11a595c1b7ed84177cc0fe02faf83` (unsigned upstream commit). + +## Executable evidence and limits + +- Actual GitPython containment RED precedes the candidate. Source distribution + rebuilding also fails before the missing source inclusion is repaired. +- One 0.55.5 wheel builds from the sdist and installs with dependency resolution + disabled on Python 3.11.15, 3.12.13 and 3.13.14. Git init/config/commit/branch/ + checkout/diff and ChangeAnalyzer consumers pass. Signed assets are unchanged. +- All 17 documentation-script tests pass after the JSON bound change. Redirect, + feed language, feed CDATA and relative-link rebuild have separate actual RED + results, then pass using selected versions at root and `/preview`. +- Full documentation builds pass for each updated plugin layer at both deployment + paths. Existing Minima/Sass deprecation warnings remain baseline output. +- Frozen parity, uv lock check, both audits, trust/licenses, Bandit, Semgrep, + strict module signatures and release-version/PyPI-ahead checks pass locally. + BasedPyright's committed npm runner passes after installation: zero errors; + 1526 existing project warnings. Changed YAML and workflow lint pass. Global + YAML output retains existing unrelated/archived errors. +- New GitPython/build tests have zero SpecFact review findings. The existing docs + test file's 20 warnings and one information finding are outside modified + assertions and explicitly retained as an unchanged-baseline exception in the + TDD ledger. No new clean-code regression is accepted. +- Historical local full smart testing reported 25 failures, one error, 3189 passes and nine + skips after marketplace installation replaces beartype inside the running + interpreter. Earlier constrained testing retains its separate frozen/latest + module-resolution conflict. The environment is restored with frozen sync. + These results are not claimed green or replaced by focused probes. +- [Hosted run 38086498399](https://github.com/nold-ai/specfact-cli/actions/runs/38086498399) + passes Python quality/tests, all runtime matrix jobs, security/trust/licenses, + signatures, Socket and reproducibility on original implementation head 6ccc37a2. + Its two installed inventories contain 180 identical normalized identities and + its two SPDX SBOMs are byte-identical. The authoritative type artifact has zero + errors. These artifacts are retained as historical evidence after re-signing. +- [Native RED run 38088121368](https://github.com/nold-ai/specfact-cli/actions/runs/38088121368) + records `failing-first-proven`, gate pass, on GitHub-verified authored head + `e9950388f97c7103b2f39f7fe4eefa5ced92282b`. The isolated startup lookup was + reproduced failing, corrected using actual trusted distribution roots and + replayed without changing the candidate hash/install controls or verifier. +- [Prior final run 38087716606](https://github.com/nold-ai/specfact-cli/actions/runs/38087716606) + records native `verified`, gate pass, on pre-email-rebinding head 19a46c0a. + Current Python head `76a42fc8fadad2fc7a17de97dd9076f7fb68a016` has a verified + GitHub signature and a fresh [final run 38088309187](https://github.com/nold-ai/specfact-cli/actions/runs/38088309187). + Protected Trusted Requirements Authority still rejects the context. Agent + acceptance and native execution never substitute for that authority. + +Re-signing corrected the commit/key email mismatch without changing any tree. +The remaining layers are re-signed sequentially. Current checks must be read from +each PR's final head after publication; older runs do not imply current readiness. + +## Dev-to-main promotion checklist + +1. Review all five PRs. The repaired local full smart suite passes; require its + final hosted counterparts and current exact-head authority. Obtain trusted + Requirements authority for the actual final context; never fabricate a human + acceptance record or relax the policy. +2. Require final-head Python and Ruby CI, both Socket checks, advisory/trust/license + gates, code review, Requirements and signature checks. Retain normalized + installed-package/SBOM and type JSON artifacts. Repeat affected checks after + any code, base, mapping, branch or signature change. +3. Merge the dependency stack bottom-to-top into dev through GitHub's stack + workflow. Inspect each parent-relative lock diff after retarget/rebase. Merge + #748's planning follow-up only once its parent is integrated; keep #747 open. +4. Open/review the dev-to-main promotion at the integrated dev SHA. Require strict + module signatures and Requirements promotion parity, four synchronized 0.55.6 + version sources, built wheel/sdist metadata and separate Security/Fixed entries. + Recheck that PyPI still reports a lower release before tagging/publishing. +5. Validate production documentation at the custom-domain root and configured + prefixed URL. Archive the whole OpenSpec change only after broader accepted + tasks finish. Refresh internal wiki status from the sibling repository after + actual merges; archive worktrees after reviewed integration. + +Risks: a rebase invalidates proof (rerun on final heads); JSON 3 breaks Jekyll's +2.x contract (keep the explicit bound); shared locks accumulate resolver churn +(regenerate each layer from its actual parent and inspect the diff). Several CI +runs and maintainer review are still required. No tag or release is published. + +Rollback: originals are captured under `refs/codex-security-backup/20261010/pr-N` +and local `original-pr-N.json` records. Additional authored/restack/email heads +are preserved under the same ref prefix. Restore each original remote branch +with an explicit lease against its current head, restore original base metadata, +and unlink native stack #757 only as part of a deliberate rollback. Revert coherent +inputs together if changes have merged; never reset shared dev/main history. + +## Fresh foundation and Python replay (2026-10-11) + +Policy #3 and prerequisite #758 are merged through normal required gates. +Protected dev 30f98cc14e074802a9f21c6b84e69e743eee6442 is the new stack baseline. +Fresh hosted RED 38119009577 is retained on 8cd2a305; the complete five-layer +plan precedes dependency implementation and remains unchanged downstream. +Local full smart tests now pass: 3291 passed, 9 existing skips, 195.20 seconds, +64% coverage. All three Python audits, trust/license, strict signatures, +Semgrep/Bandit, frozen parity, version/PyPI-ahead, typing and changed-Python +review pass. The 0.55.5 sdist/wheel and Git consumers pass on Python 3.11-3.13. +The historical failures above are preserved as such; final head CI, downstream +proof reuse and exact member grants remain pending until sequential publication. +The single 0.55.5 release includes both prerequisite and dependency fixes. + +Release adjustment: protected dev now reserves 0.55.5 through #758. The required +packaged-artifact version gate requires a strictly newer version, so the single +consolidated planned release is 0.55.6. The four canonical sources, generated +metadata and changelog are updated together; no gate or check is relaxed. + +Fresh final assembled local validation: 3291 tests passed with 9 existing skips +in 179.08 seconds and unchanged 64% coverage. Both deployment-path Jekyll builds +and all redirect/feed/relative-link probes pass. Every plugin lock was generated +from its parent conservatively, retaining Linux platforms and unrelated versions. +CI actions and each selected new plugin artifact are pinned/hash-checked in the +first layer that adopts them. The complete proof mapping and mapped containment +selector are identical to the new authored RED boundary. Current hosted status +is published on the five PRs after final-head validation; these pre-publication +notes do not substitute for that validation. diff --git a/openspec/changes/dependency-update-consolidation/TDD_EVIDENCE.md b/openspec/changes/dependency-update-consolidation/TDD_EVIDENCE.md new file mode 100644 index 000000000..b46b02ae0 --- /dev/null +++ b/openspec/changes/dependency-update-consolidation/TDD_EVIDENCE.md @@ -0,0 +1,630 @@ +# Focused repair evidence + +## Authorship and acceptance + +The human approved this focused #747 repair on 2026-10-09 Europe/Berlin. The mapping +acceptance record identifies the actual coding agent that inspected these scenarios; +it is not a human member grant and does not satisfy Trusted Requirements Authority. + +## Before repair + +After defining this scoped specification and JWT regression tests, the exact unchanged +analyzer fixture was restored. Existing Python 3.12.13 / pytest 9.1.1 executed: + +`python -m pytest tests/unit/security/test_frozen_jwt_compatibility.py tests/unit/versioning/test_analyzer.py -q --no-cov --junitxml=` + +Result: 3 failed, 2 passed in 0.97 seconds. All three analyzer tests fail because fixture +initialization stages .git/config before production analyzer assertions. The existing +host environment already has PyJWT 2.15.1, urllib3 2.8.0, virtualenv 21.14.6 and Semgrep +1.180.0; this run is fixture failing evidence and JWT control evidence, not a frozen +baseline security reproduction or candidate graph acceptance. Retain the baseline-lock +host CI RED artifact before publishing repaired inputs. + +Native selected planned evidence passed against the immutable released module fixture. +Strict selected OpenSpec passed. No dependency input or production code has changed in +this tests/spec publication. Final, artifact, consumer, full quality and Python matrix +results are pending and must be recorded separately. + +## Initial hosted authored snapshot (rejected) + +Signed commit f6c41baa9201f48a3af9536b855907c191ff6463 ran Requirements Evidence +37857823201 and retained artifact 11584408763, digest +sha256:39afadcfa7915d981c15a58ea515b0720db724d0c0894f89c47e5a72bfc366e1. +Its pytest outcomes were 1 failed (JWT option mutation), 4 passed. The producer correctly +rejected passing selectors as RED; observed maturity is incomplete, not accepted RED. +This run cannot authorize final proof. The frozen validator's analyzer outcomes differ +from local and full Hatch-based CI; preserve both rather than claiming equivalent scopes. + +Hosted type checking also caught four options-dictionary annotation errors in the new +tests. Correct them with the library's declared Options type and validate before using +a replacement authored snapshot. Preserve the original diagnostic artifact. + +## Replacement authored checks + +The replacement mapping binds only the regression that fails on the frozen validator. +The ordinary JWT control and three analyzer cases remain independently required tests, +with inspection retaining their actual execution outcomes. They are not mislabeled as +native RED/final execution proof. The agent acceptance binds the per-source digest. + +Strict selected OpenSpec, staged native test-authored planning, Markdown lint, Ruff +check/format and staged diff whitespace checks passed. BasedPyright using the declared +project and explicit Python 3.12 environment analyzed 671 files with zero errors and +zero diagnostics in the new test (1,526 existing repository warnings). The nested local +SpecFact review returned nine unknown-member warnings for jwt/pytest in the new test; +these do not reproduce with the explicit interpreter. This is a documented local +import-context limitation, not permission to ignore hosted or final review findings. +Contract input detection found no authored contract changes. Hosted replacement RED +and all repaired-input checks remain pending. + +## Accepted JWT RED and independently discovered cache lifecycle + +Authored head 0fd1d1935c26f1577b0248653a926fb5b8f2ea60, run 37859074381, artifact +11585460994 (sha256:4f842f820f0350461a80a7f4cfab8cfe236bdb62b93c69b82527cf728147ef60) +has native observed/required red, passed verdict. Producer/final delivery still fails +by design at the authored phase; the accepted RED report is not merge-ready evidence. + +The candidate five-package graph and corrected analyzer fixture passed the five focused +repository tests. Eight independent boundary probes passed. Both frozen audits, native +trust/license and export parity passed; the nearest consumer batch had 174 passing cases +and an unavailable smoke fixture. Supplying the immutable fixture separately made that +direct-launcher smoke pass. OSS Semgrep scan/gate has zero current findings; Bandit has +zero medium/high findings and retains existing low findings. Explicit-interpreter type +checking has zero errors and no new JWT diagnostics. These are preliminary candidate +checks; final stable-source checks and hosted matrix remain required. + +The fresh read-only reviewer confirmed inherited seed logs use the same cache paths in +old/new virtualenv. An inert persisted entry was still selected by 21.11.0. Upgrading +rejects new unverified downloads but does not sanitize previously compromised external +Hatch/user caches. The repository smoke fallback must use isolated app data; no shared +user-cache deletion is authorized or performed. + +After adding the cache scenario and caller regression, the unchanged caller failed the +new test in 0.77 seconds: its captured virtualenv command omitted isolated app data and +periodic-update suppression. Preserve this genuine failure and republish authored proof +with unchanged locks/fixture/caller. Native RED/final mapping now contains both failing +JWT and cache-caller regressions; acceptance is rebound. Old RED cannot substitute for +this revised mapping. + +## Accepted combined RED and repaired-input checks (9 October 2026, Europe/Berlin) + +The unchanged caller/locks/fixture authored commit +`24a67a043f133c5b03ca8f9845d586b57a288d81` (tree +`cf9dff7de058a011d6c92e55bbeb70da81f97db8`) has accepted native RED for both mapped +regressions: run `37859922355`, producer job `113592935499`, artifact `11586300680`, +digest `sha256:a221cdf9fc884acf9f1c66c922fe988c590406aed54b8ec8d709efd97ecaed1e`. +Observed/required maturity is red with a passed producer verdict. The intentionally +failing authored delivery run does not establish green final verification. + +The repaired graph changes exactly five of 184 resolved packages: PyJWT 2.15.1, +urllib3 2.8.0, virtualenv 21.11.0, Semgrep 1.179.0 and python-discovery 1.6.0. Semgrep's +nearest compatible release is necessary for the PyJWT constraint; discovery is required +by virtualenv. The Semgrep sentinel follows its exact lock while preserving floor/MCP +and no-waiver assertions. No release, module pin, trust exception or gate is changed. + +Exact source/wheel hashes and public Socket artifact alerts were reviewed before the +five candidate wheels were installed. Provenance subject/publisher metadata matched +the reviewed artifacts; cryptographic attestation verification was not performed and +Semgrep source provenance was unavailable. Virtualenv's license alert was reconciled +against its MIT LICENSE, metadata and third-party notices. Semgrep's optional Pro +installer remains outside the repository's OSS invocation; this is not a universal +dependency-safety claim. Existing external caches are not silently sanitized. + +The exact hash-installed Python 3.12.13 candidate executed the two native selectors: +2 passed in 0.70 seconds. Pytest emitted two record_property/xunit2 compatibility +warnings; the native executor's identity properties remain present. This local +pre-publication execution is not immutable-commit final reconciliation. Hosted final +Requirements proof, exact-head human authority and all effective CI remain required. + +Eight JWT/seeding/option-injection boundary probes passed, including blocked metadata, +missing/mismatched digest rejection, a matched wheel control and ordinary seed fallback. +Seven additional urllib3 probes passed in 0.08 seconds: bounded chunk-size/trailer reads, +deflate EOF with alternate trailing payloads, proxy-context identity enforcement and +ordinary chunk/deflate/default TLS behavior. Inert data and mocked download/wrap calls +exercise those boundaries without executing malicious artifacts or claiming live TLS +integration coverage. Nearest consumer checks retain their separate outcomes above. + +Final scoped Ruff format/check, full repository Ruff checks, safe-write guard, explicit +project BasedPyright (671 files, zero errors, 1,526 baseline warnings), reviewed frozen +export parity and `uv lock --check` passed. The first delivery check could not access +the sandboxed default uv cache; the isolated-cache rerun passed. Scoped YAML lint passed +after semantic-preserving formatting: parsed mapping and acceptance digest are unchanged. +Repository-wide YAML output retains unrelated existing errors despite the wrapper's +zero exit code; do not present that wrapper exit as a clean YAML result. Strict selected +OpenSpec passed; applicable all-change validation retains the unrelated governance delta +failure (39/40). Module signing/version and release checks are unchanged-scope controls. + +### Explicit local review dispositions + +Complete final SpecFact review `review-7ed23a21-a7ab-48be-bb21-8f5a9fcbbb9e` at +2026-10-08T23:49:45Z returned zero errors, three warnings and four information findings. +The following rare exceptions preserve concrete evidence; they do not waive CI or +independent producers: + +- `banned-generic-public-names` matches any public name containing `data`. It matches + the specific cache regression's `seed_app_data` and the unchanged malformed-authority + test's `metadata_failure`; neither is a generic API name. Preserve descriptive tests + and the Git-bound authored selector rather than weakening or editing the rule. +- GitPython's `IndexFile.add` has an unknown `fprogress` callback annotation. The same + diagnostic reproduces on the exact dev fixture with the explicit candidate interpreter; + changing the staging root adds no type-safety regression. Its argument remains list[str]. +- Four AST length suggestions concern byte-equivalent function bodies from dev: rootless + demo creation, registry construction, fresh-consumer proof and malformed-authority + rejection. Explicit fixture/command assembly preserves observable setup and assertions; + no behavior-preserving simplification is demonstrated by line count alone. + +### Full local test scope and environment-specific rerun + +`SMART_TEST_USE_HATCH=false python tools/smart_test_coverage.py run --level full` on the +initially hash-installed candidate returned 3,201 passed, 11 skipped, four failed and six errors +in 157.72 seconds. Network/cache/home-write restrictions explain the smoke, fixture +acquisition, export and startup failures; a smart-runner unit test also inherited the +override and asserted the default Hatch path. Preserve this failed run, not a full PASS. +The five owning test files were rerun directly without that override, with network and +isolated uv-cache access: 144 passed, two warnings in 51.90 seconds. This recheck covers +every failed/errored case without changing tests to accommodate the local restrictions. +Nested Hatch invocations in this broad scope subsequently changed 34 installed versions, +including Semgrep 1.180.0. Therefore these broad results are not proof of a stable frozen +graph throughout execution. Restore the hash-installed candidate and rerun the isolated +native selectors and focused security controls; hosted frozen delivery/matrix remain +authoritative. No dependency input is changed by this disposable-environment drift. +Hosted full-suite and matrix results are still independent required evidence. + +The restored hash-installed graph then passed seven repository regressions in 1.07 +seconds, fifteen isolated boundary/control probes in 0.58 seconds, and the native +two-selector executor in 0.53 seconds. Both final advisory gates passed. Review initially +could not load macOS trust anchors and later reported absent type/lint tools; neither +run was accepted as complete. The final run used the reviewed certifi trust bundle, +committed npm BasedPyright and separately hash-locked lint tools with the frozen Python +interpreter. No TLS verification was disabled. Semantic-only YAML formatting retained +the accepted source mapping digest +`sha256:d7740714ca372a5fc9758b11010c15a8c6c4cc46f07be41810dc5d53a1ad787d`. + +Hosted/current final review can introduce actionable findings and remains a separate gate. +Original #748 and broader #747 tasks stay open. Final publication, integration and #755 +baseline refresh remain unchecked until actually executed. + +## Process-boundary proof replay (9 October 2026, Europe/Berlin) + +The first signed repair candidate 2ad2b5adf9c90c27086cfcbed9b2730d697c3079 +retains its implementation and evidence. Final provenance rejected its direct runtime +import as changed proof support (`stale-red-proof`). It is not accepted final proof. +Use the existing process-boundary integration-test pattern: the parent freezes the same +cache, pip-install and launcher assertions; a bounded child exercises the runtime script. +No verifier, producer, trust policy or CI gate is changed. The selected cache test now +lives in its own file so unrelated direct imports cannot alter the proof-support closure. + +A temporary local probe of exactly the same child command observed missing isolated-cache +flags on the dev script and both flags on the candidate. Before retaining new RED, this +replayed authored snapshot has unchanged dev locks, fixture and runtime caller. Old RED +remains historical evidence; the revised mapping needs fresh authored acceptance/RED. +The prior candidate's passing checks are historical, not current-head final results. + +The internal wiki mirror was updated in the authoring host and its graph rebuilt on +9 October 2026. The isolated cloud review checkout lacks that sibling repository. + +- [ ] After integration, refresh the mirror's merged/dependency status and rebuild the + graph from the internal repository root; if unavailable, retain this follow-up. + +Revised parent regression executed on the unchanged dev caller: one genuine assertion +failure in 0.54 seconds (missing isolated-cache arguments). New JWT/process-boundary +tests have zero explicit-project type errors/warnings. Public #747 now records the +owner-approved focused implementation slice without replacing #748's broader plan. +The hierarchy cache refreshed successfully with 53 issues. + +## Accepted process-boundary RED and final repair + +Fresh authored commit `2b64c8c6a169db7c61f7355cb6ddf61a464be62e`, tree +`5f7bd033ed70120ebd63b97981bb576f9adea4ec`, ran hosted Requirements `37863114131`, +producer job `113603337985`, artifact `11586716892`, byte digest +`sha256:23b81974d50ffb80c4ca81dbcdec922f9207a2c59603f1965695893430568057`. +Its two mapped selectors have accepted observed/required RED with passed verdict and +no findings. Authored-phase final delivery is intentionally not green. + +Only after reading this authentic report, reapply the same five screened package +updates, coherent export, fixture staging root, Semgrep sentinel and isolated runtime +caller from the preserved first candidate. The selected parent test files and mapping +remain unchanged through the final repair. This is the fifth shared correction batch; +the earlier first candidate and rejected final provenance are retained. + +Current source mapping digest: +`sha256:21363424683cbe97c5f674bddee60aee7a808d5b6adafbbf5eb159c148e53591`. +Hosted final authority, proof, ordinary checks and current reviews remain required. +The old first candidate's green ordinary CI is not substituted for current-head CI. + +Replayed pre-publication checks: seven repository regressions passed in 1.04 seconds; +both native selectors passed in 0.48 seconds. Final SpecFact self-review +`review-9cc9cce0-b38f-4ae2-a302-d24bb42442f6` at 2026-10-09T00:11:34Z has zero errors, +two reproduced baseline warnings and four unchanged AST suggestions, with the explicit +dispositions above. The isolated parent/JWT tests have zero diagnostics. Whole-project +BasedPyright analyzed 672 files with zero errors and 1,526 baseline warnings. Ruff +check/format, scoped YAML, coherent frozen export and strict selected OpenSpec passed. +The sandboxed export attempt could not resolve the package index; permitted-access +reexecution passed. Applicable all-change validation remains 39/40 with the known +unrelated governance delta failure. No canonical-spec placeholder failures are counted +as change validation. Hosted/current final checks and reconciliation are still pending. + +## Sixth correction batch: delivery target isolation (9 October 2026, Europe/Berlin) + +The human explicitly approved one additional bounded batch after five were consumed. +Published candidate `d4835e5633b5889de0467dcd227494b34932ce23` passes ordinary hosted +CI, including the full Python 3.12 suite and security audit; local final reconciliation +is verified. Fresh hosted execution in run `37863820202` still imports the trusted base +PyJWT 2.13.0 and fails the original JWT regression. Neither the local result nor the +passing producer establishes hosted final completion. No member grant was posted for +this candidate. + +The approved test-harness patch isolates the hash-pinned delivery package rather than +changing the trusted verifier. Preparation reproduced the 2.13.0 caller-options mutation +and passed both cases against 2.15.1 in 0.77 seconds. This is preparation evidence, not +accepted native RED. Restore the five production/lock/fixture files to unchanged dev +for the authored phase, retain the same requirement/case identities, and obtain fresh +hosted RED before reapplying the screened repair. Earlier RED remains historical. +The selected parent assertions and tests must remain unchanged from this fresh RED +through final publication. Missing uv, network, hashes or execution fail the probe. + +Authored local execution of both mapped probes against unchanged dev produced two +genuine assertion failures in 0.64 seconds. The isolated JWT probe observed the seven +leaked disabled claim options; the cache probe observed missing isolation arguments. +Explicit-project type checking of the revised JWT file has zero errors/warnings; Ruff +check and selected strict OpenSpec pass. These results supplement the forthcoming +accepted hosted RED rather than replacing it. + +The revised scoped self-review completed with zero blocking findings. Its one AST +advisory suggests collapsing the low-branch probe function. Retain the explicit +subprocess argument lists: hash enforcement, binary-only/no-dependency installation, +isolated Python and bounded timeouts are independent, auditable security obligations. +Reducing those lists to one line would obscure them without removing behavior. +Markdown lint passed after removing added duplicate blank lines. The internal mirror +was updated and its graph rebuilt for this approved probe design. An initial local +staged planner attempt could not create the shared Git index lock under the sandbox; +this was unavailable local evidence, not a valid native result. + +Authored run `37896045048` on `85b69ed9f7e73c27a790ef596e8c713f6cf6cdd3` +contains both genuine assertion failures but its producer rejects binding with +`prior-red-proof-invalid`. The test-only tree still descended from earlier production +commits, which the unchanged binder rejects across history. Preserve that rejected +artifact and authored head; publish the same tested assertions from unchanged dev +without those production ancestors. Reverting production bytes does not establish +a test-only provenance chain. No acceptance or native proof is manufactured. + +## Accepted isolated-delivery RED and sixth final correction + +The clean authored commit `0f9d114be7e4fd70083d0c1a101590c2ab9ca06d`, tree `6f0c67bc1618219aa1b621f6f2548166ca0c895e`, has +accepted hosted RED in run `37896570864`, artifact `11600054315`, ZIP +byte digest `sha256:b30ddc50df79e0ceb1465baaec8f3ab8872f60e5b4f9c4d458c470651d4befa0`. Its retained report is passed/pass, observed and +required RED, with no findings. Both selected tests have genuine failures. The +unchanged producer and binder attest the test-only source history. + +Only after reading this report, reapply the same five screened package changes, +coherent export, fixture staging root, Semgrep sentinel and isolated caller from the +preserved repair. The approved package probe and cache parent tests, mapping and +acceptance record remain unchanged from this authenticated RED through final repair. +This final publication consumes the sixth owner-authorized correction batch. The +rejected earlier authored chain and all five prior batches remain accounted for. +Current-head hosted final execution, authority and completed reviews are still +required; preceding ordinary CI and local checks cannot establish overall green. + +Final pre-publication scoped suite: 25 tests passed in 2.23 seconds; both unchanged +native selectors pass. Ruff check/format, project BasedPyright (zero errors), +Markdown lint, selected strict OpenSpec, frozen export parity and uv lock check +passed. All-change validation retains only the previously recorded governance delta +failure (39/40). Final self-review completed with zero blocking findings: the same +two baseline warnings and four unchanged AST suggestions, plus the explicitly +disposed delivery-probe readability advisory. The earlier warning dispositions +and the explicit security-argument rationale apply; no finding is silently waived. +Hosted current-head checks and native final reconciliation remain independent gates. + +## Authored remaining Python slice (10 October 2026, Europe/Berlin) + +Current dev 553b46f01c6581c96dc92133b6e77404c8df7cd9 retains merged #756 +proof, tests and frozen fixes. The owner approved the five retained PRs and +0.55.5 release preparation. #747 metadata, #748 planning scope and the internal +wiki were reconciled before dependency inputs changed. Earlier evidence remains +historical; no previous RED is reused for the new containment requirement. + +The isolated Python 3.12.13 baseline was populated by uv sync --frozen --all-extras +and reused by Hatch through HATCH_ENV_TYPE_VIRTUAL_PATH=.venv. The unchanged +GitPython 3.1.61 delivery wheel reached the mocked clone boundary for ../outside; +no real clone or outside write occurred. The new containment regression and the +updated backend/release expectations returned three failures in 1.70 seconds. +The final authored containment test still fails after flag-list simplification. +The two frozen audits returned PASS; they do not cover every upstream disclosure: +GitHub's reviewed GHSA-59cr-6r3x-644w still requires GitPython 3.1.62. + +Strict selected OpenSpec and staged native test-authored planning passed. The +acceptance identifies codex:/root as implementation agent and is not a human +Trusted Requirements Authority grant. Ruff formatting/checks and explicit-project +BasedPyright passed with zero errors and 1,526 existing warnings. Scoped YAML +lint passes; the full wrapper reports unrelated archived-YAML failures despite +its zero exit code. No authored contract changed; contract detection returned its +unchanged result. Hosted RED, candidate execution and current final CI remain pending. + +The first local review returned zero errors, one baseline naming warning and +three AST suggestions. The new probe's static install flags were factored into +a descriptive immutable tuple to reduce its function length without relaxing +hash/index/no-dependency controls. The baseline warning and two baseline AST +suggestions at test_release_promotion_security_gates.py:506/243 are byte-equivalent +to dev and were already explicitly dispositioned in #756 above; preserve those +assertions and source identities rather than altering unrelated historical proof. + +Exact wheel/gem hashes match registry metadata. Package source comparisons match +upstream tags, with GitPython's sole generated __version__ substitution reconciled +against its release metadata. All five selected packages have MIT/BSD licenses. +No candidate package has yet executed. Public Socket screening of GitPython 3.1.62 +shows network/shell/filesystem/URL capabilities. Hatchling 1.32.4 reports a medium +potential-vulnerability alert for its code-version source plugin; that file is +byte-identical to the installed 1.32.0 source. This repository supplies a static +project.version, no dynamic version and no code-source configuration. The scoped +triage finds no new reachable code-version input in this build. This is not a +universal safety claim, alert dismissal or security-policy exception. Current +repository Socket checks and an explicit alert disposition remain release gates. + +A fresh registry check also found GitPython 3.2.0 and upstream additional security +fixes. Its global advisory records are currently unavailable. The version decision +was returned to the owner; 3.2.0 has not been installed or silently substituted. + +## Python stack implementation evidence (10 October 2026) + +The user approved GitPython 3.2.0 after validation. Its exact wheel SHA-256 is +`bd70c5ec05cd2b797423e7eb312147d2458d3cca92085888fba2213f85905537`; +the sdist is `fb92310af6844d96adc95ca066ed2e617c00e1dbd146a326626c81e72e18cc2e`. +GitHub authenticates the 3.2.0 tag signature and commit +`6a7180a9dfcb276755a8af99dd78155f775a6b14`. All 37 wheel Python files match +that tag except the expected generated version substitution. The wheel metadata +states BSD-3-Clause, Python >=3.8 and gitdb >=4.0.1,<5. Repository consumers use +GitCmdObjectDB; no explicit GitDB backend was found. The tagged changelog lists +six additional security fixes; missing global advisory API records do not negate +those upstream fixes. Source: . + +The final authored containment probe failed on locked 3.1.61 before adoption +(`gitpython-red-amended.log`, one failure). After the targeted refresh, the +containment/JWT/delivery/release/versioning suite passed all 42 tests. The only +package version changes in uv.lock are GitPython, Hatchling and project metadata; +PyJWT 2.15.1, Semgrep 1.179.0 and virtualenv 21.11.0 remain unchanged. + +A real offline source build failed with both the unchanged parent backend 1.32.0 +and reviewed 1.32.4: the sdist omitted the wheel's forced bundle-mapper inclusion. +The new actual-build regression failed before adding `/modules/bundle-mapper` to +the sdist include list and passed afterward (one test, 0.81 seconds). The rebuilt +wheel retains byte-identical module metadata; no signed asset changed. Both +release artifacts now build using the hash-installed backend without resolution. + +One built wheel installs and exercises init/config/commit/branch/checkout/diff and +ChangeAnalyzer consumers on Python 3.11.15, 3.12.13 and 3.13.14. CLI/package metadata +is 0.55.5 and GitPython is 3.2.0. Extra `uv pip check` reported existing Z3 5.0.0.0 +wheel-platform metadata incompatibility (`macosx_13_3_arm64`) although imports and +consumers pass; this advisory result is retained, not claimed green. The repository +CI wheel protocol does not use that extra check. No Z3 version or policy changed. + +Both frozen advisory audits, graph/export parity, uv lock check, dependency trust, +version sources, strict PyPI-ahead (latest 0.55.4), format, typing, lint, Bandit, +Semgrep (zero findings) and all four strict module signatures pass locally. +The initial license scan found test-installed Pylint in the shared validation +environment; restoring the frozen graph yields zero license violations. The +initial smart run observed 32 failures and one error after test-environment drift +and externally injected module roots. Fresh scoped reproduction passes 31 tests; +the full rerun uses child-install version constraints and the immutable fixture +without the extra module-roots variable. Its final result is recorded separately. +Repository-wide YAML emits existing archived/other-change errors; changed YAML +is independently linted. Changed-test self review now has zero findings; previous +unchanged #756 findings retain their documented historical disposition. + +Socket artifact views for GitPython 3.2.0 wheel and sdist show shell/filesystem/URL +capabilities. Hatchling 1.32.4 retains a potential-vulnerability alert for its +configuration-selected code version plugin. The flagged file is byte-identical +with backend 1.32.0 and this project uses static version metadata, but this source +triage does not replace the protected Socket verdict or create an exception. + +Hosted Requirements run 38085488787 on authored head +`d3461301618dd76501c30498bf365e0a65d77697` stopped with `acceptance-missing` +for 15 active sources before execution. It is authenticated diagnostic evidence, +not native RED proof. Local planned and test-authored gates pass using the pinned +fixture and actual agent acceptance; no human authority was fabricated. Final +protected Requirements and Socket eligibility remain necessary before merge. + +## Compatible JSON layer evidence (10 October 2026) + +PR #727 starts from the signed Python implementation. The authored JSON floor +check fails on the parent declaration (one real failure in `727-red.log`) and +passes after declaring `>= 2.21.2`, `< 3` (all 17 documentation-script tests pass). +Bundler 2.3.5 regenerated the lock conservatively; removing its incidental local +macOS platform leaves only the JSON dependency bound changed. Locked JSON 2.21.2 +and Jekyll 4.4.1 and all resolved package versions remain unchanged. JSON 3 is +explicitly deferred because Jekyll 4.4.1 requires JSON 2.x. The documentation +security and compatibility scenarios are authored before the following Ruby +behavior probes and implementation. + +The completed parent smart rerun reports 3210 passed, 13 skipped and one failed +marketplace direct-launcher integration test. That test attempts to resolve a +companion module against latest annotated-doc 0.0.5, conflicting with the frozen +0.0.4 constraint. No exception or unlocked dependency update is introduced; this +required gate remains unresolved. Hosted Requirements approval and protected +Socket eligibility also remain pending; local agent acceptance is not human +authority. + +## Redirect layer evidence (10 October 2026) + +The real Jekyll output probe fails on redirect-from 0.16.0 because an unsafe +JavaScript scheme is rendered (`752-red.log`). The earlier combined fixture +also exposed a control-character scheme parser crash; that diagnostic is retained +separately. Only redirect-from 0.17.0 and its declaration change in the generated +parent-relative delivery lock. The hash-verified reviewed gem is installed from +a local cache using a disposable platform lock; delivery inputs stay Linux-only. +The same output probe passes at root and `/preview`, covering JavaScript/data +scheme rejection, browser-normalized controls, ordinary redirect maps, external +URLs, HTML attributes and JSON-script targets. The new read-only PR workflow +installs the frozen docs bundle, runs probes and builds both deployment paths. + +The #727 self-review reports zero errors, 20 warnings and one information item. +Every finding points outside the modified JSON-floor function; those functions +are byte-identical to dev (dynamic import typing, existing duplicate loaders, +generic test fixture class name and long navigation fixture). Record a narrow +unchanged-baseline exception for this dependency-only layer rather than alter +those unrelated tests or old proof. New JSON assertions have no finding. + +## Feed layer evidence (10 October 2026) + +With the unchanged parent feed 0.17.0, the language probe fails its attribute +injection assertion and the independent CDATA probe fails to parse actual output +(`754-language-red.log`, `754-cdata-red.log`). Only feed 0.18.0 and its declared +patch floor change in the conservatively regenerated lock. Its reviewed SHA-256 +`8e6829f455b8764a8fa1bbb198d0ebb3525128dacc6ffe9d15f4df7388bc3647` +is checked before the local frozen installation. Language values round-trip in +feed, alternate-link and entry attributes without injected attributes. Content +and summary CDATA preserve the terminator text, and feed URLs honor the base URL. +All feed and inherited redirect probes pass at root and `/preview`; both full +Jekyll builds also succeed. Existing Minima/Sass deprecation warnings remain +baseline output. The PR workflow runs both feed cases with the inherited checks. + +Published Python head 6ccc37a2 has successful hosted Tests/Compatibility, all +Python launcher/wheel matrix jobs, reproducible installed-package/SBOM evidence, +both advisory audits, dependency trust/licenses, independent analysis, signatures, +quality and both Socket checks (run 38086498399). Requirements execution/authority +remain failed (run 38086498423); those failures cannot be replaced by local proof. + +## Relative-link layer evidence (10 October 2026) + +The same-site rebuild probe fails on locked relative-links 0.8.0 because target +URLs remain cached (`753-red.log`). The conservatively generated parent-relative +lock changes only relative-links to 0.9.1 and its declaration. The reviewed gem +SHA-256 `6d5d70578c669ef9ee4c67f617d91497e0506fe5c1a059e484a900e18efb202e` +is checked before installation. All inherited security probes and relative-link +checks pass at root and `/preview`, including included navigation attributes, +Markdown/nested links, fragments, external URLs and a changed target on rebuild. +Both full-site builds pass. This compatibility update is recorded under Fixed, +separately from redirect/feed/JSON/GitPython security changes. Python locks and +the single 0.55.5 release version stay inherited from #744. + +## Final stack verification and publication checkpoint (10 October 2026) + +Native stack #757 is registered in the approved five-PR order. Exact action +commits and selected Ruby artifact hash checks now make the final documentation +workflow reproducible before candidate installation. See REVIEW_AND_PROMOTION.md +for dispositions, immutable source identities, release checklist and rollback. + +The isolated verifier bug is reproduced by starting pytest with -I -S and the +explicit trusted site path: sysconfig points to the base interpreter. Resolving +the actual gitdb/smmap distribution roots repairs this without importing the +candidate in the parent or changing its hash/no-dependency install controls. +The same corrected test fails on frozen GitPython 3.1.61 (clone boundary reached) +and passes on 3.2.0. Native authored run 38087626467 records RED gate pass; final +run 38087716606 records verified gate pass. Their overall authored failure is +expected while RED is retained for final reconciliation. Original failed runs +remain historical evidence. + +GitHub initially rejected the commit email against the signing key. Re-signing +uses its verified identity and changes no tree. +Native RED run 38088121368 binds the same probe to verified authored e9950388; +current final 76a42fc8 replays it. Protected authority remains rejected; no human +role is fabricated. The full local smart run retains 25 failed, 3189 passed, nine +skipped and one error caused by in-process marketplace beartype replacement. +Frozen sync restores the environment; audits, parity, trust/licenses, independent +analysis, strict signatures, version gates and focused security/build/docs probes +pass. Installing the committed npm runner repairs the initial missing-tool type +and lint diagnostics (zero errors, 1526 unchanged project type warnings). These +partial results do not complete the outstanding readiness tasks. + +### Ruby CI cache correction + +Final hosted Ruby run 38088539828 validates all three candidate artifact hashes +then fails bundle installation because the partial vendor/cache contains only +the selected plugins, not the rest of the frozen graph. Move these reviewed gem +files to Bundler's installation cache under the Ruby 3.2 ABI path; leave the +project vendor/cache absent so unchanged locked dependencies can be fetched. +BUNDLE_FROZEN stays true, candidate hashes and version inputs stay unchanged. +The failed run remains retained; fresh full workflow validation is required. + +## Fresh protected foundation (2026-10-11 Europe/Berlin) + +Organization policy #3 merged at 0787cc7e45a12e4651e22d9447d21a29e31e98b4. +Prerequisite #758 merged through required checks at protected dev +30f98cc14e074802a9f21c6b84e69e743eee6442; its final 809cd3db head has +all ordinary/native checks and exact-head member authority. This supersedes +the local isolation failures recorded above, which remain historical evidence. +The complete five-layer plan is authored before replay on this new baseline. +The GitPython containment, sdist rebuild and backend expectation tests genuinely +fail (3 failures, 1.09 seconds) before dependency or packaging edits. +Local log: approved-744-local-red.log. A new hosted native RED is required before +implementation; prior RED and earlier implementation runs remain historical. + +Fresh native RED run 38119009577 on authored 8cd2a305f1276df3e877d5a55492d3c2a70c7bf7 +retains one genuine containment failure (0.42 seconds), gate pass and +`failing-first-proven`. The workflow intentionally remains failed until final +reconciliation; this is retained RED rather than merge-ready status. +Full native mapping: sha256:a2ba40a8ff692cfb7bca70785bd411d1aeb54d3afd824a086a3e7a830ab88ee4. +Full native plan: sha256:94e049b71b00de0d52b4afa8b7ce9b08729bc97ae5ae379db1682af179125461. +Mapped test bytes remain identical through implementation and downstream replay. + +Fresh local Python implementation (2026-10-11): 25 focused controls pass; +full smart testing passes with 3291 passed, 9 existing skips, 2 warnings in +195.20 seconds and unchanged 64% coverage. The sdist rebuilds its wheel; +hash-frozen no-resolution wheel installs and Git consumers pass on Python +3.11.15, 3.12.13 and 3.13.14. All three audits, trust/licenses, strict module +signatures, frozen parity, uv lock, version/PyPI-ahead, formatting/lint, +Semgrep/Bandit and npm-authoritative typing pass (zero errors, 1522 baseline +warnings). Current changed-Python SpecFact review passes with zero findings. +The only frozen package version changes are GitPython and Hatchling. +Final-head hosted/native proof and member grants remain required after publication. + +The required packaged-artifact version gate rejects retaining 0.55.5 after +PR PR #758 reserved that version on protected dev. The original availability condition +therefore selects the next available patch, 0.55.6, for the one consolidated +release. All four sources, generated metadata and changelog are synchronized; +no tag or release is published. The containment selector and mapping remain +unchanged. Version-sensitive build/wheel and quality checks are repeated. + +Current broad Python review retains one unchanged baseline warning and two +information findings in test_release_promotion_security_gates.py: the naming +rule flags the nested authority subprocess probe in +test_invalid_utf8_authority_is_stable_metadata_failure (line 506), and the +expanded provenance/invalid-UTF8 fixture setup produces two LOC advisories. +Those blocks are byte-identical to protected dev; the only modified assertion +is the required package version. Retaining them is an explicit narrow baseline +exception for fixture clarity; new GitPython/build tests have zero findings. +The 0.55.6 version-sensitive controls pass (39 tests); the rebuilt 0.55.6 +sdist/wheel and all three Python wheel consumer probes also pass. + +Fresh #727 replay: the security-floor test genuinely fails against the new +PR PR #744 parent (1 failure, 0.09 seconds), then all 17 documentation script tests +pass. Bundler regenerates the lock retaining JSON 2.21.2/Jekyll 4.4.1 and all +existing package identities; only the declared JSON bound changes. Complete +Requirements mapping and the mapped containment test remain inherited unchanged. + +Fresh PR #752 replay: the redirect probe genuinely rejects the 0.16.0 parent +for rendering a disallowed URI scheme, then passes with 0.17.0 at both root and +/preview. Both production documentation builds pass. The exact reviewed gem hash +is rechecked before execution. Bundler conservative regeneration changes only +the selected plugin; Linux delivery platforms remain unchanged. CI actions are +pinned and the selected gem is hash-checked before installation in this layer, +without waiting for the top layer. Workflow/YAML and Ruby syntax checks pass. + +Fresh PR #754 replay: the parent feed 0.17.0 genuinely fails separate language +attribute and CDATA/XML controls. Feed 0.18.0 passes both controls and retained +redirect probes at root and /preview; both production documentation builds +pass. The immutable gem hash is rechecked before execution and CI adds the +same pre-install hash check. Conservative lock regeneration changes only +jekyll-feed. Workflow/YAML and Ruby syntax checks pass. + +Fresh PR #753 replay: relative-links 0.8.0 genuinely fails the rebuild target +cache control. Version 0.9.1 passes that control, rewritten links, included +navigation and root/preview base URLs. All retained redirect/feed security +probes and both production documentation builds pass. The reviewed gem hash +is rechecked before execution and checked in CI before installation. +Conservative regeneration changes only relative-links; the final Ruby lock +matches the reviewed original package identities and has no resolver churn. +Workflow/YAML and Ruby syntax checks pass. Final assembled local smart testing +passes: 3291 passed, 9 existing skips, 2 warnings in 179.08 seconds, 64% coverage. +The release-reference correction only changes CHANGE_ORDER and task 2.6; +Python/Ruby implementation and mapped test bytes remain unchanged. +Exact final-head hosted proof, authority, reviews and quality results must be +read from the final PR checks, not inferred from this pre-publication snapshot. + +Fresh assembled Python review covers all eight changed Python paths: zero +errors, 21 existing warnings and 3 existing information findings. The 20 +documentation fixture warnings/one information finding retain the previously +documented narrow exception: dynamic loader type authority, generic subprocess +probe naming and intentionally explicit duplicate fixture shapes are unchanged +outside the JSON-floor assertion. The additional authority-fixture warning and +two information findings are the protected-base exception recorded above. +No finding targets the changed JSON assertions or new GitPython/build tests. +Full npm-authoritative typing has zero errors and the same 1522 baseline warnings. diff --git a/openspec/changes/dependency-update-consolidation/design.md b/openspec/changes/dependency-update-consolidation/design.md new file mode 100644 index 000000000..230d80e2e --- /dev/null +++ b/openspec/changes/dependency-update-consolidation/design.md @@ -0,0 +1,126 @@ +# Compatible dependency consolidation + +## Historical focused baseline repair (#756) + +Reuse existing #747 and its change identity. This implementation branch covers only the +focused scenarios; do not copy #748's broader inspection-only mapping into a verified claim. + +Two early assumptions: patched packages resolve together and analyzer failures indicate a +production regression. The three-package graph fails Semgrep/PyJWT and virtualenv/discovery +constraints. The five-package candidate resolves exactly PyJWT 2.15.1, urllib3 2.8.0, +virtualenv 21.11.0, Semgrep 1.179.0 and python-discovery 1.6.0. Unchanged dev reproduction +shows the analyzer fixture fails by staging .git/config before invoking production code. + +Publish spec/tests/agent acceptance first with unchanged locks and the broken fixture; +retain authentic host RED. Never fabricate run IDs, late-RED authority, reviewer identity +or prior receipts. Repair through reviewed lock generation and the narrow staging root. +Preserve trust exceptions, signatures, release versions, gates and exact module fixture. + +JWT options must remain caller-owned and expiry must remain enforced on reuse; normal signed +tokens and trailing signature padding must work. Check urllib3 proxy TLS/body boundaries, +virtualenv discovery/seeding and Semgrep invocation separately. Upgrading does not sanitize +seed-update logs inherited from an older virtualenv. Use a workspace-local app-data root and +no periodic update for the ephemeral smoke launcher fallback; preserve ordinary editable +installation and never delete the shared user cache. Hatch/user environments outside that +scoped caller need separate clean recreation after a suspected cache compromise. + +Obtain independent patch review and applicable native/quality/security/Python matrix proof. +Integrate through normal review, then update #755 and obtain its new exact-head member grant. +Roll back coherent inputs only to a reviewed safe graph, retaining evidence. Broader #748 +scope and all planned workflow runtime tasks remain outside this repair. + +## Evidence boundary corrected after first hosted run + +The released RED contract requires every selected pytest case to fail. The initial hosted +plan was rejected because only the JWT option regression failed; the legitimate control +and fixture cases passed in that frozen validator environment. Preserve the rejection. +Select the actually failing JWT regression and, after the cache amendment, its independently +failing caller regression for native RED/final execution. The +legitimate control and all three analyzer tests remain independently required in focused +checks and the full current-head suite; their inspection cases do not assert native +execution proof or waive failures. Do not change tests merely to manufacture RED. + +Source review disproved virtualenv 21.7.13's completeness: default background seed updates +can cache unverified wheels when PyPI metadata is blocked. The minimum source-complete +candidate is 21.11.0, which raises UnverifiedWheelError on unavailable metadata/digest. +Re-resolve and screen this revised exact candidate before adoption. + +The independent review and inert-cache probe confirmed that even 21.11.0 can select an +unverified wheel already logged by an old updater. This does not invalidate rejection of +new unverified downloads, but requires the smoke fallback's fresh local cache boundary. +Author and reproduce its regression before changing the caller. + +The cache regression uses a separate subprocess test with unchanged parent assertions +and a bounded child caller. This matches existing integration-test conventions and +keeps mutable implementation separate from immutable pytest support. Retain a fresh +authored/RED/final lineage and all prior rejected evidence. + +## Delivery-package probe and trusted verifier separation + +The independent Requirements consumer retains its unchanged base verifier graph. +The JWT regression therefore exercises the delivery package in an isolated child: +extract the one hash-pinned PyJWT entry from the coherent CI export, install only +its binary wheel into a temporary target with hash verification and no dependencies, +and run Python with isolated mode and site loading disabled. Frozen parent assertions +check the bound version, unchanged caller options and expiry on reuse. Fail on missing +tooling, ambiguous/missing locks, install failures or unavailable execution. No verifier +package, workflow, authority policy or gate is changed. The separate compatibility +control and full delivery graph checks remain independently required. + +## Authorized five-layer stack (2026-10-10, Europe/Berlin) + +This amendment supersedes the historical focused-slice scope exclusions above. +Start each dedicated worktree from its preceding layer and preserve merged #756 +inputs and proof. #744 owns Python/build inputs and the single 0.55.6 patch bump; +PR #727 owns the compatible JSON declaration; #752/#754/#753 own their selected +Jekyll plugins. Generate locks conservatively and inspect parent-relative diffs. + +GitPython 3.2.0 closes GHSA-59cr-6r3x-644w. Its path containment regression must +fail against the unchanged delivery wheel before adopting the screened candidate. +Hatchling 1.32.4 restores the plugin interface broken in 1.32.3; both declarations +and the build expectation must agree. Preserve PyJWT 2.15.1, urllib3 2.8.0, +virtualenv 21.11.0, Semgrep 1.179.0 and python-discovery 1.6.0. + +Jekyll 4.4.1 requires JSON ~>2.6. Use >=2.21.2,<3 and retain locked 2.21.2; +do not take #727's incidental Jekyll downgrade. JSON 3 migration is deferred. +Redirect-from 0.17.0 and feed 0.18.0 release notes identify security fixes even +where a global advisory record is unavailable. Relative-links 0.9.1 requires +Ruby >=3.0; the documented Ruby 3.2 build remains supported. Test dangerous and +ordinary redirect/feed inputs and rewritten links at root and nonempty base URLs. + +Preserve #756 proof files byte-for-byte. Its completed native test cases become +historical inspection cases in this slice's active mapping, with the original +mapping available from merged #756. A fresh authored snapshot maps the new failing +GitPython boundary only; existing controls and configuration checks remain required +independent tests. No passing control is mislabeled as RED. Subsequent layers add +their own scoped specifications and evidence before dependency adoption. + +Capture old heads before rewriting and use explicit leases. A failed lease stops +publication; re-read concurrent work rather than overwriting it. Native stack +registration is optional; linear base branches and body links are the fallback. +Rebases require fresh final-head evidence. Do not close retained PRs or #747. +Keep #748's dated inventory historical and defer its broader package assessment. + +Before promotion verify frozen parity, both audits, trust/Socket, licenses, +Python 3.11-3.13 wheel installation, GitPython consumers, backend metadata, +Jekyll output, required quality/review/Requirements and module signatures. +The promotion checklist is reviewable preparation, not authorization to merge +or publish. Restore captured refs only with new explicit leases; restore coherent +dependency inputs together and never represent vulnerable rollback as release-safe. + +## GitPython scope amendment + +On 10 October 2026 the user approved GitPython 3.2.0 after validation. +It includes the 3.1.62 containment fix and six additional security fixes listed +in the tagged upstream changelog. Python 3.7 removal does not affect the supported +3.11–3.13 matrix. The pure Python GitDB backend is deprecated; repository consumers +must retain the default GitCmdObjectDB backend. Exact artifact and consumer checks +remain required; an advisory database miss does not establish absence of risk. + +## Source artifact compatibility + +The real sdist-to-wheel build must preserve the existing forced inclusions. +Include `/modules/bundle-mapper` in the source archive, preserving its bytes and +signatures. This fixes the parent omission exposed by backend compatibility +validation. Keep this ordinary packaging repair separate from security fixes in +the changelog and retain its failing and passing actual-build regression results. diff --git a/openspec/changes/dependency-update-consolidation/proposal.md b/openspec/changes/dependency-update-consolidation/proposal.md new file mode 100644 index 000000000..6871a29e6 --- /dev/null +++ b/openspec/changes/dependency-update-consolidation/proposal.md @@ -0,0 +1,65 @@ +# Change: Consolidate compatible security dependency PRs + +## Why + +The owner authorized the remaining security and compatible package updates on +2026-10-10 Europe/Berlin, including all five existing PRs #744, #727, #752, #754 +and #753. Their shared locks and outdated bases need sequential reconciliation. +The earlier focused repair #756 merged into dev on 2026-10-09; its specifications, +tests and historical proof remain part of this change and must be preserved. + +## What Changes + +- EXTEND #744 with GitPython 3.2.0 and Hatchling 1.32.4, synchronized declarations, + build expectation, generated Python lock and hash-protected CI export. +- MODIFY #727 into compatible JSON hardening: declare `>=2.21.2,<3`, keep JSON + 2.21.2 and Jekyll 4.4.1, and strengthen the existing security-floor test. +- EXTEND #752/#754 with redirect-from 0.17.0 and feed 0.18.0 security fixes; + verify unsafe redirect and feed escaping boundaries alongside ordinary output. +- EXTEND #753 with relative-links 0.9.1 and navigation/base-URL compatibility proof. +- ADD one 0.55.6 bugfix release, subject to the live PyPI-ahead check, with all + canonical version sources, generated metadata and changelog synchronized. +- MODIFY the existing PR branches into + `dev <- #744 <- #727 <- #752 <- #754 <- #753`, retaining numbers, captured heads, + signed commits and explicit force-with-lease protection. Register a native + GitHub stack when available; otherwise retain the linked branch chain. +- MODIFY conflicting planning PR #748 and the internal wiki to reflect completed + #756 work, the selected stack and deferred broader inventory work. + +JSON 3 migration and all unrelated dependency upgrades remain deferred. No module +fixture, security policy, trusted authority, gate relaxation or workflow runtime +implementation is included. Keep #747 open and do not archive the whole change. +Close an older duplicate only after verifying its relevant fixes are covered and +linking its replacement; none was found in the inspected open PR inventory. + +## Capabilities + +### New Capabilities + +- `dependency-update-consolidation`: extends the existing #747 maintenance slices. + +### Modified Capabilities + +None. Existing delivery, signature and Requirements controls remain authoritative. + +## Impact + +- Affected specs: dependency-update-consolidation; prior #756 requirements retained. +- Affected inputs: pyproject.toml, setup.py, uv.lock, requirements/ci/locked.txt, + docs/Gemfile and docs/Gemfile.lock, canonical package versions and CHANGELOG.md. +- Integration points: GitPython consumers, Hatchling wheel builds, Jekyll output, + immutable module fixtures, frozen audits, trust/Socket, quality and Requirements. +- Review scope: each PR carries only its parent-relative layer; the final head + must pass the complete accumulated validation. Rebase invalidates head-bound proof. + +## Source Tracking + + + +- **GitHub Issue**: #747, +- **Parent Epic**: #194 +- **Project**: SpecFact CLI #1, In Progress; ownership explicitly approved. +- **Labels**: dependencies, security, openspec, change-proposal +- **Native dependencies**: no blockers/blocking edges at refreshed readback. +- **Planning PR**: #748; broader accepted work remains open. +- **Completed repair**: #756, merge cb417b5dd5d58fbe9ce50b62cb8a75868caa5ef2. diff --git a/openspec/changes/dependency-update-consolidation/requirements-evidence.yaml b/openspec/changes/dependency-update-consolidation/requirements-evidence.yaml new file mode 100644 index 000000000..8cee6d497 --- /dev/null +++ b/openspec/changes/dependency-update-consolidation/requirements-evidence.yaml @@ -0,0 +1,176 @@ +schema_version: '2' +requirements: + openspec:dependency-update-consolidation:dependency-update-consolidation:compatible-documentation-security-stack: + rationale: Compatible documentation security updates and deployment behavior + stakeholder_refs: + - nold-ai/specfact-cli#747 + - nold-ai/specfact-cli#727 + - nold-ai/specfact-cli#752 + - nold-ai/specfact-cli#754 + - nold-ai/specfact-cli#753 + touchpoints: + - id: documentation-inputs + kind: config_file + locator: docs/Gemfile + - id: documentation-lock + kind: config_file + locator: docs/Gemfile.lock + verification_cases: + - case_id: DEP-DOCS-001 + scenario_id: compatible-documentation-security-stack + method: inspection + intent: >- + Inspect the JSON declaration floor test and parent-relative Gem lock diffs, then the Ruby + redirect/feed/relative-link probes and full Jekyll root/prefixed builds for each applicable layer. + observable: >- + Retain actual failing and passing behavior probes, reviewed artifact hashes, dependency versions + and output results; JSON 2.21.2 and Jekyll 4.4.1 remain compatible without incidental graph changes. + Ruby probes and inspection do not substitute for protected native Requirements authority. + openspec:dependency-update-consolidation:dependency-update-consolidation:caller-owned-jwt-verification-options: + rationale: caller owned jwt verification options + stakeholder_refs: + - nold-ai/specfact-cli#747 + - nold-ai/specfact-cli#755 + touchpoints: + - id: jwt-graph + kind: config_file + locator: uv.lock + verification_cases: + - case_id: DEP-SEC-001 + scenario_id: caller-owned-jwt-verification-options + method: inspection + intent: >- + Preserve completed #756 native proof and independently rerun + tests/unit/security/test_frozen_jwt_compatibility.py::test_inspection_preserves_options_and_expiry_on_reuse. + observable: >- + Original merged #756 proof remains unchanged; current control results are retained separately + and failures block delivery. This completed case is not a new RED claim. + - case_id: DEP-SEC-002 + scenario_id: caller-owned-jwt-verification-options + method: inspection + intent: >- + Inspect independently required test execution: + tests/unit/security/test_frozen_jwt_compatibility.py::test_signed_token_and_trailing_signature_padding_remain_supported + observable: >- + Retain the exact current full-CI and focused-control results for + tests/unit/security/test_frozen_jwt_compatibility.py::test_signed_token_and_trailing_signature_padding_remain_supported. + This inspection is not native RED/final execution proof; unavailable or failing required tests + block delivery. + openspec:dependency-update-consolidation:dependency-update-consolidation:versioning-fixture-excludes-git-metadata: + rationale: versioning fixture excludes git metadata + stakeholder_refs: + - nold-ai/specfact-cli#747 + - nold-ai/specfact-cli#755 + touchpoints: + - id: fixture-staging + kind: config_file + locator: tests/unit/versioning/test_analyzer.py + verification_cases: + - case_id: DEP-FIX-001 + scenario_id: versioning-fixture-excludes-git-metadata + method: inspection + intent: >- + Inspect independently required test execution: + tests/unit/versioning/test_analyzer.py::TestChangeAnalyzer::test_detects_patch_changes + observable: >- + Retain the exact current full-CI and focused-control results for + tests/unit/versioning/test_analyzer.py::TestChangeAnalyzer::test_detects_patch_changes. + This inspection is not native RED/final execution proof; unavailable or failing required tests + block delivery. + - case_id: DEP-FIX-002 + scenario_id: versioning-fixture-excludes-git-metadata + method: inspection + intent: >- + Inspect independently required test execution: + tests/unit/versioning/test_analyzer.py::TestChangeAnalyzer::test_detects_additive_changes + observable: >- + Retain the exact current full-CI and focused-control results for + tests/unit/versioning/test_analyzer.py::TestChangeAnalyzer::test_detects_additive_changes. + This inspection is not native RED/final execution proof; unavailable or failing required tests + block delivery. + - case_id: DEP-FIX-003 + scenario_id: versioning-fixture-excludes-git-metadata + method: inspection + intent: >- + Inspect independently required test execution: + tests/unit/versioning/test_analyzer.py::TestChangeAnalyzer::test_detects_breaking_changes + observable: >- + Retain the exact current full-CI and focused-control results for + tests/unit/versioning/test_analyzer.py::TestChangeAnalyzer::test_detects_breaking_changes. + This inspection is not native RED/final execution proof; unavailable or failing required tests + block delivery. + openspec:dependency-update-consolidation:dependency-update-consolidation:coherent-reviewed-frozen-repair: + rationale: coherent reviewed frozen repair + stakeholder_refs: + - nold-ai/specfact-cli#747 + - nold-ai/specfact-cli#755 + touchpoints: + - id: frozen-lock + kind: config_file + locator: uv.lock + - id: frozen-export + kind: config_file + locator: requirements/ci/locked.txt + - id: isolated-smoke-seed + kind: code_symbol + locator: scripts/runtime_discovery_smoke.py::_create_pip_editable_launcher + verification_cases: + - case_id: DEP-GRAPH-001 + scenario_id: coherent-reviewed-frozen-repair + method: inspection + intent: Review graph/export, exact artifacts and independent current gate results. + observable: >- + Only required reviewed updates are present; both audits, trust/licenses, consumers and Python + matrix retain independent results. Unavailable results never imply passing. + - case_id: DEP-CACHE-001 + scenario_id: coherent-reviewed-frozen-repair + method: inspection + intent: >- + Preserve completed #756 native proof and independently rerun + tests/integration/scripts/test_runtime_discovery_fallback.py::test_pip_editable_fallback_uses_isolated_seed_cache. + observable: >- + Original merged #756 proof remains unchanged; current control results are retained separately + and failures block delivery. This completed case is not a new RED claim. + openspec:dependency-update-consolidation:dependency-update-consolidation:compatible-python-security-layer: + rationale: Compatible Python security layer preserving the merged repair + stakeholder_refs: + - nold-ai/specfact-cli#747 + - nold-ai/specfact-cli#744 + touchpoints: + - id: python-inputs + kind: config_file + locator: pyproject.toml + - id: delivery-export + kind: config_file + locator: requirements/ci/locked.txt + verification_cases: + - case_id: DEP-GIT-001 + scenario_id: compatible-python-security-layer + method: test + intent: Reject an outside checkout before cloning. + observable: The hash-pinned delivery package raises ValueError before the mocked clone boundary + is reached. + selector: + runner: pytest + node_id: >- + tests/unit/security/test_frozen_gitpython_compatibility.py::test_submodule_update_rejects_outside_checkout_before_clone + - case_id: DEP-PY-001 + scenario_id: compatible-python-security-layer + method: inspection + intent: >- + Inspect exact source/artifact, synchronized declarations, locks, release metadata and independent + compatibility/gate results. + observable: >- + Only GitPython and Hatchling versions plus project metadata change in the frozen graph; prior + #756 versions and proof remain retained. Both audits, trust/licenses, backend builds, Python + matrix and consumers have current results; unavailable or failing evidence blocks readiness. + - case_id: DEP-BUILD-001 + scenario_id: compatible-python-security-layer + method: inspection + intent: >- + Inspect the real offline build regression execution in + tests/integration/test_distribution_build.py::test_source_distribution_rebuild_preserves_bundle_mapper. + observable: >- + Retain its actual failing result before adding the missing source inclusion and its passing + result afterward. The wheel rebuilt from the sdist retains byte-identical bundle-mapper + metadata. This inspection does not substitute for authenticated native execution proof. diff --git a/openspec/changes/dependency-update-consolidation/requirements-proof/review-evidence.json b/openspec/changes/dependency-update-consolidation/requirements-proof/review-evidence.json new file mode 100644 index 000000000..53643bfbe --- /dev/null +++ b/openspec/changes/dependency-update-consolidation/requirements-proof/review-evidence.json @@ -0,0 +1,9 @@ +{ + "schema_version": "1", + "decision": "accepted", + "reviewer_id": "codex:/root", + "reviewer_role": "implementation-agent", + "recorded_at": "2026-10-11T06:43:43.367767+00:00", + "reference": "https://github.com/nold-ai/specfact-cli/issues/747", + "mapping_digest": "sha256:29ca7c7d58642a1e83726a19cb5bcf58951d6bdf9e06836d9329db543b9a91ed" +} diff --git a/openspec/changes/dependency-update-consolidation/specs/dependency-update-consolidation/spec.md b/openspec/changes/dependency-update-consolidation/specs/dependency-update-consolidation/spec.md new file mode 100644 index 000000000..16b46a2e3 --- /dev/null +++ b/openspec/changes/dependency-update-consolidation/specs/dependency-update-consolidation/spec.md @@ -0,0 +1,107 @@ +## ADDED Requirements + +### Requirement: Caller-Owned JWT Verification Options + +The frozen runtime SHALL preserve caller-owned JWT mappings across unverified inspection +and subsequent verified decoding, and SHALL retain supported signed-token behavior. + +#### Scenario: Inspection cannot weaken expiry on reuse + +- **GIVEN** an expired signed token and caller options disabling signature verification +- **WHEN** inspection is followed by verified decoding with that mapping +- **THEN** inspection leaves caller options unchanged and verified decoding rejects expiry. + +#### Scenario: Ordinary signed tokens preserve padding compatibility + +- **GIVEN** a valid signed token and an empty caller options mapping +- **WHEN** decoding normally and with trailing Base64URL padding in its signature +- **THEN** both decodes succeed and caller options remain unchanged. + +### Requirement: Versioning Fixture Excludes Git Metadata + +Versioning fixtures SHALL stage generated project files without Git metadata and retain +real patch, additive and breaking analyzer assertions. + +#### Scenario: Retained analyzer classifications execute + +- **GIVEN** a fresh repository and generated project bundle +- **WHEN** patch, additive and breaking tests initialize their repository +- **THEN** initialization excludes Git metadata and all classification assertions execute. + +### Requirement: Coherent Reviewed Frozen Repair + +The focused slice SHALL regenerate lock/export coherently, retain independent artifact, +security and compatibility evidence, and preserve existing governance. + +#### Scenario: Only necessary reviewed updates enter the graph + +- **GIVEN** the reported advisories and resolver constraints +- **WHEN** the focused graph is adopted +- **THEN** only reviewed required updates enter coherent inputs and both audits, trust, + licenses, affected consumers, supported Python and applicable quality gates are verified. + +#### Scenario: Smoke fallback does not inherit unchecked seed updates + +- **GIVEN** the standard venv builder fails and an older shared virtualenv cache may exist +- **WHEN** the editable smoke launcher uses virtualenv as its fallback +- **THEN** it seeds with isolated workspace app data and disables background periodic updates, + preserves the editable launcher, and never purges unrelated user cache contents. + +#### Scenario: Broader planning and exact authority remain separate + +- **GIVEN** broader #748 planning and the planning-only #755 workflow amendment +- **WHEN** the focused repair is published +- **THEN** broader tasks remain open, fixture proof cannot promote workflow runtime, and + new exact-commit/tree authority is explicitly obtained before claiming green. + +### Requirement: Compatible Python Security Layer + +The selected Python layer SHALL use screened GitPython 3.2.0 and Hatchling 1.32.4, +preserve the merged #756 graph and generate coherent frozen lock/export inputs. + +#### Scenario: Unsafe submodule checkout is rejected before cloning + +- **GIVEN** a submodule checkout path outside its parent repository +- **WHEN** the hash-pinned delivery GitPython package updates that submodule +- **THEN** it rejects the path before cloning or writing outside the repository. + +#### Scenario: Backend and release metadata remain coherent + +- **GIVEN** both Hatchling declarations and the four canonical package versions +- **WHEN** the selected Python layer builds its wheel +- **THEN** the backend is 1.32.4, package metadata is 0.55.6, lock/export parity holds, + both frozen audits pass and Python 3.11-3.13 installation preserves consumers. + +#### Scenario: Source distribution rebuild retains bundled assets + +- **WHEN** the reviewed backend builds a wheel from the release source distribution +- **THEN** the source distribution SHALL contain every forced wheel inclusion, + including the unchanged signed bundle-mapper assets. + +### Requirement: Compatible Documentation Security Stack + +The documentation stack SHALL retain Jekyll 4.4.1 and JSON >=2.21.2,<3, +adopt the selected redirect and feed security fixes and preserve link behavior. + +#### Scenario: JSON stays on the compatible patched major + +- **WHEN** documentation dependency inputs are regenerated +- **THEN** JSON remains locked at 2.21.2, the declaration excludes JSON 3, + and Jekyll remains at 4.4.1 without incidental resolver changes. + +#### Scenario: Redirect output rejects script targets + +- **WHEN** an unsafe scheme or an HTML/JavaScript delimiter enters a redirect target +- **THEN** the redirect plugin rejects unsafe schemes and encodes delimiters, + while ordinary root and prefixed HTTP/HTTPS redirects remain usable. + +#### Scenario: Feed metadata cannot escape XML or CDATA + +- **WHEN** feed language or post content contains XML/CDATA delimiters +- **THEN** generated feed output remains parseable and preserves metadata as data. + +#### Scenario: Relative links preserve configured deployment paths + +- **WHEN** documentation builds at root or a configured base URL +- **THEN** Markdown, included HTML links and navigation resolve to current pages, + preserving fragments and external targets across successive site builds. diff --git a/openspec/changes/dependency-update-consolidation/tasks.md b/openspec/changes/dependency-update-consolidation/tasks.md new file mode 100644 index 000000000..2f85c0f46 --- /dev/null +++ b/openspec/changes/dependency-update-consolidation/tasks.md @@ -0,0 +1,45 @@ +## 1. Historical focused repair (#756) + +- [x] 1.1 Define security/fixture scenarios and retain authentic authored RED. +- [x] 1.2 Screen and repair the five-package frozen graph and isolated smoke cache. +- [x] 1.3 Preserve independent controls, hosted quality/matrix and Requirements proof. +- [x] 1.4 Merge signed repair #756 into dev; subsequent #755 baseline adopted it. + +These completed items reference immutable #756 evidence. They do not establish +current stack readiness. Keep prior failed/rejected runs and local limitations. + +## 2. Scope and Python layer (#744) + +- [x] 2.1 Create dedicated worktree from current dev and capture all six PR heads. +- [x] 2.2 Reconcile #747/#748 and internal wiki; validate the approved scope strictly. +- [x] 2.3 Screen exact artifacts/provenance/licenses/advisories before candidate execution. +- [x] 2.4 Author GitPython boundary and configuration expectations; retain failing evidence. +- [x] 2.5 Generate coherent GitPython 3.2.0/Hatchling 1.32.4 lock and CI export. +- [x] 2.6 Synchronize 0.55.6 sources, generated metadata and Security/Fixed changelog. +- [ ] 2.7 Validate both audits, trust, Python 3.11-3.13 wheel/build and affected consumers. + +## 3. Sequential documentation layers + +- [x] 3.1 Create #727 worktree from #744; raise JSON floor without Jekyll downgrade. +- [x] 3.2 Create #752 worktree from #727; verify redirect security and ordinary redirects. +- [x] 3.3 Create #754 worktree from #752; verify feed language/CDATA escaping and XML. +- [x] 3.4 Create #753 worktree from #754; verify relative links, navigation and base URLs. +- [x] 3.5 Regenerate each lock from its parent; remove incidental resolver churn. + +## 4. Final review and publication + +- [ ] 4.1 Complete frozen parity, both audits, trust/Socket, licenses and PyPI-ahead gates. +- [ ] 4.2 Complete applicable format/type/lint/YAML/contract/smart-test gates. +- [ ] 4.3 Retain fresh SpecFact review, independent Semgrep/Bandit and strict signatures. +- [ ] 4.4 Retain authentic Requirements lifecycle and current final-head evidence. +- [x] 4.5 Publish signed commits with explicit leases and register the five-PR chain. +- [ ] 4.6 Reconcile #748's proposal branch; keep broader inventory and #747 open. +- [x] 4.7 Verify open duplicates and record each disposition; close only covered duplicates. +- [ ] 4.8 Deliver review links, exact limitations, rollback and dev-to-main checklist. +- [ ] 4.9 Clean up dedicated worktrees only after reviewed integration. + +## 5. Deferred broader scope + +- [ ] 5.1 Refresh #748's full dependency inventory and assess unselected upgrades separately. +- [ ] 5.2 Design JSON 3 migration once a compatible Jekyll dependency contract is available. +- [ ] 5.3 Archive the entire change with OpenSpec only after all accepted work completes. diff --git a/openspec/changes/dogfooding-01-full-chain-e2e-proof/design.md b/openspec/changes/dogfooding-01-full-chain-e2e-proof/design.md index 811cb70df..c5f4b78b1 100644 --- a/openspec/changes/dogfooding-01-full-chain-e2e-proof/design.md +++ b/openspec/changes/dogfooding-01-full-chain-e2e-proof/design.md @@ -1,5 +1,11 @@ ## Context +## Scope rescope — 2026-09-20 + +Measure bounded defect-detection and operational overhead on real changes. Use existing CI artifacts; retain failures/reproductions that demonstrate test sensitivity. No mandatory immutable RED history or repeated seal approvals for the lean dogfood path. Stronger assurance experiments remain explicitly selected. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + This change defines an auditable dogfooding path proving SpecFact's validation and AI-bloat defense loop on real project artifacts. @@ -21,7 +27,10 @@ and AI-bloat defense loop on real project artifacts. ## Decisions - Use a bounded PR or demo repository slice to keep proof practical and repeatable. -- Require machine-readable evidence output and rerun comparison artifacts. +- Reuse machine-readable validation output and rerun comparison artifacts; + summarize defect detection and overhead without a new evidence framework. +- Require full-chain links only for an explicitly selected experiment, preserving + its missing-link failure semantics without making it the ordinary workflow. - Couple proof completion to the validation-positioning wave exit criteria. ## Risks / Trade-offs diff --git a/openspec/changes/dogfooding-01-full-chain-e2e-proof/proposal.md b/openspec/changes/dogfooding-01-full-chain-e2e-proof/proposal.md index 54b42e1d5..e1fd9e676 100644 --- a/openspec/changes/dogfooding-01-full-chain-e2e-proof/proposal.md +++ b/openspec/changes/dogfooding-01-full-chain-e2e-proof/proposal.md @@ -1,5 +1,11 @@ # Change: Dogfooding Proof for AI-Bloat Defense and Validation +## Scope rescope — 2026-09-20 + +Measure bounded defect-detection and operational overhead on real changes. Use existing CI artifacts; retain failures/reproductions that demonstrate test sensitivity. No mandatory immutable RED history or repeated seal approvals for the lean dogfood path. Stronger assurance experiments remain explicitly selected. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + ## Why SpecFact's flagship claim is no longer that it owns the full planning chain. The @@ -19,8 +25,9 @@ an AI IDE, rerun, and show improved evidence. - rerun validation and compare evidence. - **NEW**: Define release-readiness proof criteria for AI-bloat defense and validation positioning claims. -- **NEW**: Add CI/report outputs proving the evidence loop rather than a - requirements-to-code lifecycle. +- **NEW**: Reuse existing CI/report outputs for the validation loop and summarize + defect detection and overhead. Full-chain link evidence is required only for + an explicitly selected experiment. ## Capabilities diff --git a/openspec/changes/dogfooding-01-full-chain-e2e-proof/requirements-evidence.yaml b/openspec/changes/dogfooding-01-full-chain-e2e-proof/requirements-evidence.yaml new file mode 100644 index 000000000..d5c884a64 --- /dev/null +++ b/openspec/changes/dogfooding-01-full-chain-e2e-proof/requirements-evidence.yaml @@ -0,0 +1,30 @@ +schema_version: "2" +x-planning-context: &planning-context + rationale: "Make this requirement reviewable before implementation begins." + stakeholder_refs: ["nold-ai/specfact-cli#255"] +x-planning-inspection: &planning-inspection + method: inspection + observable: >- + Strict OpenSpec validation retains the requirement and its scenarios + without claiming implementation or test execution. +x-spec-1: &spec-1 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/dogfooding-01-full-chain-e2e-proof/specs/dogfooding-full-chain-e2e/spec.md" +requirements: + openspec:dogfooding-01-full-chain-e2e-proof:dogfooding-full-chain-e2e:end-to-end-dogfooding-proof: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: DOG01-001 + scenario_id: end-to-end-dogfooding-proof + intent: "Inspect the end-to-end dogfooding proof planning contract and its scenarios." + openspec:dogfooding-01-full-chain-e2e-proof:dogfooding-full-chain-e2e:dogfooding-evidence-is-ci-consumable: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: DOG01-002 + scenario_id: dogfooding-evidence-is-ci-consumable + intent: "Inspect the dogfooding evidence is ci-consumable planning contract and its scenarios." diff --git a/openspec/changes/dogfooding-01-full-chain-e2e-proof/specs/dogfooding-full-chain-e2e/spec.md b/openspec/changes/dogfooding-01-full-chain-e2e-proof/specs/dogfooding-full-chain-e2e/spec.md index 4699f2076..c5ee0b935 100644 --- a/openspec/changes/dogfooding-01-full-chain-e2e-proof/specs/dogfooding-full-chain-e2e/spec.md +++ b/openspec/changes/dogfooding-01-full-chain-e2e-proof/specs/dogfooding-full-chain-e2e/spec.md @@ -1,22 +1,41 @@ +## Scope rescope — 2026-09-20 + +Measure bounded defect-detection and operational overhead on real changes. Use existing CI artifacts; retain failures/reproductions that demonstrate test sensitivity. No mandatory immutable RED history or repeated seal approvals for the lean dogfood path. Stronger assurance experiments remain explicitly selected. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + ## ADDED Requirements ### Requirement: End-to-End Dogfooding Proof -The system SHALL provide a reproducible dogfooding workflow that proves full-chain traceability from backlog to CI evidence. +The ordinary dogfooding workflow SHALL evaluate a bounded real change using +existing current-run CI outputs, relevant defect reproductions, remediation and +rerun comparison, with concise observations of operational overhead. It SHALL +NOT require full-chain traceability or retained RED chronology. An explicitly +selected full-chain experiment SHALL still validate all links it claims. + +#### Scenario: Ordinary dogfood uses existing validation results + +- **GIVEN** a real change with current validation results and ordinary MEB policy +- **WHEN** the dogfood run evaluates defects, remediation and the resulting rerun +- **THEN** it references existing CI artifacts and summarizes observed improvement and overhead +- **AND** absent full-chain links or historical RED records are not gate failures. #### Scenario: Full chain proof is generated for a real backlog slice -- **WHEN** dogfooding workflow runs for selected SpecFact backlog items +- **GIVEN** a full-chain assurance experiment is explicitly selected +- **WHEN** that workflow runs for selected SpecFact backlog items - **THEN** each item is traceable through requirement, architecture, spec, code/test, and evidence outputs - **AND** missing links are reported as gate failures ### Requirement: Dogfooding Evidence Is CI-Consumable -The system SHALL produce machine-readable evidence artifacts for the dogfooding run. +The system SHALL reuse existing machine-readable validation outputs and CI artifact references for ordinary dogfood. Additional link evidence SHALL be required only for an explicitly selected full-chain experiment. #### Scenario: CI validates dogfooding proof -- **WHEN** CI executes the dogfooding full-chain run +- **GIVEN** a full-chain assurance experiment is explicitly selected +- **WHEN** CI executes that dogfooding run - **THEN** evidence artifacts are emitted in a stable schema - **AND** wave gate status is derivable from those artifacts diff --git a/openspec/changes/dogfooding-01-full-chain-e2e-proof/tasks.md b/openspec/changes/dogfooding-01-full-chain-e2e-proof/tasks.md index a3f0c9470..ba6886416 100644 --- a/openspec/changes/dogfooding-01-full-chain-e2e-proof/tasks.md +++ b/openspec/changes/dogfooding-01-full-chain-e2e-proof/tasks.md @@ -1,5 +1,13 @@ # Tasks: dogfooding-01-full-chain-e2e-proof +## Scope rescope — 2026-09-20 + +Measure bounded defect-detection and operational overhead on real changes. Use existing CI artifacts; retain failures/reproductions that demonstrate test sensitivity. No mandatory immutable RED history or repeated seal approvals for the lean dogfood path. Stronger assurance experiments remain explicitly selected. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + +Implementation discipline: use focused regression/reproduction and current-run results. Any task requiring immutable hosted RED, frozen test authoring or approval receipts merely to implement this feature is superseded; tests of an explicitly selected optional seal feature remain in scope. + ## 1. Branch and proof setup - [ ] 1.1 Create dedicated worktree branch `feature/dogfooding-01-full-chain-e2e-proof` from `dev`: `scripts/worktree.sh create feature/dogfooding-01-full-chain-e2e-proof`. @@ -8,14 +16,14 @@ ## 2. Test-first and failing evidence -- [ ] 2.1 Add integration tests covering review -> evidence -> remediation packet -> rerun comparison. -- [ ] 2.2 Run tests expecting initial failure and capture in `TDD_EVIDENCE.md`. +- [ ] 2.1 Add integration cases for ordinary review -> existing CI evidence -> remediation -> rerun comparison without full-chain/RED requirements; separately retain missing-link failures for explicitly selected full-chain experiments. +- [ ] 2.2 Observe relevant regression failures before implementation and summarize them briefly; retain detailed output in ordinary CI artifacts. - [ ] 2.3 Add validation checks for evidence schema, AI-bloat findings, remediation packet references, and rerun deltas. ## 3. Implementation and verification -- [ ] 3.1 Implement the minimal proof harness required to run the AI-bloat defense loop. -- [ ] 3.2 Generate JSON evidence, cleanup forecasts, remediation packets, and rerun comparison artifacts. +- [ ] 3.1 Implement the bounded validation loop using existing CI outputs; require full-chain link evidence only when that experiment is explicitly selected. +- [ ] 3.2 Reuse JSON outputs, remediation packets and rerun artifacts; summarize detected regressions and operational overhead. - [ ] 3.3 Re-run tests and quality gates until all proof scenarios pass. ## 4. Delivery diff --git a/openspec/changes/fix-categoryless-module-command-squatting/CHANGE_VALIDATION.md b/openspec/changes/fix-categoryless-module-command-squatting/CHANGE_VALIDATION.md new file mode 100644 index 000000000..c8db48b7c --- /dev/null +++ b/openspec/changes/fix-categoryless-module-command-squatting/CHANGE_VALIDATION.md @@ -0,0 +1,659 @@ +## Current accepted path100 runtime GREEN and remaining gates, 15 October 2026 (Europe/Berlin) + +The human accepted PER-SOURCE100 sha256:03651caa143d7cbde41a9f33db9cc91182e9e05201173bf4a3bc5273c021dbdc. +Signed specification1c399e2d4ab24cbfe10859d87da00e107cafffcf precedes authored35c6f1339096cd35d53987d02658eb02f7215363. +Actual native RED38687272103/artifact11950602909/serviceZIP a883f2b809c0438be27ff29918a6916779f88c0b8284cb079d9f3db0c09fd622 +has100 genuine AssertionErrors, zero errors/skips, and verified source/tree/base/nine hashes/selectors/JUnit. +Only after that proof is verified does signed implementation e4db7795139a562a0a56572bf696e0164f90f71b restore the +accepted runtime/version0.55.10. Normal hooks, all three signatures and actual literal full-SHA provenance pass. + +Actual native GREEN38687870204 on e4db7795139a562a0a56572bf696e0164f90f71b completes producer, independent execution +and final reconciliation. Both JUnit artifacts contain100 passes, zero failures/errors/skips and matching selectors. +All18 encoding, six owned-path and five legacy suffix outcomes verify, along with allfour real service ZIP digests. +FULL review is72 with the same30 accepted nonblocking findings and zero additions; identities and multiplicities +match after only three exact unchanged BasedPyright file-root normalizations. The producer digest binds its own +JUnit; the independent execution has a separate verified JUnit. Compact final receipts omit base/tree/testhashes. +Those bind through retained authored RED, current nine Git test blobs, producer plan and actual full-SHA provenance, +not invented receipt fields. The local runtime suite has3,430 passes/nine configured skips/64% against50%, normal +30-second timeout; its exact-source quality, independent reviews and installed-wheel evidence remain scoped as below. + +CodeRabbit reviewed e4db7795 in review5528734224 at2026-10-15T05:49:27Z with explicit current-head coverage and +80.92% docstring coverage against80%. Its new task-ledger finding and historical headings are corrected here. +The review reports zero included reviews remaining and an allowance of one per hour. No paid review, billing, +configuration change or repeated unchanged request is authorized or used. Python3.12 full CI was still running +at this evidence cutoff; twelve other effective exact-app integrations passed. No all-CI or merge-ready claim. + +This documentation reconciliation preserves runtime, tests, mapping, plan selectors and dependency inputs. Its +new exact head still requires native/check/review confirmation. The companion511 release-symlink fix remains +unpublished without admitted native FULL, with Linux guest access/resource constraints recorded separately. +Concurrent local modification after the final admission observation remains outside the documented guarantee. +No target or companion merges are authorized; human719 precedes743, then human743 precedes751. + +## Historical path100 native RED before runtime GREEN, 15 October 2026 (Europe/Berlin) + +Signed specification1c399e2d4ab24cbfe10859d87da00e107cafffcf precedes signed authored tests35c6f1339096cd35d53987d02658eb02f7215363 on protected production/version0.55.9. +Actual native run38687272103, artifact11950602909, serviceZIP SHA256a883f2b809c0438be27ff29918a6916779f88c0b8284cb079d9f3db0c09fd622 verifies100 genuine AssertionErrors, +zero errors/skips and exact source/tree/base/100selectors/nine test hashes. RED gate PASS with zero proof findings. +JUnit digestsha256:c36d4df8a40cdab552871e1fe77eb203daefc65997cd3d0c5bba62a8fbad37f7. All18 encoding, six owned-path link and five legacy suffix observations match +retained protected replay and precede aggregate assertions. Extra temporal/alias full-suite tests are not separately +claimed mapped native selectors. Reviewed production/version0.55.10 restores only AFTER real service bindings verify. +Local3430/FULL72 proof remains scoped to its exact runtime/test inputs. Final native producer/independent execution, +FULL review, all required checks and completed current-head review remain required. Observation does not seal later +filesystem state. Companion remains held. No target or companion merge is authorized. + +## Historical accepted path100 signed authoring, 15 October 2026 (Europe/Berlin) + +The human accepted exact canonical PER-SOURCE100 sha256:03651caa143d7cbde41a9f33db9cc91182e9e05201173bf4a3bc5273c021dbdc +and authorized recording acceptance as djm81. The 36-file accepted candidate preserves the reviewed runtime, +nine authored test hashes, all93 earlier selectors and all18 encoding observations within the unchanged100-selector limit. +The new signed specification and tests are authored on protecteddaefe production/version0.55.9. Actual native100 RED +must verify service ZIP/source/tree/base/100selectors/nine hashes/JUnit and all required observations BEFORE +reviewed runtime/version0.55.10 restoration. Local3430 PASS, FULL72/30existing/zero new and independent reviews +remain valid within their recorded input scope; no native proof for these new bytes is yet claimed. +Historical4c acceptance and signed528/native38647654283 RED/38648161680 GREEN are retained, not reused for new bytes. +Final service producer/independent execution/FULL/all required integrations and completed current-head review remain required. +The disclosed overwritten intermediate raw review and filesystem observation limits remain. Companion511 remains held. +No target or companion merge authority is granted; human719 precedes743, then human743 precedes751. + +## Historical local path100 validation before acceptance, 15 October 2026 (Europe/Berlin) + +Canonical PER-SOURCE100 `sha256:03651caa143d7cbde41a9f33db9cc91182e9e05201173bf4a3bc5273c021dbdc` is UNACCEPTED. No new native proof or mapping acceptance is inferred from the earlier accepted4c3d cycle or the broad required-change authorization. Published signed528b9338f71a1026fcc6b42fcd99156ba35cf344 remains the historical accepted100 head with authentic native RED38647654283 and GREEN38648161680, all13 effective required integrations and completed CodeRabbit5525712106. This new amendment is local and unpublished. + +Final runtime installer SHA d53cdcab025723a1ad8f618f1de955caee82e668dca6ea596569510e4f378bc8 and packages SHA ee47ead2c731d80888f18fa604c3193c54cf43dc7c1513c08fd54063b78e8b9b reject lexical artifact roots and owned project stores before and after authentic verification. Six genuine final-admission assertion failures, zero errors/skips, preceded the trailing reobservation correction. The first attempt mutated during initial verification and passed six controls because final revalidation already caught that earlier interval; it is preserved and not represented as RED. Scope is official project category authority, preserving unreserved category-less legacy compatibility and canonical workspace aliases. Sequential observations do not seal later filesystem state or guarantee lazy-load integrity. + +Seven accepted93 test files remain unchanged. Eighth bfe5ad2727ee4589a39b086b1d7bef1d6d429d059681b57c2f27924502b14779 conserves all18 named encoding/mode observations in four nodes. Ninth35a4bd64912ffd0e45c7c4a300634834302d02419a6a38f188dd71a522e2ef19 adds two mapped path-link nodes and one mapped legacy-native node. Native plan remains100 selectors. Actual protected replay has100 genuine AssertionErrors, zero errors/skips, exact nine hashes and verified import origin. Its suite properties preserve18 encoding, six owned-link and five legacy suffix observations before aggregate assertions. Six additional inside-final-verification controls, two between-pass controls and two canonical workspace-alias controls are full-suite evidence, not separately mapped native selectors. + +Final normal full suite3430 PASS, nine configured skips, zero failures/errors,262.520 seconds,64% coverage against unchanged50% gate and normal30-second test timeout. All795 sampled inputs and installed graph remained unchanged during that run. Copied JUnit and starting hashes are719-path100-reobserved-full-serial-* under the retained private evidence root. Final FULL20-file review PASS_WITH_ADVISORY score72 with30 existing findings and zero new. Two unchanged function findings move two lines; exact AST identity and finding multiplicities are verified, with no rule/message/subject exemption. Protected99 subject exceptions remain unchanged. Types zero errors/1456 existing warnings; exact CI SAST zero findings/errors and unchanged baseline gate PASS; Bandit zero blocking medium/high; lint/format/direct changed mapping YAML/strict OpenSpec/frontmatter/delivery/version/PyPI gates PASS. Locked graphs are unchanged; existing exact frozen advisory/license and signature receipts apply within their stated scope. Existing17 protected YAML diagnostics and whole-repo launcher's false-success behavior remain disclosed. + +Normal rebuilt wheel318f7dadb1a04577d886f0d04f68ebf7299f04da5c3cdbc02634098b0eca5b68 contains the exact final runtime source bytes. Frozen offline hash-required isolated Python3.11.15/3.12.13/3.13.14 install, installed import and CLI version checks PASS. This is installation evidence, not all security probes on all interpreters. Final standalone readonly verify-fix completed before model-diverse gpt-6-astra/6.1-sol/6-luna interrogation; all final reviewers report no findings with static limits. Reviewer capacity required reuse of finished non-writing reviewers, counted once each. The earlier panel's within-admission interval was acted on; its legacy-scope concern was resolved by truthful scope clarification. Final no-comments review has zero deletions or flags. + +Evidence retention limitation: the intermediate FULL70/32 raw report was accidentally overwritten by the next review before backup. Its two findings and result survive in the original chat; the raw report is not claimed preserved.719-path100-intermediate70-retention-limit.txt discloses this loss. Final72/30 report and all failed regression attempts remain retained. New mapping acceptance, genuine signed/authored native100 RED on protected dev, restoring implementation only after verified RED, final exact-head native/required checks and completed review remain pending. Companion511 native FULL remains UNKNOWN because no authenticated eligible Darwin capsule is admitted. No merge authority is granted;743 and751 stay held. + +## Historical local path100 correction, 15 October 2026 (Europe/Berlin) + +This local correction is UNACCEPTED for native publication. The owner accepted historical100 digest4c3ded1d98d1437c1de1fb73c243ad71d804506769135ea42581e9b3781ed274, whose signed528b9338f71a1026fcc6b42fcd99156ba35cf344 GREEN run38648161680 is verified. All100 historical selectors pass, all18 encoding observations appear, all four service ZIP digests verify, FULL review is72 with30 unchanged findings, all13 effective required integrations pass and CodeRabbit review5525712106 completed. This does not establish readiness for the new path-link correction or the companion prerequisite. + +New current-head controls reproduce two genuine AssertionErrors with no errors/skips. All three owned package/.specfact/modules links gain root and source authority in each mode before correction; lower-priority installed fallback is suppressed. The correction refuses the artifact root link in strict filesystem verification and workspace-owned links at project admission, preserving canonical workspace aliases and ordinary verification. It leaves later live-filesystem mutation outside the admission observation guarantee. + +The unchanged100-selector bound is preserved by keeping all93 earlier selectors, observing the same18 encoding probes in four nodes and adding two path-link nodes plus one aggregate legacy native suffix node. All observations precede aggregate assertions. Seven accepted93 test files remain byte-identical. The changed eighth file and new ninth file require a fresh exact per-source mapping acceptance. Temporal final-revalidation and canonical alias controls also run in the full suite; their literal nodes are not claimed mapped. + +Initial full3422 passed with nine configured skips and unchanged795 sampled inputs/installed graph. A later cleanup removes three narrative test docstrings and adds two final-revalidation controls; that initial full and protected replay are intermediate evidence. Final planned digest, protected RED, full gates and independent verification are being refreshed for the exact final bytes. No hosted proof for this amended mapping or new acceptance is claimed. + +## Historical native100 RED for the preceding4c mapping — 15 October 2026 (Europe/Berlin) + +Signed specification6c63d63323e94a7c633c69ea964c85c3d1c9c71c precedes signed authored testsd20d6a56e9781a740a1c82907158c671ad3d4b7f on unchanged protected production/version. +Actual native run38647654283, artifact11932289125, service ZIP SHA2568429e0ff751d0fcd044cda08b1d4225ca2ea494b94b5330c90bbf24c384151bc, has100 genuine AssertionErrors, +zero errors/skips and exact source/tree/base/selectors plus all8 authored test hashes. Its RED gate passes +with zero proof findings. JUnit digest:sha256:6a252b0198a77bdc86e5bb7511af4843a68fe7b836c1856b8c5f870c4ea54ea5. Reviewed production/version0.55.10 is restored only +after this real service artifact and bindings are verified. Deliberate RED workflow failure is evidence, +not final merge-ready CI. Final native producer/execution/FULL review, required CI, live exact-head authority +and completed current-head review remain required; no target merge is authorized. Companion symlink blocker remains. + +## Historical Current accepted100 signed authoring — 15 October 2026 (Europe/Berlin) + +The human accepted exact per-source100 mapping4c3ded1d98d1437c1de1fb73c243ad71d804506769135ea42581e9b3781ed274 +and authorized recording as djm81. The accepted35file candidate/eight authored hashes are preserved before genuine +signed specifications/tests on protecteddaefe runtime/version. All93 prior selectors and18 named mode/header +observations in7 matrix nodes remain; unchanged executorMAX100 and all native/trust/budget/pin/threshold gates. +Actual native100 RED must verify serviceZIP/source/tree/base/100selectors/eight hashes/JUnit and all18 observations +before reviewed production/version0.55.10 restoration. The failed111 pre-JUnit artifact remains INVALID RED, not reused. +Fresh finalnative producer/execution/FULL/allrequiredintegrations/completedreview remain required. No merges authorized. + +## Historical private100 validation before owner acceptance — 15 October 2026 (Europe/Berlin) + +The human-approved111 mapping4aa84a05cf7c4dde425060734bc13cd51b6111b7c2f3759e7c45e0dab6dd9f1a and signed +spec2f248c0911e248be4ad77de289429d2e1f778c14/authored2b2b0ff109ef1a68e1adf9cde0db7be8fdf4c311 remain preserved. +Actual native111 run38641093406/artifact11930266718 serviceZIPb18c0402f024853630ced8cbfc176225dec7c16ad3a0e5fcbc53edf59f1a9d80 +failed before JUnit: protected executor rejects more than100 selectors. This is invalid RED, not assertion evidence; +no runtime restoration or unchanged retry. Protected executor/trust/budgets/timeouts/coverage/pins remain unchanged. + +A separate local worktree consolidates all18 named mode/header×grouping observations in7 nodes, retaining all93 +prior selectors and seven accepted93 files. For every probe, canonical compatibility and genuine crypto precede +actual root/source observation; registry/import state resets between probes. All observations are collected and +recorded through the typed public JUnit suite-property fixture BEFORE aggregate assertions, preventing early failure from omitting legacy or later +permission probes. New100 per-source4c3ded1d98d1437c1de1fb73c243ad71d804506769135ea42581e9b3781ed274 is UNACCEPTED. +Source217dc80fc4457c6bb2b3dd0ace4b950ceb3863c7638305551ef59ea58a75f620/runtime/packaging/locked graphs unchanged; +eighth test194bf7b8aeec489cefc74190a28c96908fd07a8cf8ec9462c5b4ba61990a443c has7 focused PASS/zeroerrors/skips/warnings. +Exact local protected100 replay has100 genuineAssertionErrors/zeroerrors/skips with verified origin/eight hashes; +properties contain18 observations (6+2+2+2+2+2+2). Nine actual classifier mutations each fail1 assertion after all +relevant observations, and source is restored exactly; these are oracle isolation, not native authority. +Final complete normal full suite passes3420/nine configured skips/zero failures/errors,258.708seconds,64% vs50%/normal30sec; all794 sampled inputs and installed graph unchanged. Independent frozen environment, copiedJUnit; no shared dependency mutation. Finaltypes0errors/1456samewarnings. Intermediate3f5 test/full3420 and1459warnings are retained;3metadata warnings structurally fixed without an exception. +Earlier standalone read-only verify-fix preceded diverse review; its truthful-status finding is corrected by this prefix. RequiredFULL19 review passes72/30exact existing identities and multiplicities/zero new after removing only exact worktree-root prefixes from three unchanged category-group file locators; no rule/message/subject exception. Final194bf standalone read-only verify-fix FIRST then3diverse reviewers completed with no actionable findings on exact source/test/mapping bytes; static limits disclosed. +Final local gates and independent review are complete; fresh exact owner acceptance remains pending before signed/native100 publication. +Actual native100 RED must verify before implementation/version0.55.10 restoration. No target/companion merge authority. +Old93 native GREEN/all13contexts applies to059c only; current published primary head remains deliberately authored2b2b. +Companion511 remains unpublished without an admitted eligible authenticated native review capsule. No candidate +execution/self-signing/host fallback/platform spoofing or unrelated scope widening. Known17 protected YAML diagnostics +and61unrelated privatewiki health gaps retained; no new baseline/threshold/timeout exception. + +## Historical accepted111 signed authoring before limit rejection — 14 October 2026 (Europe/Berlin) + +The human accepted exact per-source111 mapping4aa84a05cf7c4dde425060734bc13cd51b6111b7c2f3759e7c45e0dab6dd9f1a +and authorized actual djm81 acceptance recording. The accepted35file candidate and exact eight authored test hashes +are preserved before genuinely signed specifications/tests on protected production/version. Actual native111 RED +must verify source/tree/base/selectors/eight hashes/JUnit/service ZIP before reviewed implementation/version0.55.10 +is restored. Earlier local proof remains preserved; authored RED is not final CI or merge readiness. No merges authorized. + +## Historical published93 and local111 proof before owner acceptance — 14 October 2026 (Europe/Berlin) + +Human-accepted per-source93 baa8b661268779bc0178417806e9389523e946125128b93e64a971464f7dee4b is delivered at +signed 059c05f051cfa456ebcb8f9607bf3c733f7d358e. Signed specs2dd017f87d4f52d179b3e149216a76189b68744e and +authored testsb5b41a7fb6779e79e630b120a445b8ca10a7abdc precede implementation. Actual native RED38635629429 +has93 AssertionErrors/zero errors/skips and exact seven hashes. Actual final38636359296 has93 independent +passing controls/zero failures/errors/skips, FULL72/30existing findings and all four service ZIP digests verified. +Base/tree/test bindings use retained RED/current Git blobs/literal-fullSHA provenance/producer plan/independent +execution; compact final receipts do not contain these fields. All13 exact integration-ID required checks pass. +Current-head CodeRabbit request6178325610 was rate-limited (bot6178328363); completed review remains required. +No target or companion merge readiness/authority is claimed. + +Local111 coverage is UNACCEPTED and unpublished. An eighth file independently exercises owner/group/other +execute permission bits and the six remaining standard Mach-O headers in both modes. Apple SDK loader.h and +fat.h definitions corroborate the eight header constants. Genuine signed compatibility precedes denial; probes +are inspected, never executed. All seven accepted93 files, runtime source, packaging and locked graphs remain +unchanged. Planned mapping adds18 selectors and corrects a stale locator to the existing local admission +function; no behavior, signing format, trust anchor, provider policy, timeout or threshold changes. +Local protected111 replay records111 genuine AssertionErrors/zero errors/skips with verified import origin. +Complete normal full suite:3431passes/nine configured skips/zero failures/errors,284.891seconds,64% versus50%, +normal30second timeout; all794 sampled inputs and installed graph unchanged during the run. Copied JUnit precedes +required FULL19file review:72/30exact existing identities and multiplicities/zero new findings. Types0errors/ +1456oldwarnings. Standalone read-only verify-fix preceded three model-diverse reviewers, no actionable findings +on exact source217dc80fc4457c6bb2b3dd0ace4b950ceb3863c7638305551ef59ea58a75f620 and eighth test +SHA dd75018dcb9c35363f2a6ab572705b9dda02bb28ad36b838f4d961bdcce40a6f. Final mapping locator review is recorded +separately. Fresh exact owner acceptance and signed/native111 RED remain required before publication. Existing +93 source/graph-bound audits, SAST, Bandit, signatures, delivery and wheel installation receipts retain their +scope; they do not prove111 security probes on every interpreter. Known17 protected YAML diagnostics and61 +unrelated privatewiki health gaps remain disclosed. Companion511 stays held without an authenticated eligible +native review capsule; no self-signing/execution/fallback/spoofing or unrelated scope widening. + +## Historical signed native93 RED verified — 14 October 2026 (Europe/Berlin) + +Signed specification2dd017f87d4f52d179b3e149216a76189b68744e precedes signed authored testsb5b41a7fb6779e79e630b120a445b8ca10a7abdc on unchanged protected production/version. +Actual native run38635629429, artifact11927649039, service ZIP SHA256a1e8603213f9578ca82686fab937385993c3dd3203c266c727cbe9d26da7d612, has93 genuine AssertionErrors, +zero errors/skips and exact source/tree/base/selectors plus all7 authored test hashes. Its RED gate passes +with zero proof findings. JUnit digest:sha256:c741c8dadda5f4ad5d0d4a114bf319b7f7e61c6b12d061319352fd3a5505f0c8. Reviewed production/version0.55.10 is restored only +after this real service artifact and bindings are verified. Deliberate RED workflow failure is evidence, +not final merge-ready CI. Final native producer/execution/FULL review, required CI, live exact-head authority +and completed current-head review remain required; no target merge is authorized. Companion symlink blocker remains. + +## Historical accepted93 signed authoring — 14 October 2026 (Europe/Berlin) + +The human accepted exact per-source93 mappingbaa8b661268779bc0178417806e9389523e946125128b93e64a971464f7dee4b +and authorized actual djm81 acceptance recording. The accepted34file candidate and exact seven authored test hashes +are preserved before genuinely signed specifications/tests on protected production/version. Actual native93 RED +must verify source/tree/base/selectors/seven hashes/JUnit/service ZIP before reviewed implementation/version0.55.10 +is restored. Earlier local proof remains preserved; authored RED is not final CI or merge readiness. No merges authorized. + +## Historical local93 reviewed proof before owner acceptance — 14 October 2026 (Europe/Berlin) + +Exact PER-SOURCE93 mappingbaa8b661268779bc0178417806e9389523e946125128b93e64a971464f7dee4b remains UNACCEPTED. +The exact seventh test SHA7a7260e422772be8e7f77be1d518fcd99a0cde0388dd8b195618f81ff7bdf417 produces93 genuine +protected AssertionErrors/zero errors/skips, including paired category/group compatibility before the real tier +regression. Four existing category/group positive cases remain standalone and are not falsely counted as RED. +The final complete suite passes3413/nine configured skips/zero errors or failures,64% vs50%,normal30sec timeout; +all793 sampled inputs and installed graph remain unchanged (275.84seconds). Final requiredFULL18file review passes72/30exact +existing identities/multiplicities/zero new. Two intermediate parameter-count warnings were structurally fixed +with identity tuples, not exempted; earlier FULL68/32 and prior full/test hashes are retained as historical. +Standalone read-only verify-fix preceded the final3diverse panel, all no actionable findings, with exact source/test +hashes and explicit limits. Finaltypes0errors/1456oldwarnings; exactCISAST0/BanditblockingPASS and unchanged strict +signature/delivery/version/PyPI gates plus changed-source installedwheel3.11–3.13 pass. Original six accepted79 +authored files remain byte-identical. Current79 native/CI/review validates published59ad only, not this unpublished93. +Fresh exact owner acceptance and actual signed native93RED must precede implementation restoration/publication. +No merge authority; companion511 admission blocker persists and human719merge precedes743 then751. + +## Historical local93 validation before tuple cleanup — 14 October 2026 (Europe/Berlin) + +Canonical PER-SOURCE93 `sha256:baa8b661268779bc0178417806e9389523e946125128b93e64a971464f7dee4b` is **UNACCEPTED**. The actual owner-accepted79 record remains historical +and does not authorize this amendment. Current published signed59adbda0016bd15e7c53f54bb8a0a669c463870a has +verified native79 RED38628199251 and final38628713533, all13 effective required integrations, and actual completed +CodeRabbit current-head review5523994227 (14 October2026 20:09:31UTC). No merge readiness is claimed: companion511 +and new extensionless/identity/ledger annotations remain open. Twenty older fully fixed threads were replied with +pushed implementation/native proof and resolved; new unresolved findings are not conveniently closed. + +The new extensionless finding genuinely reproduces12 AssertionErrors on published59ad, zero errors/skips: +an otherwise authentic signed project artifact gains category authority while containing an unsigned excluded +executable. Mode-only/shebang/ELF/PE/Mach-O/fat-Mach-O harmless probes are inspected, never executed. The correction +detects executable mode and standard headers independently of suffix and revalidates every signed file's canonical +membership/content after genuine cache reproduction. Ordinary excluded non-executable diagnostics retain loading. +Signing serialization/API, pinned trust, native artifact policy, provider priority, caches and all6accepted79 +control files remain unchanged. This does not claim detection of arbitrary interpretable data or atomic sealing. + +A separate seventh test file retains18 standalone controls and adds2paired identity controls. Genuine category/group +crypto and existing explicit validation refusals run FIRST with reset state; the genuine tier regression runs LAST. +The4standalone category/group cases already pass protected dev and remain full-suite compatibility controls; their +literal nodes are not mapped as RED. The intermediate97 replay (93failures/4passes/0errors/skips) is preserved and +not claimed all-failing. Final mapping93 retains79 cases plus12extensionless probes and2paired same-authority identity +controls. Final protected93 replay has93 genuineAssertionErrors/zeroerrors/skips, verified protected import origin and +all7exact test hashes. No failed setup is counted as RED and no native rule is weakened. + +Final complete normal full suite:3,413passed/nine configured skips/zero failures/errors/two existing deprecations, +266.74seconds,64%coverage vs unchanged50%,normal30secondtimeout; all793 sampled inputs and installed graph unchanged. +JUnit was copied before another shared pipeline. Strict typing:0errors/1456existingwarnings. Exact committed CI SAST: +0findings/0errors, unchanged baselinePASS. Blocking Bandit:zero medium/high findings. Direct changed-mapping YAMLlint, +newtest lint/format, strict OpenSpec/frontmatter/reproducible delivery/version/PyPIahead/strict core signatures pass. +Dependency graph inputs remain unchanged; prior exact all3frozen advisory audits/licenses retain applicability. +Rebuilt wheel SHA2566b2ad6ef5f5d72e15152bd97735a6ef7c265777e8a1f02d25756a628686faa41 contains exact changed source +and passes frozenofflinehashlocked installed import/CLIversion on actualPython3.11.15/3.12.13/3.13.14. This is +installation proof, not93security probes on each interpreter. Final FULL18file review passes72 with30exact old nonblocking identities/multiplicities and zero new findings. + +Standalone read-only verify-fix FIRST then3model-diverse interrogation reviewers report no actionable findings on +source217dc80fc4457c6bb2b3dd0ace4b950ceb3863c7638305551ef59ea58a75f620 and test56b21612a461c6c504105a1c78e86c67b7382581faa9ce74ee33eb6c55ac5de2. +Their static limits and exact hashes are retained. The17exact protected YAML diagnostics/launcher false-success +behavior and61unrelated privatewiki health gaps remain historical disclosed limitations, no new exception/configchange. +After final local gates, fresh owner93acceptance and genuine signed/native93RED must precede implementation restoration +and final hosted proof. Current79 receipts cannot validate93bytes. No target/companion merge authority; verified human719 +merge precedes743, then751. Companion scripts-only511 remains unpublished/UNKNOWN without authenticated eligible +Darwin native review; no candidate execution/selfsigning/fallback/spoofing or unrelated scope expansion. + +Evidence:719-executable-identity-final-before.xml,719-inventory93-protected-red-summary.json/xml, +719-inventory93-full-serial-summary.json and copiedJUnit,719-inventory93-types.json, +719-inventory93-sast.json/gate.log,719-inventory93-bandit.log,719-inventory93-wheel-matrix.json, +719-native79-final-verified-summary.json and719-native79-effective-required-checks.json. + +## Signed native79 RED verified — 14 October 2026 (Europe/Berlin) + +Signed specification2c6a51f9b6d996950a65fc53953485a1e6371238 precedes signed authored tests2677520e53459da7f85bd92da59f8c78ed1657b2 on unchanged protected production/version. +Actual native run38628199251, artifact11924956747, service ZIP SHA256b267eacf3ac27c75a768e7967f1dfd09841f92b7ae5feddc4d1bcc1c2ee907a0, has79 genuine AssertionErrors, +zero errors/skips and exact source/tree/base/selectors plus all6 authored test hashes. Its RED gate passes +with zero proof findings. JUnit digest:sha256:f2abfa7c9954e35acd86601d93838ccf2bfd061ebe26ff7462048cea318af604. Reviewed production/version0.55.10 is restored only +after this real service artifact and bindings are verified. Deliberate RED workflow failure is evidence, +not final merge-ready CI. Final native producer/execution/FULL review, required CI, live exact-head authority +and completed current-head review remain required; no target merge is authorized. Companion symlink blocker remains. + +## Historical accepted79 signed authoring — 14 October 2026 (Europe/Berlin) + +The human accepted exact per-source79 mapping88a043ab007a694dc2139ab3580c1951974d0380346da4fca2099cec4bafdad7 +and authorized actual djm81 acceptance recording. This does not authorize merging. The reviewed candidate and +exact six test hashes are preserved before authoring signed specs/tests on protected production/version. +Native RED must be genuinely verified before restoring production/version0.55.10. Local GREEN receipts below +belong to the preserved reviewed candidate; current authored RED is not readiness or final hosted proof. + +## Historical local79 coverage amendment — 14 October 2026 (Europe/Berlin) + +Canonical PER-SOURCE79 mapping `sha256:88a043ab007a694dc2139ab3580c1951974d0380346da4fca2099cec4bafdad7` is UNACCEPTED. The accepted75 record and all five authored test hashes +remain unchanged. Published signed a51a9b572103542b94f26171d8d7e07cf0987dcb has actual native75 RED/GREEN proof, +all13 effective exact-integration required checks passing and FULL review72/30 unchanged nonblocking findings. +Current-head CodeRabbit review was requested once; completion is not yet verified. The companion symlink finding +remains blocked by unavailable admitted native review. No readiness or merge authority is claimed. + +Three additional review annotations identify coverage gaps, not a newly demonstrated production vulnerability. +A separate control file adds four real public controls without changing production or accepted75 test bytes. +Both publisher variants first pass genuine checksum/signature verification under the same test bundled anchor; +canonical project registration succeeds and noncanonical registration is denied in each grouping mode. The +mapped cycle control executes the existing unchanged healthy-cycle body, then verifies compatibility behind an +ineligible priority copy. The original standalone positive remains in the full suite; its literal node is not +claimed mapped. The conflict control observes the actual warning call via a standard logger while retaining +public inactivity and unrelated-survival assertions. It does not claim separate schema/bridge snapshots or +customer console delivery. No global solver or filesystem sealing guarantee is introduced. + +Specifications/design preceded the new tests. All79 final selectors produce genuine AssertionErrors on protected +daefe0c75207632e054d96c36557673afab86a3a, verified import origin, zero errors/skips. Separate isolated current-source +mutations removing only the publisher check or the warning produce respectively2 and1 AssertionErrors, proving +the intended new oracles; these mutations are disclosed as oracle validation, not genuine protected/native RED. +The final new-file SHA256 is950c31f3d0714b45e552bd691bead3b6e5037bde963921321deae7c03f94adb9. + +Final complete normal frozen suite:3,393 passing tests/nine configured skips/zero failures or errors/64% line +coverage against50%/normal30-second timeout/254.45seconds. All792 sampled inputs and installed graph are unchanged. +FULL17file review passes72 with30 exact existing diagnostic identities/multiplicities and zero new findings. +An earlier sandbox attempt failed Ruff/Semgrep analyzer/certificate access; failed artifacts remain retained. +The unchanged host retry with frozen tools passes; no failure is substituted or gate weakened. Strict types +report zero errors/1,456 existing warnings. Exact committed CI SAST has zero findings/errors and its unchanged +baseline gate passes. New-test lint/format, direct mapping YAML lint and strict OpenSpec validation pass. +Source, package/delivery inputs and dependency graph are unchanged since the verified75 candidate, so its frozen +three-graph audits, strict signatures and actual installed wheel3.11–3.13 evidence retain their exact applicability. +No installation matrix is represented as79 security probes on each interpreter. + +Standalone read-only security verify-fix precedes three model-diverse reviewers; all reported boundaries fixed +and no final actionable findings. Limits remain explicit: test trust anchor and exercised combinations only. +Production and all five accepted test files are unchanged. After exact79 mapping acceptance, genuinely reauthor +signed spec/tests on protected production and verify actual native79 RED for exact six hashes before restoring +production/version. Normal signed hooks, fullSHA provenance, exact leases, live-head grants, native final execution, +all required integrations and completed current-head review remain mandatory. Resolve the three coverage threads +only after pushed validation; the separate companion symlink finding remains open. #743/#751 remain held for +strictly sequential verified human parent merges. The sections below are historical receipts. + +Evidence:719-review-oracles79-protected-red-summary.json,719-review-oracles79-protected-red.xml, +719-review-oracles-final-red-summary.json,719-review-oracles-full-serial-summary.json, +719-review-oracles-full-serial-junit.xml,719-review-oracles-full-mode-review.json, +719-review-oracles-review-identity-comparison.json,719-review-oracles-types.json,719-review-oracles79-sast.json. + +## Historical signed native75 RED verified — 14 October 2026 (Europe/Berlin) + +Signed specificationd3fea4aa1717ce139090d35492045673b8dd3cea precedes signed authored testsed3a7526b1103e19413a1ab7a87108b49e482132 on unchanged protected production/version. +Actual native run38618333057, artifact11920194147, service ZIP SHA25663b450a14d21c9072007166ead265c9e271cb43f814bffa5ffde893d0f8435a1, has75 genuine AssertionErrors, +zero errors/skips and exact source/tree/base/selectors plus all5 authored test hashes. Its RED gate passes +with zero proof findings. JUnit digest:sha256:953e2094351d8800fabac7baa019a9c7a9388392071d61b61dffe06076ca5bdd. Reviewed production/version0.55.10 is restored only +after this real service artifact and bindings are verified. Deliberate RED workflow failure is evidence, +not final merge-ready CI. Final native producer/execution/FULL review, required CI, live exact-head authority +and completed current-head review remain required; no target merge is authorized. Companion symlink blocker remains. + +## Historical accepted75 signed authoring — 14 October 2026 (Europe/Berlin) + +The human accepted exact per-source75 mapping7fff9b05caebbeae57598e0342782cb42fdf6271449681c065ebf200a7df4dcd +and authorized actual djm81 acceptance recording. This does not authorize merging. The reviewed candidate and +exact five test hashes are preserved before authoring signed specs/tests on protected production/version. +Native RED must be genuinely verified before restoring production/version0.55.10. Local GREEN receipts below +belong to the preserved reviewed candidate; current authored RED is not readiness or final hosted proof. + +## Historical local 75-control selection correction — 14 October 2026 (Europe/Berlin) + +The human approved bounded refusal of contradictory priority selections. The exact canonical per-source mapping +is `sha256:7fff9b05caebbeae57598e0342782cb42fdf6271449681c065ebf200a7df4dcd` and remains UNACCEPTED. The accepted63 record remains unchanged and applies only to its historical +bytes. The new separate test file adds 12 controls; all four previously authored files retain their accepted hashes. + +Specifications preceded the public configured-root discovery and real test-key signature controls. All 75 final +selectors reproduce genuine AssertionErrors on protected `daefe0c75207632e054d96c36557673afab86a3a`, with verified +production import origin, zero errors and zero skips. The self-dependency reproducer independently failed before +its candidate-version correction. Intermediate synthetic-path and candidate-list diagnostics remain retained +and are not final RED authority. A consistent dependency cycle also passes as positive compatibility coverage. + +The implementation applies unchanged local compatibility, schema, integrity and root-authority gates before +duplicate selection. Dependency selection preserves provider priority and reconsiders consumers against selected +versions, including each candidate's own enabled state and version. Repeated selection vectors refuse only the +varying names and recompute the remaining candidates and dependents. The complete selected set is revalidated +before registration effects. No global solver, arbitrary iteration limit, persistent trust cache, atomic filesystem +sealing claim, threshold or provider-pin change is introduced. + +Final normal frozen validation passes 3,389 tests, nine configured skips, zero failures/errors and 64% line coverage +against the unchanged 50% threshold in 268.45 seconds. The normal 30-second timeout remains enforced; all 791 +sampled inputs and installed dependencies stay unchanged. The version-source test actually executes with a clean +index. A preceding staged-index run legitimately skipped that test; its result is intermediate, not final authority. +FULL review covers 16 files and passes score72 with the same 30 nonblocking findings and zero new findings. +Strict types report zero errors and the same 1,456 warnings. Exact CI SAST has zero findings/errors; all three +frozen security audits pass. Delivery and version-source checks pass, and strict local0.55.10 is ahead of PyPI0.55.4. +The rebuilt wheel installs, imports from site-packages and invokes its actual CLI on Python3.11–3.13 with offline, +hash-locked dependencies. This installation matrix does not substitute for hosted security-control execution. + +Standalone read-only verify-fix preceded model-diverse interrogation. Initial interrogation caught a self-version +selection flaw; spec/test/assertion RED preceded its fix. The fresh verifier and all three final reviewers report +no remaining findings. The final new test SHA256 is +`c40c7f69d61c5b17c64d171c320140bd3264ae5baf3f7b4811304dfa36603a91`. + +Direct YAML lint of this changed mapping passes. Five long keys were wrapped with escaped quoted continuations; +parsed values and the canonical mapping are unchanged. The repository-wide launcher incorrectly returns zero +when yamllint emits errors. Its 17 remaining diagnostics are exact unchanged protected-baseline subjects in four +unrelated historical/planning files. This narrow documented existing exception is enumerated in +`719-selection75-yaml-baseline-comparison.json`; it covers no new diagnostics or changed mapping errors. The +launcher, lint configuration, baseline files and thresholds are unchanged. Do not describe its zero exit as clean +repository-wide lint. A tool-detection correction is a separate follow-up, not silently included in this change. + +Published signed c89701859ea3f83f477f733f4efe35389166566b still carries accepted63/native63 proof and completed +current-head review. The local75 correction is unpublished. After exact mapping acceptance, genuinely author signed +spec/tests on protected production and verify actual native75 RED before restoring implementation/version. Normal +signed hooks, exact leases, live exact-head grants, hosted integrations, final service digests and completed review +remain required. Companion issue511 retains its missing admitted-native-review blocker and symlink thread remains +open. #719 is not merge-ready; human merges remain mandatory before #743 and then #751. + +Evidence: `719-selection75-protected-red-summary.json`, `719-selection75-protected-red.xml`, +`719-selection-full-serial-summary.json`, `719-selection-full-serial-junit.xml`, +`719-selection-full-mode-review.json`, `719-selection75-types.json`, `719-selection75-sast.json`, +`719-selection75-wheel-matrix.json` and `719-selection75-yaml-baseline-comparison.json` under the retained evidence root. + +## Historical local 74-control selection prototype — 14 October 2026 (Europe/Berlin) + +The human approved bounded refusal of contradictory dependency-priority selections. Specification preceded the new separate test file. All 11 newly mapped controls reproduce genuine AssertionErrors on both protected daefe0c7 and published c8970185, with verified production import origins and zero errors/skips. Fixtures use actual configured-root discovery and real test-key crypto through the unchanged ordinary verifier API. A separate consistent-cycle positive control remains mandatory compatibility coverage in the focused/full suites. Earlier synthetic-candidate/path diagnostics are retained as intermediate, not final RED authority. + +The correction applies unchanged core/schema/integrity/root-authority checks before duplicate selection. Contextual dependency reselection preserves provider priority and reconsiders consumers against actual selected versions. Repeated vectors identify the oscillating names; refusal removes only that component and recomputes the rest. All selected artifacts are revalidated before schema, bridge, protocol, source or command effects. No global solver, arbitrary iteration cap, persistent trust cache, filesystem sealing claim or gate relaxation is introduced. + +The initial complete frozen suite passes 3,388 tests with nine configured skips, zero failures/errors and unchanged 791 inputs/dependencies. The final strengthened discovery controls and structurally simplified cycle setup still require final full validation and independent review. Final mandatory SAST has zero findings/errors and strict types have zero errors with the same 1,456 historical warnings. The 74-control canonical mapping is UNACCEPTED; prior accepted63/native proof remains historical authority for its exact bytes only. Fresh signed native RED/GREEN, required CI and actual completed current-head review remain required before publication/readiness. The companion symlink prerequisite remains blocked by missing admitted native review; no target merge is authorized. + +## Historical implemented 63-control candidate — 2026-10-14 (Europe/Berlin) + +The owner accepted canonical source mapping `6a137ae6` with all 63 selectors. Signed specification08e1e002 +precedes signed authored tests3bdaa89b on protected production. Actual native RED38559677185/artifact11891502779 +has 63 genuine assertion failures, zero errors/skips, verified service ZIP digest, exact source/tree/base, +four test hashes and JUnit binding. Its RED gate passes with no proof findings. Reviewed production and bugfix +0.55.10 follow only after inspection of this real artifact. Earlier bad_email heads are preserved but were not +granted authority; both corrected authoring commits have matching signing identity. + +The reviewed local candidate passes 3,376 full-suite tests, nine configured skips, zero failures/errors and +64% coverage against 50% in 253.64 seconds with the normal30-second timeout. All790 sampled inputs and installed +dependencies remain unchanged. Required FULL15file review passes score72 with30 unchanged nonblocking findings; +strict typing has zero errors. Standalone security verification then three-model interrogation has no findings. +These local receipts do not substitute for fresh final native execution, required CI and completed current-head +review. The companion protective symlink prerequisite is authorized but not yet delivered. #719 is not merge-ready; +no target merge is authorized, and #743/#751 remain held for sequential verified human merges. + +## Historical record: Current signed authoring phase — 2026-10-14 (Europe/Berlin) + +The human accepted exact canonical 63-control mapping `6a137ae6` and authorized the protective companion +symlink prerequisite. Both approvals are recorded; neither authorizes merging. Reviewed production and bugfix +0.55.10 are preserved in an immutable accepted snapshot. Specification and test commits are genuinely authored +on protected `daefe0c7` with production/version unchanged until actual native RED is verified. Local reviewed +GREEN receipts below describe the preserved candidate, not this deliberately test-only authoring phase. + +## Historical record: Current local 63-control correction — 2026-10-14 (Europe/Berlin) + +The local canonical mapping is +`sha256:6a137ae6c8d7f0ef499d4e62df6808a7c9c2eea8706748445fc9cb26269cf880`, covering 63 exact selectors. +The human accepted this exact mapping and authorized djm81 recording on 2026-10-14 (Europe/Berlin). +Signed/native publication is not complete. The published signed head `367358b7` retains the accepted +61-control mapping. Its native run `38546226366` passes producer, independent execution and final proof, +with verified service digests and the exact source reference. All 13 required statuses pass, and CodeRabbit +completed its current-head review at `2026-10-14T10:04:14Z`. Those receipts do not prove this local amendment. + +Finding `QrRG` identifies a valid primary manifest with malformed alternate `metadata.yaml` that escapes +YAML parsing and aborts startup. The specification precedes two public controls, which reproduce genuine +assertion failures on signed head `367358b7` before the fix. Canonical serialization now converts YAML parser +failures to the existing invalid-payload `ValueError`, allowing strict project admission to reject the package +before registration effects. Valid signed bytes and ordinary APIs remain unchanged. Separate preexisting +ordinary YAML paths are not claimed globally remediated. + +All three accepted test files remain unchanged; the two new controls live in a separate file. The exact +63 selectors produce 63 genuine assertion failures on protected `daefe0c7`, with zero errors or skips. +Focused validation passes 69 tests. The complete normal frozen suite passes 3,376 tests with nine configured +skips, zero failures or errors, and 64% coverage against the unchanged 50% threshold in 253.64 seconds. +The normal 30-second timeout remains enforced. All 790 sampled inputs and the installed dependency graph +stay unchanged. Required FULL review covers 15 files and passes at score 72 with 30 existing nonblocking +findings and no new findings. Strict typing reports zero errors and 1,456 existing warnings; the exact CI +SAST gate passes with zero current findings. Standalone read-only security verification followed by three +model-diverse reviewers reports no findings. The rebuilt wheel installs and invokes its installed CLI on +Python 3.11–3.13 using offline, hash-locked dependencies. + +An earlier full run failed when review-only module roots leaked into discovery and concurrent Hatch commands +changed the shared runtime dependencies. Its failed JUnit and log remain retained as failed evidence. The +serial rerun restored committed dependencies, removed the review-only roots and verified graph stability. +No test, gate, timeout, threshold, provider pin or trust policy was weakened. + +With exact mapping acceptance recorded, signed specification and test authoring, native RED before implementation, +current-head final proof, required CI and completed review remain mandatory. Companion release-symlink finding +`CejA` remains unfixed. The human authorized its protective companion prerequisite; implementation and validation +remain required before closure. #719 is not merge-ready; +no target merge is authorized. #743 waits for verified human #719 merge, and #751 waits for human #743 merge. +The sections below preserve historical receipts. + +## Historical record: Current accepted 61-control validation — 2026-10-14 (Europe/Berlin) + +The authoritative accepted source mapping is +`sha256:87a4aba6128b7dbe42d916f190fc274ed6b1fae839df9c8474c6615435c334d0`, covering 61 exact selectors. +The signed acceptance record is `requirements-proof/review-evidence.json`; previous 51-control dc7c and 34-control +records below are historical receipts. Their dated head and pending-state statements do not describe the current candidate. + +Signed specification 91e2b08c precedes authored tests 1c4a59c2 on protected daefe0c7. Actual native RED run +38543898314 retains 61 assertion failures, zero errors/skips and exact test hashes before implementation 24889ba9. +Native producer, independent execution and final reconciliation run 38544777662 all pass at implementation 24889ba9: +61 passing controls, zero failures/errors/skips, final verified evidence with no proof findings, and required FULL +review PASS at score 72. Actual service ZIP digests and JUnit bindings are retained in the delivery manifest. +Completed CodeRabbit review covers implementation 24889ba9; docstring coverage is 88.10% against the unchanged 80% threshold. + +The complete local frozen suite passes 3,374 tests with nine configured skips, zero failures/errors and 64% coverage +against 50% (253.413 seconds, normal 30-second timeout). This documentation correction preserves the accepted mapping, +all authored test bytes and validated runtime/delivery inputs. Current-head CI/review and evidence-backed annotation +resolution remain required before declaring merge-ready. No target merge is authorized; #743 and #751 wait for their +preceding human merge. The chronological sections below preserve historical evidence and limitations. + +## Historical record: Current local review amendment — 2026-10-14 (Europe/Berlin) + +The current local candidate has 61 controls. Its canonical per-source mapping is +`sha256:87a4aba6128b7dbe42d916f190fc274ed6b1fae839df9c8474c6615435c334d0` and is owner accepted. +The human approved this exact 61-control mapping and recording acceptance as djm81 on 2026-10-14 +(Europe/Berlin). The accepted 51-control dc7c record below remains historical for its exact mapping and bytes. Signed local 9aae28cf is unpublished; remote +4ab42909 is not merge-ready. #743 and #751 remain held until verified sequential human merges. + +Eight public controls reproduce actual indirect mounting/member execution or alias-created reserved roots with +zero errors/skips on 9aae28cf before this correction. Exact final selectors replay against protected daefe0c7: +61 genuine assertion failures, zero errors/skips. The shared resolver preserves explicit bundle precedence and +namespaced fallback; authority covers categorized project packages, official bundle mount claims and direct roots +before selection or side effects. Authentic official bundles, non-project routing and unreserved category-less +legacy commands without mount claims remain supported. Native .so/.pyd controls extend omitted-artifact coverage; +a missing required signed fixture fails clearly and is never skipped. Focused 67 tests pass. Full enforcement +passes at score 72 with the same 30 existing nonblocking findings; strict typing has zero errors. Standalone +security verification and all three adversarial reviewers report no findings. The complete frozen suite passes 3,374 tests with nine configured skips, zero failures/errors and 64% coverage +against the unchanged 50% requirement (253.413 seconds, normal 30-second timeout). All 789 sampled inputs stay +unchanged. Fresh signed/native proof, current-head hosted CI, genuine review completion and annotation resolution +remain required. No gate, threshold, timeout, signing key, provider pin or execution rule is weakened. + +## Historical record: Current accepted delivery — 2026-10-14 (Europe/Berlin) + +The owner accepted the canonical 51-control source mapping +`sha256:dc7c63570b4252d2a1c3dc44c2a7fefd18702fc6b1d769d888613d9b0c2011ee`. +Signed specification 9022172a and authored tests d61f5223 precede implementation 4ab42909. +Actual retained native RED run 38524710157 reproduces all 51 assertion failures with zero errors or skips. +Implementation run 38525863853 passes all 51 controls; its full review fails on existing fixture typing +warnings in the expanded scope. Changed-mode advisory output does not satisfy full enforcement. + +The correction supplies accurate fixture/callback types, preserves autouse fixture behavior, and retains +category-error propagation plus malformed-model rejection through equivalent exception classification. +The accepted mapping and all three authored control files remain unchanged. Standalone security verification +and three model-diverse reviewers report no findings. The complete frozen suite passes 3,364 tests with nine +configured skips, zero failures/errors and 64% coverage against the unchanged 50% threshold; strict typing +reports zero errors. Mandatory full review now passes at score 72 with 30 existing nonblocking findings. +No enforcement mode, threshold, timeout, provider pin, signature or proof boundary changed. + +A fresh signed head must still pass native producer/execution/final reconciliation, every required hosted check, +completed current-head review and annotation resolution. #719 is not yet merge-ready. #743 and #751 remain held +until their preceding PR is verified human-merged. The sections below preserve historical evidence and limitations. + +## Historical record: Current correction: authenticated project installation + +## Historical record: Pending final admission amendment — 2026-10-14 (Europe/Berlin) + +The current local mapping contains **51 exact controls and is not owner accepted**. Earlier canonical b991 +acceptance (bound to aggregate830) remains historical and unchanged. Intermediate43/47/49 digests must not be used +as current acceptance. No fresh acceptance question has been issued. Published head dfbffcb5 remains historical for +this dirty candidate; #743 and #751 stay held until sequential human merges. + +Independent verification and three model-diverse reviewers agreed on a during-compiler admission gap. Two +faithful real-compiler controls first reproduced transient activation: real standard bytecode was inserted into an +already enumerated source directory, or an observed signed source was replaced. Neither cryptographic verification +nor compiler decisions were mocked. The correction completes cache reproduction from authenticated source bytes +before a fresh, compiler-free executable membership/content traversal. The existing command authority recheck now +precedes schema, bridge, protocol and active-source effects. Ordinary verifier APIs and signing remain unchanged. +This closes the concrete compiler interleaving; it does not seal mutable files against unrestricted later writes. + +The earlier signature-callback mutation controls also now observe every actual activation, including roots cleared +by later lazy registration. An initial final-state-only PASS for the inserted-source case was a false negative and +is explicitly invalidated. The strengthened controls reproduce two genuine activation assertion failures before +membership/digest binding. The initial missing-helper baseline replay and malformed forged-app cache probes are +also invalid evidence; subsequent public, valid-app controls replace them. + +The local focused run currently passes59 tests. The exact51-selector protected replay, final full frozen suite, +fresh independent verification and actual hosted native proof/current-head review remain required. Tests prevent +pytest from writing its unsigned assertion-rewritten caches into immutable signed fixture copies, while explicit +standard subprocess-generated caches and forged loadable caches remain exercised. No production admission rule, +timeout, coverage threshold, provider pin, signature or proof trust has been relaxed. + +Expanded review scope retains only the documented protected99 finding identities and multiplicities:90 literal +source statements and9 equivalent unchanged naming/member diagnostic AST subjects. The protected source review and +identity comparison live in the private evidence packet; this narrow existing exception does not cover any new +finding. New complexity, naming, missing-contract, unused-private-fixture and long-test advisories were corrected +structurally. Final review must prove zero new findings rather than relying on the smaller historical18 scope. + +On 13 October 2026 (Europe/Berlin), a final-head P1 review found a supported project-scope regression in f8ffe7c6. +The concrete local correction preserves authenticated official project roots and rejects unsigned, forged, tampered, +untracked-source and symlink-payload claims before effects. All 34 exact protected-base controls genuinely fail before +the patch and the full final frozen suite passes 3348 tests with 9 disclosed skips. Zero new local review findings. + +Current mapping is sha256:8303300c47c5e382a612170d7eb7c24fa2f146ea0615bc379e54a27656beb8c6; fresh owner acceptance and signed/native proof are still pending. +Earlier E550/f8 claims below are historical. No gate or trusted evidence boundary is weakened; the existing ordinary +artifact-verifier API is unchanged. Full current-head CI/review and sequential planning-child proof remain required. + +# Historical record: Current phase: approved guard after documented native RED + +Protected base daefe0c75207632e054d96c36557673afab86a3a has coherent version0.55.9. Exact source mapping +E550 and genuine owner acceptance are unchanged. Publish specification and authored tests before +production and retain fresh native RED; implementation targets available bugfix0.55.10 afterward. +All earlier claims below are historical. Current-head gates and sequential child proof remain required. + +## Historical record: Historical validation records + +## Historical record: Current phase: refreshed specification after #767 + +Protected base is c6af68a0fdf2ea798dd22c6750f077e217c938f5, version 0.55.8. Prior passing claims below +are historical; exact current-base authored and final proof remain required. New implementation targets +0.55.9 only after native RED. Scope and genuine e550f23e owner acceptance are unchanged. + +## Historical record: Current protected-base readiness + +Protected dev is c4ee5be8457f69848bf9bc2150ebc3df56b52d4f, version 0.55.7. Prerequisite #761 is merged. +The approved security scope and exact source mapping e550f23e remain unchanged. Historical validation below +is retained as provenance; implementation, fresh native RED/final execution and all final-head gates must +be regenerated in this cycle. #743/#751 stay at planned maturity because paired modules #483 remains Todo +on 12 October 2026. No gate, trusted source, signature or acceptance requirement is weakened. + +## Historical record: Historical Change validation: workspace root-command registration + +Date: 2026-10-11 (Europe/Berlin). Refreshed dev: `4e7c38c2830f4bc53ffe78eeb0a2bfbabfc711e2`. + +## Historical record: Verified scope and interfaces + +Discovery computes and preserves metadata.source; YAML source fields cannot grant source authority. +Default grouped registration rejects category-less project packages. Category-less user/marketplace/custom +sources and explicitly disabled grouping retain unreserved legacy commands. Every external resolved path +is denied registration/extension of permanent `init`, `module`, `upgrade` commands, in both registries and +both modes, before constructing a package loader. Core category or source labels do not grant authority. + +The MODIFIED module-grouping block reconciles the existing missing-category requirement and retains its +scenario names for native archive compatibility. Categorized routing remains unchanged. No dependency +upgrade, new Python API, module payload or manifest signature change is introduced. A 0.55.7 patch release +synchronizes all canonical version sources and generated frozen metadata; PyPI currently remains 0.55.4. + +## Historical record: Readiness and evidence + +Issue #718 is open, parent #352, labeled and assigned to the SpecFact CLI project/Todo; native blocker and +blocking lists are empty. The earlier project-access blocker is resolved. Matching private wiki maintenance +is required before finalization. Broad future contracts in #743/#751 remain planning under owner direction. + +Strict OpenSpec validation and whitespace validation pass. The earlier nine-control evidence and first 78 passing +controls are historical. The amended mapping +uses twenty exact executable boundary selectors; historical native authored proof and current local passing/full-suite +evidence is recorded in TDD_EVIDENCE.md. Commands, environment and current-head hosted proof +references are recorded in TDD_EVIDENCE.md; pending broad/current-head checks are not passing claims. + +## Historical record: Limits and rollback + +This guard prevents the demonstrated root-command routes; it does not sandbox intentionally invoked +categorized module code or authenticate every extension. Existing unsigned-workspace policy is unchanged. +Explicit legacy mode remains a user-selected compatibility risk for unreserved commands. Revert the +registration/docs/version commit as a coherent patch to restore the prior behavior; restore captured PR +heads only with exact force-with-lease protection and revalidate changed ancestry/evidence. + +## Historical record: Expanded current review boundary + +The owner accepted amended mapping a9ba0dfd after confirmation that all 14 remaining behavior failures +will be fixed. Valid manifests reproduce 14 real failures against facb690 and 17 against protected dev. +Draft fixture and incompatible reconciliation runs are discarded, never claimed as passing proof. +The released exact-selector/all-failing RED contract is preserved through atomic boundary tests that +exercise rejection and supported counterparts with identical code in RED and final. The amended selection +has ten genuine failing exact nodes before implementation and 85 focused passes afterward; native +corrected RED passes in run 38176059675 and the full suite passes 3,299 tests with ten disclosed skips. +Final-head execution and required checks remain pending. Rejected packages are +filtered by `_registration_packages` before package registration side effects; `_package_registration_allowed` +checks effective built-in containment against `get_modules_root()`. Integrity-root exemptions are not broadened. + +## Historical record: Final review amendment + +The final source mapping e550f23e maps twenty exact nodes, including nine independent reserved flat +claims. All twenty fail genuinely against protected baseline 4e7c38c and all nineteen security controls +pass in the current implementation. Source a9ba0dfd and its published native proof are historical. +Exact owner acceptance is recorded. Protected prerequisite #761/#760 integration, a later coherent patch version, +regenerated native authored/final proof and final-head hosted review remain outstanding. + +## Historical record: Signed native 63-control RED retained — 2026-10-14 (Europe/Berlin) + +Signed specification08e1e002 precedes signed authored tests3bdaa89b on unchanged protected production/version. +Actual native run38559677185, artifact11891502779, service ZIP SHA256357579c646597eabacb22c348a411b9b9a5834fb14ef9de3557a7ea7b2f1c0df, has63 genuine assertion failures, +zero errors/skips and exact source/tree/base/selectors plus four authored test hashes. Its RED gate passes +with no proof findings. JUnit digest: sha256:b69be84744b3e5bed0ea66cf3e6d7e7e3c891afab40fc0809d7c6da32c0b05fc. Reviewed production/version0.55.10 is restored only +after this service artifact and all cryptographic bindings are verified. The deliberate RED workflow failure +is evidence, not merge-ready CI. Final independent execution/FULL review, CI, authority and completed review +remain required; no target merge is authorized. Rejected bad_email heads are preserved but are not authority. diff --git a/openspec/changes/fix-categoryless-module-command-squatting/TDD_EVIDENCE.md b/openspec/changes/fix-categoryless-module-command-squatting/TDD_EVIDENCE.md new file mode 100644 index 000000000..61f45e29a --- /dev/null +++ b/openspec/changes/fix-categoryless-module-command-squatting/TDD_EVIDENCE.md @@ -0,0 +1,947 @@ +## Current accepted path100 runtime GREEN and remaining gates, 15 October 2026 (Europe/Berlin) + +The human accepted PER-SOURCE100 sha256:03651caa143d7cbde41a9f33db9cc91182e9e05201173bf4a3bc5273c021dbdc. +Signed specification1c399e2d4ab24cbfe10859d87da00e107cafffcf precedes authored35c6f1339096cd35d53987d02658eb02f7215363. +Actual native RED38687272103/artifact11950602909/serviceZIP a883f2b809c0438be27ff29918a6916779f88c0b8284cb079d9f3db0c09fd622 +has100 genuine AssertionErrors, zero errors/skips, and verified source/tree/base/nine hashes/selectors/JUnit. +Only after that proof is verified does signed implementation e4db7795139a562a0a56572bf696e0164f90f71b restore the +accepted runtime/version0.55.10. Normal hooks, all three signatures and actual literal full-SHA provenance pass. + +Actual native GREEN38687870204 on e4db7795139a562a0a56572bf696e0164f90f71b completes producer, independent execution +and final reconciliation. Both JUnit artifacts contain100 passes, zero failures/errors/skips and matching selectors. +All18 encoding, six owned-path and five legacy suffix outcomes verify, along with allfour real service ZIP digests. +FULL review is72 with the same30 accepted nonblocking findings and zero additions; identities and multiplicities +match after only three exact unchanged BasedPyright file-root normalizations. The producer digest binds its own +JUnit; the independent execution has a separate verified JUnit. Compact final receipts omit base/tree/testhashes. +Those bind through retained authored RED, current nine Git test blobs, producer plan and actual full-SHA provenance, +not invented receipt fields. The local runtime suite has3,430 passes/nine configured skips/64% against50%, normal +30-second timeout; its exact-source quality, independent reviews and installed-wheel evidence remain scoped as below. + +CodeRabbit reviewed e4db7795 in review5528734224 at2026-10-15T05:49:27Z with explicit current-head coverage and +80.92% docstring coverage against80%. Its new task-ledger finding and historical headings are corrected here. +The review reports zero included reviews remaining and an allowance of one per hour. No paid review, billing, +configuration change or repeated unchanged request is authorized or used. Python3.12 full CI was still running +at this evidence cutoff; twelve other effective exact-app integrations passed. No all-CI or merge-ready claim. + +This documentation reconciliation preserves runtime, tests, mapping, plan selectors and dependency inputs. Its +new exact head still requires native/check/review confirmation. The companion511 release-symlink fix remains +unpublished without admitted native FULL, with Linux guest access/resource constraints recorded separately. +Concurrent local modification after the final admission observation remains outside the documented guarantee. +No target or companion merges are authorized; human719 precedes743, then human743 precedes751. + +## Historical path100 native RED before runtime GREEN, 15 October 2026 (Europe/Berlin) + +Signed specification1c399e2d4ab24cbfe10859d87da00e107cafffcf precedes signed authored tests35c6f1339096cd35d53987d02658eb02f7215363 on protected production/version0.55.9. +Actual native run38687272103, artifact11950602909, serviceZIP SHA256a883f2b809c0438be27ff29918a6916779f88c0b8284cb079d9f3db0c09fd622 verifies100 genuine AssertionErrors, +zero errors/skips and exact source/tree/base/100selectors/nine test hashes. RED gate PASS with zero proof findings. +JUnit digestsha256:c36d4df8a40cdab552871e1fe77eb203daefc65997cd3d0c5bba62a8fbad37f7. All18 encoding, six owned-path link and five legacy suffix observations match +retained protected replay and precede aggregate assertions. Extra temporal/alias full-suite tests are not separately +claimed mapped native selectors. Reviewed production/version0.55.10 restores only AFTER real service bindings verify. +Local3430/FULL72 proof remains scoped to its exact runtime/test inputs. Final native producer/independent execution, +FULL review, all required checks and completed current-head review remain required. Observation does not seal later +filesystem state. Companion remains held. No target or companion merge is authorized. + +## Historical accepted path100 signed authoring, 15 October 2026 (Europe/Berlin) + +The human accepted exact canonical PER-SOURCE100 sha256:03651caa143d7cbde41a9f33db9cc91182e9e05201173bf4a3bc5273c021dbdc +and authorized recording acceptance as djm81. The 36-file accepted candidate preserves the reviewed runtime, +nine authored test hashes, all93 earlier selectors and all18 encoding observations within the unchanged100-selector limit. +The new signed specification and tests are authored on protecteddaefe production/version0.55.9. Actual native100 RED +must verify service ZIP/source/tree/base/100selectors/nine hashes/JUnit and all required observations BEFORE +reviewed runtime/version0.55.10 restoration. Local3430 PASS, FULL72/30existing/zero new and independent reviews +remain valid within their recorded input scope; no native proof for these new bytes is yet claimed. +Historical4c acceptance and signed528/native38647654283 RED/38648161680 GREEN are retained, not reused for new bytes. +Final service producer/independent execution/FULL/all required integrations and completed current-head review remain required. +The disclosed overwritten intermediate raw review and filesystem observation limits remain. Companion511 remains held. +No target or companion merge authority is granted; human719 precedes743, then human743 precedes751. + +## Historical local path100 validation before acceptance, 15 October 2026 (Europe/Berlin) + +Canonical PER-SOURCE100 `sha256:03651caa143d7cbde41a9f33db9cc91182e9e05201173bf4a3bc5273c021dbdc` is UNACCEPTED. No new native proof or mapping acceptance is inferred from the earlier accepted4c3d cycle or the broad required-change authorization. Published signed528b9338f71a1026fcc6b42fcd99156ba35cf344 remains the historical accepted100 head with authentic native RED38647654283 and GREEN38648161680, all13 effective required integrations and completed CodeRabbit5525712106. This new amendment is local and unpublished. + +Final runtime installer SHA d53cdcab025723a1ad8f618f1de955caee82e668dca6ea596569510e4f378bc8 and packages SHA ee47ead2c731d80888f18fa604c3193c54cf43dc7c1513c08fd54063b78e8b9b reject lexical artifact roots and owned project stores before and after authentic verification. Six genuine final-admission assertion failures, zero errors/skips, preceded the trailing reobservation correction. The first attempt mutated during initial verification and passed six controls because final revalidation already caught that earlier interval; it is preserved and not represented as RED. Scope is official project category authority, preserving unreserved category-less legacy compatibility and canonical workspace aliases. Sequential observations do not seal later filesystem state or guarantee lazy-load integrity. + +Seven accepted93 test files remain unchanged. Eighth bfe5ad2727ee4589a39b086b1d7bef1d6d429d059681b57c2f27924502b14779 conserves all18 named encoding/mode observations in four nodes. Ninth35a4bd64912ffd0e45c7c4a300634834302d02419a6a38f188dd71a522e2ef19 adds two mapped path-link nodes and one mapped legacy-native node. Native plan remains100 selectors. Actual protected replay has100 genuine AssertionErrors, zero errors/skips, exact nine hashes and verified import origin. Its suite properties preserve18 encoding, six owned-link and five legacy suffix observations before aggregate assertions. Six additional inside-final-verification controls, two between-pass controls and two canonical workspace-alias controls are full-suite evidence, not separately mapped native selectors. + +Final normal full suite3430 PASS, nine configured skips, zero failures/errors,262.520 seconds,64% coverage against unchanged50% gate and normal30-second test timeout. All795 sampled inputs and installed graph remained unchanged during that run. Copied JUnit and starting hashes are719-path100-reobserved-full-serial-* under the retained private evidence root. Final FULL20-file review PASS_WITH_ADVISORY score72 with30 existing findings and zero new. Two unchanged function findings move two lines; exact AST identity and finding multiplicities are verified, with no rule/message/subject exemption. Protected99 subject exceptions remain unchanged. Types zero errors/1456 existing warnings; exact CI SAST zero findings/errors and unchanged baseline gate PASS; Bandit zero blocking medium/high; lint/format/direct changed mapping YAML/strict OpenSpec/frontmatter/delivery/version/PyPI gates PASS. Locked graphs are unchanged; existing exact frozen advisory/license and signature receipts apply within their stated scope. Existing17 protected YAML diagnostics and whole-repo launcher's false-success behavior remain disclosed. + +Normal rebuilt wheel318f7dadb1a04577d886f0d04f68ebf7299f04da5c3cdbc02634098b0eca5b68 contains the exact final runtime source bytes. Frozen offline hash-required isolated Python3.11.15/3.12.13/3.13.14 install, installed import and CLI version checks PASS. This is installation evidence, not all security probes on all interpreters. Final standalone readonly verify-fix completed before model-diverse gpt-6-astra/6.1-sol/6-luna interrogation; all final reviewers report no findings with static limits. Reviewer capacity required reuse of finished non-writing reviewers, counted once each. The earlier panel's within-admission interval was acted on; its legacy-scope concern was resolved by truthful scope clarification. Final no-comments review has zero deletions or flags. + +Evidence retention limitation: the intermediate FULL70/32 raw report was accidentally overwritten by the next review before backup. Its two findings and result survive in the original chat; the raw report is not claimed preserved.719-path100-intermediate70-retention-limit.txt discloses this loss. Final72/30 report and all failed regression attempts remain retained. New mapping acceptance, genuine signed/authored native100 RED on protected dev, restoring implementation only after verified RED, final exact-head native/required checks and completed review remain pending. Companion511 native FULL remains UNKNOWN because no authenticated eligible Darwin capsule is admitted. No merge authority is granted;743 and751 stay held. + +## Historical local path100 correction, 15 October 2026 (Europe/Berlin) + +This local correction is UNACCEPTED for native publication. The owner accepted historical100 digest4c3ded1d98d1437c1de1fb73c243ad71d804506769135ea42581e9b3781ed274, whose signed528b9338f71a1026fcc6b42fcd99156ba35cf344 GREEN run38648161680 is verified. All100 historical selectors pass, all18 encoding observations appear, all four service ZIP digests verify, FULL review is72 with30 unchanged findings, all13 effective required integrations pass and CodeRabbit review5525712106 completed. This does not establish readiness for the new path-link correction or the companion prerequisite. + +New current-head controls reproduce two genuine AssertionErrors with no errors/skips. All three owned package/.specfact/modules links gain root and source authority in each mode before correction; lower-priority installed fallback is suppressed. The correction refuses the artifact root link in strict filesystem verification and workspace-owned links at project admission, preserving canonical workspace aliases and ordinary verification. It leaves later live-filesystem mutation outside the admission observation guarantee. + +The unchanged100-selector bound is preserved by keeping all93 earlier selectors, observing the same18 encoding probes in four nodes and adding two path-link nodes plus one aggregate legacy native suffix node. All observations precede aggregate assertions. Seven accepted93 test files remain byte-identical. The changed eighth file and new ninth file require a fresh exact per-source mapping acceptance. Temporal final-revalidation and canonical alias controls also run in the full suite; their literal nodes are not claimed mapped. + +Initial full3422 passed with nine configured skips and unchanged795 sampled inputs/installed graph. A later cleanup removes three narrative test docstrings and adds two final-revalidation controls; that initial full and protected replay are intermediate evidence. Final planned digest, protected RED, full gates and independent verification are being refreshed for the exact final bytes. No hosted proof for this amended mapping or new acceptance is claimed. + +## Historical native100 RED for the preceding4c mapping — 15 October 2026 (Europe/Berlin) + +Signed specification6c63d63323e94a7c633c69ea964c85c3d1c9c71c precedes signed authored testsd20d6a56e9781a740a1c82907158c671ad3d4b7f on unchanged protected production/version. +Actual native run38647654283, artifact11932289125, service ZIP SHA2568429e0ff751d0fcd044cda08b1d4225ca2ea494b94b5330c90bbf24c384151bc, has100 genuine AssertionErrors, +zero errors/skips and exact source/tree/base/selectors plus all8 authored test hashes. Its RED gate passes +with zero proof findings. JUnit digest:sha256:6a252b0198a77bdc86e5bb7511af4843a68fe7b836c1856b8c5f870c4ea54ea5. Reviewed production/version0.55.10 is restored only +after this real service artifact and bindings are verified. Deliberate RED workflow failure is evidence, +not final merge-ready CI. Final native producer/execution/FULL review, required CI, live exact-head authority +and completed current-head review remain required; no target merge is authorized. Companion symlink blocker remains. + +## Historical Current accepted100 signed authoring — 15 October 2026 (Europe/Berlin) + +The human accepted exact per-source100 mapping4c3ded1d98d1437c1de1fb73c243ad71d804506769135ea42581e9b3781ed274 +and authorized recording as djm81. The accepted35file candidate/eight authored hashes are preserved before genuine +signed specifications/tests on protecteddaefe runtime/version. All93 prior selectors and18 named mode/header +observations in7 matrix nodes remain; unchanged executorMAX100 and all native/trust/budget/pin/threshold gates. +Actual native100 RED must verify serviceZIP/source/tree/base/100selectors/eight hashes/JUnit and all18 observations +before reviewed production/version0.55.10 restoration. The failed111 pre-JUnit artifact remains INVALID RED, not reused. +Fresh finalnative producer/execution/FULL/allrequiredintegrations/completedreview remain required. No merges authorized. + +## Historical private100 validation before owner acceptance — 15 October 2026 (Europe/Berlin) + +The human-approved111 mapping4aa84a05cf7c4dde425060734bc13cd51b6111b7c2f3759e7c45e0dab6dd9f1a and signed +spec2f248c0911e248be4ad77de289429d2e1f778c14/authored2b2b0ff109ef1a68e1adf9cde0db7be8fdf4c311 remain preserved. +Actual native111 run38641093406/artifact11930266718 serviceZIPb18c0402f024853630ced8cbfc176225dec7c16ad3a0e5fcbc53edf59f1a9d80 +failed before JUnit: protected executor rejects more than100 selectors. This is invalid RED, not assertion evidence; +no runtime restoration or unchanged retry. Protected executor/trust/budgets/timeouts/coverage/pins remain unchanged. + +A separate local worktree consolidates all18 named mode/header×grouping observations in7 nodes, retaining all93 +prior selectors and seven accepted93 files. For every probe, canonical compatibility and genuine crypto precede +actual root/source observation; registry/import state resets between probes. All observations are collected and +recorded through the typed public JUnit suite-property fixture BEFORE aggregate assertions, preventing early failure from omitting legacy or later +permission probes. New100 per-source4c3ded1d98d1437c1de1fb73c243ad71d804506769135ea42581e9b3781ed274 is UNACCEPTED. +Source217dc80fc4457c6bb2b3dd0ace4b950ceb3863c7638305551ef59ea58a75f620/runtime/packaging/locked graphs unchanged; +eighth test194bf7b8aeec489cefc74190a28c96908fd07a8cf8ec9462c5b4ba61990a443c has7 focused PASS/zeroerrors/skips/warnings. +Exact local protected100 replay has100 genuineAssertionErrors/zeroerrors/skips with verified origin/eight hashes; +properties contain18 observations (6+2+2+2+2+2+2). Nine actual classifier mutations each fail1 assertion after all +relevant observations, and source is restored exactly; these are oracle isolation, not native authority. +Final complete normal full suite passes3420/nine configured skips/zero failures/errors,258.708seconds,64% vs50%/normal30sec; all794 sampled inputs and installed graph unchanged. Independent frozen environment, copiedJUnit; no shared dependency mutation. Finaltypes0errors/1456samewarnings. Intermediate3f5 test/full3420 and1459warnings are retained;3metadata warnings structurally fixed without an exception. +Earlier standalone read-only verify-fix preceded diverse review; its truthful-status finding is corrected by this prefix. RequiredFULL19 review passes72/30exact existing identities and multiplicities/zero new after removing only exact worktree-root prefixes from three unchanged category-group file locators; no rule/message/subject exception. Final194bf standalone read-only verify-fix FIRST then3diverse reviewers completed with no actionable findings on exact source/test/mapping bytes; static limits disclosed. +Final local gates and independent review are complete; fresh exact owner acceptance remains pending before signed/native100 publication. +Actual native100 RED must verify before implementation/version0.55.10 restoration. No target/companion merge authority. +Old93 native GREEN/all13contexts applies to059c only; current published primary head remains deliberately authored2b2b. +Companion511 remains unpublished without an admitted eligible authenticated native review capsule. No candidate +execution/self-signing/host fallback/platform spoofing or unrelated scope widening. Known17 protected YAML diagnostics +and61unrelated privatewiki health gaps retained; no new baseline/threshold/timeout exception. + +## Historical accepted111 signed authoring before limit rejection — 14 October 2026 (Europe/Berlin) + +The human accepted exact per-source111 mapping4aa84a05cf7c4dde425060734bc13cd51b6111b7c2f3759e7c45e0dab6dd9f1a +and authorized actual djm81 acceptance recording. The accepted35file candidate and exact eight authored test hashes +are preserved before genuinely signed specifications/tests on protected production/version. Actual native111 RED +must verify source/tree/base/selectors/eight hashes/JUnit/service ZIP before reviewed implementation/version0.55.10 +is restored. Earlier local proof remains preserved; authored RED is not final CI or merge readiness. No merges authorized. + +## Historical published93 and local111 proof before owner acceptance — 14 October 2026 (Europe/Berlin) + +Human-accepted per-source93 baa8b661268779bc0178417806e9389523e946125128b93e64a971464f7dee4b is delivered at +signed 059c05f051cfa456ebcb8f9607bf3c733f7d358e. Signed specs2dd017f87d4f52d179b3e149216a76189b68744e and +authored testsb5b41a7fb6779e79e630b120a445b8ca10a7abdc precede implementation. Actual native RED38635629429 +has93 AssertionErrors/zero errors/skips and exact seven hashes. Actual final38636359296 has93 independent +passing controls/zero failures/errors/skips, FULL72/30existing findings and all four service ZIP digests verified. +Base/tree/test bindings use retained RED/current Git blobs/literal-fullSHA provenance/producer plan/independent +execution; compact final receipts do not contain these fields. All13 exact integration-ID required checks pass. +Current-head CodeRabbit request6178325610 was rate-limited (bot6178328363); completed review remains required. +No target or companion merge readiness/authority is claimed. + +Local111 coverage is UNACCEPTED and unpublished. An eighth file independently exercises owner/group/other +execute permission bits and the six remaining standard Mach-O headers in both modes. Apple SDK loader.h and +fat.h definitions corroborate the eight header constants. Genuine signed compatibility precedes denial; probes +are inspected, never executed. All seven accepted93 files, runtime source, packaging and locked graphs remain +unchanged. Planned mapping adds18 selectors and corrects a stale locator to the existing local admission +function; no behavior, signing format, trust anchor, provider policy, timeout or threshold changes. +Local protected111 replay records111 genuine AssertionErrors/zero errors/skips with verified import origin. +Complete normal full suite:3431passes/nine configured skips/zero failures/errors,284.891seconds,64% versus50%, +normal30second timeout; all794 sampled inputs and installed graph unchanged during the run. Copied JUnit precedes +required FULL19file review:72/30exact existing identities and multiplicities/zero new findings. Types0errors/ +1456oldwarnings. Standalone read-only verify-fix preceded three model-diverse reviewers, no actionable findings +on exact source217dc80fc4457c6bb2b3dd0ace4b950ceb3863c7638305551ef59ea58a75f620 and eighth test +SHA dd75018dcb9c35363f2a6ab572705b9dda02bb28ad36b838f4d961bdcce40a6f. Final mapping locator review is recorded +separately. Fresh exact owner acceptance and signed/native111 RED remain required before publication. Existing +93 source/graph-bound audits, SAST, Bandit, signatures, delivery and wheel installation receipts retain their +scope; they do not prove111 security probes on every interpreter. Known17 protected YAML diagnostics and61 +unrelated privatewiki health gaps remain disclosed. Companion511 stays held without an authenticated eligible +native review capsule; no self-signing/execution/fallback/spoofing or unrelated scope widening. + +## Historical signed native93 RED verified — 14 October 2026 (Europe/Berlin) + +Signed specification2dd017f87d4f52d179b3e149216a76189b68744e precedes signed authored testsb5b41a7fb6779e79e630b120a445b8ca10a7abdc on unchanged protected production/version. +Actual native run38635629429, artifact11927649039, service ZIP SHA256a1e8603213f9578ca82686fab937385993c3dd3203c266c727cbe9d26da7d612, has93 genuine AssertionErrors, +zero errors/skips and exact source/tree/base/selectors plus all7 authored test hashes. Its RED gate passes +with zero proof findings. JUnit digest:sha256:c741c8dadda5f4ad5d0d4a114bf319b7f7e61c6b12d061319352fd3a5505f0c8. Reviewed production/version0.55.10 is restored only +after this real service artifact and bindings are verified. Deliberate RED workflow failure is evidence, +not final merge-ready CI. Final native producer/execution/FULL review, required CI, live exact-head authority +and completed current-head review remain required; no target merge is authorized. Companion symlink blocker remains. + +## Historical accepted93 signed authoring — 14 October 2026 (Europe/Berlin) + +The human accepted exact per-source93 mappingbaa8b661268779bc0178417806e9389523e946125128b93e64a971464f7dee4b +and authorized actual djm81 acceptance recording. The accepted34file candidate and exact seven authored test hashes +are preserved before genuinely signed specifications/tests on protected production/version. Actual native93 RED +must verify source/tree/base/selectors/seven hashes/JUnit/service ZIP before reviewed implementation/version0.55.10 +is restored. Earlier local proof remains preserved; authored RED is not final CI or merge readiness. No merges authorized. + +## Historical local93 reviewed proof before owner acceptance — 14 October 2026 (Europe/Berlin) + +Exact PER-SOURCE93 mappingbaa8b661268779bc0178417806e9389523e946125128b93e64a971464f7dee4b remains UNACCEPTED. +The exact seventh test SHA7a7260e422772be8e7f77be1d518fcd99a0cde0388dd8b195618f81ff7bdf417 produces93 genuine +protected AssertionErrors/zero errors/skips, including paired category/group compatibility before the real tier +regression. Four existing category/group positive cases remain standalone and are not falsely counted as RED. +The final complete suite passes3413/nine configured skips/zero errors or failures,64% vs50%,normal30sec timeout; +all793 sampled inputs and installed graph remain unchanged (275.84seconds). Final requiredFULL18file review passes72/30exact +existing identities/multiplicities/zero new. Two intermediate parameter-count warnings were structurally fixed +with identity tuples, not exempted; earlier FULL68/32 and prior full/test hashes are retained as historical. +Standalone read-only verify-fix preceded the final3diverse panel, all no actionable findings, with exact source/test +hashes and explicit limits. Finaltypes0errors/1456oldwarnings; exactCISAST0/BanditblockingPASS and unchanged strict +signature/delivery/version/PyPI gates plus changed-source installedwheel3.11–3.13 pass. Original six accepted79 +authored files remain byte-identical. Current79 native/CI/review validates published59ad only, not this unpublished93. +Fresh exact owner acceptance and actual signed native93RED must precede implementation restoration/publication. +No merge authority; companion511 admission blocker persists and human719merge precedes743 then751. + +## Historical local93 validation before tuple cleanup — 14 October 2026 (Europe/Berlin) + +Canonical PER-SOURCE93 `sha256:baa8b661268779bc0178417806e9389523e946125128b93e64a971464f7dee4b` is **UNACCEPTED**. The actual owner-accepted79 record remains historical +and does not authorize this amendment. Current published signed59adbda0016bd15e7c53f54bb8a0a669c463870a has +verified native79 RED38628199251 and final38628713533, all13 effective required integrations, and actual completed +CodeRabbit current-head review5523994227 (14 October2026 20:09:31UTC). No merge readiness is claimed: companion511 +and new extensionless/identity/ledger annotations remain open. Twenty older fully fixed threads were replied with +pushed implementation/native proof and resolved; new unresolved findings are not conveniently closed. + +The new extensionless finding genuinely reproduces12 AssertionErrors on published59ad, zero errors/skips: +an otherwise authentic signed project artifact gains category authority while containing an unsigned excluded +executable. Mode-only/shebang/ELF/PE/Mach-O/fat-Mach-O harmless probes are inspected, never executed. The correction +detects executable mode and standard headers independently of suffix and revalidates every signed file's canonical +membership/content after genuine cache reproduction. Ordinary excluded non-executable diagnostics retain loading. +Signing serialization/API, pinned trust, native artifact policy, provider priority, caches and all6accepted79 +control files remain unchanged. This does not claim detection of arbitrary interpretable data or atomic sealing. + +A separate seventh test file retains18 standalone controls and adds2paired identity controls. Genuine category/group +crypto and existing explicit validation refusals run FIRST with reset state; the genuine tier regression runs LAST. +The4standalone category/group cases already pass protected dev and remain full-suite compatibility controls; their +literal nodes are not mapped as RED. The intermediate97 replay (93failures/4passes/0errors/skips) is preserved and +not claimed all-failing. Final mapping93 retains79 cases plus12extensionless probes and2paired same-authority identity +controls. Final protected93 replay has93 genuineAssertionErrors/zeroerrors/skips, verified protected import origin and +all7exact test hashes. No failed setup is counted as RED and no native rule is weakened. + +Final complete normal full suite:3,413passed/nine configured skips/zero failures/errors/two existing deprecations, +266.74seconds,64%coverage vs unchanged50%,normal30secondtimeout; all793 sampled inputs and installed graph unchanged. +JUnit was copied before another shared pipeline. Strict typing:0errors/1456existingwarnings. Exact committed CI SAST: +0findings/0errors, unchanged baselinePASS. Blocking Bandit:zero medium/high findings. Direct changed-mapping YAMLlint, +newtest lint/format, strict OpenSpec/frontmatter/reproducible delivery/version/PyPIahead/strict core signatures pass. +Dependency graph inputs remain unchanged; prior exact all3frozen advisory audits/licenses retain applicability. +Rebuilt wheel SHA2566b2ad6ef5f5d72e15152bd97735a6ef7c265777e8a1f02d25756a628686faa41 contains exact changed source +and passes frozenofflinehashlocked installed import/CLIversion on actualPython3.11.15/3.12.13/3.13.14. This is +installation proof, not93security probes on each interpreter. Final FULL18file review passes72 with30exact old nonblocking identities/multiplicities and zero new findings. + +Standalone read-only verify-fix FIRST then3model-diverse interrogation reviewers report no actionable findings on +source217dc80fc4457c6bb2b3dd0ace4b950ceb3863c7638305551ef59ea58a75f620 and test56b21612a461c6c504105a1c78e86c67b7382581faa9ce74ee33eb6c55ac5de2. +Their static limits and exact hashes are retained. The17exact protected YAML diagnostics/launcher false-success +behavior and61unrelated privatewiki health gaps remain historical disclosed limitations, no new exception/configchange. +After final local gates, fresh owner93acceptance and genuine signed/native93RED must precede implementation restoration +and final hosted proof. Current79 receipts cannot validate93bytes. No target/companion merge authority; verified human719 +merge precedes743, then751. Companion scripts-only511 remains unpublished/UNKNOWN without authenticated eligible +Darwin native review; no candidate execution/selfsigning/fallback/spoofing or unrelated scope expansion. + +Evidence:719-executable-identity-final-before.xml,719-inventory93-protected-red-summary.json/xml, +719-inventory93-full-serial-summary.json and copiedJUnit,719-inventory93-types.json, +719-inventory93-sast.json/gate.log,719-inventory93-bandit.log,719-inventory93-wheel-matrix.json, +719-native79-final-verified-summary.json and719-native79-effective-required-checks.json. + +## Signed native79 RED verified — 14 October 2026 (Europe/Berlin) + +Signed specification2c6a51f9b6d996950a65fc53953485a1e6371238 precedes signed authored tests2677520e53459da7f85bd92da59f8c78ed1657b2 on unchanged protected production/version. +Actual native run38628199251, artifact11924956747, service ZIP SHA256b267eacf3ac27c75a768e7967f1dfd09841f92b7ae5feddc4d1bcc1c2ee907a0, has79 genuine AssertionErrors, +zero errors/skips and exact source/tree/base/selectors plus all6 authored test hashes. Its RED gate passes +with zero proof findings. JUnit digest:sha256:f2abfa7c9954e35acd86601d93838ccf2bfd061ebe26ff7462048cea318af604. Reviewed production/version0.55.10 is restored only +after this real service artifact and bindings are verified. Deliberate RED workflow failure is evidence, +not final merge-ready CI. Final native producer/execution/FULL review, required CI, live exact-head authority +and completed current-head review remain required; no target merge is authorized. Companion symlink blocker remains. + +## Historical accepted79 signed authoring — 14 October 2026 (Europe/Berlin) + +The human accepted exact per-source79 mapping88a043ab007a694dc2139ab3580c1951974d0380346da4fca2099cec4bafdad7 +and authorized actual djm81 acceptance recording. This does not authorize merging. The reviewed candidate and +exact six test hashes are preserved before authoring signed specs/tests on protected production/version. +Native RED must be genuinely verified before restoring production/version0.55.10. Local GREEN receipts below +belong to the preserved reviewed candidate; current authored RED is not readiness or final hosted proof. + +## Historical local79 coverage amendment — 14 October 2026 (Europe/Berlin) + +Canonical PER-SOURCE79 mapping `sha256:88a043ab007a694dc2139ab3580c1951974d0380346da4fca2099cec4bafdad7` is UNACCEPTED. The accepted75 record and all five authored test hashes +remain unchanged. Published signed a51a9b572103542b94f26171d8d7e07cf0987dcb has actual native75 RED/GREEN proof, +all13 effective exact-integration required checks passing and FULL review72/30 unchanged nonblocking findings. +Current-head CodeRabbit review was requested once; completion is not yet verified. The companion symlink finding +remains blocked by unavailable admitted native review. No readiness or merge authority is claimed. + +Three additional review annotations identify coverage gaps, not a newly demonstrated production vulnerability. +A separate control file adds four real public controls without changing production or accepted75 test bytes. +Both publisher variants first pass genuine checksum/signature verification under the same test bundled anchor; +canonical project registration succeeds and noncanonical registration is denied in each grouping mode. The +mapped cycle control executes the existing unchanged healthy-cycle body, then verifies compatibility behind an +ineligible priority copy. The original standalone positive remains in the full suite; its literal node is not +claimed mapped. The conflict control observes the actual warning call via a standard logger while retaining +public inactivity and unrelated-survival assertions. It does not claim separate schema/bridge snapshots or +customer console delivery. No global solver or filesystem sealing guarantee is introduced. + +Specifications/design preceded the new tests. All79 final selectors produce genuine AssertionErrors on protected +daefe0c75207632e054d96c36557673afab86a3a, verified import origin, zero errors/skips. Separate isolated current-source +mutations removing only the publisher check or the warning produce respectively2 and1 AssertionErrors, proving +the intended new oracles; these mutations are disclosed as oracle validation, not genuine protected/native RED. +The final new-file SHA256 is950c31f3d0714b45e552bd691bead3b6e5037bde963921321deae7c03f94adb9. + +Final complete normal frozen suite:3,393 passing tests/nine configured skips/zero failures or errors/64% line +coverage against50%/normal30-second timeout/254.45seconds. All792 sampled inputs and installed graph are unchanged. +FULL17file review passes72 with30 exact existing diagnostic identities/multiplicities and zero new findings. +An earlier sandbox attempt failed Ruff/Semgrep analyzer/certificate access; failed artifacts remain retained. +The unchanged host retry with frozen tools passes; no failure is substituted or gate weakened. Strict types +report zero errors/1,456 existing warnings. Exact committed CI SAST has zero findings/errors and its unchanged +baseline gate passes. New-test lint/format, direct mapping YAML lint and strict OpenSpec validation pass. +Source, package/delivery inputs and dependency graph are unchanged since the verified75 candidate, so its frozen +three-graph audits, strict signatures and actual installed wheel3.11–3.13 evidence retain their exact applicability. +No installation matrix is represented as79 security probes on each interpreter. + +Standalone read-only security verify-fix precedes three model-diverse reviewers; all reported boundaries fixed +and no final actionable findings. Limits remain explicit: test trust anchor and exercised combinations only. +Production and all five accepted test files are unchanged. After exact79 mapping acceptance, genuinely reauthor +signed spec/tests on protected production and verify actual native79 RED for exact six hashes before restoring +production/version. Normal signed hooks, fullSHA provenance, exact leases, live-head grants, native final execution, +all required integrations and completed current-head review remain mandatory. Resolve the three coverage threads +only after pushed validation; the separate companion symlink finding remains open. #743/#751 remain held for +strictly sequential verified human parent merges. The sections below are historical receipts. + +Evidence:719-review-oracles79-protected-red-summary.json,719-review-oracles79-protected-red.xml, +719-review-oracles-final-red-summary.json,719-review-oracles-full-serial-summary.json, +719-review-oracles-full-serial-junit.xml,719-review-oracles-full-mode-review.json, +719-review-oracles-review-identity-comparison.json,719-review-oracles-types.json,719-review-oracles79-sast.json. + +## Historical signed native75 RED verified — 14 October 2026 (Europe/Berlin) + +Signed specificationd3fea4aa1717ce139090d35492045673b8dd3cea precedes signed authored testsed3a7526b1103e19413a1ab7a87108b49e482132 on unchanged protected production/version. +Actual native run38618333057, artifact11920194147, service ZIP SHA25663b450a14d21c9072007166ead265c9e271cb43f814bffa5ffde893d0f8435a1, has75 genuine AssertionErrors, +zero errors/skips and exact source/tree/base/selectors plus all5 authored test hashes. Its RED gate passes +with zero proof findings. JUnit digest:sha256:953e2094351d8800fabac7baa019a9c7a9388392071d61b61dffe06076ca5bdd. Reviewed production/version0.55.10 is restored only +after this real service artifact and bindings are verified. Deliberate RED workflow failure is evidence, +not final merge-ready CI. Final native producer/execution/FULL review, required CI, live exact-head authority +and completed current-head review remain required; no target merge is authorized. Companion symlink blocker remains. + +## Historical accepted75 signed authoring — 14 October 2026 (Europe/Berlin) + +The human accepted exact per-source75 mapping7fff9b05caebbeae57598e0342782cb42fdf6271449681c065ebf200a7df4dcd +and authorized actual djm81 acceptance recording. This does not authorize merging. The reviewed candidate and +exact five test hashes are preserved before authoring signed specs/tests on protected production/version. +Native RED must be genuinely verified before restoring production/version0.55.10. Local GREEN receipts below +belong to the preserved reviewed candidate; current authored RED is not readiness or final hosted proof. + +## Historical local 75-control selection correction — 14 October 2026 (Europe/Berlin) + +The human approved bounded refusal of contradictory priority selections. The exact canonical per-source mapping +is `sha256:7fff9b05caebbeae57598e0342782cb42fdf6271449681c065ebf200a7df4dcd` and remains UNACCEPTED. The accepted63 record remains unchanged and applies only to its historical +bytes. The new separate test file adds 12 controls; all four previously authored files retain their accepted hashes. + +Specifications preceded the public configured-root discovery and real test-key signature controls. All 75 final +selectors reproduce genuine AssertionErrors on protected `daefe0c75207632e054d96c36557673afab86a3a`, with verified +production import origin, zero errors and zero skips. The self-dependency reproducer independently failed before +its candidate-version correction. Intermediate synthetic-path and candidate-list diagnostics remain retained +and are not final RED authority. A consistent dependency cycle also passes as positive compatibility coverage. + +The implementation applies unchanged local compatibility, schema, integrity and root-authority gates before +duplicate selection. Dependency selection preserves provider priority and reconsiders consumers against selected +versions, including each candidate's own enabled state and version. Repeated selection vectors refuse only the +varying names and recompute the remaining candidates and dependents. The complete selected set is revalidated +before registration effects. No global solver, arbitrary iteration limit, persistent trust cache, atomic filesystem +sealing claim, threshold or provider-pin change is introduced. + +Final normal frozen validation passes 3,389 tests, nine configured skips, zero failures/errors and 64% line coverage +against the unchanged 50% threshold in 268.45 seconds. The normal 30-second timeout remains enforced; all 791 +sampled inputs and installed dependencies stay unchanged. The version-source test actually executes with a clean +index. A preceding staged-index run legitimately skipped that test; its result is intermediate, not final authority. +FULL review covers 16 files and passes score72 with the same 30 nonblocking findings and zero new findings. +Strict types report zero errors and the same 1,456 warnings. Exact CI SAST has zero findings/errors; all three +frozen security audits pass. Delivery and version-source checks pass, and strict local0.55.10 is ahead of PyPI0.55.4. +The rebuilt wheel installs, imports from site-packages and invokes its actual CLI on Python3.11–3.13 with offline, +hash-locked dependencies. This installation matrix does not substitute for hosted security-control execution. + +Standalone read-only verify-fix preceded model-diverse interrogation. Initial interrogation caught a self-version +selection flaw; spec/test/assertion RED preceded its fix. The fresh verifier and all three final reviewers report +no remaining findings. The final new test SHA256 is +`c40c7f69d61c5b17c64d171c320140bd3264ae5baf3f7b4811304dfa36603a91`. + +Direct YAML lint of this changed mapping passes. Five long keys were wrapped with escaped quoted continuations; +parsed values and the canonical mapping are unchanged. The repository-wide launcher incorrectly returns zero +when yamllint emits errors. Its 17 remaining diagnostics are exact unchanged protected-baseline subjects in four +unrelated historical/planning files. This narrow documented existing exception is enumerated in +`719-selection75-yaml-baseline-comparison.json`; it covers no new diagnostics or changed mapping errors. The +launcher, lint configuration, baseline files and thresholds are unchanged. Do not describe its zero exit as clean +repository-wide lint. A tool-detection correction is a separate follow-up, not silently included in this change. + +Published signed c89701859ea3f83f477f733f4efe35389166566b still carries accepted63/native63 proof and completed +current-head review. The local75 correction is unpublished. After exact mapping acceptance, genuinely author signed +spec/tests on protected production and verify actual native75 RED before restoring implementation/version. Normal +signed hooks, exact leases, live exact-head grants, hosted integrations, final service digests and completed review +remain required. Companion issue511 retains its missing admitted-native-review blocker and symlink thread remains +open. #719 is not merge-ready; human merges remain mandatory before #743 and then #751. + +Evidence: `719-selection75-protected-red-summary.json`, `719-selection75-protected-red.xml`, +`719-selection-full-serial-summary.json`, `719-selection-full-serial-junit.xml`, +`719-selection-full-mode-review.json`, `719-selection75-types.json`, `719-selection75-sast.json`, +`719-selection75-wheel-matrix.json` and `719-selection75-yaml-baseline-comparison.json` under the retained evidence root. + +## Historical local 74-control selection prototype — 14 October 2026 (Europe/Berlin) + +The human approved bounded refusal of contradictory dependency-priority selections. Specification preceded the new separate test file. All 11 newly mapped controls reproduce genuine AssertionErrors on both protected daefe0c7 and published c8970185, with verified production import origins and zero errors/skips. Fixtures use actual configured-root discovery and real test-key crypto through the unchanged ordinary verifier API. A separate consistent-cycle positive control remains mandatory compatibility coverage in the focused/full suites. Earlier synthetic-candidate/path diagnostics are retained as intermediate, not final RED authority. + +The correction applies unchanged core/schema/integrity/root-authority checks before duplicate selection. Contextual dependency reselection preserves provider priority and reconsiders consumers against actual selected versions. Repeated vectors identify the oscillating names; refusal removes only that component and recomputes the rest. All selected artifacts are revalidated before schema, bridge, protocol, source or command effects. No global solver, arbitrary iteration cap, persistent trust cache, filesystem sealing claim or gate relaxation is introduced. + +The initial complete frozen suite passes 3,388 tests with nine configured skips, zero failures/errors and unchanged 791 inputs/dependencies. The final strengthened discovery controls and structurally simplified cycle setup still require final full validation and independent review. Final mandatory SAST has zero findings/errors and strict types have zero errors with the same 1,456 historical warnings. The 74-control canonical mapping is UNACCEPTED; prior accepted63/native proof remains historical authority for its exact bytes only. Fresh signed native RED/GREEN, required CI and actual completed current-head review remain required before publication/readiness. The companion symlink prerequisite remains blocked by missing admitted native review; no target merge is authorized. + +## Historical implemented 63-control candidate — 2026-10-14 (Europe/Berlin) + +The owner accepted canonical source mapping `6a137ae6` with all 63 selectors. Signed specification08e1e002 +precedes signed authored tests3bdaa89b on protected production. Actual native RED38559677185/artifact11891502779 +has 63 genuine assertion failures, zero errors/skips, verified service ZIP digest, exact source/tree/base, +four test hashes and JUnit binding. Its RED gate passes with no proof findings. Reviewed production and bugfix +0.55.10 follow only after inspection of this real artifact. Earlier bad_email heads are preserved but were not +granted authority; both corrected authoring commits have matching signing identity. + +The reviewed local candidate passes 3,376 full-suite tests, nine configured skips, zero failures/errors and +64% coverage against 50% in 253.64 seconds with the normal30-second timeout. All790 sampled inputs and installed +dependencies remain unchanged. Required FULL15file review passes score72 with30 unchanged nonblocking findings; +strict typing has zero errors. Standalone security verification then three-model interrogation has no findings. +These local receipts do not substitute for fresh final native execution, required CI and completed current-head +review. The companion protective symlink prerequisite is authorized but not yet delivered. #719 is not merge-ready; +no target merge is authorized, and #743/#751 remain held for sequential verified human merges. + +## Historical record: Current signed authoring phase — 2026-10-14 (Europe/Berlin) + +The human accepted exact canonical 63-control mapping `6a137ae6` and authorized the protective companion +symlink prerequisite. Both approvals are recorded; neither authorizes merging. Reviewed production and bugfix +0.55.10 are preserved in an immutable accepted snapshot. Specification and test commits are genuinely authored +on protected `daefe0c7` with production/version unchanged until actual native RED is verified. Local reviewed +GREEN receipts below describe the preserved candidate, not this deliberately test-only authoring phase. + +## Historical record: Current local 63-control correction — 2026-10-14 (Europe/Berlin) + +The local canonical mapping is +`sha256:6a137ae6c8d7f0ef499d4e62df6808a7c9c2eea8706748445fc9cb26269cf880`, covering 63 exact selectors. +The human accepted this exact mapping and authorized djm81 recording on 2026-10-14 (Europe/Berlin). +Signed/native publication is not complete. The published signed head `367358b7` retains the accepted +61-control mapping. Its native run `38546226366` passes producer, independent execution and final proof, +with verified service digests and the exact source reference. All 13 required statuses pass, and CodeRabbit +completed its current-head review at `2026-10-14T10:04:14Z`. Those receipts do not prove this local amendment. + +Finding `QrRG` identifies a valid primary manifest with malformed alternate `metadata.yaml` that escapes +YAML parsing and aborts startup. The specification precedes two public controls, which reproduce genuine +assertion failures on signed head `367358b7` before the fix. Canonical serialization now converts YAML parser +failures to the existing invalid-payload `ValueError`, allowing strict project admission to reject the package +before registration effects. Valid signed bytes and ordinary APIs remain unchanged. Separate preexisting +ordinary YAML paths are not claimed globally remediated. + +All three accepted test files remain unchanged; the two new controls live in a separate file. The exact +63 selectors produce 63 genuine assertion failures on protected `daefe0c7`, with zero errors or skips. +Focused validation passes 69 tests. The complete normal frozen suite passes 3,376 tests with nine configured +skips, zero failures or errors, and 64% coverage against the unchanged 50% threshold in 253.64 seconds. +The normal 30-second timeout remains enforced. All 790 sampled inputs and the installed dependency graph +stay unchanged. Required FULL review covers 15 files and passes at score 72 with 30 existing nonblocking +findings and no new findings. Strict typing reports zero errors and 1,456 existing warnings; the exact CI +SAST gate passes with zero current findings. Standalone read-only security verification followed by three +model-diverse reviewers reports no findings. The rebuilt wheel installs and invokes its installed CLI on +Python 3.11–3.13 using offline, hash-locked dependencies. + +An earlier full run failed when review-only module roots leaked into discovery and concurrent Hatch commands +changed the shared runtime dependencies. Its failed JUnit and log remain retained as failed evidence. The +serial rerun restored committed dependencies, removed the review-only roots and verified graph stability. +No test, gate, timeout, threshold, provider pin or trust policy was weakened. + +With exact mapping acceptance recorded, signed specification and test authoring, native RED before implementation, +current-head final proof, required CI and completed review remain mandatory. Companion release-symlink finding +`CejA` remains unfixed. The human authorized its protective companion prerequisite; implementation and validation +remain required before closure. #719 is not merge-ready; +no target merge is authorized. #743 waits for verified human #719 merge, and #751 waits for human #743 merge. +The sections below preserve historical receipts. + +## Historical record: Current accepted 61-control validation — 2026-10-14 (Europe/Berlin) + +The authoritative accepted source mapping is +`sha256:87a4aba6128b7dbe42d916f190fc274ed6b1fae839df9c8474c6615435c334d0`, covering 61 exact selectors. +The signed acceptance record is `requirements-proof/review-evidence.json`; previous 51-control dc7c and 34-control +records below are historical receipts. Their dated head and pending-state statements do not describe the current candidate. + +Signed specification 91e2b08c precedes authored tests 1c4a59c2 on protected daefe0c7. Actual native RED run +38543898314 retains 61 assertion failures, zero errors/skips and exact test hashes before implementation 24889ba9. +Native producer, independent execution and final reconciliation run 38544777662 all pass at implementation 24889ba9: +61 passing controls, zero failures/errors/skips, final verified evidence with no proof findings, and required FULL +review PASS at score 72. Actual service ZIP digests and JUnit bindings are retained in the delivery manifest. +Completed CodeRabbit review covers implementation 24889ba9; docstring coverage is 88.10% against the unchanged 80% threshold. + +The complete local frozen suite passes 3,374 tests with nine configured skips, zero failures/errors and 64% coverage +against 50% (253.413 seconds, normal 30-second timeout). This documentation correction preserves the accepted mapping, +all authored test bytes and validated runtime/delivery inputs. Current-head CI/review and evidence-backed annotation +resolution remain required before declaring merge-ready. No target merge is authorized; #743 and #751 wait for their +preceding human merge. The chronological sections below preserve historical evidence and limitations. + +## Historical record: Current local review amendment — 2026-10-14 (Europe/Berlin) + +The current local candidate has 61 controls. Its canonical per-source mapping is +`sha256:87a4aba6128b7dbe42d916f190fc274ed6b1fae839df9c8474c6615435c334d0` and is owner accepted. +The human approved this exact 61-control mapping and recording acceptance as djm81 on 2026-10-14 +(Europe/Berlin). The accepted 51-control dc7c record below remains historical for its exact mapping and bytes. Signed local 9aae28cf is unpublished; remote +4ab42909 is not merge-ready. #743 and #751 remain held until verified sequential human merges. + +Eight public controls reproduce actual indirect mounting/member execution or alias-created reserved roots with +zero errors/skips on 9aae28cf before this correction. Exact final selectors replay against protected daefe0c7: +61 genuine assertion failures, zero errors/skips. The shared resolver preserves explicit bundle precedence and +namespaced fallback; authority covers categorized project packages, official bundle mount claims and direct roots +before selection or side effects. Authentic official bundles, non-project routing and unreserved category-less +legacy commands without mount claims remain supported. Native .so/.pyd controls extend omitted-artifact coverage; +a missing required signed fixture fails clearly and is never skipped. Focused 67 tests pass. Full enforcement +passes at score 72 with the same 30 existing nonblocking findings; strict typing has zero errors. Standalone +security verification and all three adversarial reviewers report no findings. The complete frozen suite passes 3,374 tests with nine configured skips, zero failures/errors and 64% coverage +against the unchanged 50% requirement (253.413 seconds, normal 30-second timeout). All 789 sampled inputs stay +unchanged. Fresh signed/native proof, current-head hosted CI, genuine review completion and annotation resolution +remain required. No gate, threshold, timeout, signing key, provider pin or execution rule is weakened. + +# Historical record: Current authenticated-project proof cycle (2026-10-13) + +## Historical record: Pending final admission amendment — 2026-10-14 (Europe/Berlin) + +The human accepted the current **51-control canonical mapping** `sha256:dc7c63570b4252d2a1c3dc44c2a7fefd18702fc6b1d769d888613d9b0c2011ee` and authorized recording acceptance as djm81 on 2026-10-14 (Europe/Berlin). Earlier canonical b991 +acceptance (bound to aggregate830) remains historical and unchanged. Intermediate43/47/49 digests must not be used +as current acceptance. No fresh acceptance question has been issued. Published head dfbffcb5 remains historical for +this dirty candidate; #743 and #751 stay held until sequential human merges. + +Independent verification and three model-diverse reviewers agreed on a during-compiler admission gap. Two +faithful real-compiler controls first reproduced transient activation: real standard bytecode was inserted into an +already enumerated source directory, or an observed signed source was replaced. Neither cryptographic verification +nor compiler decisions were mocked. The correction completes cache reproduction from authenticated source bytes +before a fresh, compiler-free executable membership/content traversal. The existing command authority recheck now +precedes schema, bridge, protocol and active-source effects. Ordinary verifier APIs and signing remain unchanged. +This closes the concrete compiler interleaving; it does not seal mutable files against unrestricted later writes. + +The earlier signature-callback mutation controls also now observe every actual activation, including roots cleared +by later lazy registration. An initial final-state-only PASS for the inserted-source case was a false negative and +is explicitly invalidated. The strengthened controls reproduce two genuine activation assertion failures before +membership/digest binding. The initial missing-helper baseline replay and malformed forged-app cache probes are +also invalid evidence; subsequent public, valid-app controls replace them. + +The local focused run currently passes59 tests. The exact51-selector protected replay, final full frozen suite, +fresh independent verification and actual hosted native proof/current-head review remain required. Tests prevent +pytest from writing its unsigned assertion-rewritten caches into immutable signed fixture copies, while explicit +standard subprocess-generated caches and forged loadable caches remain exercised. No production admission rule, +timeout, coverage threshold, provider pin, signature or proof trust has been relaxed. + +Expanded review scope retains only the documented protected99 finding identities and multiplicities:90 literal +source statements and9 equivalent unchanged naming/member diagnostic AST subjects. The protected source review and +identity comparison live in the private evidence packet; this narrow existing exception does not cover any new +finding. New complexity, naming, missing-contract, unused-private-fixture and long-test advisories were corrected +structurally. Final review must prove zero new findings rather than relying on the smaller historical18 scope. + +Protected baseline: daefe0c75207632e054d96c36557673afab86a3a after #768, core0.55.9. +Accepted source mapping: b991ca3e19c5f0b40e0c8152308e32434347dcc084f30231588325803722b826. +Approved normalized report digest: 8303300c47c5e382a612170d7eb7c24fa2f146ea0615bc379e54a27656beb8c6. +The pinned provider verifies their single-source binding and all34 unchanged mapped cases. + +Previous final3a07929d and authoredb8fe4bdb have actual34-control native RED/final evidence on this base. +New review strengthens the two code-review selectors to exercise the actual review subgroup with its verified +codebase dependency in both authentic and forged paths. The mapping and runtime patch remain unchanged; +new test bytes require a fresh signed specification/authored/native RED/final cycle before stack restacking. +All earlier current-cycle labels below are historical for their explicit heads. No gate is weakened. + +## Historical record: Historical 20-control cycle after #768 (2026-10-13) + +Protected dev is daefe0c75207632e054d96c36557673afab86a3a, version0.55.9. This signed specification +precedes unchanged accepted E550 authored controls and genuine failing-before execution. No previous +RED/final report is forwarded as proof for this baseline. Production and coherent bugfix0.55.10 follow +fresh native RED. All recorded earlier-base results below remain historical for their stated heads. + +## Historical record: Historical evidence + +## Historical record: Historical authored phase after #767 + +Protected dev is c6af68a0fdf2ea798dd22c6750f077e217c938f5 after owner-authorized merged #767. +Production remains at protected c6af68a0; unchanged accepted controls are now authored and genuinely failing. All earlier +RED/GREEN claims below are historical and are not forwarded as fresh current-base proof. The earlier signed +security head f6481418 had complete green CI/native authority and completed review before this integration. +Exact e550f23e owner acceptance is preserved; current authored native proof and 0.55.9 implementation follow +in that order. Required gates remain enforced, and all three target PRs stay unmerged. + +## Historical record: Historical TDD evidence: workspace registration isolation + +Date: 2026-10-11 (Europe/Berlin). Protected baseline: dev +`4e7c38c2830f4bc53ffe78eeb0a2bfbabfc711e2`. Local environment: macOS, CPython 3.13.14, +dedicated frozen uv environment. The published PR remains unmerged. + +## Historical record: Retained first implementation history + +Historical authored head `8a03cf5898335f22dc411d75e22ea74d700df0fa` and native RED +[run 38172950463](https://github.com/nold-ai/specfact-cli/actions/runs/38172950463) preceded the first +implementation `facb6904843ab38521e8129024fddfa25b38247e`. The original digest was explicitly accepted +by djm81. That implementation passed 78 focused tests and a fresh full run: 3,298 passed, 11 disclosed +skips, two dependency deprecation warnings, 328.73 seconds. An unchanged native rerun after stack +registration passed producer, independent execution and final consumer. This historical evidence does +not cover the amended mapping or test file and is not reused as current proof. + +## Historical record: Review corrections and valid failing-before evidence + +Added specification deltas first for official category roots, rejected-package import isolation, +effective operator-selected built-ins and reconciliation of shipped categorized routing in disabled mode. +The first review test invocation had a collection syntax error; subsequent draft category fixtures lacked +required grouping fields and failed manifest validation before registration. Both are discarded as proof. + +Corrected all six category manifests to their valid canonical bundle/group fields and repeated the probes +before final production edits. Command: `.venv/bin/pytest -q -o addopts=''` +`tests/unit/security/test_module_command_squatting.py`. + +- Against historical implementation facb690: 14 actual assertion/behavior failures, nine passes, + zero skips, 1.79 seconds. Twelve category-root cases, import-shadow execution and effective-root + compatibility reproduce the remaining gaps. +- Against protected baseline 4e7c38c, with the existing disabled-grouping bootstrap control also selected: + 17 actual assertion/behavior failures, seven passes, zero skips, 1.36 seconds. This includes the + original missing-category and permanent-core/symlink vulnerabilities. + +Signed proposal/test history is rebuilt from protected dev so no production change precedes the current +native authored boundary. The earlier authored replay 9fef6a3f contains draft fixtures and is superseded; +its hosted run is not accepted as current proof. Corrected tests must stay byte-identical after native RED. + +The amended mapping digest is +`sha256:a9ba0dfde727a2a82f037212ebe5c6eeacd2574602e7d6ca9e4e3900cbdeb95c`. +The human owner explicitly approved recording acceptance as djm81 after confirmation that all 14 remaining +behavior failures will be fixed. The old acceptance is not carried across a changed mapping digest. + +## Historical record: Passing-after + +The corrected security and registry/category-routing/bootstrap selection passes 99 tests, zero skips, +two existing dependency warnings, 7.58 seconds. Rejected packages are excluded before schema, bridge, +protocol, active-import and category-mount side effects. All six official category root names are denied +to project packages in both modes. Core command authority uses effective operator-selected discovery; +other built-in integrity exemptions remain bound to the running package tree. Removed flat bundle shims +are not restored. Those results are historical; the exact current atomic proof and full suite are recorded below. + +## Historical record: Independent gates and limits + +The first implementation passed frozen delivery/uv lock, all three security audits, trust register, +license gate and strict module signatures; no dependency payload changed. All four project version +sources agree at 0.55.7. Strict PyPI-ahead was 0.55.7 > 0.55.4. BasedPyright reported zero errors and +1,522 baseline warnings; Ruff, Semgrep and blocking Bandit findings were clean. The expanded candidate +must rerun applicable gates. No skip, threshold, signature check, fixture pin or acceptance requirement is +weakened. A new test type warning was fixed through a typed monkeypatch path replacement, without suppression. + +First-implementation broad review and independent merged-baseline review each contained five warnings, +five information findings and zero errors, with zero new findings. Narrow unchanged-baseline exceptions +covered public discovery naming, typed parser duplication, deliberate propagation/None branches and unrelated +release-fixture styles. Expanded code review must verify that no new findings remain; current tests are clean. + +## Historical record: Exact atomic selector correction + +The released provider requires each selected RED node to fail with an exact test identity, including parameter IDs when +used. +Run 38175451297 is discarded because positive-only controls passed in RED and parameter expansions did not +match the declared node IDs. The accepted mapping digest and selector names remain unchanged. Each exact +case now checks a rejected workspace/external boundary together with its supported counterpart; the same +assertions run in RED and final, without stage flags or artificial failure. Parameter combinations execute +inside the selected case, with isolated registries and monkeypatch contexts. Full independent tests also +retain all six root names and both modes. Corrected native authored proof now passes; final-head execution and required +checks remain pending. + +## Historical record: Historical corrected native authored boundary and local candidate + +Signed authored head `ea99fac1cb95aa7942dc7ffc571e7907bd113b74`, tree +`4e64ab987ae639dd6eee3270579270c1fed369a6`, precedes all current production edits. +[Native authored run 38176059675](https://github.com/nold-ai/specfact-cli/actions/runs/38176059675) +produces gate_decision=pass, observed_maturity=red, failing-first-proven, and no findings. +All ten exact selectors fail genuinely. The producer job failing is expected at authored maturity. +The report binds protected merge base 4e7c38c, the exact source SHA/tree, both selected test file hashes, +complete plan and aggregate mapping. Both selected test files are byte-identical in the final candidate. +The approved source mapping remains a9ba0dfd. djm81's authorized exact-head grant is comment 6113926468; +authority run 38176059763 attempt 2 succeeds. Neither this grant nor authored RED is final execution proof. + +Fresh atomic full smart run: 3,299 passed, ten disclosed skips, two existing dependency deprecation +warnings, 170.28 seconds, 64% coverage. A subsequent small extraction of the pre-side-effect package +filter removes a newly introduced CC13 complexity warning; security/bootstrap controls pass again +(23 passed, zero skips, 2.79 seconds). All behavior and native selected test bytes remain unchanged. +Broad final review scores 92 with zero new findings: its 13 findings equal the ten independently measured +merged-baseline findings plus the three independently measured category-test type warnings. Narrow +unchanged-baseline exceptions retain the existing public discovery naming, parser shapes, deliberate +exception/None paths, unrelated release-fixture styles and category `commands` unknown types. The +reported unused `_clear_registry` is an unchanged pytest autouse fixture invoked by pytest, so deleting +it would remove necessary isolation. No warning is suppressed or silently treated as a new-code pass. + +Current frozen lock/export parity, all three advisory audits, dependency-trust register, normal full +license scan, strict four-module signature verification, four version sources, strict PyPI-ahead and +repository lint/type-error threshold all pass. Current native final execution, the complete hosted +3.11–3.13 wheel/runtime matrix, required checks and external review remain pending at final publication. + +## Historical record: Final flat-mode and spec-routing review amendment (2026-10-12) + +A further review identifies missing category-less reserved-name controls and a remaining obsolete spec +flat-shim requirement. Reconcile the existing collision requirement as MODIFIED, retaining its scenario +name and actual spec api routing, then add probes before any further production edits. Core names and +all six official category roots are rejected through the actual flat path in explicit legacy mode. +The spec compatibility probe executes an eligible bundle-native app through its real api subgroup; +initial incorrect single-command fixtures are corrected to the shipped native-group shape and are not proof. + +Expanded mapping sha256:3dc2def04ee75c873f493cab0f33976f80cfedbf0b24b9fa84ff1fd1136ac871 adds one +executable spec-routing requirement. All eleven exact selectors genuinely fail against protected baseline +code (1.09 seconds, zero passes/skips). The implemented candidate's ten security functions pass (0.59 +seconds, zero skips). Fresh full suite: 3,301 passed, nine disclosed skips, two existing dependency warnings, +193.16 seconds, 64% coverage. Final test-only review has zero findings. Current source mapping needs fresh +human acceptance: accepted a9ba0dfd is historical for this amendment and is not silently relabeled. +Fresh signed authored native RED and final execution remain mandatory; published a63156e1's green checks +cover its earlier accepted mapping. The separate protective prerequisite #760 must integrate before final +stack restacking; coherent project version and native proof will be refreshed on that protected baseline. + +## Historical record: Final exact reserved-name mapping (2026-10-12) + +Final source mapping: sha256:e550f23e9423463f15e629e32071ec664655bc307a165af3a3961225e2f7276f. +Nine independent parameter IDs exercise category-less init, module, upgrade, project, backlog, code, +spec, govern and requirements claims in explicit legacy mode. The unchanged released provider accepts +all twenty exact selectors. Against an isolated immutable 4e7c38c archive, all twenty genuinely fail: +zero passes, errors or skips, and the protected JUnit plugin records exactly the mapped node IDs. +The current implemented security file passes all nineteen controls. Fresh full suite: 3,310 passed, nine disclosed +skips, two existing dependency warnings, 161.27 seconds, +64% coverage. Native authored RED and final execution must be regenerated after prerequisite #760. +Earlier a9ba0dfd acceptance and ea99fac1/a63156e1 native evidence remain historical. They do not cover +this final digest or the expanded test-file hash. Owner djm81 approved this exact mapping and the separate prerequisite +grants on 2026-10-12; genuine acceptance is recorded. + +## Historical record: Import-free eligibility review correction + +A new external review correctly identifies `_build_bundle_to_group()` importing category modules whose +module bodies initialize apps during eligibility. Update the no-side-effect spec first, then a real +import-observation control fails against published a63156e1 (0.89 seconds). Admission now reuses the +existing `CATEGORY_TO_GROUP_COMMAND` constant, and the builder mapping reads those same root names. +No duplicate policy table or category builder import is needed during rejection. This independent +control is additional to the twenty mapped RED selectors; final selected test-file hashes include it. +Fresh focused/full and code-review results are required after this production correction. + +Final frozen-code full run after the import-free correction: 3,311 passed, nine disclosed skips, two +existing dependency deprecation warnings, 168.62 seconds, 64% coverage. The production and both mapped +test-file hashes are checked before/after and unchanged. Final security test hash is +sha256:4228aaab9dffd3ec00b7ea272cc5d2dcabd6bc16c3b9b25e978fe9be7afd4b96. Focused compatibility +passes 86 tests; independent current review introduces zero findings against the measured baseline. +Initial broad-object import observer annotations caused four actual type errors and were corrected +with the real import argument types, without suppression. An intervening full run predates the last +builder-table reduction and is historical; only the frozen final run validates these bytes. + +Protective prerequisite is concrete PR #761 (issue #760), with verified signed authored head 436bd0a3 +and genuine native RED retained by run 38180902947. It must pass required checks/review and receive +owner merge authorization before this stack consumes protected helper bytes. No stack member merges. + +## Historical record: Protected-base cycle after #767 + +Existing guards, twenty exact selectors and genuine accepted mapping are unchanged in scope. The preceding +base c4ee5be8 and authored 61a78348/native run 38292251267 are retained as historical proof only. Current +source authority is protected c6af68a0; specification precedes new authored controls and actual native RED. +No current implementation or passing claim is made in this specification phase. + +Signed current specification ff914942 precedes authored tests. All twenty exact accepted selectors genuinely +fail against protected c6af68a0, with zero passes/errors/skips. No production, workflow or version input +changes from that protected baseline precede the new native authored proof. Test bytes match 61a78348; +the earlier native proof is historical and is not forwarded into the current cycle. + +## Historical record: Fresh authored local RED after #768 (2026-10-13) + +Signed specification4857072be6a888feb087322746c10f3fffed6301 precedes unchanged accepted controls. +The twenty exact E550 selectors genuinely fail against protecteddaefe0c7, with zero errors/skips. +Failures reach actual security/compatibility assertions, including the loader-wrapped assertion that +a hostile dependency shadowed its approved source. They are not missing dependency or fixture errors. +Source registration guards remain untouched. Publish signed authored controls and retain actual native +RED before production; previous-base reports are not forwarded. Parsed-equivalent YAML formatting +preserves exact mapping acceptance. A symlink-resolved launcher left its virtual environment and is +excluded from behavioral proof; the actual worktree interpreter produced the retained valid JUnit. + +## Historical record: Review docstring correction before final proof (2026-10-13) + +The root pre-merge report has genuine37.50% docstring coverage against unchanged80% policy. +Control docstrings now state the exact security/compatibility intent without changing executable AST +or accepted E550 mapping. Test bytes change, so production is withheld again and the complete twenty +selectors must receive fresh native RED on this signed authored head. Earlier native run38439782972 +and artifact11835560037 remain genuine but historical for their old control bytes. No stale proof is +forwarded or requirement weakened. Current final execution, CI, review and child validation stay pending. + +## Historical record: Fresh documented native RED and approved restoration (2026-10-13) + +Signed documented authored2b9bd70061ece71b8c91c17f6a00b2c5fcb590a3 has actual native +run38441692211/artifact11836226121, red/pass with twenty genuine failures, zero errors/skips/findings. +Service ZIP digestacf156464a7591d0af4a9ec4bcf7163fc476c3ec8f76f4c63a49d36b0383de50, JUnit +and source/test/protecteddaefe0c7 bindings verify. Only this current-byte RED is forwarded. +Approved guards and coherent0.55.10 restore afterward; the changed admission boundary gains an accurate +docstring without logic changes. Documented authored controls remain byte-identical through restoration. +All current local, final-native, authority, CI, review and sequential child gates remain required. + +## Historical record: Current complete local verification after #768 (2026-10-13) + +All twenty mapped controls and86 focused registry/compatibility tests pass. The complete smart suite +passes3333 tests, 10 skips, zero failures/errors in186.54s. All788 source/test/delivery +inputs match before/after execution. Documented authored control bytes are unchanged from native2b9bd700 +RED. Canonical typing has zero errors and the same1522 baseline warnings; frozen type runtime install, +format/lint/YAML/contracts, strict OpenSpec/docs, lock/export delivery checks, coherent0.55.10 source +metadata, strict PyPI-ahead, three frozen audits, trust, licenses, independent Semgrep/Bandit and strict +signatures pass. No signed module asset, dependency graph, provider pin, threshold or timeout changes. + +Full local review retains13 existing protected-baseline advisories. Independent tree-identical protected +review confirms the same diagnostic identities and source statements, allowing only verified line shifts. +Explicit scoped exception: unrelated manifest-parsing and promotion-fixture advisories plus existing +registry-fixture type/auto-use diagnostics remain outside accepted security behavior; expanding them +would require unrelated behavior/spec/test changes. No new finding, suppressor or enforcement change. +The canonical checkout lacks a virtual environment, so its environment-incomplete review is excluded; +the full frozen protected checkout establishes the actual baseline. + +A Semgrep gate invocation omitted its mandatory baseline argument and is excluded. The corrected gate +uses tools/semgrep/sast-baseline.json and passes with zero findings. All normal signed hooks and literal +current-head RED provenance must pass before publication, then exact-head authority, native final proof, +required CI and completed review must pass; targetthree remain unmerged. + +## Historical record: Authenticated project bundle correction (2026-10-13, Europe/Berlin) + +Reviewed signed head f8ffe7c6 passed hosted gates, but its blanket workspace-root denial also rejected supported +official project installations. Specification precedes paired real-artifact controls for the six canonical bundles +and the signed code-review add-on in grouped and legacy modes. Protected dev remains daefe0c75207632e054d96c36557673afab86a3a. + +All 34 exact amended selectors fail genuinely on an isolated protected source snapshot; the fourteen new selectors +also fail on preserved f8ffe7c6 at the supported-installation assertion. There are zero errors or skips in either RED. +Intermediate stricter-key prototypes reproduce fourteen additional untracked-Git-source admission failures and +fourteen escaping-directory-symlink failures before their corresponding fixes. No environment/import failure counts +as behavioral RED. Final controls retain all original unsigned, permanent-core, import-shadowing and routing probes. + +The strict project verifier checks the complete filesystem payload, rejects payload symlink members, requires +checksum and core-bundled-key signature, and has no unsigned, environment-key, receipt or backend fallback. Existing +ordinary verifier API/defaults remain unchanged. Signed ancillary bundle commands and official add-ons remain compatible. + +Final frozen Hatch 3.13 full smart suite: 3348 passed, 9 disclosed skips, zero failures/errors, +202.483 seconds, 64% coverage against unchanged 50%; normal pytest timeout is 30 seconds. All 789 sampled +source/test/delivery inputs are unchanged during completion. Earlier incomplete or misconfigured direct launchers +are excluded. One complete direct run had 23 launcher failures (22 system-Python signing/fallback failures and one +isolated pytest inheriting a timeout flag); the normal frozen Hatch/PATH launcher resolves all without source, +threshold, signature or test-enforcement changes. + +Final local review has 18 advisories exactly matched by category, severity, tool, rule, normalized path, message, +multiplicity and diagnostic source statement to the tree-identical protected source. Zero new findings remain. +Explicit narrow exception retains existing unrelated manifest/parser/fixture/type advisories plus five unchanged +installer advisories (uninstall complexity, existing parameter counts and existing discovery naming). This expanded +baseline reflects reviewing the newly touched installer file; no finding is suppressed and no threshold changes. +Formatting, lint, canonical typing (zero errors; same 1522 warnings), contracts/scenarios, docs, YAML, strict OpenSpec, +Semgrep (zero findings), blocking Bandit, all three frozen Python audits, licenses, delivery/lock/export parity, +four strict module signatures, version sources and strict PyPI-ahead checks pass locally. Core bugfix is 0.55.10. + +Amended mapping sha256:8303300c47c5e382a612170d7eb7c24fa2f146ea0615bc379e54a27656beb8c6 remains planned, not owner accepted. E550 acceptance is historical and is preserved externally. +Normal signed publication requires fresh exact mapping acceptance, then a genuinely re-authored specification/test +and native RED cycle on protected dev before production restoration. Fresh final native execution, signed-head member +authority, supported wheel matrix, full current-head review and every required hosted gate remain mandatory. This local +record does not substitute for them. Keep the P1 thread unresolved until the published correction is validated. +Do not restack #743/#751 or merge any of the three targets before those root checks are complete. + +## Historical record: Approved 34-control protected-base publication cycle (2026-10-13) + +The human explicitly accepted mapping8303300c and authorized recording it as djm81 after reviewing the concrete +correction and final3348-pass local suite. That exact acceptance is recorded in requirements-proof/review-evidence.json. +Re-author signed specification and all final control bytes on protected daefe0c7 before restoring production. Actual +new native authored RED, authorized verified-head grants and independent final execution/review/CI remain required. +The earlier published f8/E550 proof is historical. No target merge is authorized; #719/#743/#751 remain unmerged. + +## Historical record: Fresh approved authored controls on protected daefe + +Signed specification3c86f8bf36042e21a2c7fa4513c07ec546b0c59d precedes all four final control files. +The actual current-base worktree reproduces34 genuine assertion failures, zero errors/skips, before +any runtime, version or release fixture change. Test bytes match the reviewed control hashes. The +normal signed authored commit and new native producer RED follow; production remains withheld. + +## Historical record: Acceptance identity correction without scope change + +The owner approved normalized report digest8303300c. The pinned provider proves that its sole source +binds exactly to source mappingb991ca3e19c5f0b40e0c8152308e32434347dcc084f30231588325803722b826. +Every parsed mapping field and all34 normalized cases match the reviewed packet. The first authored +hook correctly rejected a record using the wrapper digest as a source digest. The corrected owner record +uses the bound source digest and retains approved_report_mapping_digest; the original decision timestamp +and approved scope are preserved. No acceptance is invented and no gate is modified or bypassed. + +## Historical record: Fresh exact native RED before restored production + +Signed authoredb8fe4bdb2a261c0ae05cfb27bf29c9c7cca21f32, tree07c9f55b7414924c674c2587117ce01647c5a123, +has actual native run38456512770/artifact11842191935 with verified service ZIP digest +9bda609cdfe093f75758cc4a92f1ac7e6101daff04afb75f04187141f3da4eb5. Report red/pass has34 genuine +failures, zero errors/skips/findings, exact source/tree/daefe base and all mapped test hashes. The normal +workflow deliberately retains a failed required check at RED until final reconciliation; this is not +a passing final CI claim. Source acceptanceb991ca3e is canonically bound to approved wrapper8303300c. +Only now restore the reviewed production/docs/coherent0.55.10 snapshot; final test bytes remain unchanged. +Independent final execution, signed-head owner authority, review and every required hosted check remain pending. + +## Historical record: Stronger subgroup controls before their new authored cycle + +The code-review parameters now register the authentic codebase dependency in both authentic and forged paths, +observe the actual review subgroup, and reject every forged extension without losing the trusted dependency. +Both focused variants pass. The normal frozen full suite again passes3348 tests with nine skips, zero failures/errors, +64% coverage and unchanged30-second pytest timeout. Only the strengthened test file differs among789 source/test/ +delivery inputs; runtime bytes remain identical to validated3a07929d. Local review of the stronger file is PASS with +zero findings. Every accepted source mapping field and all34 normalized cases remain identical; no new acceptance +is needed. Historical conflicting current-cycle labels are corrected. A fresh signed/native cycle binds the new bytes. + +## Historical record: Current stronger authored controls before production + +Signed specification95d2c245dfb70d024c3aca87d5fc3f55270e583b precedes the four final stronger +control files. Actual protected-base execution produces34 genuine assertion failures, zero errors/skips. +The accepted source mappingb991ca3e and normalized report8303300c remain unchanged. The review subgroup +control now observes the authentic extension and forged rejection alongside the actual codebase dependency. +Production/version/delivery inputs remain protected-daefe exact until new native RED is retained. + +## Historical record: Current stronger native RED before restored runtime + +Signed specification95d2c245 precedes authoredd532d559baff8ae7a7891f6253194abdc4ca3ee0, +tree05392bc59964c1b58abc8d6afd9cd3fd808a5329. Native run38461628947/artifact11845892383 +has service ZIP digest92efad746131f89763cf054c654f85e77318bafe6f72e35acab531e799406369. +Actual report red/pass has34 genuine failures, zero errors/skips/findings, exact source/tree/daefe base +and all stronger mapped test hashes. The normal RED check remains failed until final reconciliation. +Only after this proof restore the unchanged reviewed runtime and coherent0.55.10 delivery artifacts; +all789 stronger full-suite-validated inputs now match. Source acceptanceb991/report830 remain unchanged. +New final native execution, current-head review and every required gate remain pending; leave the stack unmerged. + +## Historical record: Verified51-control native RED before implementation (2026-10-14, Europe/Berlin) + +Human-approved canonical mapping sha256:dc7c63570b4252d2a1c3dc44c2a7fefd18702fc6b1d769d888613d9b0c2011ee is recorded as djm81. +Signed specification9022172a8b692b396478ba4bf2de9d0b9a7c0b17 precedes signed authored +d61f5223dab995488aad32f747b880cbaf7b9415, whose production tree remains protecteddaefe0c7. Normal hooks passed. +Native run38524710157 artifact11875134008 verifies exact service archive digest +d8e91521a8d15eed5162d74a1135872190b88d01cc9b4f61353645dd7c1a6890. +The actual red report passes with no proof findings, exact source/tree/base,51 selected controls,51 assertion +failures,0errors/skips and exact unchanged authored test hashes. JUnit digestsha256:4ba9e09cafb0f3643769ec315aa9eba874b22023f31bbee8bbfcd3045d3588a7. +The trusted workflow intentionally exits1 after retaining RED until final reconciliation; this is not final +merge-ready CI. Restore reviewed production only after this actual proof, keeping the same tests and accepted +source mapping. Final independent producer/execution/reconciliation, all required CI and completed exact-head +review remain required. No target merge is authorized. + +## Historical record: Native full-review correction (2026-10-14, Europe/Berlin) + +Current implementation4ab42909 passes51 native controls; its independent full review fails the unchanged +score threshold because the expanded file scope includes existing untyped fixture diagnostics. Local changed-mode +review and a documented protected exception do not replace full enforcement. Preserve full review and its thresholds. +Use actual pytest fixture/callback types in three already-touched tests, preserving all assertions and authored +control files. Name two autouse pytest fixtures as fixture APIs, avoiding the false unused-private classification. +The manifest boundary still propagates category errors and skips other malformed models/YAML; one explicit error +classifier preserves both outcomes without the misleading standalone identity-handler pattern. Direct valid/category/ +model/YAML controls pass. Full scoped review now PASS,score72,30existing findings,0blocking simplifications and0errors. +No suppression, threshold, provider, signature, execution or CI policy changed. The same51 signed test hashes and +accepted canonical mapping remain immutable; fresh signed-head native execution/full review are still required. + +## Historical record: Signed 61-control native RED retained — 2026-10-14 (Europe/Berlin) + +Signed spec 91e2b08c4bed111df06b8cb9ae19b5f9cb628419 precedes signed authored tests +1c4a59c27debf79988f712b3b0ecabd409daf15f on unchanged protected production daefe0c7. +Actual service run 38543898314, artifact 11882834900, ZIP digest +334585be08338fda30cfc1c576cf097c135499c04371c231cf763c648463b166, retains RED with gate pass and no findings. +JUnit digest 7b817415b0d437acf0d46b742a229d02f5b5870e9057bdd0b79ba0a2754d1157 binds 61 genuine assertion failures, +zero errors or skips, exact authored source/tree/base/selectors and all three final test-file hashes. +The trusted workflow deliberately leaves the RED producer/final chain failed until final reconciliation; +this is not a green CI claim. Reviewed production and coherent version 0.55.10 are restored only after +inspection and cryptographic binding of this real native artifact. The accepted mapping and authored tests +remain unchanged; final independent execution/FULL review, CI, authority and actual completed review remain required. + +## Historical record: Signed native 63-control RED retained — 2026-10-14 (Europe/Berlin) + +Signed specification08e1e002 precedes signed authored tests3bdaa89b on unchanged protected production/version. +Actual native run38559677185, artifact11891502779, service ZIP SHA256357579c646597eabacb22c348a411b9b9a5834fb14ef9de3557a7ea7b2f1c0df, has63 genuine assertion failures, +zero errors/skips and exact source/tree/base/selectors plus four authored test hashes. Its RED gate passes +with no proof findings. JUnit digest: sha256:b69be84744b3e5bed0ea66cf3e6d7e7e3c891afab40fc0809d7c6da32c0b05fc. Reviewed production/version0.55.10 is restored only +after this service artifact and all cryptographic bindings are verified. The deliberate RED workflow failure +is evidence, not merge-ready CI. Final independent execution/FULL review, CI, authority and completed review +remain required; no target merge is authorized. Rejected bad_email heads are preserved but are not authority. diff --git a/openspec/changes/fix-categoryless-module-command-squatting/design.md b/openspec/changes/fix-categoryless-module-command-squatting/design.md new file mode 100644 index 000000000..ea970ea25 --- /dev/null +++ b/openspec/changes/fix-categoryless-module-command-squatting/design.md @@ -0,0 +1,162 @@ +## Context + +Registration already separates categorized commands from flat commands in `_register_commands_for_package`. Grouped +mode is the default, but the function currently ignores that mode and routes every category-less manifest to +`_register_command_flat_path`. + +## Decision + +Reject ineligible packages before schema, bridge, protocol, active-import-root and category-mount side effects. Make +registration skip category-less project packages when grouping is enabled. Discovery already preserves the computed +source in metadata; do not trust a YAML source field. Preserve category-less non-project sources and explicit legacy +registration. For permanent core commands, require `_is_builtin_module_package(package_dir)` before creating any loader +in both modes. That check resolves paths against the effective built-in discovery root selected by operator +configuration, so forged categories, names and source labels or escaping symlinks cannot grant authority. + +This is the smallest guard at the trust boundary before package registration side effects. It also avoids relying on a +denylist of former flat aliases, so an attacker cannot squat any other unoccupied root name. + +## Alternatives Considered + +- **Restore removed flat shims:** rejected because it reintroduces unsupported command behavior and only protects names +supplied by installed official bundles. +- **Reserve only former flat names:** rejected because it leaves arbitrary root command squatting possible. +- **Require signatures for all workspace modules:** broader trust-policy change that may be valuable separately but +would alter installation and local-development workflows beyond this vulnerability. + +## Compatibility and Rollback + +Eligible categorized modules and canonical grouped commands are unchanged. Unauthenticated project packages claiming official category +roots are rejected entirely in both modes. Preserve supported project-scoped official bundle installation only after +strict complete filesystem checksum and signature verification against the core-bundled key, together with canonical signed +publisher identity, tier, category, group and reserved-root checks. Read that key independently of workspace +configuration or environment overrides; missing keys or verifier failures reject admission. Unsigned/test flags and +installation receipts alone grant no privileged roots. The project-root check includes untracked source files +in Git workspaces; ordinary artifact-verifier defaults remain compatible for other callers. Keep ordinary compatibility, dependency and integrity checks. Category-less project modules continue to register unreserved root names +only when callers explicitly disable grouping. Other legacy discovery sources remain compatible. External packages +cannot extend permanent core commands in either mode. This does not sandbox intentionally invoked categorized module +code or make arbitrary workspaces trusted. The related category-command-groups delta reconciles the shipped categorized +behavior; disabled grouping never restores removed flat bundle shims. Rollback restores the coherent registration +patch. + +## Executable payload coverage + +The strict project-root verifier checks executable files omitted by the signing payload filters before granting +authority. It rejects standalone bytecode and excluded Python/native source artifacts. Legitimate interpreter +caches remain compatible only after their code and filename match a fresh compilation of covered signed source, +using the current interpreter and optimization level. Cache verification never executes candidate code. Fixture +copies represent published artifacts, excluding locally generated bytecode; controls then add both genuine and +forged caches explicitly. The ordinary artifact-verifier contract and upstream signing format remain unchanged. + +Capture covered Python source bytes while constructing the canonical checksum/signature payload. Authenticate that +payload before compiler work. Derive expected cache bytes through the standard file compiler using a private +temporary copy of those captured bytes and the original filename; compare bodies and remove temporary artifacts. +The workspace source is never reopened for cache reproduction. Preserve existing SAST rules and baselines. Native +formats omitted from signing are denied explicitly; this is contract coverage, not a claimed native-load exploit. + +Discovery/admission correction: retain candidate metadata through a keyword-only opt-in on the existing discovery +wrapper, backed by the existing shadow-retaining traversal. Default list/state/lifecycle discovery remains deduplicated. +Registration merges name-based enablement first, then selects the first authorized enabled candidate in stable source +priority. Only admitted winners provide dependency availability/versions and mounting inputs; disabled names retain +false diagnostic state. Root authority remains in the registry and ordinary integrity/schema/dependency gates remain. +No generic callback, second root scanner, mutable global selection state or verifier-result cache is introduced. + +The private frozen registration selection keeps admitted packages, actual versions and dependency availability +together. This represents one completed admission decision; callers cannot independently rebuild availability from +rejected candidate names. It does not add a public API or alter stored module state. + +Manifest binding: retain canonical root manifest bytes in the same private filesystem snapshot as the authenticated +payload and covered Python sources. The exact bytes used in each manifest digest also feed metadata normalization. +Reuse and expose the existing canonical metadata factory without changing normalization policy. Compare whole +normalized models, excluding only separately authenticated integrity, injected discovery source and post-admission +protocol_operations. Select module-package.yaml before metadata.yaml solely from captured membership. Refuse +mismatch before any registration effects; no live manifest reread, persistent marker, duplicate parser or metadata +replacement pipeline. Public boolean verifier signatures and signing serialization remain stable. + +### Complete cache reproduction before live admission validation + +The independent verifier and all three adversarial reviewers identified compilation interleaved with the final +executable traversal. Reproduce expected standard cache bodies from captured authenticated source first. Then +perform the final fresh executable traversal, comparing live source/native membership and digests and cache headers +and bodies without any compiler work. Move the existing command authority recheck ahead of schema, bridge, +protocol and active-source effects. Keep the ordinary verifier and signing format unchanged. This closes the +concrete during-compiler insertion/replacement path; it does not seal mutable files against unrestricted writes +after admission. A private execution copy would change package/import/resource paths and is not required for this +specific ordering correction. + +## Indirect category authority — review correction, 2026-10-14 + +Category roots can be induced by installed-bundle metadata independently of commands naming a root. Category +builders also fetch registered member loaders, so authorization must cover category capabilities before candidate +selection and all registration effects. The correction uses existing metadata at the same admission boundary, +without importing builders during eligibility. Authentic official project copies retain strict core-key authority; +unsigned flags and workspace metadata remain insufficient. New public controls must reproduce actual category +mounting and harmless member execution before implementation, including existing roots and both grouping modes. + +## Manifest parser boundary — review correction, 2026-10-14 + +Primary discovery can accept a valid module-package.yaml while the filesystem payload includes malformed +metadata.yaml. Normalize YAML parser failures to the existing invalid-payload ValueError at canonical manifest +serialization, so the reported strict project path preserves fail-closed behavior without scattered startup guards. +Preexisting ordinary-verifier parse paths are not claimed globally remediated; ordinary APIs and valid signed bytes +remain unchanged. +Public registration controls capture an actual YAML error only to assert it does not escape; genuine signed-bundle +compatibility is exercised before each hostile alternate-manifest case. No parsing or signature check is skipped. + +## Full eligibility before duplicate selection + +Current c897 checks authority before deduplication but checks core, schema, integrity and dependency eligibility afterward. A trusted yet incompatible project copy can therefore hide a usable installed copy. Reuse the existing validators at selection, before any effects, and derive availability and versions from the selected set. Dependency changes require reconsideration rather than permanent rejection of a consumer that can become eligible after provider fallback. Preserve discovery priority and existing consistent cycle behavior; no global provider backtracking is promised. A repeated contradictory selection must fail closed with explicit diagnostics and bounded termination. Implementation shape remains under independent design comparison before production edits. + +### Approved convergence policy — 14 October 2026 + +The human approved bounded conflict refusal. Preserve provider priority and reconsider consumers against actual selected versions. Seed independently eligible candidates before evaluating dependencies, so consistent cycles retain existing behavior. Synchronous priority reselection has finitely many vectors; detect a repeated vector, identify names that changed in its cycle and refuse that conflicting component before effects. Remove those names and recompute remaining dependencies, preserving unrelated packages. Do not impose an arbitrary pass cap that might reject a nonrepeating convergent selection. No global provider downgrade/search is added. Repeat all independent gates for the entire selected set before any effects; a concrete candidate invalidated during final revalidation is removed for this invocation and selection is recomputed. The authenticated filesystem remains mutable after observation; no atomic sealing claim is made. + +## Additional review coverage controls — 14 October 2026 + +Three current-head review annotations identify test oracles, not a demonstrated new production vulnerability. +Add a separate control file without altering the five accepted75 authored files. Verify that noncanonical project +publisher metadata has a valid signature under the test bundled trust anchor before asserting denial, isolating +canonical identity from checksum rejection. Map paired consistent-cycle compatibility with an ineligible priority +copy, preserving the existing healthy-cycle positive suite control. Observe the actual warning when conflicting +priority selections are refused; inactive modules alone do not establish an explicit diagnostic. Specifications +already require canonical publisher identity, consistent cycles and explicit bounded conflict refusal. These +strengthened controls require fresh exact mapping acceptance and signed/native reauthoring before publication. +No production behavior, pin, threshold, timeout, signature format or global dependency policy is expanded. + +## Extensionless inventory and canonical identity review follow-up — 14 October 2026 + +The current fixed executable suffix inventory can omit an unsigned executable in excluded directories. +Classify executable mode and standard shebang/ELF/PE/Mach-O headers regardless of suffix, without candidate +execution or deserialization. Revalidate authenticated signed-file membership/content before effects, preserving +canonical manifest serialization and genuine cache reproduction. Ordinary excluded diagnostics remain allowed. +Add a separate authored control file so all six accepted79 files remain exact. Verify genuine crypto for each +tier/category/bundle group variant before public registration denial; existing grouping validation remains part +of admission. These controls do not claim isolation of each internal guard or an executable-load exploit. +Fresh exact mapping acceptance and signed native proof remain mandatory after final local validation. + +The protected replay already rejects four category/group variants. They remain standalone compatibility +controls in the complete suite and are not falsely counted as RED. Two paired identity controls verify genuine +category and group refusal first, reset registration between each, then exercise the real tier regression. +Native mapping retains79 existing selectors plus12 extensionless probes and these2 paired controls (93 total). +The paired controls call shared local bodies rather than treating pytest test functions as helpers. + +## Independent mode/header coverage — 14 October 2026 + +Two review annotations identify remaining oracle coverage, not a production vulnerability. Preserve all seven +accepted93 authored files and add a separate eighth file. Use owner/group/other execute bits individually and +the six Mach-O magic encodings not already covered by the93 controls. Check the constants against the local +Apple SDK mach-o/loader.h and fat.h. Every probe first proves canonical signed compatibility and genuine crypto; +then public registration must deny the omitted executable in both modes. No candidate binary is executed. +Existing production217dc80 and pins remain unchanged. Fresh exact mapping acceptance/native proof remain required. + +## Protected selector-bound correction + +Native111 run38641093406 failed before pytest/JUnit because the unchanged protected executor rejects more than100 selectors. This artifact is retained as invalid RED, with no runtime restoration. Preserve every one of18 new mode/header observations in7 exact nodes: one observes3 isolated execute bits in both modes;6 nodes each observe one missing Mach-O encoding in both modes. Collect root/source results and record every named observation before aggregate assertions, reset registry/roots between probes, and keep canonical compatibility/genuine signature checks first. This produces100 selectors without changing native executor, execution budget, trust, thresholds, pins, runtime or the seven accepted93 authored files. The accepted111 record is historical; the resulting new exact mapping requires fresh owner acceptance before signed/native publication. + +## Project path-link follow-up + +Actual discovery preserves the lexical package directory. Descendant inventory alone omits that root and its discovery-owned `.specfact/modules` ancestors. Keep the existing verifier API and reject a symlinked package root at strict filesystem verification. At project authority admission, reject linked `.specfact` or `modules` components before verification and selection effects. Workspace discovery already resolves its starting workspace, so the bounded check preserves canonical OS path aliases above that anchor. Ordinary verification, source fallback and signed payload format remain unchanged. Loader pinning and atomic filesystem sealing remain outside this correction. Retain grouped and legacy public controls with real crypto and actual discovery. + +Preserve the protected100-selector cap by keeping all93 earlier selectors and grouping the same18 encoding observations into four nodes. Add two path-link nodes, one per grouping mode, that collect package/modules/.specfact denial and real installed fallback before assertions. Add one legacy node collecting all five native suffix observations. Independent observation inventories remain explicit. The previous accepted100 digest and signed RED/GREEN remain historical; these changed hashes and selectors need fresh per-source acceptance before native publication. + +Final path observation follows cryptographic and executable-inventory verification. Reobserve the artifact root in its strict verifier and the two project-owned store components at the project boundary. A link introduced between the initial path checks and snapshot resolution must deny official project authority and preserve an eligible installed copy. These sequential observations do not seal filesystem state after admission. diff --git a/openspec/changes/fix-categoryless-module-command-squatting/proposal.md b/openspec/changes/fix-categoryless-module-command-squatting/proposal.md new file mode 100644 index 000000000..47dcdada4 --- /dev/null +++ b/openspec/changes/fix-categoryless-module-command-squatting/proposal.md @@ -0,0 +1,83 @@ +# Change: Block category-less module command squatting + +## Why + +Workspace modules are discovered from repository-controlled `.specfact/modules` directories. When category grouping is +enabled, a category-less module can currently register an arbitrary command at the CLI root. That allows a hostile +checkout to claim a removed flat command such as `validate` and execute package Python when stale automation invokes +it. + +## What Changes + +- Skip category-less project/workspace packages during command registration when category grouping is enabled; preserve +other discovery sources. +- Preserve the explicit legacy flat-registration behavior when category grouping is disabled. +- Add regression coverage proving a category-less workspace module cannot claim a root command or have its loader +invoked in grouped mode. +- Reserve permanent core root commands for resolved built-in package paths in every mode; reject category/core and +source-label impersonation. Reject unauthenticated project category-root claims before any registration effects. +- Preserve supported official `--scope project` installation only after full artifact verification against the +core-bundled key and canonical signed bundle metadata; retain all ordinary compatibility and integrity checks. + +## Capabilities + +### Modified Capabilities + +- `module-grouping` (amend the existing registration and missing-category requirement) +- `command-package-runtime-validation` (reserve permanent and official category root commands) +- `category-command-groups` (reconcile the shipped categorized-routing behavior in disabled grouping mode) + +## Impact + +- **Code:** `src/specfact_cli/registry/module_packages.py` module command registration. +- **Tests:** focused registry tests for category-less packages in grouped and legacy modes. +- **Documentation:** review `README.md`, `docs/`, `docs/index.md`, and navigation; no update is expected because +category-less root commands are already outside the documented grouped CLI contract. +- **Compatibility:** default grouped mode becomes fail-closed; explicitly disabled grouping retains legacy flat +registration. +- **Offline behavior:** unchanged; admission uses local metadata and artifact verification with the core-bundled public key. +- **Rollback:** revert the registration guard and its regression tests if an unexpected supported category-less grouped +module is identified. + +## Source Tracking + +- **GitHub Issue**: #718 +- **Issue URL**: +- **Parent Feature**: #352 +- **Repository**: nold-ai/specfact-cli +- **Last Synced Status**: Open; parent relationship to #352 and required labels are set, project assignment is verified +as SpecFact CLI/Todo and native dependency lists are empty. + +## Prior protected validation cycle after #761 + +Prerequisite #760 shipped through #761 into protected dev at c4ee5be8457f69848bf9bc2150ebc3df56b52d4f. +The protected helper authenticates unchanged inherited runtime proof while preserving #743/#751 as planning. +The prior candidate f6481418 was re-authored against that baseline with accepted mapping e550f23e and native RED +before implementation. No earlier-head proof, candidate helper or stale acceptance substitutes for the new cycle. +That cycle prepared coherent 0.55.8 artifacts after RED and left all three targets unmerged for owner review. + +## Previous protected consumer integration after #767 (2026-10-12) + +Merged #767 is protected dev c6af68a0fdf2ea798dd22c6750f077e217c938f5, version 0.55.8. +The earlier security candidate f6481418 passed all current-head gates and review on its original base. +Re-author specification and unchanged accepted controls against this new base before restoring production. +Owner acceptance e550f23e remains exact and genuine; new signed-head grants and native proof are required. +Current security bugfix release is the next available patch, 0.55.9. Leave #719/#743/#751 unmerged. + +## Protected integration after #768 (2026-10-13) + +The owner-approved protective #768 merged into dev as daefe0c75207632e054d96c36557673afab86a3a, +version0.55.9. Genuine final root proof is now authenticated separately from planning-child receipts, +retaining full protected-dev review, frozen provider validation, signatures, authority and execution. +Re-author specification and unchanged accepted E550 controls on this protected baseline, publish genuine +native RED, then restore the approved guards and coherent available bugfix0.55.10. Earlier-base evidence +is historical. Sequentially restack and validate both planning children; leave all three PRs unmerged. + +## Project-scoped bundle review correction (2026-10-13) + +The final-head review identified a supported-installation regression: blanket project category-root rejection also +excluded the six authenticated official bundles installed with `--scope project`. Keep the unsigned workspace and +permanent-core boundaries while admitting only canonical official bundles verified with the core-bundled key. +Extend specification first, author paired real-artifact compatibility/forgery controls, reproduce both the regression +and protected-base security failures, then implement. The amended mapping requires fresh owner acceptance; E550 +remains historical authority only. No gate, verifier, signature requirement or project-scope support is weakened. diff --git a/openspec/changes/fix-categoryless-module-command-squatting/requirements-evidence.yaml b/openspec/changes/fix-categoryless-module-command-squatting/requirements-evidence.yaml new file mode 100644 index 000000000..bcca3ec62 --- /dev/null +++ b/openspec/changes/fix-categoryless-module-command-squatting/requirements-evidence.yaml @@ -0,0 +1,1019 @@ +schema_version: '2' +requirements: + openspec:fix-categoryless-module-command-squatting:module-grouping:registry-groups-modules-by-category-when-loading: + rationale: Project modules cannot claim arbitrary root commands in default grouped mode. + stakeholder_refs: + - nold-ai/specfact-cli#718 + touchpoints: + - id: registration-policy + kind: code_symbol + locator: src/specfact_cli/registry/module_packages.py::_register_commands_for_package + verification_cases: + - case_id: SQUAT-001 + scenario_id: registry-groups-modules-by-category-when-loading + method: test + intent: Register a real category-less project manifest and check root registry and execution marker. + observable: The removed validate command is absent and its package has not executed. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_missing_category_cannot_squat_root + - case_id: SQUAT-COMPAT + scenario_id: registry-groups-modules-by-category-when-loading + method: test + intent: Execute explicit legacy workspace registration. + observable: An unreserved legacy command remains loadable only when grouping is explicitly disabled. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_explicit_legacy_workspace_registration_remains_available + - case_id: SQUAT-SOURCES + scenario_id: registry-groups-modules-by-category-when-loading + method: test + intent: Execute all non-project legacy source controls. + observable: User, marketplace and custom unreserved legacy commands remain available. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_non_workspace_legacy_registration_remains_available + - case_id: SQUAT-SHADOW + scenario_id: registry-groups-modules-by-category-when-loading + method: test + intent: Load an eligible package alongside a hostile project package with valid schema and bridge declarations. + observable: The schema and bridge registries remain unchanged; only the approved dependency is imported and + the rejected source is never activated or executed. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_rejected_workspace_cannot_shadow_eligible_package_imports + ? "openspec:fix-categoryless-module-command-squatting:command-package-runtime-validation:\ + permanent-core-commands-cannot-be-extended-by-external-packages" + : rationale: Declaring core metadata cannot grant authority to extend permanent root commands. + stakeholder_refs: + - nold-ai/specfact-cli#718 + touchpoints: + - id: core-authority + kind: code_symbol + locator: src/specfact_cli/registry/module_packages.py::_register_commands_for_package + verification_cases: + - case_id: SQUAT-002 + scenario_id: permanent-core-commands-cannot-be-extended-by-external-packages + method: test + intent: Attempt to extend every permanent root with an external core-category manifest. + observable: Existing root loaders remain unchanged and no external module entry or execution appears. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_core_category_cannot_extend_permanent_roots + - case_id: SQUAT-PATH + scenario_id: permanent-core-commands-cannot-be-extended-by-external-packages + method: test + intent: Execute forged provenance and escaping symlink controls in both modes. + observable: Both registries reject an external resolved path despite a forged builtin label. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_external_core_claim_rejected_even_with_forged_source_and_builtin_symlink + - case_id: SQUAT-BUILTIN + scenario_id: permanent-core-commands-cannot-be-extended-by-external-packages + method: test + intent: Load a real signed built-in init package. + observable: The actual built-in command remains loadable. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_real_builtin_core_registration_remains_available + - case_id: SQUAT-REPO + scenario_id: permanent-core-commands-cannot-be-extended-by-external-packages + method: test + intent: Use operator-configured discovery from a different checkout. + observable: Its contained built-in package retains authority without trusting a manifest source claim. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_explicit_repository_root_preserves_builtin_authority + - case_id: SQUAT-FLAT-INIT + scenario_id: permanent-core-commands-cannot-be-extended-by-external-packages + method: test + intent: Independently exercise the category-less init claim with grouping explicitly disabled. + observable: No root command, module entry, active import source or hostile execution is admitted. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_reserved_flat_claim_is_denied[init] + - case_id: SQUAT-FLAT-MODULE + scenario_id: permanent-core-commands-cannot-be-extended-by-external-packages + method: test + intent: Independently exercise the category-less module claim with grouping explicitly disabled. + observable: No root command, module entry, active import source or hostile execution is admitted. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_reserved_flat_claim_is_denied[module] + - case_id: SQUAT-FLAT-UPGRADE + scenario_id: permanent-core-commands-cannot-be-extended-by-external-packages + method: test + intent: Independently exercise the category-less upgrade claim with grouping explicitly disabled. + observable: No root command, module entry, active import source or hostile execution is admitted. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_reserved_flat_claim_is_denied[upgrade] + ? "openspec:fix-categoryless-module-command-squatting:command-package-runtime-validation:\ + workspace-packages-cannot-supply-official-category-roots" + : rationale: Unauthenticated project category capabilities cannot create or extend official roots, including + indirect bundle mounts and member loaders. + stakeholder_refs: + - nold-ai/specfact-cli#718 + touchpoints: + - id: registration-policy + kind: code_symbol + locator: src/specfact_cli/registry/module_packages.py::_package_local_registration_eligible + verification_cases: + - case_id: SQUAT-CATEGORY + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Attempt unsigned claims to all six official category roots with existing root/module sentinel loaders + and then empty registries in both grouping modes. + observable: Original loader and metadata identities and Typer apps remain unchanged; the unverified project + package cannot create or extend roots, activate imports or create category mounts. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_cannot_supply_official_category_roots + - case_id: SQUAT-IMPORT-FREE + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Invoke public grouped registration for a category-less project root claim while observing imports. + observable: Admission imports no category builder or initializes an app; root/module registries, active imports + and the hostile execution marker remain empty. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_eligibility_does_not_import_category_builders + - case_id: SQUAT-FLAT-PROJECT + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Independently exercise the category-less project claim with grouping explicitly disabled. + observable: No root command, module entry, active import source or hostile execution is admitted. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_reserved_flat_claim_is_denied[project] + - case_id: SQUAT-FLAT-BACKLOG + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Independently exercise the category-less backlog claim with grouping explicitly disabled. + observable: No root command, module entry, active import source or hostile execution is admitted. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_reserved_flat_claim_is_denied[backlog] + - case_id: SQUAT-FLAT-CODE + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Independently exercise the category-less code claim with grouping explicitly disabled. + observable: No root command, module entry, active import source or hostile execution is admitted. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_reserved_flat_claim_is_denied[code] + - case_id: SQUAT-FLAT-SPEC + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Independently exercise the category-less spec claim with grouping explicitly disabled. + observable: No root command, module entry, active import source or hostile execution is admitted. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_reserved_flat_claim_is_denied[spec] + - case_id: SQUAT-FLAT-GOVERN + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Independently exercise the category-less govern claim with grouping explicitly disabled. + observable: No root command, module entry, active import source or hostile execution is admitted. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_reserved_flat_claim_is_denied[govern] + - case_id: SQUAT-FLAT-REQUIREMENTS + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Independently exercise the category-less requirements claim with grouping explicitly disabled. + observable: No root command, module entry, active import source or hostile execution is admitted. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_reserved_flat_claim_is_denied[requirements] + - case_id: SQUAT-INDIRECT-FALSE-TRUE + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Attempt actual project/plan bundle-driven mounting with existing root False and grouping True. + observable: No unsigned member executes; no module entry/import root is activated, and existing root identity + is retained. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_bundle_member_cannot_induce_category_mount[False-True] + - case_id: SQUAT-INDIRECT-FALSE-FALSE + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Attempt actual project/plan bundle-driven mounting with existing root False and grouping False. + observable: No unsigned member executes; no module entry/import root is activated, and existing root identity + is retained. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_bundle_member_cannot_induce_category_mount[False-False] + - case_id: SQUAT-INDIRECT-TRUE-TRUE + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Attempt actual project/plan bundle-driven mounting with existing root True and grouping True. + observable: No unsigned member executes; no module entry/import root is activated, and existing root identity + is retained. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_bundle_member_cannot_induce_category_mount[True-True] + - case_id: SQUAT-INDIRECT-TRUE-FALSE + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Attempt actual project/plan bundle-driven mounting with existing root True and grouping False. + observable: No unsigned member executes; no module entry/import root is activated, and existing root identity + is retained. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_bundle_member_cannot_induce_category_mount[True-False] + - case_id: SQUAT-CORE-MEMBER-TRUE + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Attempt a core-category project plan member extension through a real existing project builder, + grouping True. + observable: The unsigned member never executes; the original member app identity and rejected source exclusion + remain intact. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_core_member_cannot_extend_existing_category[True] + - case_id: SQUAT-CORE-MEMBER-FALSE + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Attempt a core-category project plan member extension through a real existing project builder, + grouping False. + observable: The unsigned member never executes; the original member app identity and rejected source exclusion + remain intact. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_core_member_cannot_extend_existing_category[False] + - case_id: SQUAT-LEGACY-ALIAS-BUNDLE + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Attempt an official category mount through legacy bundle alias metadata with grouping disabled. + observable: No reserved project root or active source is admitted despite the unreserved literal command. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_legacy_alias_cannot_mount_category[bundle] + - case_id: SQUAT-LEGACY-ALIAS-NAME + scenario_id: workspace-packages-cannot-supply-official-category-roots + method: test + intent: Attempt an official category mount through legacy name alias metadata with grouping disabled. + observable: No reserved project root or active source is admitted despite the unreserved literal command. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_workspace_legacy_alias_cannot_mount_category[name] + ? "openspec:fix-categoryless-module-command-squatting:category-command-groups:\ + category-grouping-enabled-config-flag-controls-grouping-behaviour" + : rationale: Reconcile the existing shipped category routing contract without restoring removed flat shims. + stakeholder_refs: + - nold-ai/specfact-cli#718 + touchpoints: + - id: category-policy + kind: code_symbol + locator: src/specfact_cli/registry/module_packages.py::register_module_package_commands + verification_cases: + - case_id: SQUAT-FLAG + scenario_id: category-grouping-enabled-config-flag-controls-grouping-behaviour + method: test + intent: Check categorized routing with grouping explicitly disabled. + observable: Removed bundle shims remain absent. + selector: + runner: pytest + node_id: tests/unit/registry/test_category_groups.py::test_bootstrap_with_category_grouping_disabled_still_has_no_flat_shims + ? "openspec:fix-categoryless-module-command-squatting:category-command-groups:\ + spec-module-sub-command-avoids-collision-with-group-command-name" + : rationale: Reconcile the completed flat-shim migration while reserving the workspace spec root. + stakeholder_refs: + - nold-ai/specfact-cli#718 + touchpoints: + - id: spec-category-routing + kind: code_symbol + locator: src/specfact_cli/registry/module_packages.py::register_module_package_commands + verification_cases: + - case_id: SQUAT-SPEC-API + scenario_id: spec-module-sub-command-avoids-collision-with-group-command-name + method: test + intent: Reject a category-less workspace spec root in legacy mode and execute an eligible api route. + observable: The workspace loader stays inactive and the eligible spec api validate command succeeds. + selector: + runner: pytest + node_id: tests/unit/security/test_module_command_squatting.py::test_spec_api_routing_preserves_reserved_workspace_root + ? "openspec:fix-categoryless-module-command-squatting:command-package-runtime-validation:\ + authenticated-project-bundles-retain-official-category-roots" + : rationale: Preserve supported official project installation without giving unsigned flags, metadata claims, + receipts or workspace keys category-root authority. + stakeholder_refs: + - nold-ai/specfact-cli#718 + touchpoints: + - id: verified-project-root + kind: code_symbol + locator: src/specfact_cli/registry/module_packages.py::_verified_project_bundle_root + - id: complete-artifact-verifier + kind: code_symbol + locator: src/specfact_cli/registry/module_installer.py::verify_module_artifact_from_filesystem + - id: candidate-discovery + kind: code_symbol + locator: src/specfact_cli/registry/module_packages.py::discover_all_package_metadata + - id: enabled-authority-selection + kind: code_symbol + locator: src/specfact_cli/registry/module_packages.py::_registration_packages + - id: canonical-metadata-normalization + kind: code_symbol + locator: src/specfact_cli/registry/module_packages.py::normalize_module_manifest + - id: authenticated-manifest-binding + kind: code_symbol + locator: src/specfact_cli/registry/module_installer.py::_metadata_matches_authenticated_manifest + verification_cases: + - case_id: SQUAT-VERIFIED-PROJECT-GROUPED + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official project project artifact in grouped mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[project-grouped] + - case_id: SQUAT-VERIFIED-PROJECT-LEGACY + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official project project artifact in legacy mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[project-legacy] + - case_id: SQUAT-VERIFIED-BACKLOG-GROUPED + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official backlog project artifact in grouped mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[backlog-grouped] + - case_id: SQUAT-VERIFIED-BACKLOG-LEGACY + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official backlog project artifact in legacy mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[backlog-legacy] + - case_id: SQUAT-VERIFIED-CODEBASE-GROUPED + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official codebase project artifact in grouped mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[codebase-grouped] + - case_id: SQUAT-VERIFIED-CODEBASE-LEGACY + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official codebase project artifact in legacy mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[codebase-legacy] + - case_id: SQUAT-VERIFIED-CODE-REVIEW-GROUPED + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official code-review project artifact in grouped mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[code-review-grouped] + - case_id: SQUAT-VERIFIED-CODE-REVIEW-LEGACY + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official code-review project artifact in legacy mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[code-review-legacy] + - case_id: SQUAT-VERIFIED-SPEC-GROUPED + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official spec project artifact in grouped mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[spec-grouped] + - case_id: SQUAT-VERIFIED-SPEC-LEGACY + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official spec project artifact in legacy mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[spec-legacy] + - case_id: SQUAT-VERIFIED-GOVERN-GROUPED + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official govern project artifact in grouped mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[govern-grouped] + - case_id: SQUAT-VERIFIED-GOVERN-LEGACY + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official govern project artifact in legacy mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[govern-legacy] + - case_id: SQUAT-VERIFIED-REQUIREMENTS-GROUPED + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official requirements project artifact in grouped mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[requirements-grouped] + - case_id: SQUAT-VERIFIED-REQUIREMENTS-LEGACY + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real signed official requirements project artifact in legacy mode, then exercise unverified + replacements and verifier failures. + observable: The authentic package retains its root, activated source and loadable Typer app; unsigned, tampered, + forged-publisher/signature, receipt, workspace-key, untracked Git source, escaping-directory-symlink, + loadable unsigned cached/standalone bytecode and excluded executable source copies plus missing/empty/malformed + keys and unavailable verification cause no root or active-import effects. Authentic current-interpreter + caches compiled from covered signed source retain verified repeated registration and loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_verified_project_bundle_preserves_root_and_rejects_forgery[requirements-legacy] + - case_id: SQUAT-SOURCE-SNAPSHOT + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Pair real official codebase loading with a valid-app cache and source replacement between reads. + observable: The altered source snapshot is rejected before compiler work or active imports; a public lazy + retry with restored signed source still rejects the forged cache and registers no category root. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_signed_source_snapshot_cannot_be_replaced + - case_id: SQUAT-EXCLUDED-NATIVE-DLL + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Pair real official codebase loading with a harmless unsigned dll artifact under excluded logs. + observable: The unchanged signed bundle remains loadable before mutation; adding the omitted native artifact + denies category-root registration and active imports without executing native code. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_project_root_rejects_excluded_native_artifacts[dll] + - case_id: SQUAT-EXCLUDED-NATIVE-DYLIB + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Pair real official codebase loading with a harmless unsigned dylib artifact under excluded logs. + observable: The unchanged signed bundle remains loadable before mutation; adding the omitted native artifact + denies category-root registration and active imports without executing native code. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_project_root_rejects_excluded_native_artifacts[dylib] + - case_id: SQUAT-EXCLUDED-NATIVE-BUNDLE + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Pair real official codebase loading with a harmless unsigned bundle artifact under excluded logs. + observable: The unchanged signed bundle remains loadable before mutation; adding the omitted native artifact + denies category-root registration and active imports without executing native code. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_project_root_rejects_excluded_native_artifacts[bundle] + - case_id: SQUAT-INSTALLED-FALLBACK-GROUPED + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise real same-name project/user discovery and public registration in grouped mode. + observable: A rejected unsigned project claim cannot suppress the authentic installed codebase root or its + review add-on; a verified project copy retains normal priority and default discovery stays deduplicated. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_rejected_project_preserves_same_name_installed_bundle[grouped] + - case_id: SQUAT-INSTALLED-FALLBACK-LEGACY + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise real same-name project/user discovery and public registration in legacy mode. + observable: A rejected unsigned project claim cannot suppress the authentic installed codebase root or its + review add-on; a verified project copy retains normal priority and default discovery stays deduplicated. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_rejected_project_preserves_same_name_installed_bundle[legacy] + - case_id: SQUAT-DISABLED-PAYLOAD-STATE + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Observe actual source payload reads with a project root disabled by state, then explicitly re-enable it. + observable: Disabled source is neither read nor activated; explicit re-enablement preserves authentic loading + while enabling an unsigned replacement never grants category-root authority. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_disabled_project_roots_skip_payload_verification[state] + - case_id: SQUAT-DISABLED-PAYLOAD-DISABLE-OVERRIDE + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Observe actual source payload reads with a project root disabled by disable-override, then explicitly re-enable it. + observable: Disabled source is neither read nor activated; explicit re-enablement preserves authentic loading + while enabling an unsigned replacement never grants category-root authority. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_disabled_project_roots_skip_payload_verification[disable-override] + - case_id: SQUAT-MANIFEST-BINDING-SCHEMA + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Let real discovery read altered schema metadata with original integrity, then restore the signed manifest. + observable: The restored artifact genuinely verifies, but unsigned discovery metadata causes no schema, bridge, + constructor, command or active-source effects; fresh discovery of authentic metadata restores normal loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_discovery_fields_match_signed_manifest[schema] + - case_id: SQUAT-MANIFEST-BINDING-BRIDGE + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Let real discovery read altered bridge metadata with original integrity, then restore the signed manifest. + observable: The restored artifact genuinely verifies, but unsigned discovery metadata causes no schema, bridge, + constructor, command or active-source effects; fresh discovery of authentic metadata restores normal loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_discovery_fields_match_signed_manifest[bridge] + - case_id: SQUAT-MANIFEST-BINDING-COMMANDS + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Let real discovery read altered commands metadata with original integrity, then restore the signed manifest. + observable: The restored artifact genuinely verifies, but unsigned discovery metadata causes no schema, bridge, + constructor, command or active-source effects; fresh discovery of authentic metadata restores normal loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_discovery_fields_match_signed_manifest[commands] + - case_id: SQUAT-MANIFEST-BINDING-VERSION + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Let real discovery read altered version metadata with original integrity, then restore the signed manifest. + observable: The restored artifact genuinely verifies, but unsigned discovery metadata causes no schema, bridge, + constructor, command or active-source effects; fresh discovery of authentic metadata restores normal loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_discovery_fields_match_signed_manifest[version] + - case_id: SQUAT-LIVE-INVENTORY-ADDED-SOURCE + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Mutate added-source immediately after real signature verification of the captured artifact. + observable: No unauthenticated live executable may activate its package even transiently; restoring the authentic + inventory preserves public registration and command loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_live_executable_inventory_matches_verified_snapshot[added-source] + - case_id: SQUAT-LIVE-INVENTORY-CHANGED-SOURCE + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Mutate changed-source immediately after real signature verification of the captured artifact. + observable: No unauthenticated live executable may activate its package even transiently; restoring the authentic + inventory preserves public registration and command loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_live_executable_inventory_matches_verified_snapshot[changed-source] + - case_id: SQUAT-CACHE-PHASE-ADDED-BYTECODE + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Deny equivalent static added-bytecode forgery, then mutate during real standard cache compilation. + observable: Static and compiler-interleaved forgeries cause no transient source activation or command registration; + a fresh compiler-free final inventory denies mutation while restoring the authentic artifact preserves public loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_cache_compilation_finishes_before_admission_inventory[added-bytecode] + - case_id: SQUAT-CACHE-PHASE-CHANGED-SOURCE + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Deny equivalent static changed-source forgery, then mutate during real standard cache compilation. + observable: Static and compiler-interleaved forgeries cause no transient source activation or command registration; + a fresh compiler-free final inventory denies mutation while restoring the authentic artifact preserves public loading. + selector: + runner: pytest + node_id: tests/unit/security/test_project_bundle_authority.py::test_cache_compilation_finishes_before_admission_inventory[changed-source] + - case_id: SQUAT-EXCLUDED-NATIVE-SO + intent: Deny unsigned omitted so executable artifacts while separately verifying authentic project compatibility. + method: test + observable: The unchanged signed bundle remains loadable before mutation; adding the omitted native artifact + denies category-root registration and active imports without executing native code. + scenario_id: authenticated-project-bundles-retain-official-category-roots + selector: + node_id: tests/unit/security/test_project_bundle_authority.py::test_project_root_rejects_excluded_native_artifacts[so] + runner: pytest + - case_id: SQUAT-EXCLUDED-NATIVE-PYD + intent: Deny unsigned omitted pyd executable artifacts while separately verifying authentic project compatibility. + method: test + observable: The unchanged signed bundle remains loadable before mutation; adding the omitted native artifact + denies category-root registration and active imports without executing native code. + scenario_id: authenticated-project-bundles-retain-official-category-roots + selector: + node_id: tests/unit/security/test_project_bundle_authority.py::test_project_root_rejects_excluded_native_artifacts[pyd] + runner: pytest + - case_id: SQUAT-MALFORMED-ALTERNATE-TRUE + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register a real official-looking project copy with malformed alternate metadata YAML while preserving + authentic compatibility, grouping True. + observable: No YAML parser exception escapes public registration; no rejected category/import effects occur, + while the authentic counterpart remains loadable. + selector: + runner: pytest + node_id: tests/unit/security/test_malformed_module_manifests.py::test_malformed_alternate_manifest_is_denied_without_startup_failure[True] + - case_id: SQUAT-MALFORMED-ALTERNATE-FALSE + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register a real official-looking project copy with malformed alternate metadata YAML while preserving + authentic compatibility, grouping False. + observable: No YAML parser exception escapes public registration; no rejected category/import effects occur, + while the authentic counterpart remains loadable. + selector: + runner: pytest + node_id: tests/unit/security/test_malformed_module_manifests.py::test_malformed_alternate_manifest_is_denied_without_startup_failure[False] + - case_id: SQUAT-SELECTION-01 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_conflicting_dependency_cycle_refuses_only_unstable_component through actual configured-root + discovery and public registration with genuine test-key signatures. + observable: Priority selection terminates, none of the four contradictory candidates activates or mounts + a category, and the unrelated signed module activates normally. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_conflicting_dependency_cycle_refuses_only_unstable_component + - case_id: SQUAT-SELECTION-02 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_dependency_selection_reconsiders_higher_priority_consumer through actual configured-root + discovery and public registration with genuine test-key signatures. + observable: The higher-priority consumer and fallback version2 provider activate; neither the unnecessary + consumer fallback nor missing-dependency provider activates. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_dependency_selection_reconsiders_higher_priority_consumer + - case_id: SQUAT-SELECTION-03 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_final_revalidation_rejects_payload_changed_after_initial_check through actual configured-root + discovery and public registration with genuine test-key signatures. + observable: After the actual verifier first accepts a copy, a faithful observer changes its payload; the + changed copy never activates and the still-signed fallback does. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_final_revalidation_rejects_payload_changed_after_initial_check + - case_id: SQUAT-SELECTION-04 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_integrity_rejected_user_copy_keeps_signed_fallback through actual configured-root discovery + and public registration with genuine test-key signatures. + observable: The genuinely signed installed fallback activates while the checksum-invalid copy contributes + no active source. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_integrity_rejected_user_copy_keeps_signed_fallback + - case_id: SQUAT-SELECTION-05 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_rejected_provider_cannot_satisfy_consumer_dependencies through actual configured-root + discovery and public registration with genuine test-key signatures. + observable: Neither rejected provider nor dependent consumer activates; no category root is mounted for the + rejected provider. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_rejected_provider_cannot_satisfy_consumer_dependencies + - case_id: SQUAT-SELECTION-06 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_signed_ineligible_project_preserves_eligible_installed_copy[core-grouped] through actual + configured-root discovery and public registration with genuine test-key signatures. + observable: The first fully eligible signed installation activates its exact source and exposes its probe + command; the rejected higher-priority copy does not activate. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_signed_ineligible_project_preserves_eligible_installed_copy[core-grouped] + - case_id: SQUAT-SELECTION-07 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_signed_ineligible_project_preserves_eligible_installed_copy[core-legacy] through actual + configured-root discovery and public registration with genuine test-key signatures. + observable: The first fully eligible signed installation activates its exact source and exposes its probe + command; the rejected higher-priority copy does not activate. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_signed_ineligible_project_preserves_eligible_installed_copy[core-legacy] + - case_id: SQUAT-SELECTION-08 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_signed_ineligible_project_preserves_eligible_installed_copy[dependency-grouped] through + actual configured-root discovery and public registration with genuine test-key signatures. + observable: The first fully eligible signed installation activates its exact source and exposes its probe + command; the rejected higher-priority copy does not activate. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_signed_ineligible_project_preserves_eligible_installed_copy[dependency-grouped] + - case_id: SQUAT-SELECTION-09 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_signed_ineligible_project_preserves_eligible_installed_copy[dependency-legacy] through + actual configured-root discovery and public registration with genuine test-key signatures. + observable: The first fully eligible signed installation activates its exact source and exposes its probe + command; the rejected higher-priority copy does not activate. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_signed_ineligible_project_preserves_eligible_installed_copy[dependency-legacy] + - case_id: SQUAT-SELECTION-10 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_signed_ineligible_project_preserves_eligible_installed_copy[schema-grouped] through + actual configured-root discovery and public registration with genuine test-key signatures. + observable: The first fully eligible signed installation activates its exact source and exposes its probe + command; the rejected higher-priority copy does not activate. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_signed_ineligible_project_preserves_eligible_installed_copy[schema-grouped] + - case_id: SQUAT-SELECTION-11 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise test_signed_ineligible_project_preserves_eligible_installed_copy[schema-legacy] through + actual configured-root discovery and public registration with genuine test-key signatures. + observable: The first fully eligible signed installation activates its exact source and exposes its probe + command; the rejected higher-priority copy does not activate. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_signed_ineligible_project_preserves_eligible_installed_copy[schema-legacy] + - case_id: SQUAT-SELECTION-12 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Exercise a declared versioned self-dependency through actual configured-root discovery and public + registration using genuine signed versions1 and2. + observable: The version2 fallback activates and version1 does not; each candidate is evaluated with its own + enabled identity and version without changing another provider priority. + selector: + runner: pytest + node_id: tests/unit/security/test_package_candidate_eligibility.py::test_versioned_self_dependency_selects_eligible_fallback + - case_id: SQUAT-REVIEW-01 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical and noncanonical project publishers under the same real test anchor in grouped mode. + observable: Canonical authority retains its root; valid crypto for a noncanonical publisher grants no root or import source. + selector: + runner: pytest + node_id: tests/unit/security/test_registration_review_oracles.py::test_validly_signed_noncanonical_publisher_is_denied[True] + - case_id: SQUAT-REVIEW-02 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical and noncanonical project publishers under the same real test anchor in legacy mode. + observable: Canonical authority retains its root; valid crypto for a noncanonical publisher grants no root or import source. + selector: + runner: pytest + node_id: tests/unit/security/test_registration_review_oracles.py::test_validly_signed_noncanonical_publisher_is_denied[False] + - case_id: SQUAT-REVIEW-03 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register a healthy signed cyclic pair alone, then behind an ineligible same-name priority copy. + observable: Both consistent cycle commands remain registered and activated; the incompatible priority copy remains inactive. + selector: + runner: pytest + node_id: tests/unit/security/test_registration_review_oracles.py::test_consistent_cycle_survives_ineligible_priority_copy + - case_id: SQUAT-REVIEW-04 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Register the real contradictory version cycle and observe its actual warning through a standard logger. + observable: The conflicting modules remain inactive, the unrelated package survives, and the explicit refusal names both + module identities. + selector: + runner: pytest + node_id: tests/unit/security/test_registration_review_oracles.py::test_conflict_refusal_has_explicit_registration_diagnostic + - case_id: SQUAT-INVENTORY-01 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[mode-grouped] + - case_id: SQUAT-INVENTORY-02 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[mode-legacy] + - case_id: SQUAT-INVENTORY-03 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[shebang-grouped] + - case_id: SQUAT-INVENTORY-04 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[shebang-legacy] + - case_id: SQUAT-INVENTORY-05 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[elf-grouped] + - case_id: SQUAT-INVENTORY-06 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[elf-legacy] + - case_id: SQUAT-INVENTORY-07 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[pe-grouped] + - case_id: SQUAT-INVENTORY-08 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[pe-legacy] + - case_id: SQUAT-INVENTORY-09 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[mach-o-grouped] + - case_id: SQUAT-INVENTORY-10 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[mach-o-legacy] + - case_id: SQUAT-INVENTORY-11 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[fat-mach-o-grouped] + - case_id: SQUAT-INVENTORY-12 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Authenticate canonical compatibility and an ordinary excluded diagnostic before inspecting + unsigned executable bytes or mode. + observable: Unsigned executable grants no category root or active source; no candidate artifact is executed. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_excluded_extensionless_executable_is_denied[fat-mach-o-legacy] + - case_id: SQUAT-INVENTORY-13 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Verify genuine signatures and existing category/group refusal before the canonical tier regression. + observable: All three changed identity fields retain denial and canonical compatibility, with state reset between checks. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_all_signed_identity_fields_retain_canonical_authority[grouped] + - case_id: SQUAT-INVENTORY-14 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Verify genuine signatures and existing category/group refusal before the canonical tier regression. + observable: All three changed identity fields retain denial and canonical compatibility, with state reset between checks. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_identity_oracles.py::test_all_signed_identity_fields_retain_canonical_authority[legacy] + - case_id: SQUAT-BOUNDED-ENCODING-01 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Observe every named encoding in both registration modes after canonical compatibility and genuine crypto. + observable: All18 root/source observations are recorded in four nodes before aggregate denial without probe execution. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_encoding_matrix.py::test_each_executable_encoding_is_denied[execute-bits] + - case_id: SQUAT-BOUNDED-ENCODING-02 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Observe every named encoding in both registration modes after canonical compatibility and genuine crypto. + observable: All18 root/source observations are recorded in four nodes before aggregate denial without probe execution. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_encoding_matrix.py::test_each_executable_encoding_is_denied[thin-32] + - case_id: SQUAT-BOUNDED-ENCODING-03 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Observe every named encoding in both registration modes after canonical compatibility and genuine crypto. + observable: All18 root/source observations are recorded in four nodes before aggregate denial without probe execution. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_encoding_matrix.py::test_each_executable_encoding_is_denied[big-64] + - case_id: SQUAT-BOUNDED-ENCODING-04 + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Observe every named encoding in both registration modes after canonical compatibility and genuine crypto. + observable: All18 root/source observations are recorded in four nodes before aggregate denial without probe execution. + selector: + runner: pytest + node_id: tests/unit/security/test_executable_encoding_matrix.py::test_each_executable_encoding_is_denied[fat-little] + - case_id: SQUAT-PROJECT-PATH-LINK-GROUPED + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Discover genuine signed copies through package, modules and specfact links with real installed fallback. + observable: All three owned path links gain no command or source authority and the eligible installed copy remains active. + selector: + runner: pytest + node_id: tests/unit/security/test_package_path_link_authority.py::test_project_path_links_have_no_authority[grouped] + - case_id: SQUAT-PROJECT-PATH-LINK-LEGACY + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Discover genuine signed copies through package, modules and specfact links with real installed fallback. + observable: All three owned path links gain no command or source authority and the eligible installed copy remains active. + selector: + runner: pytest + node_id: tests/unit/security/test_package_path_link_authority.py::test_project_path_links_have_no_authority[legacy] + - case_id: SQUAT-NATIVE-LEGACY-ALL + scenario_id: authenticated-project-bundles-retain-official-category-roots + method: test + intent: Observe all five omitted native suffixes in legacy routing after genuine signed compatibility. + observable: Every suffix loses root and source authority, with all observations recorded before aggregate denial. + selector: + runner: pytest + node_id: tests/unit/security/test_package_path_link_authority.py::test_legacy_native_suffixes_have_no_authority diff --git a/openspec/changes/fix-categoryless-module-command-squatting/requirements-proof/review-evidence.json b/openspec/changes/fix-categoryless-module-command-squatting/requirements-proof/review-evidence.json new file mode 100644 index 000000000..4346ab9d1 --- /dev/null +++ b/openspec/changes/fix-categoryless-module-command-squatting/requirements-proof/review-evidence.json @@ -0,0 +1,9 @@ +{ + "schema_version": "1", + "decision": "accepted", + "reviewer_id": "djm81", + "reviewer_role": "repository-owner", + "recorded_at": "2026-10-15T05:28:39.218096+00:00", + "reference": "https://github.com/nold-ai/specfact-cli/pull/719", + "mapping_digest": "sha256:03651caa143d7cbde41a9f33db9cc91182e9e05201173bf4a3bc5273c021dbdc" +} diff --git a/openspec/changes/fix-categoryless-module-command-squatting/specs/category-command-groups/spec.md b/openspec/changes/fix-categoryless-module-command-squatting/specs/category-command-groups/spec.md new file mode 100644 index 000000000..5fb9f24e4 --- /dev/null +++ b/openspec/changes/fix-categoryless-module-command-squatting/specs/category-command-groups/spec.md @@ -0,0 +1,38 @@ +## MODIFIED Requirements + +### Requirement: `category_grouping_enabled` config flag controls grouping behaviour + +The system SHALL read the grouping flag from operator environment or user configuration. Categorized +installed bundles SHALL retain category routing in either mode, preserving the shipped removal of flat +bundle shims. The flag SHALL control only the compatibility permission for category-less project packages; +permanent core and official category root reservations SHALL remain enforced in both modes. + +#### Scenario: Grouping enabled by default + +- **GIVEN** no explicit grouping flag +- **WHEN** the CLI initializes +- **THEN** grouped mode SHALL default to true +- **AND** category-less project packages SHALL be rejected + +#### Scenario: Grouping disabled via config + +- **GIVEN** the operator explicitly disables grouping +- **WHEN** the CLI initializes +- **THEN** categorized installed bundles SHALL retain category routing without flat bundle shims +- **AND** category-less project packages MAY register only unreserved legacy root commands +- **AND** permanent core and official category roots SHALL remain reserved + +### Requirement: spec module sub-command avoids collision with group command name + +The system SHALL mount the `spec` module as the `api` sub-command within the `spec` category group to +avoid a name collision. The shipped migration window has ended; removed flat bundle shims SHALL NOT +be restored. This routing SHALL remain valid in either grouping mode, and project packages SHALL NOT +supply or replace the reserved `spec` root through category-less legacy registration. + +#### Scenario: spec module mounts as `api` sub-command within spec group + +- **GIVEN** the `specfact-spec` bundle is installed +- **WHEN** the user runs `specfact spec --help` +- **THEN** the spec module SHALL appear as `api` within the category group +- **AND** its sub-commands SHALL remain accessible through `specfact spec api ` +- **AND** removed flat-shim delegation SHALL remain absent diff --git a/openspec/changes/fix-categoryless-module-command-squatting/specs/command-package-runtime-validation/spec.md b/openspec/changes/fix-categoryless-module-command-squatting/specs/command-package-runtime-validation/spec.md new file mode 100644 index 000000000..0473e0352 --- /dev/null +++ b/openspec/changes/fix-categoryless-module-command-squatting/specs/command-package-runtime-validation/spec.md @@ -0,0 +1,239 @@ +## ADDED Requirements + +### Requirement: Permanent Core Commands Cannot Be Extended By External Packages + +The system SHALL reserve `init`, `module` and `upgrade` for packages whose resolved path belongs to the +effective built-in module tree selected by the operator-controlled discovery configuration. External packages SHALL NOT +register or extend these commands in either command +registry, regardless of category, grouping mode, discovery source label or manifest name. + +#### Scenario: Workspace package impersonates a core category + +- **GIVEN** a workspace package declares `category: core` and a permanent core command +- **WHEN** package commands are registered and the existing root command is invoked +- **THEN** the original built-in loader SHALL remain unchanged +- **AND** no external loader SHALL be registered or executed for that command + +#### Scenario: Forged source and symlink cannot grant core authority + +- **GIVEN** an external package claims a built-in source or is symlinked from the built-in tree +- **WHEN** its resolved path is outside the built-in tree and it declares a permanent core command +- **THEN** both registries SHALL reject that external command in either grouping mode +- **AND** legitimate built-in commands SHALL continue to load + +#### Scenario: Explicit repository root preserves legitimate built-ins + +- **GIVEN** the operator sets SPECFACT_REPO_ROOT to another checkout containing built-in modules +- **WHEN** discovery loads a permanent core package from that effective root +- **THEN** resolved contained packages SHALL retain core authority +- **AND** a symlink escaping that root SHALL NOT gain core authority + +### Requirement: Workspace Packages Cannot Supply Official Category Roots + +Unauthenticated project packages SHALL NOT supply official category root command names in either mode, regardless of +claimed category, bundle or source fields in their manifest. Rejected packages SHALL cause no registration +side effects, including category-builder imports or app initialization during eligibility checks. Eligible +non-project installed bundles SHALL retain their category roots. + +#### Scenario: Workspace manifest impersonates an official category root + +- **GIVEN** a project package declares any official category root command +- **WHEN** package commands are registered in either grouping mode +- **THEN** the entire package SHALL be rejected before registration side effects +- **AND** existing category root loaders SHALL remain unchanged +- **AND** the rejected package SHALL NOT create a category mount through bundle metadata + +#### Scenario: Workspace bundle metadata cannot induce or extend an official category mount + +- **GIVEN** a project package whose category, bundle identity or registered member commands can create or extend an official category group +- **AND** its declared commands need not contain the category root name +- **WHEN** registration runs in either grouping mode with an empty or existing root registry +- **THEN** the package SHALL require the same authenticated official project-bundle authority before selection and registration effects +- **AND** an unauthenticated package SHALL NOT mount a category, extend its loaders or activate executable sources +- **AND** authenticated official bundles, non-project packages and explicit unreserved category-less legacy commands SHALL remain supported + +### Requirement: Authenticated Project Bundles Retain Official Category Roots + +Official bundles installed with `--scope project` SHALL retain their canonical category root in both grouping modes +only when their complete filesystem artifact checksum and signature verify against the public key bundled with the +core CLI. Verification SHALL include untracked module source files in a project Git checkout and SHALL reject +symlink members whose directory traversal or target is not covered by that complete artifact. Executable Python or +native-library files omitted from the signed payload SHALL NOT grant project-root authority. A Python source cache +may remain compatible only when its current-interpreter code is independently reproduced from a covered signed +source file. Verification SHALL authenticate one captured source-byte snapshot before compiler work, and SHALL +compile exactly those authenticated bytes with the original filename and current optimization level. A later +filesystem reread SHALL NOT replace the captured source. Cache verification SHALL neither execute nor deserialize +candidate code. Unsupported native-library artifacts, including DLL, dylib and bundle files in excluded directories, +SHALL remain outside privileged project-root authority. Standalone +bytecode and executable files under excluded directories SHALL fail closed. +Their signed official publisher identity, tier, category and bundle group SHALL match the canonical root. The +claimed reserved category commands SHALL consist only of that root; other signed bundle commands remain compatible. This admission check SHALL precede schema, bridge, protocol, command and active-import effects. Discovery SHALL retain same-name candidates until enabled state and root authority select the first eligible installation in existing source-priority order. Rejected project claims SHALL NOT suppress eligible installed alternatives or satisfy dependencies. Disabled names SHALL perform no artifact payload verification or registration effects; normal metadata discovery and stored/explicit enablement semantics SHALL remain unchanged. +Permanent core command authority SHALL remain limited to resolved built-in paths. + +Unsigned/test flags, environment-supplied public keys, publisher/name/category claims, missing keys, unavailable +signature backends, self-recomputed checksums and installation receipts SHALL NOT grant project category-root +authority. Tampered payloads or signatures SHALL be rejected without registration or import effects. + +#### Scenario: Verified project-scoped official bundle keeps its category root + +- **GIVEN** any canonical official bundle or official category add-on is installed in the project module root +- **WHEN** its complete checksum and signature verify against the core-bundled public key before registration +- **THEN** its canonical category root SHALL remain registered in both grouping modes +- **AND** the normal compatibility, dependency and integrity checks SHALL still apply +- **AND** an authenticated official category add-on SHALL expose its declared subgroup alongside its verified dependency +- **AND** an unverified replacement SHALL NOT extend that dependency group +- **AND** unsigned, tampered or self-signed replacements SHALL remain excluded even with permissive test flags + +#### Scenario: Workspace key and receipt cannot create official authority + +- **GIVEN** a workspace copy claims an official bundle identity and supplies its own signature key or receipt +- **WHEN** registration checks the project category-root claim +- **THEN** environment overrides, local receipts and unsigned/test flags SHALL NOT replace core-bundled key verification +- **AND** a missing key or unavailable verifier SHALL fail closed before package side effects + +#### Scenario: Source replacement cannot disconnect cache and signature verification + +- **GIVEN** an otherwise authentic project bundle has a forged valid-app cache and its source changes between reads +- **WHEN** project-root admission verifies the artifact +- **THEN** authentication and cache reproduction SHALL use the same captured source bytes +- **AND** an unauthenticated snapshot SHALL be rejected before compilation, registration or import activation + +#### Scenario: Rejected project copy cannot suppress a same-name installed bundle + +- **GIVEN** an authentic installed bundle and an unverified project copy claim the same module name +- **WHEN** public registration selects candidates in either grouping mode +- **THEN** the installed bundle and its dependents SHALL retain their commands and activated source +- **AND** the rejected project copy SHALL NOT register commands or activate source +- **AND** a verified project copy SHALL retain its normal higher discovery priority +- **AND** ordinary discovery and state consumers SHALL retain their deduplicated result contract + +#### Scenario: Disabled project roots avoid payload verification + +- **GIVEN** a project bundle is disabled by stored state or an explicit disable override +- **WHEN** public registration reads its metadata +- **THEN** its payload SHALL NOT be hashed, compiled, imported or activated +- **AND** explicitly enabling the authentic bundle SHALL preserve normal loading +- **AND** enabling an unsigned replacement SHALL NOT grant category-root authority + +#### Scenario: Discovery metadata must match the authenticated manifest + +- **GIVEN** discovery observes altered schema, bridge, command or version fields while integrity stays unchanged +- **AND** the signed manifest is restored before artifact verification +- **WHEN** project-root authority authenticates the captured artifact +- **THEN** every normalized registration metadata field SHALL match that same authenticated manifest snapshot +- **AND** mismatched metadata SHALL be rejected before schema, bridge, command or active-import effects +- **AND** comparison SHALL preserve canonical category normalization, discovery provenance and derived protocol state +- **AND** a fresh discovery of the authentic restored manifest SHALL retain normal project-root loading + +#### Scenario: Executable coverage matches the authenticated snapshot + +- **GIVEN** a project artifact genuinely authenticates its captured file inventory +- **AND** a Python executable is added or modified before admission finishes +- **WHEN** coverage validates the live executable files +- **THEN** signed-payload eligibility alone SHALL NOT count as authenticated coverage +- **AND** executable membership and content SHALL match the authenticated snapshot before activation +- **AND** legitimate unchanged artifacts and reproducible caches SHALL retain normal loading + +#### Scenario: Cache reproduction precedes the final admission inventory + +- **GIVEN** a genuinely signed project bundle has authentic current-interpreter source caches +- **AND** an executable is inserted or a previously observed source is changed while a cache is reproduced +- **WHEN** public registration decides project-root authority +- **THEN** cache reproduction SHALL finish before a fresh final executable membership/content check +- **AND** that final check SHALL NOT invoke compilation or execute candidate code +- **AND** no unauthenticated package SHALL register schema, bridge, protocol, commands or active source even transiently +- **AND** any command-stage authority recheck SHALL precede all registration effects +- **AND** restoring the authentic package SHALL preserve normal public loading + +#### Scenario: Malformed alternate manifest cannot abort project admission + +- **GIVEN** a discovered project package has a valid primary manifest and malformed alternate manifest YAML +- **WHEN** artifact admission canonicalizes the complete filesystem payload in either grouping mode +- **THEN** parsing failure SHALL deny the package without escaping public registration or aborting startup +- **AND** no rejected root/import effects SHALL occur while authentic project bundles remain loadable + +#### Scenario: Full eligibility precedes duplicate selection + +- **GIVEN** two genuine same-name copies in discovery priority order +- **WHEN** the higher-priority copy fails core compatibility, schema compatibility, integrity or its declared dependencies +- **THEN** the first lower-priority copy passing all existing eligibility gates SHALL supply its commands and activated source +- **AND** rejected copies SHALL NOT supply dependency availability, versions, schema/bridge effects or category mounts +- **AND** a compatible higher-priority copy SHALL retain its priority and disabled copies SHALL remain unread + +#### Scenario: Dependency fallback uses a coherent selected set + +- **GIVEN** a selected provider falls back to another version and same-name consumers have distinct requirements +- **WHEN** selection checks declared dependencies before registration effects +- **THEN** selection SHALL reconsider eligible consumer copies against the actual selected versions +- **AND** missing providers SHALL NOT be supplied by rejected or disabled candidates +- **AND** already consistent dependency cycles SHALL retain existing edge-local compatibility +- **AND** contradictory priority selections SHALL terminate with explicit refusal rather than oscillation or partial registration +- **AND** registration SHALL NOT introduce a global dependency solver or downgrade a provider solely to satisfy a consumer + +#### Scenario: A candidate self-dependency uses its own selected version + +- **GIVEN** two same-name signed copies declare a versioned dependency on their own module identity +- **WHEN** the higher-priority version fails that constraint and the fallback satisfies it +- **THEN** candidate evaluation SHALL use the candidate's own enabled identity and actual version +- **AND** the fallback SHALL activate without downgrading or changing any other provider's priority + +#### Scenario: Extensionless executable payloads cannot evade authenticated inventory + +- **GIVEN** an otherwise authentic project bundle contains an unsigned file under a signing-excluded directory +- **AND** that file has executable mode bits or a shebang, ELF, PE, or Mach-O executable header regardless of its suffix +- **WHEN** public registration checks project-root authority in either grouping mode +- **THEN** the omitted executable SHALL fail closed before activation +- **AND** unchanged signed artifacts and ordinary excluded non-executable diagnostics SHALL retain loading +- **AND** admission SHALL inspect bytes and mode only, without executing candidate artifacts + +#### Scenario: Every canonical identity field has genuine signature coverage + +- **GIVEN** each noncanonical publisher, tier, category, or bundle group variant has a genuine valid artifact signature +- **WHEN** public project registration applies its existing validation and official-root policy +- **THEN** every variant SHALL remain inactive in both grouping modes +- **AND** the equivalent canonical signed bundle SHALL retain its command + +#### Scenario: Every executable mode class and Mach-O encoding is covered + +- **GIVEN** an otherwise authentic project artifact contains an unsigned excluded file +- **AND** only its owner, group, or other execute permission bit is set, or its bytes use any thin/fat 32/64-bit Mach-O magic in either byte order +- **WHEN** public project registration checks the file in either grouping mode +- **THEN** each individual mode/header class SHALL be refused before activation +- **AND** canonical signed compatibility SHALL remain, and probes SHALL be inspected without execution + +#### Scenario: Bounded selectors preserve every executable encoding observation + +- **GIVEN** the protected native proof executor admits at most100 exact selectors +- **WHEN** executable encoding controls consolidate owner/group/other modes and paired registration modes +- **THEN** every probe's genuine signature, canonical compatibility, root admission and active-source result is observed before aggregate denial assertions +- **AND** the seven existing accepted93 files and all runtime/proof execution limits remain unchanged + +#### Scenario: Project package path links cannot grant root authority + +- **GIVEN** a genuinely signed official bundle reached through a symlink at its package directory, workspace `.specfact` directory or workspace `modules` directory +- **WHEN** project registration runs in either grouping mode +- **THEN** the linked package SHALL obtain no root command or source import authority +- **AND** rejection SHALL occur before selection effects while preserving eligible lower-priority installations +- **AND** ordinary signed directories reached from the canonical workspace SHALL retain authority, including workspaces originally selected through system path aliases +- **AND** package and owned-store link checks SHALL be repeated after artifact authentication and final inventory verification, before granting official project authority +- **AND** this admission observation SHALL NOT claim atomic sealing against later filesystem mutation + +#### Scenario: Omitted native library denial retains legacy routing coverage + +- **GIVEN** each omitted `.dll`, `.dylib`, `.bundle`, `.so` or `.pyd` file under a genuinely signed project bundle's excluded diagnostics directory +- **WHEN** legacy registration runs after an authentic compatibility control +- **THEN** each bundle SHALL obtain no root command or source import authority +- **AND** all named suffix observations SHALL be collected before the aggregate assertion without executing the probe bytes + +### Admission observation boundary + +Filesystem admission authenticates the source snapshot and executable inventory observed during admission and standard cache compilation. It prevents unauthenticated paths from gaining authority during those steps. The registered loader later reads the live filesystem. A concurrent writer replacing otherwise admitted ordinary files after the final observation and before a later lazy load is outside this guarantee. This change does not seal the filesystem or execute an immutable snapshot. Official project category-root authority requires unlinked package and owned workspace-store components at the final admission observation. Category-less, unreserved legacy routing keeps its existing compatibility policy. + +#### Scenario: Path-link controls preserve the protected selector bound + +- **GIVEN** the unchanged100-selector execution limit and93 preceding exact selectors +- **WHEN** new path-link and legacy native controls are mapped +- **THEN** the same18 encoding observations SHALL be recorded in four nodes before aggregate assertions +- **AND** two path-link nodes SHALL record all three owned path components plus installed fallback in each grouping mode +- **AND** one legacy native node SHALL record all five suffix denials before its assertion +- **AND** no prior93 selector, analyzer pin, execution budget or proof limit SHALL be weakened diff --git a/openspec/changes/fix-categoryless-module-command-squatting/specs/module-grouping/spec.md b/openspec/changes/fix-categoryless-module-command-squatting/specs/module-grouping/spec.md new file mode 100644 index 000000000..2fad7da2b --- /dev/null +++ b/openspec/changes/fix-categoryless-module-command-squatting/specs/module-grouping/spec.md @@ -0,0 +1,51 @@ +## MODIFIED Requirements + +### Requirement: Registry groups modules by category when loading + +The registry SHALL read category metadata and group categorized modules. Missing-category project packages SHALL be +skipped in default grouped mode; other discovery sources and explicit legacy mode retain flat registration, except +permanent core commands reserved by the runtime-validation contract. + +#### Scenario: Registry collects category groups from installed modules + +- **GIVEN** `category_grouping_enabled` is `true` (default) +- **WHEN** the registry initialises and scans installed modules +- **THEN** it SHALL produce a `dict[str, list[ModulePackage]]` mapping each `bundle_group_command` to its member +modules +- **AND** SHALL treat `core` category modules as ungrouped top-level commands + +#### Scenario: Registry falls back to flat mounting when grouping disabled + +- **GIVEN** `category_grouping_enabled` is explicitly `false` +- **AND** a project module omits category and declares an unreserved root command +- **WHEN** commands are registered +- **THEN** the declared command SHALL retain flat registration +- **AND** categorized modules SHALL retain their current category registration + +#### Scenario: Module with missing category fields is handled gracefully + +- **GIVEN** a manifest omits category without an inferable legacy group +- **WHEN** the registry reads the manifest +- **THEN** it SHALL retain missing category without granting core authority +- **AND** SHALL warn without preventing startup +- **AND** command registration SHALL apply the discovery-source and grouping policy + +#### Scenario: Category-less workspace module cannot claim a root command + +- **GIVEN** default grouped mode and a discovered project module with no category +- **WHEN** package commands are registered +- **THEN** no declared root command SHALL be registered +- **AND** its loader SHALL NOT execute through those commands + +#### Scenario: Non-workspace legacy module remains supported + +- **GIVEN** a user, marketplace or custom package omits category and declares an unreserved command +- **WHEN** grouped-mode commands are registered +- **THEN** legacy flat registration SHALL remain available for that source + +#### Scenario: Rejected workspace package has no registration side effects + +- **GIVEN** a project package rejected by the root-command policy +- **WHEN** another eligible package lazily imports a Python dependency +- **THEN** the rejected package SHALL NOT enter active import roots or shadow that dependency +- **AND** SHALL NOT register schema extensions, service bridges or category mounts diff --git a/openspec/changes/fix-categoryless-module-command-squatting/tasks.md b/openspec/changes/fix-categoryless-module-command-squatting/tasks.md new file mode 100644 index 000000000..bb56d09c2 --- /dev/null +++ b/openspec/changes/fix-categoryless-module-command-squatting/tasks.md @@ -0,0 +1,218 @@ +# Tasks: fix-categoryless-module-command-squatting + +## TDD / SDD order (enforced) + +Specification deltas precede scenario-derived tests; tests must fail on vulnerable HEAD before production code changes. +Production code is implemented only after failing evidence is recorded. + +## 1. Worktree and readiness + +- [x] 1.1 Confirm work occurs on a non-protected branch in an attached git worktree. +- [x] 1.2 Verify the vulnerability against current registration code and check for overlapping active OpenSpec scope. +- [x] 1.3 Create and link public GitHub issue #718 under module-security feature #352 with required labels. +- [x] 1.4 Validate this change strictly before implementation. +- [x] 1.5 Verify current issue #718 project/Todo, parent #352, labels and empty native dependency lists; earlier +project-access block is resolved. +- [x] 1.6 Update `specfact-cli-internal/wiki/sources/fix-categoryless-module-command-squatting.md` and rebuild the +internal wiki graph when the sibling checkout is available. + +## 2. Specification and failing evidence + +- [x] 2.1 Add delta scenarios for fail-closed grouped mode and explicit legacy compatibility. +- [x] 2.2 Add focused registry tests derived from both scenarios. +- [x] 2.3 Run the focused tests against vulnerable HEAD and record failing-before evidence. + +## 3. Implementation + +- [x] 3.1 Skip unverified category-less workspace registration in grouped mode, preserving the authenticated + official-bundle exception (including specfact-project); reserve permanent core commands for resolved built-in paths + in both modes. +- [x] 3.2 Preserve categorized routing, non-project legacy sources and explicit unreserved flat registration. + +## 4. Verification and documentation review + +- [x] 4.1 Run focused registry and category-routing tests and record passing evidence. The reviewed runtime has + 3,430 local passes and native100 GREEN38687870204 at signed e4db7795; later documentation-head gates remain separate. +- [x] 4.2 Run formatting, type, lint, YAML, contract, smart-test, Semgrep, and Bandit gates required for the touched + scope. Final runtime evidence and unchanged-input contract reuse are recorded in TDD_EVIDENCE.md; new exact-head + hosted checks remain required. The 17 unchanged protected YAML diagnostics and launcher limitation remain disclosed. +- [ ] 4.3 Refresh `.specfact/code-review.json`, resolve all findings, and record the result. Runtime FULL72 has + 30 exact existing nonblocking findings and zero additions. This documentation correction needs its own exact-head + checks and completed review; the unresolved companion release-symlink annotation also prevents closure. +- [x] 4.4 Review `README.md`, `docs/`, `docs/index.md`, and sidebar navigation for impact; update only if documented + behavior changes. The runtime commit updates the affected module category/security references. +- [x] 4.5 Confirm no signed module assets or manifests changed; otherwise bump and re-sign affected modules and run + signature verification. The core artifact admission code changes no signed bundle assets or cryptographic format. +- [x] 4.6 Validate the OpenSpec change strictly after implementation; repeat after this documentation reconciliation. + +## 5. Release and pull request + +- [x] 5.1 Apply the required patch version bump across canonical version files and add a Security changelog entry. +- [x] 5.2 Commit with a Conventional Commit message and push the branch. Signed implementation e4db7795 is published. +- [x] 5.3 Create a pull request to `dev` using the repository template and link the OpenSpec change and GitHub issue. + +## Post-merge cleanup + +- [ ] Archive the completed change with `openspec archive fix-categoryless-module-command-squatting` after merge. +- [ ] Remove the implementation worktree and prune the local branch after merge. + +## 6. Current review corrections + +- [x] 6.1 Extend spec deltas before tests for official category roots and rejected-package import isolation. +- [x] 6.2 Capture actual failing tests against facb690 before implementation. +- [x] 6.3 Record human acceptance of mapping a9ba0dfd and corrected native authored RED (run 38176059675); final +e550f23e acceptance/proof remains tracked by 6.7. +- [ ] 6.4 Reject packages before all registration side effects and preserve effective built-in discovery. +- [ ] 6.5 Run focused/full/quality gates, restack, and verify final current-head review and Requirements. + +- [x] 6.6 Add executable reserved-flat-name controls and reconcile the ended spec shim requirement. +- [ ] 6.7 Obtain exact final mapping acceptance, regenerate authored/final proof after prerequisite #760, and validate +the final stack. + +- [ ] 6.8 Reproduce category-builder imports during eligibility and use the existing static category map before side +effects. + +## 7. Protected-base validation cycle after #761 + +- [x] 7.1 Refresh protected dev and capture all existing stack heads and the approved candidate. +- [x] 7.2 Preserve owner acceptance for exact mapping e550f23e; record earlier validation as historical. +- [ ] 7.3 Publish signed specification and authored tests before implementation; retain fresh native RED. +- [ ] 7.4 Restore the approved security correction, synchronize 0.55.8 artifacts, and verify final exact-head gates. +- [ ] 7.5 Restack both planning children, issue authorized signed-head grants and validate scoped-token proof reuse. +- [ ] 7.6 Resolve only validated findings and leave the final three PRs unmerged. + +## 8. Protected-base cycle after #767 + +- [x] 8.1 Verify protected merge c6af68a0 and retain previously green heads as historical evidence. +- [ ] 8.2 Publish signed specification and unchanged accepted tests; retain genuine current-base native RED. +- [ ] 8.3 Restore the accepted guard, synchronize 0.55.9 and validate all current-head required gates. +- [ ] 8.4 Restack both planning children and verify independent protected-context proof reuse. +- [ ] 8.5 Close only verified findings and leave all three target PRs unmerged. + +## 9. Protected-base cycle after #768 + +- [x] 9.1 Verify protected merge daefe0c7, preserve captured heads and exact E550 owner acceptance, + restate signed specification and keep all earlier-base evidence historical. +- [x] 9.2 Publish accepted controls, document their intent without logic changes, and retain fresh native RED. +- [ ] 9.3 Restore approved guards, synchronize available bugfix0.55.10, and pass all local/current-head gates. +- [ ] 9.4 Sequentially restack #743/#751 and verify independent protected-context proof reuse. +- [ ] 9.5 Resolve only validated findings and leave all three target PRs unmerged. + +## 10. Authenticated project bundle compatibility + +- [x] 10.1 Verify the supported project installer and all six official artifact signatures; capture reviewed f8ffe7c6. +- [x] 10.2 Specify strict authenticated project-root admission before adding controls or production changes. +- [x] 10.3 Author paired official-artifact and forgery controls; retain genuine regression and protected-base RED. +- [x] 10.4 Implement strict pinned-key admission, retain unsigned/core/import boundaries and pass local gates. +- [x] 10.5 Obtain exact amended mapping acceptance and publish signed authored/native RED before final production. +- [ ] 10.6 Validate final current-head execution, review, authority and required checks before resolving the finding. + +## 12. Admission proof and executable coverage review + +- [x] 12.1 Specify executable payload coverage before adding cache/bytecode controls. +- [ ] 12.2 Strengthen existing-loader and schema/bridge controls and map the public import-free admission probe. +- [ ] 12.3 Reproduce unsigned cache execution and protected-base failures before implementation. +- [ ] 12.4 Preserve authentic compiled-source caches while denying uncovered executable artifacts. +- [ ] 12.5 Obtain exact amended mapping acceptance; regenerate signed native RED/final proof and full review. + +- [ ] 12.6 Prove source-swap, omitted native-format and valid-app cache failures before snapshot implementation. +- [ ] 12.7 Authenticate captured source before standard compiler work and preserve all SAST rules. +- [ ] 12.8 Reverify the fix and independent findings on final test/source hashes before native publication. + +- [ ] 12.9 Reproduce same-name project shadow rejection hiding actual installed roots and dependents through public discovery. +- [ ] 12.10 Reproduce payload verification for disabled project roots with stored-state and explicit override controls. +- [ ] 12.11 Select first enabled authorized candidate before sorting; retain ordinary discovery and admitted dependency views. +- [ ] 12.12 Revalidate final exact mapping, full suite, native proof and completed review before declaring #719 merge-ready. + +- [ ] 12.13 Reproduce real discovery-to-verification manifest replacement for schema, bridge, command and version fields. +- [ ] 12.14 Bind whole normalized registration metadata to the same authenticated canonical manifest snapshot. +- [ ] 12.15 Revalidate exact amended mapping and all native/local/hosted gates before declaring root merge-ready. + +- [ ] 12.16 Reproduce executable addition/replacement after real signature verification but before admission finishes. +- [ ] 12.17 Enforce authenticated executable membership/content rather than signing-filter eligibility alone. + +- [ ] 12.18 Reproduce executable insertion/replacement during genuine cache reproduction and observe transient effects. +- [ ] 12.19 Reproduce caches before the final compiler-free executable inventory check; move final authority before effects. + +## Indirect category review follow-up + +- [ ] Reproduce bundle-induced mounts and existing-root replacement through public registration. +- [ ] Bind indirect category capabilities to authenticated project authority before effects. +- [x] Extend native-format controls for `.so` and `.pyd` and retain clear required fixture diagnostics. +- [ ] Revalidate full delivery, canonical mapping acceptance and exact-current native RED/GREEN/review. + +## Alternate-manifest review follow-up + +- [ ] Reproduce actual YAML parser escape through public registration in both grouping modes. +- [ ] Normalize parser failure at canonical payload serialization and verify denial/legitimate compatibility. +- [ ] Revalidate final source mapping, complete quality/security checks and exact native proof before publication. + +## Review follow-up: complete candidate eligibility + +- [x] Specify full local eligibility and coherent dependency selection before new tests. +- [ ] Reproduce actual compatible fallback and dependency-version failures through public registration, including genuine signatures. +- [ ] Compare candidate designs and implement the smallest coherent selection boundary without registration effects. +- [ ] Verify fresh exact test bytes, full gates, standalone verify-fix, then model-diverse interrogation. +- [ ] Recompute canonical mapping and request fresh owner acceptance only after concrete completed validation. + +## Additional review coverage controls — 14 October 2026 + +Three current-head review annotations identify test oracles, not a demonstrated new production vulnerability. +Add a separate control file without altering the five accepted75 authored files. Verify that noncanonical project +publisher metadata has a valid signature under the test bundled trust anchor before asserting denial, isolating +canonical identity from checksum rejection. Map paired consistent-cycle compatibility with an ineligible priority +copy, preserving the existing healthy-cycle positive suite control. Observe the actual warning when conflicting +priority selections are refused; inactive modules alone do not establish an explicit diagnostic. Specifications +already require canonical publisher identity, consistent cycles and explicit bounded conflict refusal. These +strengthened controls require fresh exact mapping acceptance and signed/native reauthoring before publication. +No production behavior, pin, threshold, timeout, signature format or global dependency policy is expanded. + +- [x] Validate valid-signature noncanonical publisher denial separately from checksum rejection. +- [x] Map consistent cycle compatibility paired with an ineligible priority override. +- [x] Validate the exact warning for contradictory priority refusal, including silent-warning mutation RED. +- [x] Record exact79 acceptance and verify signed native79 RED38628199251. +- [x] Verify final native79 GREEN38628713533/FULL72/all13 required integrations on the published head. + +## Extensionless and identity review follow-up — 14 October 2026 + +- [x] Specify executable mode/header inventory and genuine-signature field coverage before new tests. +- [x] Reproduce extensionless excluded executable authority on the signed published head without executing it. +- [x] Verify canonical compatibility and valid-crypto noncanonical tier/category/group denial in both modes. +- [x] Implement inventory admission and complete full validation, standalone verify-fix then diverse interrogation. +- [x] Record exact93 acceptance and verify signed native93 RED38635629429 before implementation restoration. +- [x] Verify published059c native93 GREEN38636359296/FULL72/all13 required integrations. + +## Individual execute-bit and Mach-O coverage — 14 October 2026 + +- [x] Specify separate coverage before tests, keeping all seven accepted93 files unchanged. +- [x] Prove each POSIX execute class and remaining six Mach-O encodings independently through public registration. +- [x] Complete exact-source full gates and standalone verification before diverse interrogation. +- [ ] Obtain fresh canonical mapping acceptance and signed native proof before publication. + +## Historical accepted100 selector consolidation + +- [x] Record native111 pre-JUnit limit failure and preserve authored2b2b/protected production. +- [x] Specify seven nodes covering all18 probe/mode observations before changing the eighth test file. +- [x] Verify all observations precede aggregate denial, isolated execute-bit/header mutations fail, protected100 RED and current100 GREEN. +- [x] Complete final full3420/FULL19score72/types0/1456 with unchanged inputs/graphs. +- [x] Complete final standalone verify-fix before diverse review on194bf bytes, no actionable findings. +- [x] Record actual exact4c3d mapping acceptance before signed/native100 publication. +- [x] Obtain signed native100 RED38647654283 with100 genuine assertion failures and all18 encoding observations before runtime restoration. +- [x] Verify published528b9338 native100 GREEN38648161680, all13 required integrations and current-head CodeRabbit5525712106. + +## Project path-link and legacy native follow-up + +- [x] Trace root and workspace-store links through actual discovery and retained loaders. +- [x] Capture genuine public failing controls for root and store links in both modes. +- [x] Correct the existing strict/project boundaries and preserve canonical workspace aliases. +- [x] Add explicit legacy coverage for all five omitted native suffixes. +- [x] Complete full gates and independent verification before a new exact canonical mapping review. +- [x] Obtain exact036 mapping acceptance and authentic signed native RED38687272103/GREEN38687870204. +- [ ] Complete the final documentation head checks/review and companion511 before merge readiness. + +- [x] Specify final path reobservation after authentication and inventory before adding its regressions. +- [x] Reproduce links introduced inside verification, then verify final root/store reobservation and fallback. + +- [x] Complete final3430 full tests, FULL72/30 with zero new findings, types, SAST, Bandit and exact-source installed-wheel matrix. +- [x] Complete standalone readonly verification before three diverse reviewers on final root/store reobservation. +- [x] Obtain exact036 path100 mapping acceptance before signed/native publication; actual djm81 record is retained. diff --git a/openspec/changes/fix-native-planning-lineage/CHANGE_VALIDATION.md b/openspec/changes/fix-native-planning-lineage/CHANGE_VALIDATION.md new file mode 100644 index 000000000..8ad6eca20 --- /dev/null +++ b/openspec/changes/fix-native-planning-lineage/CHANGE_VALIDATION.md @@ -0,0 +1,8 @@ +# Validation: native planning consumer lineage + +Issue #760 is open, parent #355, labeled bug/security/change-proposal and assigned to project/Todo. +Native blocker lists are empty. The dedicated worktree starts at protected c4ee5be8 after merged #761. +Scope is the reproduced consumer preflight defect, then protected integration and stack revalidation. +Exact mapping acceptance, native RED/GREEN, signatures, authority and required checks/review remain. +The private wiki is mirrored. #719 is green on f6481418; #743 is blocked; #751 is not restacked yet. +Keep all three unmerged. Rollback restores captured immutable heads. diff --git a/openspec/changes/fix-native-planning-lineage/TDD_EVIDENCE.md b/openspec/changes/fix-native-planning-lineage/TDD_EVIDENCE.md new file mode 100644 index 000000000..1a53637d0 --- /dev/null +++ b/openspec/changes/fix-native-planning-lineage/TDD_EVIDENCE.md @@ -0,0 +1,56 @@ +# Current phase: implementation after genuine native RED + +Date: 2026-10-12 (Europe/Berlin). Protected baseline c4ee5be8457f69848bf9bc2150ebc3df56b52d4f. +Actual #743 run 38295529623 passes producer planning and authority, then execution fails on +planning-parent-to-dev ancestry before any provider command runs. This is diagnostic evidence, +not selector RED. No tests, workflow or version inputs change in this specification head. +Exact owner acceptance and native authored proof remain required. + +## Authored controls after signed specification + +Signed specification 6e5c7c6f precedes tests. Actual workflow shell preflight in isolated real Git histories +produces two genuine assertion failures, zero errors/skips, 2.62 seconds. The earlier draft used an incorrect +job key and is discarded; native proof may retain only the corrected behavioral controls. Owner djm81 +accepted exact mapping 8f07b2b3 and authorized verified exact-head grants for #767. The genuine acceptance +record is preserved. No workflow or version edit precedes native authored RED; implementation remains pending. + +## Native RED before implementation + +Authored head 064040e0500ca9802ed4e5be4ed9a3f462f73d09 has genuine native run 38297691150: +passed verdict, gate pass, red maturity, failing-first-proven, two failures, no errors/skips/findings. +The exact accepted mapping and authored test bytes remain unchanged. Only after that report is retained +does the independent execution preflight validate protected-to-review and planning-to-candidate ancestry +separately. Ordinary trust and root/run/signature/authority, scope, control-object and plan/context equality +are untouched. Coherent 0.55.8 artifacts and all final-head gates remain required. + +## Local passing-after gates (2026-10-12, Europe/Berlin) + +Dedicated frozen CPython 3.12 environment uses HATCH_ENV_TYPE_VIRTUAL_PATH=.venv. Reviewed immutable module and +documentation fixtures are explicit. No module-root override is present during full tests. Forty-one focused +lineage/planning/native-stack/promotion controls pass in 10.31 seconds. The full smart suite passes as recorded in +the retained log; all 760 tracked source/test/script/tool/workflow/delivery inputs match before and after execution. + +Formatting, zero-error canonical typing, error-level lint, actionlint/workflow shell lint, repository YAML command, +direct clean mapping YAML lint, contracts, documentation, strict selected OpenSpec, lock/export parity, frozen +delivery, version sources and strict PyPI-ahead pass. Canonical typing retains 1,522 existing warnings. The +repository-wide YAML command retains diagnostics only for pre-existing mapping/archive formatting; the new mapping +has no findings. The locked dependency export payload is unchanged; only the root project version changes. + +All three frozen Python audits, dependency trust, licenses, strict four-manifest signature verification, independent +Semgrep and blocking Bandit gates pass. Runtime contracts, exploration and 22 scenario tests pass. No module payload +or manifest changes; no source, pin, exception, threshold or required gate is relaxed. + +Local SpecFact review completes with three unchanged promotion-fixture advisories: one generic naming warning and +two structure information findings. They match independently measured protected c4ee5be8 file-byte analysis; zero +new findings. Explicit baseline exceptions preserve those unrelated security test structures rather than expanding +this lineage correction. The authored lineage test review has zero findings. No rule or analyzer is suppressed. + +Genuine native authored RED run 38297691150/artifact 11769210296 and accepted mapping 8f07b2b3 remain bound to +authored source 064040e0. Selected test bytes and acceptance are unchanged. Local verification logs, actual review +and input-freeze artifacts are under /private/tmp/specfact-docs-stack-20261012. Final signed-head provenance, native +execution, authority, hosted checks and completed external review remain pending. The owner approved mapping/grants, +not merging; protected integration must precede target restacking. + +The preserved full-suite JUnit independently records 3,321 cases, zero failures/errors and ten skips; both new +controls pass. Coverage/timeout/nonzero-exit diagnostics in the console belong to intentionally passing negative +tests of the smart-test harness, not failed suite cases. No log, result or threshold is rewritten. diff --git a/openspec/changes/fix-native-planning-lineage/design.md b/openspec/changes/fix-native-planning-lineage/design.md new file mode 100644 index 000000000..bd00a102f --- /dev/null +++ b/openspec/changes/fix-native-planning-lineage/design.md @@ -0,0 +1,15 @@ +# Design: Two authenticated lineage relationships + +Protected context independently authenticates root identity, signatures, exact-root Requirements and +member authority, regular planning scope and current protected control objects. Its planning_base_commit +is an authenticated proposal boundary; proof_base_commit remains the validator trust boundary. + +Native stack github.base_ref is dev even when the REST child base is the runtime parent's branch. +Compare proof-base ancestry to the immutable review merge base, and separately compare evidence-base +ancestry to the candidate. Validate both identities. Without a planning override the evidence base is the +proof base, preserving ordinary validation. No check, provider pin, signature or scope control is disabled. + +Exercise the actual workflow shell preflight in isolated real Git histories. Controls cover ordinary and +native planning ancestry, unrelated trust, unrelated planning and evidence newer than the candidate. +Test bytes remain immutable from native RED through GREEN. Rollback restores the coherent workflow/version +patch and captured heads; children remain blocked until reviewed protected integration and revalidation. diff --git a/openspec/changes/fix-native-planning-lineage/proposal.md b/openspec/changes/fix-native-planning-lineage/proposal.md new file mode 100644 index 000000000..58309619d --- /dev/null +++ b/openspec/changes/fix-native-planning-lineage/proposal.md @@ -0,0 +1,41 @@ +# Change: Validate native planning lineage separately from trust + +## Why + +After merged #761, #743's producer authenticates inherited #719 proof and selects planned maturity. +Independent execution fails before reconciliation: native stack CI exposes dev as its review base, +while the workflow requires the newer planning parent to be an ancestor of that protected review base. +Actual run 38295529623 reproduces the failure. Both bases are valid but serve different purposes. + +## What Changes + +- Preserve the authenticated protected proof base as verifier and review trust authority. +- Require that proof base to be an ancestor of the immutable review merge base. +- Require the authenticated planning/evidence parent to be an ancestor of the candidate source. +- Preserve the ordinary lane, exact-root check/run/authority authentication, scope whitelist, + object equality, plan/context equality, independent execution and every protected gate. +- Add actual shell/Git controls for valid native planning ancestry and forged trust/candidate lineage. + +## Capabilities + +### New Capabilities + +- requirements-native-stack-lineage: independent consumer lineage validation for native planning. + +### Modified Capabilities + +None. Provider semantics and inherited-proof eligibility are unchanged. + +## Impact + +Only the independent execution preflight, focused controls, evidence and coherent bugfix metadata. +Reviewed protected integration is required before planning children consume changed workflow objects. +No runtime module or deferred roadmap implementation. + +## Source Tracking + + +- Existing issue #760; parent #355; SpecFact CLI/Todo; bug, security and change-proposal labels. +- Native dependency lists are empty. Prerequisite #761 is merged at protected c4ee5be8. +- Owner requests continued validation and forbids weakening security/stability gates. +- New exact mapping acceptance, authorized signed-head grants and protected merge remain required. diff --git a/openspec/changes/fix-native-planning-lineage/requirements-evidence.yaml b/openspec/changes/fix-native-planning-lineage/requirements-evidence.yaml new file mode 100644 index 000000000..67d06123a --- /dev/null +++ b/openspec/changes/fix-native-planning-lineage/requirements-evidence.yaml @@ -0,0 +1,37 @@ +schema_version: '2' +requirements: + openspec:fix-native-planning-lineage:requirements-native-stack-lineage:preserve-native-planning-parent-lineage: + rationale: Native planning and ordinary protected ancestry reach reconciliation without shifting verifier trust. + stakeholder_refs: + - nold-ai/specfact-cli#760 + touchpoints: + - id: independent-lineage + kind: config_file + locator: .github/workflows/requirements-evidence.yml + verification_cases: + - case_id: NATIVE-LINEAGE + scenario_id: preserve-native-planning-parent-lineage + method: test + intent: Execute actual workflow shell preflight against protected, runtime and planning Git ancestry. + observable: Native planning and ordinary protected ancestry reach reconciliation without shifting verifier + trust. + selector: + runner: pytest + node_id: tests/unit/security/test_native_planning_lineage.py::test_native_planning_parent_preserves_review_trust + openspec:fix-native-planning-lineage:requirements-native-stack-lineage:reject-independent-lineage-forgery: + rationale: The real consumer preflight rejects every invalid boundary before provider execution. + stakeholder_refs: + - nold-ai/specfact-cli#760 + touchpoints: + - id: independent-lineage + kind: config_file + locator: .github/workflows/requirements-evidence.yml + verification_cases: + - case_id: NATIVE-FORGERY + scenario_id: reject-independent-lineage-forgery + method: test + intent: Exercise unrelated trust, unrelated planning and candidate-newer evidence in actual Git histories. + observable: The real consumer preflight rejects every invalid boundary before provider execution. + selector: + runner: pytest + node_id: tests/unit/security/test_native_planning_lineage.py::test_independent_lineage_rejects_forged_trust_or_parent diff --git a/openspec/changes/fix-native-planning-lineage/requirements-proof/review-evidence.json b/openspec/changes/fix-native-planning-lineage/requirements-proof/review-evidence.json new file mode 100644 index 000000000..176eb66c3 --- /dev/null +++ b/openspec/changes/fix-native-planning-lineage/requirements-proof/review-evidence.json @@ -0,0 +1,8 @@ +{ + "decision": "accepted", + "reviewer_id": "djm81", + "reviewer_role": "repository-owner", + "recorded_at": "2026-10-12T19:25:21.516956+00:00", + "reference": "https://github.com/nold-ai/specfact-cli/pull/767", + "mapping_digest": "sha256:8f07b2b300326ff199daed499240b6875d947c6431518ececf52fec3b3a160a0" +} diff --git a/openspec/changes/fix-native-planning-lineage/specs/requirements-native-stack-lineage/spec.md b/openspec/changes/fix-native-planning-lineage/specs/requirements-native-stack-lineage/spec.md new file mode 100644 index 000000000..0c734f82a --- /dev/null +++ b/openspec/changes/fix-native-planning-lineage/specs/requirements-native-stack-lineage/spec.md @@ -0,0 +1,28 @@ +## ADDED Requirements + +### Requirement: Preserve native planning parent lineage + +Independent execution SHALL distinguish protected verifier trust from authenticated planning selection. +The protected proof base SHALL be an ancestor of the immutable review merge base. The planning/evidence +base SHALL be an ancestor of the candidate source. Both commit identities SHALL be valid. Native review +projection SHALL NOT replace authenticated planning selection or protected validator authority. + +#### Scenario: Planning parent is newer than the protected review base + +- **GIVEN** authenticated protected dev, a verified security parent and an eligible planning child +- **WHEN** native CI exposes dev as the review base +- **THEN** independent execution SHALL admit valid planning ancestry before ordinary reconciliation +- **AND** ordinary protected-base validation SHALL remain available +- **AND** plan/context equality and inherited proof/authority authentication SHALL remain required + +### Requirement: Reject independent lineage forgery + +Independent execution SHALL reject unrelated protected trust, unrelated planning ancestry or evidence +newer than the candidate. Planning selection SHALL NOT substitute for protected review trust. + +#### Scenario: Valid planning ancestry cannot conceal an unrelated trust base + +- **GIVEN** valid planning ancestry but an unrelated protected proof commit +- **WHEN** independent execution validates its immutable lineage +- **THEN** execution SHALL fail before evidence reconciliation +- **AND** unrelated or candidate-newer planning ancestry SHALL also be rejected diff --git a/openspec/changes/fix-native-planning-lineage/tasks.md b/openspec/changes/fix-native-planning-lineage/tasks.md new file mode 100644 index 000000000..950668844 --- /dev/null +++ b/openspec/changes/fix-native-planning-lineage/tasks.md @@ -0,0 +1,23 @@ +## 1. Scope and specification + +- [x] 1.1 Verify native failure, issue metadata, protected baseline and dedicated worktree. +- [x] 1.2 Specify separate review-trust and candidate-planning ancestry; mirror private dependencies. +- [x] 1.3 Validate strictly and commit signed specification before tests. + +## 2. Authored proof + +- [x] 2.1 Add real shell/Git controls and reproduce both exact selectors failing before changes. +- [x] 2.2 Obtain exact owner mapping acceptance and prerequisite signed-head grant authorization. +- [x] 2.3 Publish genuine native authored RED with unchanged test bytes. + +## 3. Implementation and validation + +- [x] 3.1 Correct independent lineage validation and synchronize available patch metadata. +- [x] 3.2 Run focused/full, frozen-delivery, security, quality, review and signature gates. +- [ ] 3.3 Publish signed final head; validate actual native proof, authority and completed review. +- [ ] 3.4 Obtain owner protected integration authorization after required checks pass. + +## 4. Stack and cleanup + +- [ ] 4.1 Restack/reprove the three target PRs after protected integration; leave them unmerged. +- [ ] 4.2 Archive via OpenSpec CLI after merge, maintain private wiki and preserve rollback evidence. diff --git a/openspec/changes/fix-planning-evidence-multichange/TDD_EVIDENCE.md b/openspec/changes/fix-planning-evidence-multichange/TDD_EVIDENCE.md new file mode 100644 index 000000000..13f808f3c --- /dev/null +++ b/openspec/changes/fix-planning-evidence-multichange/TDD_EVIDENCE.md @@ -0,0 +1,15 @@ +# Regression evidence + +On 2026-09-21 (Europe/Berlin), before workflow edits: + +`hatch run pytest tests/unit/workflows/test_requirements_planning_scope.py -q --no-cov` + +Result: **2 failed, 1 passed**. Fresh planning selection incorrectly selected an implementation review path; the producer also added that record. The implementation single-change selection and multiple-change rejection passed. These tests execute the workflow's shell selection blocks, not a reimplementation of the selection logic. + +After the five-line workflow correction, the new tests and existing Requirements workflow suite pass: **29 passed** (`hatch run pytest tests/unit/workflows/test_requirements_planning_scope.py tests/unit/workflows/test_requirements_evidence_delivery_workflow.py -q --no-cov`). + +The first CI run (35569263871) reproduced both failures but rejected the proof because the unchanged implementation-preservation test passed. Keep that test in the ordinary regression suite and select only the two genuinely failing regression tests for the existing failing-before proof contract. No failure is manufactured and no runtime code changes in this correction. + +The corrected test-only commit `3e9a4ada2eed2a6a60427c3d5e517f33c5c92c79` produced the accepted RED artifact in run [35569603411](https://github.com/nold-ai/specfact-cli/actions/runs/35569603411): `gate_decision=pass`, `observed_maturity=red`, no findings. The overall failing-test run remains failed as expected; this is not a claim that its full pipeline passed. + +Independent implementation review accepted the five-line guard change with no blocking findings. Reproducible-delivery validation and `uv lock --check` passed; BasedPyright in the worktree Hatch environment reported zero errors (existing repository warnings remain). The initial type invocation outside that environment could not resolve the project dependencies and is not used as passing evidence. Final remote verification is pending. diff --git a/openspec/changes/fix-planning-evidence-multichange/design.md b/openspec/changes/fix-planning-evidence-multichange/design.md new file mode 100644 index 000000000..ddd70d46d --- /dev/null +++ b/openspec/changes/fix-planning-evidence-multichange/design.md @@ -0,0 +1,7 @@ +# Design + +Retain the existing path-based maturity classifier. Guard implementation review-record selection with `planning_maturity != planned`. Producer, fresh consumer, and final verifier must agree so optional or unrelated approval records cannot alter planning reports or plan identity. + +The existing planning validator remains authoritative for completeness; independently recomputed plans must still match. Above planned maturity, retain the one-active-change restriction and existing approval, test, provenance, and final verification. Promotion reuse keeps its independent trusted verification and must not fall through to ordinary planning success. + +Regression tests execute the actual shell selection blocks extracted from workflow YAML with planning and implementation scopes. They cover multiple changes, a single change without approval, unrelated approval records, implementation rejection, and retained implementation selection. Existing workflow tests protect promotion and artifact trust. No new runtime helper or public interface is introduced. diff --git a/openspec/changes/fix-planning-evidence-multichange/proposal.md b/openspec/changes/fix-planning-evidence-multichange/proposal.md new file mode 100644 index 000000000..545ce4764 --- /dev/null +++ b/openspec/changes/fix-planning-evidence-multichange/proposal.md @@ -0,0 +1,30 @@ +# Change: Honor planning maturity across Requirements workflow stages + +## Why + +The producer supports planning-only evidence, but later verification rejects multiple active changes before reaching the planned branch. This blocks coordinated specification maintenance such as PR #741. + +## What Changes + +- Use implementation review-evidence selection only above planned maturity in the producer, fresh consumer, and final verifier. +- Continue validating all selected planning sources and comparing independently regenerated plans. +- Preserve implementation scope restrictions, execution/provenance checks, trusted authority, and promotion verification. + +## Capabilities + +### New Capabilities + +- `requirements-planning-workflow`: consistent planning-only review selection in all existing workflow stages. + +## Impact + +Only the Requirements workflow and its regression tests change. This is a prerequisite bug fix for planning integration, not R09 activation or a new approval mechanism. No CLI/API, module payload, or dependency changes. Contributor-facing behavior is documented by the scoped specification; README, published guides, and navigation remain accurate. Rollback is a revert of the workflow change, restoring the current planning restriction. + +## Source Tracking + +- **Repository**: nold-ai/specfact-cli +- **GitHub Issue**: #745 +- **Issue URL**: +- **Last Synced Status**: open +- **Parent Feature**: #366 +- **Dependencies**: none; unblocks integration of #740 / PR #741. diff --git a/openspec/changes/fix-planning-evidence-multichange/requirements-evidence.yaml b/openspec/changes/fix-planning-evidence-multichange/requirements-evidence.yaml new file mode 100644 index 000000000..1226e20c2 --- /dev/null +++ b/openspec/changes/fix-planning-evidence-multichange/requirements-evidence.yaml @@ -0,0 +1,25 @@ +schema_version: "2" +requirements: + openspec:fix-planning-evidence-multichange:requirements-planning-workflow:planning-only-review-evidence-selection: + rationale: Planning validation must not require implementation approval while implementation restrictions remain enforced. + touchpoints: + - id: requirements-workflow + kind: config_file + locator: .github/workflows/requirements-evidence.yml + verification_cases: + - case_id: planning-consumer-selection + scenario_id: planning-only-review-evidence-selection + method: test + intent: Execute fresh and final workflow selection for zero, one, and multiple planning changes with an unrelated approval present. + observable: Planning selection succeeds without any implementation review path. + selector: + runner: pytest + node_id: tests/unit/workflows/test_requirements_planning_scope.py::test_planned_changes_do_not_select_implementation_reviews + - case_id: planning-producer-selection + scenario_id: planning-only-review-evidence-selection + method: test + intent: Execute producer selection with a review record present at planned and test-authored maturity. + observable: Only implementation evidence receives the approval record. + selector: + runner: pytest + node_id: tests/unit/workflows/test_requirements_planning_scope.py::test_producer_keeps_implementation_approval_out_of_planning diff --git a/openspec/changes/fix-planning-evidence-multichange/requirements-proof/review-evidence.json b/openspec/changes/fix-planning-evidence-multichange/requirements-proof/review-evidence.json new file mode 100644 index 000000000..f116d9359 --- /dev/null +++ b/openspec/changes/fix-planning-evidence-multichange/requirements-proof/review-evidence.json @@ -0,0 +1,9 @@ +{ + "schema_version": "1", + "decision": "accepted", + "reviewer_id": "Codex:/root/core_pr_review", + "reviewer_role": "independent AI plan reviewer", + "recorded_at": "2026-09-21T06:39:59Z", + "reference": "https://github.com/nold-ai/specfact-cli/issues/745#issuecomment-5756417918", + "mapping_digest": "sha256:5f8b8cfa8805717beaca84fb17f0bc9602a2149b6f6bdc0742ad84f308a738c6" +} diff --git a/openspec/changes/fix-planning-evidence-multichange/specs/requirements-planning-workflow/spec.md b/openspec/changes/fix-planning-evidence-multichange/specs/requirements-planning-workflow/spec.md new file mode 100644 index 000000000..ed742225b --- /dev/null +++ b/openspec/changes/fix-planning-evidence-multichange/specs/requirements-planning-workflow/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: Planning-only review evidence selection + +The Requirements workflow SHALL validate planning-only sources without selecting implementation approval records. Producer, fresh consumer, and final verifier SHALL preserve source completeness and independently regenerated plan comparison. This boundary SHALL NOT weaken implementation scope, execution, provenance, trusted authority, or promotion verification. + +#### Scenario: Multiple planning changes + +- **GIVEN** only planning artifacts change across multiple active OpenSpec changes +- **WHEN** the producer, fresh consumer, and final verifier select evidence +- **THEN** implementation review records are not selected and the existing planned validator decides every source's completeness + +#### Scenario: Single planning change and unrelated approval + +- **GIVEN** a planning-only change has no implementation approval and an unrelated active approval exists +- **WHEN** workflow stages select evidence +- **THEN** neither a missing approval nor the unrelated approval affects planning validation + +#### Scenario: Implementation restrictions remain + +- **GIVEN** changed paths require implementation evidence +- **WHEN** workflow stages select review evidence +- **THEN** the existing implementation selection is retained and multiple active change scopes remain rejected by the fresh and final verifiers diff --git a/openspec/changes/fix-planning-evidence-multichange/tasks.md b/openspec/changes/fix-planning-evidence-multichange/tasks.md new file mode 100644 index 000000000..806c15e39 --- /dev/null +++ b/openspec/changes/fix-planning-evidence-multichange/tasks.md @@ -0,0 +1,25 @@ +# Tasks + +Specs precede tests; capture failing tests before changing the workflow. + +## 1. Prepare + +- [x] 1.1 Create an isolated worktree from origin/dev and initialize its environment. +- [x] 1.2 Confirm issue #745, parent #366, project assignment, and no prerequisite blockers. +- [x] 1.3 Define the planning-only boundary and retain implementation/promotion restrictions. + +## 2. Reproduce and fix + +- [x] 2.1 Add behavioral tests of actual workflow shell selection and record failing evidence. +- [x] 2.2 Guard implementation review selection above planned maturity across all stages. +- [x] 2.3 Run affected workflow tests, lint, and repository-required checks; record results. + +## 3. Review and deliver + +- [x] 3.1 Review trust boundaries and documentation impact; synchronize the internal wiki summary. +- [x] 3.2 Commit signed changes, push, and open a prerequisite PR to dev linked to #745. +- [ ] 3.3 After authorized merge, refresh dependent planning PR #741 and verify remote gates. + +## Post-merge + +- [ ] Archive with `openspec archive fix-planning-evidence-multichange` after delivery, then remove the isolated worktree. diff --git a/openspec/changes/fix-planning-review-proof-context/TDD_EVIDENCE.md b/openspec/changes/fix-planning-review-proof-context/TDD_EVIDENCE.md new file mode 100644 index 000000000..5cbe3bce3 --- /dev/null +++ b/openspec/changes/fix-planning-review-proof-context/TDD_EVIDENCE.md @@ -0,0 +1,118 @@ +# Finalized inherited review context evidence + +Date: 2026-10-13 (Europe/Berlin). Protected base c6af68a0 after reviewed #767. Signed specification +34620aa7 precedes authored controls. Canonical two-selector mapping a0ec5078 was accepted by owner djm81. + +## Genuine failing-before evidence + +Actual #743 run 38303860784 passes authenticated planned reconciliation, then the immutable Code Review +provider rejects its planning report: finalized proof requires execution_proof.run_stage=final. +The final clean authored control bytes reproduce two genuine failures in 3.60 seconds, zero errors/skips, +against protected behavior. The ordinary genuine-final positive control passes. Fixtures are explicitly +synthetic Git/service boundary inputs and are never published as actual passing execution proof. + +## Initial local passing candidate + +Both actual independent/final workflow review shells now pass with the frozen provider final-context +reader. They preserve full inherited protected-dev Python paths, full enforcement and unchanged genuine +root proof bytes, leaving planning receipts unchanged. Producer-only, foreign/incomplete proof, foreign +artifact repository, expiry, corruption, ambiguity, oversized metadata, stale root checks and changed live +native head all fail closed. Ordinary finalized context remains accepted. Source helper bytes come only +from protected origin/dev; live native/event resolution and root checks are repeated before retrieval. + +Full smart suite: 3313 passed, ten skips, two existing dependency warnings, 197.71 seconds. Raw JUnit +contains 3323 cases with zero failures/errors. All 787 tracked source/test/script/workflow/tool/delivery +inputs match before/after execution. Expected failure/timeout/coverage console diagnostics originate in +passing negative smart-test harness controls, not failing cases. + +Format/lint, zero-error canonical typing (1522 existing warnings), YAML, strict selected OpenSpec, +documentation, contracts/scenarios, three frozen audits, licenses, trust, Bandit/Semgrep and four strict +manifest signatures pass. No signed module asset changed. Local SpecFact helper/test review has zero +findings after resolving all new complexity/shape/type advisories; no rule or threshold is suppressed. +Frozen delivery, coherent 0.55.9 source/lock metadata and exact CI export payload parity pass. Strict +PyPI-ahead confirms 0.55.9 > 0.55.4. Proof-bound provider pins and all review/authority gates are retained. +Repository YAML command retains existing mapping/archive style diagnostics; its successful exit is not +claimed as zero formatting diagnostics. Newly authored mapping validates at planned maturity with no findings. + +## Pending required publication + +Owner acceptance/grants and genuine native RED are retained; unchanged-test signed implementation, exact-head +provenance/final execution, required CI and completed +review remain required. Local results do not substitute for those gates. Separate owner merge authorization +is still required after final hosted validation. Target #719/#743/#751 remain unmerged. After protected +integration, reprove root on the new base and validate both planning consumers before final readiness. + +Full touched-Python review independently retains three unchanged promotion-fixture advisories +(one naming warning and two fixture-shape information findings), exactly matching protected c6af68a0. +Explicit exception: these pre-existing fixtures are outside the correction, and changing them would +expand accepted proof/control scope. No new finding or rule suppression is accepted. + +## Final compatibility-preserving candidate + +Primary frozen provider lifecycle explicitly supports `passing-after-legacy-tdd-ledger`. Signed +compatibility specification precedes a real one-failure/one-pass control: ordinary approved legacy +context passes the immutable provider, while the transport-only RED field restriction rejects it. +The correction removes that invented restriction and retains the provider's complete proof/basis +validation, authentic exact root, artifact integrity, full inherited review and unchanged planning receipts. + +Final clean authored bytes again reproduce two genuine failures against protected workflow behavior +with zero errors/skips before restoration. Both complete actual workflow/provider controls pass, including +ordinary and inherited RED/approved legacy contexts and all forgery cases. New helper/test review has +zero findings; canonical types still have zero errors and the same 1522 existing warnings. + +The full suite was repeated after this behavior correction: 3313 passed, ten skips, two existing warnings, +198.52 seconds. Raw JUnit has zero failures/errors; all 787 inputs are unchanged. Earlier local test SHA256 +ee736256f43eaf546bb93c763bdd8f5d4540e20a1bcb1cb75b3fe44d8dd2109c (historical launcher). Final native proof and +protected integration remain pending. + +## Corrected genuine native authored RED + +Native run 38313685016 is superseded and is not accepted as behavioral proof: its isolated launcher +failed the ordinary trusted-core positive control. The correction explicitly loads the trusted core +path as the actual workflow does, retaining the accepted mapping and unchanged owner record. + +Signed authored head `908bc3ad40f318e58e3596960be05335058f440e`, native run `38314212556`, +artifact `11776331094` reproduce the two finalized-proof rejections after ordinary context passes, +with zero errors/skips/findings and red/pass reconciliation. Artifact service SHA256 +`b2c979cbe3a149b98859459050c50bcbaca96c52e8837a95a8b20c8edab980da` and JUnit SHA256 +`975cd18b5c9303c01f51d4bde8c562849132975d6f6310a9aa183e468121b8ea` verify. +Final authored test SHA256 `0af9e8a14160498124e115e79ff0426291d32ee8eb5e54311b2101dea88318fd` +remains unchanged through implementation. Signed head grant comment 6133420933 records genuine djm81 authority. +Full local validation is repeated after the launcher correction. Final native execution, required CI, +completed current-head review and separate merge approval remain required. #719/#743/#751 remain unmerged. + +## Final complete local verification (2026-10-13) + +The first final full run retained one direct-smoke180s timeout (3312 other cases passed). Unchanged +targeted smoke passes24.29s. The complete suite is repeated with the same frozen inputs and unmodified +timeout:3313 passed, ten skips, zero failures/errors in 198.43s. All787 inputs match before/after. +Both actual consumer/provider controls and unchanged runtime smoke pass in that complete run. +No test scope, timeout, package pin, authority, review threshold or execution requirement is reduced. + +Final local lint/strict OpenSpec/docs, frozen delivery/version/strict PyPI-ahead, three strict frozen +audits, dependency trust, licenses and Bandit pass. Canonical typing has zero errors and the unchanged +1522 baseline warnings. Final helper/test SpecFact review has zero findings; the explicitly documented +three unrelated promotion-fixture advisories match the protected baseline. Actual current root57f +final artifact11773232592 is accepted byte-for-byte with its genuine service digest by a separate local +compatibility probe; this does not replace required native final execution or separate merge approval. + +## Review warning correction (2026-10-13) + +CodeRabbit completed review of signed head364dd781 with no new actionable comments or unresolved threads. +Its pre-merge docstring check still reported55.56%, below the unchanged80% requirement. The ten undocumented +new helper functions now describe their validation boundaries; all13 helper functions have docstrings. +Executable AST excluding docstrings is unchanged. Canonical mapping A0EC and authored control SHA256 +0af9e8a14160498124e115e79ff0426291d32ee8eb5e54311b2101dea88318fd remain unchanged. + +The complete suite passes again:3314 passed, nine skips, zero failures/errors in197.05s. +The version-matches-HEAD control previously skipped during the version bump now executes and passes. +All787 tracked inputs match the validated candidate snapshot. Format/lint/YAML/contracts and frozen +local helper/test review pass with zero new findings. Canonical typing has zero errors and the same1522 +protected-baseline warnings. Current-head native execution, authority, required CI and completed review +must pass again after signed publication; separate merge authorization remains required. + +Actual root artifact11773232592 from run38306990823 verifies its genuine service archive digest and passes +both the transport and immutable provider final-context reader, preserving source57f51829 and report bytes. +This local compatibility probe does not substitute for authenticated native child execution. Fixed argv +with a literal executable/hostname and internal repository/numeric routes refutes the automated shell +injection warning; no shell command is constructed and independent security gates remain required. diff --git a/openspec/changes/fix-planning-review-proof-context/design.md b/openspec/changes/fix-planning-review-proof-context/design.md new file mode 100644 index 000000000..d84e7295f --- /dev/null +++ b/openspec/changes/fix-planning-review-proof-context/design.md @@ -0,0 +1,24 @@ +# Design: Keep planning receipts separate from execution proof + +The current root/child helper authenticates regular closed planning scope, signed exact root, +current protected workflow objects and successful root Requirements/authority runs. Repeat that +validation before retrieving review context, matching the previously authenticated context exactly. +Use the resulting Requirements run id to select one non-expired requirements-evidence artifact. +Validate its workflow repository ids, root head/branch/run identity and service digest. Read only one +bounded regular requirements-evidence.json member without extracting archive files. Reject malformed, +ambiguous, foreign, expired or corrupted archives. The report must identify the exact root and retain +schema 2, final stage, verified maturity, the existing provider-supported RED or approved legacy TDD basis, complete plans/selectors and no findings; +the immutable provider still performs its full finalized-proof validation before Code Review. + +Both independent and final consumers use that unchanged root report only for inherited Python review. +They retain their own independently computed planned receipt/plan and protected-dev review diff. +Ordinary consumers keep their existing final report. The immutable provider retains its existing approved legacy-ledger support; the transport helper must not add a RED-only field restriction. No synthetic JUnit, stamped final stage, reduced +review scope, weakened enforcement or candidate-owned helper is permitted. The helper is materialized +only from protected origin/dev beside the protected native context readers. + +Tests execute the actual workflow review shells in real isolated Git histories, with exact root artifact +metadata/ZIP responses and the actual frozen provider final-context reader. Positive controls establish +ordinary and native acceptance and retained inherited review paths. Negative controls retain planning +bytes and reject artifact/proof forgery before successful review. Test bytes stay unchanged after RED. +Rollback restores coherent workflow/helper/version inputs and captured branch heads; changed ancestry +requires fresh proof. Scope excludes module runtime, unrelated refactors and archive maintenance. diff --git a/openspec/changes/fix-planning-review-proof-context/proposal.md b/openspec/changes/fix-planning-review-proof-context/proposal.md new file mode 100644 index 000000000..7fb962cef --- /dev/null +++ b/openspec/changes/fix-planning-review-proof-context/proposal.md @@ -0,0 +1,41 @@ +# Change: Preserve finalized proof context for inherited Code Review + +## Why + +After protected #767, actual #743 run 38303860784 authenticates the root and passes planned +reconciliation, then Code Review correctly rejects the planning report because it is not a complete +finalized execution proof. Native CI reviews inherited Python changes against dev, so that review +must consume the authentic root execution proof rather than the child planning receipt. + +## What Changes + +- Independently reauthenticate native planning context and download the exact root Requirements artifact. +- Verify artifact repository, run, branch, head, name, expiry, digest and bounded archive/report identity. +- Require verified finalized root evidence; preserve its bytes and the separate child planning receipt. +- Retain full inherited Python review against protected dev in both independent and final consumers. +- Reject stale, forged, ambiguous, malformed or producer/planning context; preserve ordinary review. +- Keep provider pins, review enforcement, scope whitelist, signatures and every required check. + +## Capabilities + +### New Capabilities + +- requirements-planning-review-context: authentic root final proof for inherited review. + +### Modified Capabilities + +None. No provider semantics, policy thresholds or deferred runtime delivery change. + +## Impact + +Protected workflow review-context selection and a stdlib-only artifact reader, focused actual workflow +controls, evidence and coherent bugfix metadata. Integration on dev is required before planning +children consume the protected helper. Target PRs #719/#743/#751 remain unmerged. + +## Source Tracking + + +- Existing issue #760, parent #355, SpecFact CLI/Todo; bug/security/change-proposal; no native dependencies. +- #761 and #767 are merged; this fixes the next independently observed consumer boundary. +- Owner authorized stability/security protections and forbids convenient gate weakening. +- Exact mapping acceptance, verified-head grants and separately authorized protected merge remain required. diff --git a/openspec/changes/fix-planning-review-proof-context/requirements-evidence.yaml b/openspec/changes/fix-planning-review-proof-context/requirements-evidence.yaml new file mode 100644 index 000000000..2c6dc2d9a --- /dev/null +++ b/openspec/changes/fix-planning-review-proof-context/requirements-evidence.yaml @@ -0,0 +1,36 @@ +schema_version: '2' +requirements: + openspec:fix-planning-review-proof-context:requirements-planning-review-context:preserve-finalized-inherited-review-context: + rationale: Real workflow consumers retain inherited Python review and genuine finalized root proof. + stakeholder_refs: + - nold-ai/specfact-cli#760 + touchpoints: + - id: inherited-review + kind: config_file + locator: .github/workflows/requirements-evidence.yml + verification_cases: + - case_id: PLANNING-REVIEW-FINAL + scenario_id: preserve-finalized-inherited-review-context + method: test + intent: Execute both actual review shells with ordinary and native proof context. + observable: Genuine root final proof reaches the immutable provider and full inherited scope remains. + selector: + runner: pytest + node_id: tests/unit/security/test_planning_review_context.py::test_actual_consumers_preserve_final_root_context + openspec:fix-planning-review-proof-context:requirements-planning-review-context:reject-inherited-review-context-forgery: + rationale: Forged artifact context fails closed while genuine final proof and separate planning receipts survive. + stakeholder_refs: + - nold-ai/specfact-cli#760 + touchpoints: + - id: inherited-review + kind: config_file + locator: .github/workflows/requirements-evidence.yml + verification_cases: + - case_id: PLANNING-REVIEW-FORGERY + scenario_id: reject-inherited-review-context-forgery + method: test + intent: Exercise actual workflow with foreign, stale, ambiguous, corrupted and non-final root proof. + observable: Invalid inherited proof never authorizes review; valid exact-root final proof remains accepted. + selector: + runner: pytest + node_id: tests/unit/security/test_planning_review_context.py::test_actual_consumers_reject_context_forgery diff --git a/openspec/changes/fix-planning-review-proof-context/requirements-proof/review-evidence.json b/openspec/changes/fix-planning-review-proof-context/requirements-proof/review-evidence.json new file mode 100644 index 000000000..9a828b1c0 --- /dev/null +++ b/openspec/changes/fix-planning-review-proof-context/requirements-proof/review-evidence.json @@ -0,0 +1,8 @@ +{ + "decision": "accepted", + "reviewer_id": "djm81", + "reviewer_role": "repository-owner", + "recorded_at": "2026-10-12T21:55:31.111494+00:00", + "reference": "https://github.com/nold-ai/specfact-cli/pull/768", + "mapping_digest": "sha256:a0ec5078fe769838f653c4069b0cd5752b46a5d8038fcbb132564d38f7a85c5e" +} diff --git a/openspec/changes/fix-planning-review-proof-context/specs/requirements-planning-review-context/spec.md b/openspec/changes/fix-planning-review-proof-context/specs/requirements-planning-review-context/spec.md new file mode 100644 index 000000000..206597e1c --- /dev/null +++ b/openspec/changes/fix-planning-review-proof-context/specs/requirements-planning-review-context/spec.md @@ -0,0 +1,30 @@ +## ADDED Requirements + +### Requirement: Preserve finalized inherited review context + +Independent and final Code Review consumers SHALL retain protected-dev Python review scope and full +enforcement. For authenticated planning children they SHALL use unchanged finalized execution proof +from the exact verified root Requirements run. Child planned evidence SHALL remain separate and SHALL +NOT be relabeled as execution. Ordinary final proof review SHALL remain available. Existing provider-supported RED and approved legacy +TDD proof bases SHALL retain the same complete-proof validation and authority gates. + +#### Scenario: Native planning reviews inherited runtime changes + +- **GIVEN** a signed verified root with genuine final proof and a closed planning child +- **WHEN** independent or final Code Review sees inherited Python changes against protected dev +- **THEN** the immutable provider SHALL receive the authentic root finalized context +- **AND** the child planning receipt and inherited review paths SHALL remain unchanged + +### Requirement: Reject inherited review context forgery + +Each consumer SHALL independently reauthenticate the live native context and uniquely bind the artifact +to the successful root Requirements run, repository, branch and head. It SHALL verify expiry, archive +digest, bounded regular report identity, finalized stage and exact-root proof. Missing, stale, foreign, +ambiguous, corrupted, producer-only or planning-only evidence SHALL fail closed without fabricated proof. + +#### Scenario: A planning receipt cannot authorize inherited Python review + +- **GIVEN** an invalid artifact or report and a valid planning child +- **WHEN** a consumer prepares finalized review context +- **THEN** review SHALL fail before accepting that context +- **AND** valid genuine root proof SHALL still pass with all normal review gates enforced diff --git a/openspec/changes/fix-planning-review-proof-context/tasks.md b/openspec/changes/fix-planning-review-proof-context/tasks.md new file mode 100644 index 000000000..f004247bc --- /dev/null +++ b/openspec/changes/fix-planning-review-proof-context/tasks.md @@ -0,0 +1,23 @@ +## 1. Scope + +- [x] 1.1 Verify actual #743 failure, protected baseline, issue metadata and dedicated worktree. +- [x] 1.2 Specify separate planning and finalized review contexts; mirror privately and validate strictly. +- [x] 1.3 Commit signed specification before authored controls. + +## 2. Proof + +- [x] 2.1 Execute actual workflow/provider controls and retain genuine failing-before evidence. +- [x] 2.2 Obtain exact mapping acceptance and signed-head grant authorization. +- [x] 2.3 Publish authored controls and retain genuine native RED. + +## 3. Delivery + +- [x] 3.1 Authenticate immutable root artifact context without reducing review scope or enforcement. +- [x] 3.2 Run applicable full quality/security/delivery/review/signature/version gates. +- [ ] 3.3 Verify final signed-head native proof, authority and completed review. +- [ ] 3.4 Obtain separate protected merge authorization after all required checks pass. + +## 4. Follow-up + +- [ ] 4.1 Reprove/restack #719/#743/#751; leave them unmerged with no unresolved findings. +- [ ] 4.2 Archive merged prerequisites with the official CLI, maintain wiki and rollback evidence. diff --git a/openspec/changes/governance-01-evidence-output/CHANGE_VALIDATION.md b/openspec/changes/governance-01-evidence-output/CHANGE_VALIDATION.md index b9318d358..69f2299e8 100644 --- a/openspec/changes/governance-01-evidence-output/CHANGE_VALIDATION.md +++ b/openspec/changes/governance-01-evidence-output/CHANGE_VALIDATION.md @@ -1,6 +1,6 @@ # Change Validation: governance-01-evidence-output -- **Validated on (UTC):** 2026-03-22T22:28:26+00:00 +- **Historical validation snapshot (UTC):** 2026-03-22T22:28:26+00:00 - **Workflow:** /wf-validate-change (proposal-stage dry-run validation) - **Strict command:** `openspec validate governance-01-evidence-output --strict` - **Result:** PASS @@ -26,3 +26,7 @@ - Required artifacts are present and parseable. - Strict OpenSpec validation passed. - Change remains authoritative for the evidence envelope schema. + +## Current planning amendment — 2026-09-20 + +The normative deltas now preserve independent current execution, optional chronology, unassessed coverage and original producer verdicts. This is planning-only; no runtime readiness follows from the historical snapshot above. Strict OpenSpec and scoped Markdown/whitespace checks passed for this amendment. Serialization uses distinct ADDED requirements rather than replacing producer-owned Full Chain Validation or Policy Engine, so archival cannot delete those owners' scenarios or require an absent target requirement. Core retains the envelope contract; modules retains emission and file persistence. The earlier broad dependency observations are historical, not new blockers on R09 or workflow adoption. diff --git a/openspec/changes/governance-01-evidence-output/design.md b/openspec/changes/governance-01-evidence-output/design.md index 4d85cf23f..c2232ca43 100644 --- a/openspec/changes/governance-01-evidence-output/design.md +++ b/openspec/changes/governance-01-evidence-output/design.md @@ -1,5 +1,11 @@ ## Context +## Scope rescope — 2026-09-20 + +Emit compact current-run observations and artifact references; historical chronology is separate and optional. Do not require a RED ledger, seal lineage, or the complete validation graph just to record lean CI results. Existing graph-output integration remains this issue's scope; must not depend on its delivery. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + This change implements proposal scope for `governance-01-evidence-output` from the 2026-02-15 architecture-layer integration plan. It is proposal-stage only and defines implementation strategy without changing runtime code. ## Goals / Non-Goals @@ -38,5 +44,9 @@ This change implements proposal scope for `governance-01-evidence-output` from t ## Open Questions -- Dependency summary: Depends on validation-02-full-chain-engine and policy-02-packs-and-modes. +- Dependency summary: Core depends on policy-02-packs-and-modes; validation-02 is a downstream envelope producer, not a prerequisite for this contract. - Whether additional cross-change sequencing constraints should be hard-blocked in `openspec/CHANGE_ORDER.md`. + +## Current contract and archival boundary — 2026-09-20 + +The core envelope contract can precede graph producers. Runtime emitters consume that contract; graph consumers may integrate later using the existing envelope. Serialization requirements are additive and do not replace the producer-owned Full Chain Validation or Policy Engine requirements during archival. Full-chain availability is required only for actual full-chain emission, not for delivering the envelope or serializing current-only results. R09 and workflow adoption remain independent of this broader emitter feature. Earlier wording that requires validation-02 before the whole envelope is historical. diff --git a/openspec/changes/governance-01-evidence-output/proposal.md b/openspec/changes/governance-01-evidence-output/proposal.md index d5bfe846e..73d2ca709 100644 --- a/openspec/changes/governance-01-evidence-output/proposal.md +++ b/openspec/changes/governance-01-evidence-output/proposal.md @@ -1,5 +1,11 @@ # Change: Evidence & Audit Output for CI/CD Pipelines +## Scope rescope — 2026-09-20 + +Emit compact current-run observations and artifact references; historical chronology is separate and optional. Do not require a RED ledger, seal lineage, or the complete validation graph just to record lean CI results. Existing graph-output integration remains this issue's scope; must not depend on its delivery. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + ## Why Teams need machine-readable evidence that validation ran, policies were diff --git a/openspec/changes/governance-01-evidence-output/requirements-evidence.yaml b/openspec/changes/governance-01-evidence-output/requirements-evidence.yaml new file mode 100644 index 000000000..cee4b783f --- /dev/null +++ b/openspec/changes/governance-01-evidence-output/requirements-evidence.yaml @@ -0,0 +1,38 @@ +schema_version: "2" +x-planning-context: &planning-context + rationale: "Make this requirement reviewable before implementation begins." + stakeholder_refs: ["nold-ai/specfact-cli#247"] +x-planning-inspection: &planning-inspection + method: inspection + observable: >- + Strict OpenSpec validation retains the requirement and its scenarios + without claiming implementation or test execution. +x-spec-1: &spec-1 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/governance-01-evidence-output/specs/governance-evidence-output/spec.md" +requirements: + openspec:governance-01-evidence-output:governance-evidence-output:governance-evidence-output: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: GOV01-001 + scenario_id: governance-evidence-output + intent: "Inspect the governance evidence output planning contract and its scenarios." + openspec:governance-01-evidence-output:governance-evidence-output:full-chain-evidence-serialization: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: GOV01-002 + scenario_id: full-chain-evidence-serialization + intent: "Inspect the full chain evidence serialization planning contract and its scenarios." + openspec:governance-01-evidence-output:governance-evidence-output:policy-evidence-serialization: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: GOV01-003 + scenario_id: policy-evidence-serialization + intent: "Inspect the policy evidence serialization planning contract and its scenarios." diff --git a/openspec/changes/governance-01-evidence-output/specs/full-chain-validation/spec.md b/openspec/changes/governance-01-evidence-output/specs/full-chain-validation/spec.md deleted file mode 100644 index f64fd80b8..000000000 --- a/openspec/changes/governance-01-evidence-output/specs/full-chain-validation/spec.md +++ /dev/null @@ -1,12 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Full Chain Validation - -Full-chain validation SHALL emit governance-ready evidence artifacts. - -#### Scenario: Evidence artifact is written with stable schema envelope - -- **GIVEN** full-chain validation executes with evidence output enabled -- **WHEN** command completes -- **THEN** evidence includes schema version, timestamp, profile, policy mode, layer summaries, and overall status -- **AND** artifact path is printed for CI ingestion. diff --git a/openspec/changes/governance-01-evidence-output/specs/governance-evidence-output/spec.md b/openspec/changes/governance-01-evidence-output/specs/governance-evidence-output/spec.md index 7e4589c80..cbf217379 100644 --- a/openspec/changes/governance-01-evidence-output/specs/governance-evidence-output/spec.md +++ b/openspec/changes/governance-01-evidence-output/specs/governance-evidence-output/spec.md @@ -1,9 +1,17 @@ +## Scope rescope — 2026-09-20 + +Emit compact current-run observations and artifact references; historical chronology is separate and optional. Do not require a RED ledger, seal lineage, or the complete validation graph just to record lean CI results. Existing graph-output integration remains this issue's scope; must not depend on its delivery. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + ## ADDED Requirements ### Requirement: Governance Evidence Output The system SHALL produce machine-readable governance evidence suitable for CI and audit ingestion. +The system SHALL represent `current_execution` independently from optional historical chronology. Current passing tests SHALL NOT establish historical failing-before evidence, full requirement coverage or protected CI authority. Missing requirement/architecture/spec mappings SHALL remain unassessed coverage or explicit findings under the selected graph policy, never fabricated completeness. Independent producer verdicts SHALL retain their original status and source reference. Ordinary current-run verification SHALL NOT require full-chain execution, chronology, transcripts, approval receipts or duplicate proof execution. + #### Scenario: CI-consumable evidence includes policy and exception context - **GIVEN** validation runs in CI mode @@ -16,7 +24,7 @@ The system SHALL produce machine-readable governance evidence suitable for CI an - **GIVEN** full-chain validation has transition results - **WHEN** governance evidence is emitted - **THEN** each layer contains pass/fail/advisory counts and coverage percentages -- **AND** overall verdict is derivable from the evidence alone. +- **AND** overall verdict is derivable from the evidence alone under the selected policy, without elevating unassessed coverage or optional chronology into a passing claim. #### Scenario: Evidence carries clean-code results as a parallel quality dimension @@ -24,3 +32,55 @@ The system SHALL produce machine-readable governance evidence suitable for CI an - **WHEN** governance evidence is emitted - **THEN** the envelope includes a top-level `code_quality` section with category counts and verdict - **AND** clean-code data does not redefine or replace the traceability `validation_results.layers` structure + +#### Scenario: Current tests pass while graph coverage is incomplete + +- **GIVEN** ordinary current tests pass and required graph mappings are absent +- **WHEN** evidence is evaluated or serialized +- **THEN** `current_execution` remains separate from incomplete graph coverage +- **AND** missing mappings retain their own findings or unassessed status rather than becoming complete coverage +- **AND** no historical chronology or protected CI authority is inferred. + +#### Scenario: Independent producer failure survives current test success + +- **GIVEN** current tests pass but an independently required review or policy producer fails +- **WHEN** evidence is evaluated or serialized +- **THEN** both original verdicts and source references remain available +- **AND** the passing test result does not turn the failed required gate into acceptance. + +#### Scenario: Current-only evidence has no historical receipt + +- **GIVEN** a valid current-run result with no historical chronology or prior session receipt +- **WHEN** ordinary current-run verification consumes it +- **THEN** absence of optional history does not invalidate the current-execution claim +- **AND** no full-chain run, transcript, approval receipt or duplicate proof execution is required. + +### Requirement: Full Chain Evidence Serialization + +Full-chain result summaries SHALL be representable in the governance envelope without redefining transition validation. The system SHALL represent `current_execution` independently from optional historical chronology. Current passing tests SHALL NOT establish historical failing-before evidence, full requirement coverage or protected CI authority. Missing requirement/architecture/spec mappings SHALL remain unassessed coverage or explicit findings under the selected graph policy, never fabricated completeness. Independent producer verdicts SHALL retain their original status and source reference. Ordinary current-run verification SHALL NOT require full-chain execution, chronology, transcripts, approval receipts or duplicate proof execution. + +#### Scenario: Supplied full-chain results fit the envelope + +- **GIVEN** a producer supplies full-chain result summaries +- **WHEN** the governance envelope serializes them +- **THEN** it retains schema version, timestamp, profile, policy mode, layer summaries and overall status +- **AND** missing mappings and optional chronology keep their own distinct status. + +#### Scenario: Envelope contract exists before the full-chain producer + +- **GIVEN** only current execution and independent policy/review results are available +- **WHEN** the envelope is validated +- **THEN** absence of the optional full-chain producer does not invalidate the current result +- **AND** the envelope does not claim full-chain execution or coverage. + +### Requirement: Policy Evidence Serialization + +Policy evaluation outputs SHALL be serializable into governance evidence records without changing the producer policy, verdict or trusted-authority boundary. Current execution and optional chronology SHALL remain separate claims. + +#### Scenario: Policy rule results include evidence-ready fields + +- **GIVEN** policy validation completes +- **WHEN** evidence serialization runs +- **THEN** each rule result includes rule ID, severity, mode, outcome and producer reference +- **AND** serialization preserves an independent failure even when current tests pass +- **AND** serialization alone does not authenticate a local result as protected CI evidence. diff --git a/openspec/changes/governance-01-evidence-output/specs/policy-engine/spec.md b/openspec/changes/governance-01-evidence-output/specs/policy-engine/spec.md deleted file mode 100644 index 92ca09576..000000000 --- a/openspec/changes/governance-01-evidence-output/specs/policy-engine/spec.md +++ /dev/null @@ -1,12 +0,0 @@ -## MODIFIED Requirements - -### Requirement: Policy Engine - -Policy evaluation outputs SHALL be serializable into governance evidence records. - -#### Scenario: Policy rule results include evidence-ready fields - -- **GIVEN** policy validation completes -- **WHEN** evidence serialization runs -- **THEN** each rule result includes rule ID, severity, mode, and outcome -- **AND** output can be consumed by CI gates without additional transformation. diff --git a/openspec/changes/governance-01-evidence-output/tasks.md b/openspec/changes/governance-01-evidence-output/tasks.md index 7365379b0..18e2cba02 100644 --- a/openspec/changes/governance-01-evidence-output/tasks.md +++ b/openspec/changes/governance-01-evidence-output/tasks.md @@ -1,5 +1,13 @@ # Tasks: governance-01-evidence-output +## Scope rescope — 2026-09-20 + +Emit compact current-run observations and artifact references; historical chronology is separate and optional. Do not require a RED ledger, seal lineage, or the complete validation graph just to record lean CI results. Existing graph-output integration remains this issue's scope; must not depend on its delivery. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + +Implementation discipline: use focused regression/reproduction and current-run results. Any task requiring immutable hosted RED, frozen test authoring or approval receipts merely to implement this feature is superseded; tests of an explicitly selected optional seal feature remain in scope. + ## 1. Branch and dependency guardrails - [ ] 1.1 Create dedicated worktree branch `feature/governance-01-evidence-output` from `dev` before implementation work: `scripts/worktree.sh create feature/governance-01-evidence-output`. @@ -11,7 +19,7 @@ - [ ] 2.1 Finalize `specs/` deltas for all listed capabilities and cross-check scenario completeness. - [ ] 2.2 Add/update tests mapped to new and modified scenarios. -- [ ] 2.3 Run targeted tests to capture failing-first behavior and record results in `TDD_EVIDENCE.md`. +- [ ] 2.3 Run targeted tests before behavior edits and summarize relevant failures and subsequent passes in concise validation notes with existing local or CI references; no authored TDD ledger is required under the effective lean policy. ## 3. Implementation @@ -30,3 +38,8 @@ - [ ] 5.1 Update `openspec/CHANGE_ORDER.md` status/dependency notes if implementation sequencing changed. - [ ] 5.2 Open a PR from `feature/governance-01-evidence-output` to `dev` with spec/test/code/docs evidence. + +## Current-evidence boundary acceptance + +- [ ] Test current-only verification without history, incomplete mappings alongside passing tests, and independent failed producer gates; retain producer statuses and references. +- [ ] Verify native archival preserves the normative boundaries and adds serialization requirements without replacing producer-owned validation or policy requirements. diff --git a/openspec/changes/isolate-semgrep-security-runtime/.openspec.yaml b/openspec/changes/isolate-semgrep-security-runtime/.openspec.yaml new file mode 100644 index 000000000..9076a6609 --- /dev/null +++ b/openspec/changes/isolate-semgrep-security-runtime/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-20 diff --git a/openspec/changes/isolate-semgrep-security-runtime/CHANGE_VALIDATION.md b/openspec/changes/isolate-semgrep-security-runtime/CHANGE_VALIDATION.md new file mode 100644 index 000000000..69d02098c --- /dev/null +++ b/openspec/changes/isolate-semgrep-security-runtime/CHANGE_VALIDATION.md @@ -0,0 +1,166 @@ +# Change validation + +The owner explicitly rescoped PR #782 on 2026-10-21 Europe/Berlin to official +packages only, removal of the rejected fork, and an exact temporary MCP advisory +exception if the upstream pin prevents a compatible upgrade. The certificate +correction remains published at 66ee3d5. Earlier sections below retain certificate +checkpoints and their original limitations. The superseded scanner plans remain +in Git history rather than in this hotfix. No fork adoption or publication is +claimed or authorized. + +The original frozen audit reports certifi 2026.7.22 / GHSA-6q48-6c7w-9gw2 and +MCP 1.29.0 / CVE-2026-59951. The reviewed certifi advisory recommends >=2026.10.19. +The old hash-verified bundle contains the compromised CA fingerprint; the patched +bundle excludes it. Authored test evidence must be recorded before dependency edits. +The same test and canonical frozen checks must pass after refresh. Full security +audit remains blocked until MCP is actually remediated. + +Sources accessed 2026-10-20. + +- [Reviewed certifi advisory](https://github.com/advisories/GHSA-6q48-6c7w-9gw2). +- [Failed frozen audit](https://github.com/nold-ai/specfact-cli/actions/runs/39492552089/job/118869420960). +- [Certifi release metadata](https://pypi.org/pypi/certifi/2026.10.19/json). + +## Actual bounded implementation + +The working tree now requires certifi >=2026.10.19 in both runtime declarations, +regenerates the complete frozen lock/export normally, and synchronizes all four +core version sources to 0.55.11 with a Security changelog entry. The lock retains +184 package records; only certifi and the project version change. Native scanner +code, audit coverage, exception manifests and signature policy remain unchanged. + +The old installed CA test fails on the compromised fingerprint; the refreshed +frozen installation passes the same test. Native run 39498007620 retains genuine +failing-first proof at d6d8f908. See `TDD_EVIDENCE.md` for identities and limits. +Independent scope/mapping review and separate final dependency review accepted +this bounded unit. Neither acceptance supplies human authority or aggregate green. + +Normal commit hooks passed signature scope, synchronized versions and dependency +trust, then failed `frozen-cve-audit` solely on MCP 1.29.0 / CVE-2026-59951. +At that earlier checkpoint no fix commit, push, audit exclusion or hook bypass followed. The owner subsequently approved one exact local frozen-audit hook exception. It was consumed for published commit 66ee3d58959e7798a6a7a5a1ed5c41d1db37fbc0. Other hooks and all required CI remained enforced. This does not renew the exception or waive the still-failing MCP audit. Merge and release remain blocked. + +## Review triage for the existing release-security test + +The 0.55.11 bump exposed the existing fixed 0.55.10 release assertion. Updating +that single expected constant preserves every original cross-source assertion. +The release/security test file and new CA test then passed all 20 selected tests. +An independent reviewer accepted this correction and the evidence updates. + +Whole-file SpecFact review reports a naming warning for +`test_invalid_utf8_authority_is_stable_metadata_failure`. The actual rule uses a +substring regex containing `data`, which matches the domain word `metadata`. +The existing test name specifically identifies malformed UTF-8 authority input +and its stable failure result. This is a false naming finding, retained with this +explicit disposition rather than renaming an unrelated security control. + +Two informational length suggestions concern the existing fresh-consumer test +and malformed-authority CLI argument fixture. Their assertions and explicit +argument list make the security controls reviewable. A comprehension or shared +fixture abstraction would not improve these tests. Retain them without suppressing +rules or changing protected verification behavior. These three whole-file +findings are dispositions, not claims that the raw review report is empty. + +## Final local quality results + +The final `hatch run smart-test` exits 0 with 3429 passed, 11 skipped and two +dependency deprecation warnings. Full Ruff format/lint, explicit-interpreter type +checking, documentation/command contracts and the contract gate pass. Contract +selection reports no changed contract inputs and reuses its cached result. +The source-bearing patch and fixed release expectation have separate independent +PASS verdicts. No accepted new review defect remains; whole-file suggestions have +the explicit dispositions above. Final CI and authority still require a published +fix commit, which the MCP audit currently prevents. + +## Corrected scope and current verification + +Retain official Semgrep 1.179.0 and MCP 1.29.0. Latest official Semgrep 1.182.0 +still exact-pins the same affected MCP, confirmed in actual artifact metadata +and a fresh normal resolver failure. The owner-authorized register entry +selects only MCP 1.29.0 / CVE-2026-59951 and its GHSA alias, with review +2026-10-21 and inclusive expiry 2026-11-04. Every other advisory and strict +audit error remains blocking. The scanner still imports affected MCP, so +reviewed automation avoiding server startup is mitigation rather than proof +of unreachability. Review weekly and remove the exception upon a verified +official compatible fix. + +The rejected workflow, producer, manifests, runtime patches and their tests +are removed. Certificate artifact verification remains. Requirements mappings +now cover certificate trust and bounded exception behavior. Prior candidate +review acceptance is withdrawn and cannot transfer to this map. + +The actual audit fails before the entry and all three strict audits pass after +it, visibly reporting the exact waived finding. Gate tests additionally prove +other versions/advisories, mixed findings, skipped subjects and expiry fail. +See TDD_EVIDENCE.md for exact logs and counts. No audit engine or required CI +workflow was modified. Signing is CI/CD only. + +A future dependency-only release must start from published v0.55.4 commit +d579970565530c3fd7b98bad4de90cf874c2a99d. Unreleased dev runtime changes must +not be promoted as part of that hotfix. The old rejected-fork release fixture +is not adoption evidence for this scope. No release or merge is performed. + +## Current gate and review checkpoint + +The pinned Requirements consumer accepts the corrected three-case plan. A fresh +independent technical reviewer, Codex agent `/root/official_scope_acceptance`, +accepts mapping digest +`sha256:9400b195f3c46b54c8fafbd8461de73716aeeb17318f5c39e54863091e3c39f3`. +This is mapping review, not human authority or final native lifecycle admission. +The frozen-artifact requirement deliberately tests candidate wheel bytes rather +than the trusted verifier's installed certifi. The retained installed test runs +in the candidate environment as supplemental delivery evidence. + +Ruff formatting/lint, BasedPyright type/lint, YAML, contract, license and Bandit +gates exit 0. The scoped smart-test command exits 0 with 53 selected unit tests +passing. Its raw pytest coverage report is 5 percent and emits a threshold +message that the scoped wrapper documents as expected. This is not a full-suite +coverage pass. Required full delivery CI remains enforced. + +Normal frozen regeneration changes no dependency selection beyond the existing +certifi correction. Reproducibility, `uv lock --check` and version-source checks +pass. Actual official Semgrep SAST reports zero findings against a zero-finding +baseline. SpecFact review identified test complexity/parameter-count issues, +which must be fixed before commit. No review error is waived. + +## Bounded review findings and dispositions + +The two new test complexity/parameter-count findings require correction rather +than a waiver. The scenario inputs use a named record, and lock inventory and +exception fields use exact collection comparisons. All nine adversarial test +IDs and both mapped selectors remain unchanged. + +Whole-file review also emits 44 reportUnknownMemberType warnings on the existing +dynamically loaded audit fixture at unchanged lines before the new scenario +record. Focused type checking finds no warning in the changed functions. These +pre-existing fixture warnings are retained as a narrow documented exception to +whole-file cleanup, because rewriting every existing fixture is unrelated to +the dependency hotfix. No type rule or severity threshold is suppressed. The +existing naming false positive and two informational length suggestions retain +the earlier explicit dispositions above. Required error-level type checking +and clean-code checks remain enforced. + +Final bounded review by Codex agent `/root/bounded_policy_final_review` accepts +the test refinement. The final SpecFact report is PASS_WITH_ADVISORY with zero +errors and no introduced warning. Its remaining 44 fixture type warnings, one +existing naming warning and two existing informational findings have the +bounded dispositions above. No complexity finding remains on changed tests. + +## Native official-only verification and selector correction + +At b3bf7cf5, native run 39572650717 passes Security Audit with the exact +reviewed MCP exception. Native Requirements run 39572650571 records 11 tests +with zero failures, errors or skips. Its reconciliation rejects both the +missing prior-RED admission and the unexpanded parametrized selector. + +The mapping now names all nine parameter IDs explicitly. Fresh independent +review by `/root/expanded_selector_map_review` confirms the 11 unique selectors +match actual native JUnit and accepts mapping digest +`sha256:9be3a10b220655e55c299b9b3d69c2be0c0b029b1db1c0942abfea1ea6c4f86d`. +This supersedes the earlier three-case mapping acceptance. The prior native +report remains immutable evidence of its original failure. Prior-RED admission +and exact-head human authority are still required. Neither local test evidence +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. diff --git a/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md new file mode 100644 index 000000000..5f12440db --- /dev/null +++ b/openspec/changes/isolate-semgrep-security-runtime/TDD_EVIDENCE.md @@ -0,0 +1,452 @@ +# Certificate trust-store regression + +## Failing before dependency changes + +On 2026-10-20 Europe/Berlin, the existing frozen lock was installed with +`uv sync --locked --all-extras` into an isolated environment using Python 3.13.14. +It installed certifi 2026.7.22. No dependency input had been edited. + +Command from this worktree: + +```bash +/private/tmp/specfact-dependency-audit-20261020/certifi-tools/bin/python -m pytest tests/unit/security/test_certificate_trust_store.py -q --no-cov +``` + +The actual SSL trust-store comparison failed as intended: + +```text +Extra items in the left set: +'86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b' +1 failed in 0.06s +``` + +The installed bundle still trusted the compromised emSign ECC Root CA - G3. +The full local capture is +`/private/tmp/specfact-dependency-audit-20261020/certifi-root-red.log`. +The frozen audit independently reported GHSA-6q48-6c7w-9gw2 and the separate MCP +finding. Passing-after evidence must use the same test against the refreshed +frozen installation. This local record does not replace native CI execution proof. + +## Native CI failing-first proof + +The typed test was published at commit +`d6d8f9089bb55f8e834a55dfcac43a8a5e4c61ca` before dependency changes. +[Requirements run 39498007620](https://github.com/nold-ai/specfact-cli/actions/runs/39498007620) +retained artifact `12297219535`, digest +`sha256:10fefe0c9b89815a6a7b29cbe5bca56680fe0fed3aefae693328dfbe6e5406e8`. +The actual JUnit records one intended fingerprint failure, no errors and no skips +on Python 3.12.15. Native reconciliation reports `observed_maturity: red`, +`gate_decision: pass`, and `implementation_evidence: failing-first-proven`. +The run's delivery checks remain blocked at this intermediate failing-test stage. +This is not final implementation or authority evidence. + +## Passing after frozen refresh + +The canonical `hatch run refresh-frozen-delivery` regenerated the Python lock and +hash-protected CI export after the certifi minimum and 0.55.11 metadata changes. +`uv sync --locked --all-extras` then installed the exact graph in the isolated +environment. The same typed test passed on Python 3.13.14 with certifi 2026.10.19: + +```text +1 passed in 0.05s +``` + +The lock retains 184 packages. Only certifi 2026.7.22 to 2026.10.19 and the project +0.55.10 to 0.55.11 changed version. The installed store has 120 roots and excludes +the compromised fingerprint. Delivery/export parity, lock consistency, license, +dependency trust, synchronized versions, strict PyPI-ahead and Bandit checks pass. +The full audit still fails solely on MCP 1.29.0 / CVE-2026-59951. Both other frozen +exports pass their actual audits. No audit exclusion or resolver override exists. + +These passing-after observations describe the local implementation working tree. +Final commit-bound CI evidence and new exact-head authority remain required. +The local logs and package accounting are in +`/private/tmp/specfact-dependency-audit-20261020/certifi-fix-result.txt`. + +## Release verification corrections + +The broader smart-test attempt had 3427 passes and two failures. Its launcher +selected the Requirements fixture instead of the separately pinned smoke fixture. +Using the exact smoke fixture, then warming the new hash-locked certifi wheel for +its offline child, made the real launcher pass. The first failed results remain +in the local trail and are not reported as passes. + +The other failure was the existing release test's fixed 0.55.10 expectation against +the required 0.55.11 patch. Updating only that expected value preserves all original +assertions. All 20 release/security and CA tests then passed. The final full +smart-test rerun is recorded separately after completion; targeted passes do not +turn the earlier failed run into a green gate. + +## Final local smart gate + +With the separately pinned smoke fixture and frozen cache prepared, +`hatch run smart-test` exited 0. Its actual result was: + +```text +3429 passed, 11 skipped, 2 warnings in 262.82s +``` + +The two warnings are dependency deprecations, not failed tests. The earlier failed +run remains retained. Final local success does not waive MCP, final native CI or +exact-head human authority. Normal dependency commit remains blocked by the +unchanged `frozen-cve-audit` failure. + +## Official-only correction, 2026-10-21 + +The owner rejected the producer/fork and authorized an exact temporary MCP +exception when official Semgrep prevents patched MCP. Superseded producer +evidence remains in Git at 46970dc1 and the private operator archive. It is not +current acceptance evidence and supplies no publication or authority approval. + +Latest official Semgrep 1.182.0 wheel metadata requires MCP 1.29.0. A fresh +normal `uv pip compile` with `semgrep==1.182.0` and `mcp>=1.30.0,<2` fails: + +```text +Because semgrep>=1.182.0 depends on mcp==1.29.0 ... +your requirements are unsatisfiable. +``` + +The original strict frozen audit exits 1 for MCP 1.29.0 / CVE-2026-59951. +Before adding the register entry, the new exact-policy and gate tests record +6 failed and 4 passed, including the intended empty-register policy mismatch +and unwaived approved findings. After adding the owner-authorized entry, +all 41 audit/policy tests pass. Twelve unchanged certificate artifact and +adversarial-input tests pass using real hash-pinned official wheel downloads. + +All three unchanged strict audit commands then exit 0. The CI graph visibly +reports `WAIVED: mcp==1.29.0 vuln=CVE-2026-59951`; the other two graphs contain +no unreviewed findings. This is accepted temporary risk, not remediation. + +Local evidence is retained under +`/private/tmp/specfact-dependency-audit-20261020/official-only/` in +`latest-compatible.log`, `security-before.log`, `exception-tests-red.log`, +`exception-tests-green.log`, `certificate-tests.log` and `security-after.log`. +The gate implementation and all required workflow checks are unchanged. +Normal commit hooks, final CI and exact-head human authority remain pending. + +## Native official-only verification and selector correction + +At b3bf7cf5, native run 39572650717 passes Security Audit with the exact +reviewed MCP exception. Native Requirements run 39572650571 records 11 tests +with zero failures, errors or skips. Its reconciliation rejects both the +missing prior-RED admission and the unexpanded parametrized selector. + +The mapping now names all nine parameter IDs explicitly. Fresh independent +review by `/root/expanded_selector_map_review` confirms the 11 unique selectors +match actual native JUnit and accepts mapping digest +`sha256:9be3a10b220655e55c299b9b3d69c2be0c0b029b1db1c0942abfea1ea6c4f86d`. +This supersedes the earlier three-case mapping acceptance. The prior native +report remains immutable evidence of its original failure. Prior-RED admission +and exact-head human authority are still required. Neither local test evidence +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. + +### 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. + +### 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 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. + +### 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. + +### 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 new file mode 100644 index 000000000..dec8e70c7 --- /dev/null +++ b/openspec/changes/isolate-semgrep-security-runtime/design.md @@ -0,0 +1,151 @@ +# Official dependency hotfix design + +## Decision + +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 + +One register record selects package `mcp`, version `1.29.0`, primary advisory +`CVE-2026-59951` and its `GHSA-84m7-p3x7-pcfv` alias, review/expiry dates, +rationale and mitigation. Matching is exact to the emitted primary ID. The +existing register applies across audited graphs, so document the real path +`specfact-cli[scanning/dev] -> semgrep==1.179.0 -> mcp==1.29.0`. + +## Risk and mitigation + +The affected SDK remains installed and imported by ordinary Semgrep startup. +The exception does not fix it or prove it unreachable. Restrict use to ordinary +scan/SAST commands and do not start an exposed stateful Streamable HTTP server. +Review official Semgrep weekly and remove the exception immediately after a +normal, compatible patched graph passes artifact, installation and audit checks. +The gate rejects the record after 2026-11-04, requiring renewed review or removal. + +Keep tests proving other versions/advisories, mixed findings, skipped audit +subjects and invalid dates block. Do not remove the audit from any requirements +file. Retain inert certificate-wheel verification under trusted dependencies. + +## Release boundary + +The four core version sources remain 0.55.11. Future dependency-only publication +must be prepared from actual published v0.55.4, with payload comparison proving +that unreleased dev runtime changes are absent. Do not reuse the rejected-fork +release fixture. No signed module assets change. Signing remains CI/CD only. + +## Superseded work + +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. + +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. + +## 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/proposal.md b/openspec/changes/isolate-semgrep-security-runtime/proposal.md new file mode 100644 index 000000000..74e8a2af8 --- /dev/null +++ b/openspec/changes/isolate-semgrep-security-runtime/proposal.md @@ -0,0 +1,93 @@ +# Change: Patch certificate trust using official dependencies + +## Why + +PR #782 must remain a dependency-only hotfix. On 2026-10-21 Europe/Berlin the +owner rejected owning or publishing a Semgrep fork and explicitly requested its +removal. The owner authorized a temporary MCP exception if official Semgrep +prevents a compatible upgrade. Official Semgrep 1.182.0 still requires +`mcp==1.29.0`; normal resolution with `mcp>=1.30.0,<2` is unsatisfiable. + +## What Changes + +- Retain official certifi >=2026.10.19 and coherent core patch version 0.55.11. +- Remove the rejected scanner producer, recipe, workflow, runtime patches and + fork-specific tests and Requirements mappings. Preserve history in Git. +- Keep the existing official Semgrep 1.179.0 / MCP 1.29.0 frozen pair. Updating + Semgrep to 1.182.0 would retain the same advisory and expand this hotfix. +- Use the existing vulnerability register for only MCP 1.29.0 / + CVE-2026-59951 (GHSA-84m7-p3x7-pcfv), reviewed 2026-10-21 and expiring + 2026-11-04 inclusive. Remove it as soon as a verified compatible upstream + 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 + +### New Capabilities + +- `isolated-scanner-runtime`: certificate artifact safety and temporary exact + advisory treatment for the unchanged official scanner graph. + +### Modified Capabilities + +- 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 + +No application behavior, scanner implementation, module manifest or publisher +changes. Ordinary base installs do not declare MCP or Semgrep. Opt-in scanning +still delivers affected MCP. Reviewed automation invokes ordinary scans rather +than an MCP server; this does not establish universal unreachability. Users must +not expose a stateful Streamable HTTP MCP server while this exception applies. +The advisory concerns session-memory exhaustion, not an established package +compromise. + +A later hotfix release must start from published v0.55.4 commit +`d579970565530c3fd7b98bad4de90cf874c2a99d`, not include unreleased dev runtime +changes. This PR prepares dependencies/versioning on dev; no release, merge or +promotion is authorized. Final commit/tree authority remains separate. + +## Source Tracking + +- **Source Repository**: nold-ai/specfact-cli +- **Issue URL**: +- **Last Synced Status**: Todo, confirmed 2026-10-21. + +## Acceptance Criteria + +- Frozen and installed certifi exclude compromised emSign ECC Root CA - G3 DER + SHA256 `86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b`. +- No rejected fork implementation, production workflow or adoption plan remains. +- Normal official dependency metadata and a fresh resolver prove the MCP pin. +- 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 new file mode 100644 index 000000000..b3fa10281 --- /dev/null +++ b/openspec/changes/isolate-semgrep-security-runtime/requirements-evidence.yaml @@ -0,0 +1,163 @@ +schema_version: '2' +requirements: + ? openspec:isolate-semgrep-security-runtime:isolated-scanner-runtime:exclude-compromised-certificate-authorities-from-delivered-trust + : rationale: Inspect the exact frozen certificate artifact independently of the trusted verifier's installed dependencies. + stakeholder_refs: + - nold-ai/specfact-cli#780 + touchpoints: + - id: runtime-floor + kind: config_file + locator: pyproject.toml + - id: frozen-delivery + kind: config_file + locator: requirements/ci/locked.txt + - id: frozen-trust-test + kind: test + locator: tests/unit/security/test_frozen_dependency_artifacts.py + verification_cases: + - case_id: CERTIFI-COMPROMISED-ROOT-ABSENT + scenario_id: exclude-compromised-certificate-authorities-from-delivered-trust + method: test + intent: Verify the candidate lock/export wheel hash and parse its inert CA data with trusted SSL code. + observable: The trust store is nonempty and excludes the compromised emSign ECC Root CA - G3 DER SHA256. + selector: + runner: pytest + node_id: tests/unit/security/test_frozen_dependency_artifacts.py::test_frozen_certifi_bundle_excludes_compromised_root + openspec:isolate-semgrep-security-runtime:isolated-scanner-runtime:bound-the-temporary-official-mcp-advisory-exception: + rationale: Use the existing strict gate with only the owner-approved exact, expiring MCP advisory exception + while official Semgrep pins the affected version. + stakeholder_refs: + - nold-ai/specfact-cli#780 + touchpoints: + - id: reviewed-exception + kind: config_file + locator: ci/vulnerability-audit-exceptions.json + - id: exception-policy-test + kind: test + locator: tests/unit/security/test_release_promotion_security_gates.py + verification_cases: + - case_id: MCP-OFFICIAL-EXCEPTION-EXACT + scenario_id: bound-the-temporary-official-mcp-advisory-exception + method: test + intent: Bind the only exception to the official MCP package/version/advisory and review/expiry dates. + observable: The graph remains official and the exception is exact and time bounded. + selector: + runner: pytest + node_id: tests/unit/security/test_release_promotion_security_gates.py::test_frozen_graph_uses_official_semgrep_with_bounded_mcp_exception + - case_id: MCP-EXACT-CVE + scenario_id: bound-the-temporary-official-mcp-advisory-exception + method: test + intent: Approve only the exact MCP CVE on its review date. + observable: Only the approved version/advisory within its approval window succeeds; unrelated/mixed advisories, + other versions, skipped subjects and invalid dates fail. + selector: + runner: pytest + node_id: tests/unit/scripts/test_security_audit_gate.py::test_committed_mcp_exception_is_exact_and_time_bounded[exact-cve] + - case_id: MCP-EXACT-GHSA + scenario_id: bound-the-temporary-official-mcp-advisory-exception + method: test + intent: Approve the same reviewed advisory under its exact GHSA primary ID. + observable: Only the approved version/advisory within its approval window succeeds; unrelated/mixed advisories, + other versions, skipped subjects and invalid dates fail. + selector: + runner: pytest + node_id: tests/unit/scripts/test_security_audit_gate.py::test_committed_mcp_exception_is_exact_and_time_bounded[exact-ghsa] + - case_id: MCP-EXPIRY-DAY + scenario_id: bound-the-temporary-official-mcp-advisory-exception + method: test + intent: Keep the review valid through its inclusive expiry date. + observable: Only the approved version/advisory within its approval window succeeds; unrelated/mixed advisories, + other versions, skipped subjects and invalid dates fail. + selector: + runner: pytest + node_id: tests/unit/scripts/test_security_audit_gate.py::test_committed_mcp_exception_is_exact_and_time_bounded[expiry-day] + - case_id: MCP-OTHER-VERSION + scenario_id: bound-the-temporary-official-mcp-advisory-exception + method: test + intent: Reject the same advisory for another package version. + observable: Only the approved version/advisory within its approval window succeeds; unrelated/mixed advisories, + other versions, skipped subjects and invalid dates fail. + selector: + runner: pytest + node_id: tests/unit/scripts/test_security_audit_gate.py::test_committed_mcp_exception_is_exact_and_time_bounded[other-version] + - case_id: MCP-UNRELATED-ADVISORY + scenario_id: bound-the-temporary-official-mcp-advisory-exception + method: test + intent: Reject an advisory outside the approved IDs. + observable: Only the approved version/advisory within its approval window succeeds; unrelated/mixed advisories, + other versions, skipped subjects and invalid dates fail. + selector: + runner: pytest + node_id: tests/unit/scripts/test_security_audit_gate.py::test_committed_mcp_exception_is_exact_and_time_bounded[unrelated-advisory] + - case_id: MCP-MIXED-ADVISORIES + scenario_id: bound-the-temporary-official-mcp-advisory-exception + method: test + intent: Reject a graph containing both waived and unreviewed advisories. + observable: Only the approved version/advisory within its approval window succeeds; unrelated/mixed advisories, + other versions, skipped subjects and invalid dates fail. + selector: + runner: pytest + node_id: tests/unit/scripts/test_security_audit_gate.py::test_committed_mcp_exception_is_exact_and_time_bounded[mixed-advisories] + - case_id: MCP-SKIPPED + scenario_id: bound-the-temporary-official-mcp-advisory-exception + method: test + intent: Reject incomplete strict-audit coverage even with a matching advisory. + observable: Only the approved version/advisory within its approval window succeeds; unrelated/mixed advisories, + other versions, skipped subjects and invalid dates fail. + selector: + runner: pytest + node_id: tests/unit/scripts/test_security_audit_gate.py::test_committed_mcp_exception_is_exact_and_time_bounded[skipped] + - case_id: MCP-EXPIRED + scenario_id: bound-the-temporary-official-mcp-advisory-exception + method: test + intent: Reject the register after its expiry date. + observable: Only the approved version/advisory within its approval window succeeds; unrelated/mixed advisories, + other versions, skipped subjects and invalid dates fail. + selector: + runner: pytest + node_id: tests/unit/scripts/test_security_audit_gate.py::test_committed_mcp_exception_is_exact_and_time_bounded[expired] + - case_id: MCP-FUTURE-REVIEW + scenario_id: bound-the-temporary-official-mcp-advisory-exception + method: test + intent: Reject a review date later than the audit date. + observable: Only the approved version/advisory within its approval window succeeds; unrelated/mixed advisories, + other versions, skipped subjects and invalid dates fail. + 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 new file mode 100644 index 000000000..1f6bb66f4 --- /dev/null +++ b/openspec/changes/isolate-semgrep-security-runtime/requirements-proof/review-evidence.json @@ -0,0 +1,9 @@ +{ + "schema_version": "1", + "decision": "accepted", + "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 new file mode 100644 index 000000000..338c7011c --- /dev/null +++ b/openspec/changes/isolate-semgrep-security-runtime/specs/isolated-scanner-runtime/spec.md @@ -0,0 +1,90 @@ +## ADDED Requirements + +### Requirement: Exclude compromised certificate authorities from delivered trust + +The exact frozen certifi wheel SHALL exclude compromised +emSign ECC Root CA - G3 DER SHA256 +`86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b` +while retaining a nonempty CA store. Canonical inputs SHALL require certifi +>=2026.10.19. Trusted Requirements proof SHALL inspect hash-bound inert CA data +without importing candidate dependencies into its verifier. + +#### Scenario: Frozen certificate artifact rejects the compromised root + +- **GIVEN** the certifi wheel selected by the frozen lock and hash export +- **WHEN** verified CA data is parsed by trusted SSL code +- **THEN** the store is nonempty and excludes the compromised DER fingerprint. + +### Requirement: Bound the temporary official MCP advisory exception + +The unchanged official Semgrep/MCP graph SHALL use only an exact reviewed +MCP 1.29.0 / CVE-2026-59951 exception, including its GHSA alias, from 2026-10-21 +through 2026-11-04 inclusive. Every required audit SHALL remain strict and fail +closed on other versions, advisories, skipped dependencies, malformed records +or expiry. No Semgrep fork or conflicting dependency override SHALL be adopted. + +#### Scenario: Only the approved official MCP finding is waived + +- **GIVEN** the reviewed MCP register record and the official frozen graph +- **WHEN** the existing audit gate evaluates it on the review date +- **THEN** only the exact authorized finding passes with a visible WAIVED report. + +#### Scenario: An unrelated or expired finding remains blocking + +- **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. + +#### 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. + +#### 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. + +#### 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 new file mode 100644 index 000000000..b1cdf7168 --- /dev/null +++ b/openspec/changes/isolate-semgrep-security-runtime/tasks.md @@ -0,0 +1,62 @@ +# Implementation tasks + +## 1. Retain delivered certificate correction + +- [x] Preserve real pre-fix certificate failure and native CI evidence. +- [x] Retain official certifi 2026.10.19 and synchronized core 0.55.11. +- [x] Verify frozen artifact and installed certificate tests after cleanup. + +## 2. Remove rejected fork + +- [x] Remove producer, runtime patches, candidate workflow and fork tests. +- [x] Retire fork-specific mappings and superseded planning from the PR diff. +- [ ] Revalidate corrected scope and align internal wiki/issue/PR metadata. + +## 3. Use official packages and bounded exception + +- [x] Verify latest official artifact metadata and normal resolver conflict. +- [x] Capture actual frozen audit failure before the exception. +- [x] Test exact matching, other versions/advisories, skipped subjects and expiry. +- [x] Add only the authorized expiring MCP register entry. +- [x] Pass all three strict frozen audits and required quality gates. + +## 4. Review and finalization + +- [x] Obtain independent code and Requirements mapping review. +- [ ] Commit and push with normal hooks and CI/CD-only signing. +- [ ] Verify required CI and respond to real review findings. +- [ ] Obtain actual final commit/tree authority when required. +- [ ] 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. +- [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. + +## 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. diff --git a/openspec/changes/preflight-01-design-contract-core/design.md b/openspec/changes/preflight-01-design-contract-core/design.md index d66f60f3f..bb60b98f5 100644 --- a/openspec/changes/preflight-01-design-contract-core/design.md +++ b/openspec/changes/preflight-01-design-contract-core/design.md @@ -1,5 +1,11 @@ ## Context +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + This change defines the durable core boundary for a pre-implementation assurance loop. The loop may ingest OpenSpec, Spec Kit, repository governance, GitHub hierarchy, architecture records, and repository state, but those sources remain authoritative in their own domains. Core normalizes and verifies identities; it does not execute the workflow. Current upstream patterns support this split. OpenSpec exposes proposal and verification workflows under harness-specific command forms, Spec Kit treats clarification and cross-artifact analysis as pre-implementation quality gates, and both allow the invocation surface to vary by agent. The shared contract therefore must be independent of slash-command spelling and harness packaging. @@ -38,7 +44,11 @@ Scope entries use the closed roles `source`, `test`, `docs`, `generated`, `evide Each affected behavior or interface records the closed risk dimensions `boundary`, `malformed_or_missing_input`, `state_transition`, `idempotency`, `cache`, `error`, `status`, `timeout`, `unknown_precedence`, `path`, `repository_lifecycle`, `platform`, and `compatibility`. Every dimension is either `covered`, with references to existing Requirements requirement/scenario/case identities, or `not_applicable`, with a non-empty rationale. Covered cases identify their earliest required execution stage: `slice`, `commit`, `prepush`, or `ci`. -At Requirements `planned` maturity, the contract binds the existing mapping/plan digest and exact requirement, scenario, and verification-case identities plus their method, intent, observable, and touchpoints; it does not invent or require a selector before a test exists. After failing-first test authoring, the Requirements-owned test-authored plan supplies the exact pytest selector. Preflight verifies that it refines the same planned case and requires explicit approval of a successor seal, preserving the implementation-lineage origin, before production implementation proceeds. A later checkpoint may then select a subset of the exact identities bound by that test-authored successor. Adding, removing, replacing, or changing a bound identity requires validation, approval, and a new seal. Existing Requirements contracts remain authoritative for maturity, pytest selector syntax, plan identity, and JUnit reconciliation. +When an explicitly selected assurance policy requires the seal workflow, at +Requirements `planned` maturity, the contract binds the existing mapping/plan digest and exact requirement, scenario, and verification-case identities plus their method, intent, observable, and touchpoints; it does not invent or require a selector before a test exists. After failing-first test authoring, the Requirements-owned test-authored plan supplies the exact pytest selector. Preflight verifies that it refines the same planned case and requires explicit approval of a successor seal, preserving the implementation-lineage origin, before production implementation proceeds. A later checkpoint may then select a subset of the exact identities bound by that test-authored successor. Adding, removing, replacing, or changing a bound identity requires validation, approval, and a new seal. Existing Requirements contracts remain authoritative for maturity, pytest selector syntax, plan identity, and JUnit reconciliation. + +This successor-seal sequence applies only inside that selected policy. Ordinary +MEB implementation does not require a seal or frozen selector mapping. ### 3. Deterministic validation result diff --git a/openspec/changes/preflight-01-design-contract-core/proposal.md b/openspec/changes/preflight-01-design-contract-core/proposal.md index 59ea557bf..ed6aa481c 100644 --- a/openspec/changes/preflight-01-design-contract-core/proposal.md +++ b/openspec/changes/preflight-01-design-contract-core/proposal.md @@ -1,5 +1,11 @@ # Change: Deterministic Pre-Implementation Design Contract Core +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + ## Why Implementation can start from change artifacts that are individually plausible but collectively stale, incomplete, contradictory, or wider than the approved intent. The core repository needs a stable, runtime-neutral contract for representing what was reviewed, what remained unknown, what was approved, and whether the reviewed inputs have changed. diff --git a/openspec/changes/preflight-01-design-contract-core/specs/preflight-assurance-contracts/spec.md b/openspec/changes/preflight-01-design-contract-core/specs/preflight-assurance-contracts/spec.md index 7307de862..040b95789 100644 --- a/openspec/changes/preflight-01-design-contract-core/specs/preflight-assurance-contracts/spec.md +++ b/openspec/changes/preflight-01-design-contract-core/specs/preflight-assurance-contracts/spec.md @@ -1,9 +1,25 @@ +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + ## ADDED Requirements ### Requirement: Normalized preflight design contract +These assurance obligations SHALL apply only when an explicitly selected +preflight assurance policy requests them; they SHALL NOT gate ordinary MEB delivery. + The system SHALL define a versioned preflight design contract that records the exact reviewed change identity, source identities, implementation-lineage identity and origin baseline, role-classified scope, component ownership, approved influence relationships or explicit no-impact dispositions, exclusions, assumptions, unknowns, dependencies, interfaces, acceptance criteria, risk dimensions, verification stages, test intent, risks, rollback intent, and approval policy. The first approved seal in an implementation lineage SHALL set the immutable origin repository plus full base commit/tree identities. Every successor seal for refinement or reapproval SHALL bind its predecessor seal and preserve that origin baseline even when its current reviewed source snapshot changes. +#### Scenario: Ordinary delivery has not selected preflight assurance + +- **GIVEN** ordinary MEB delivery has not selected a preflight assurance policy +- **WHEN** implementation or delivery readiness is evaluated +- **THEN** no preflight seal, checkpoint, successor approval, or historical chronology is required +- **AND** required current checks still apply, without claiming that unevaluated optional assurance passed. + #### Scenario: Contract preserves source and scope identity - **GIVEN** a change assembled from OpenSpec, repository governance, GitHub metadata, and repository state @@ -41,6 +57,9 @@ The system SHALL define a versioned preflight design contract that records the e ### Requirement: Seal-bound semantic verification intent +These assurance obligations SHALL apply only when an explicitly selected +preflight assurance policy requests them; they SHALL NOT gate ordinary MEB delivery. + The system SHALL bind the closed semantic risk dimensions `boundary`, `malformed_or_missing_input`, `state_transition`, `idempotency`, `cache`, `error`, `status`, `timeout`, `unknown_precedence`, `path`, `repository_lifecycle`, `platform`, and `compatibility` plus existing Requirements verification-plan identities without defining a second test-selector contract. Every affected behavior or interface SHALL contain every closed dimension. #### Scenario: Applicable risk dimension is covered @@ -122,6 +141,9 @@ The system SHALL define versioned canonical bytes for digesting preflight contra ### Requirement: Approval seal contract +These assurance obligations SHALL apply only when an explicitly selected +preflight assurance policy requests them; they SHALL NOT gate ordinary MEB delivery. + The system SHALL define a seal that binds an exact contract digest, validation-result digest, source-snapshot digest, implementation-lineage identity, immutable origin repository/base commit/base tree, monotonic lineage sequence, optional predecessor-seal digest, approval decision, approver identity, and approval time. #### Scenario: Approved and unchanged contract verifies diff --git a/openspec/changes/preflight-01-design-contract-core/tasks.md b/openspec/changes/preflight-01-design-contract-core/tasks.md index 1d6914bd3..be7994a44 100644 --- a/openspec/changes/preflight-01-design-contract-core/tasks.md +++ b/openspec/changes/preflight-01-design-contract-core/tasks.md @@ -1,5 +1,13 @@ # Tasks: preflight-01-design-contract-core +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + +Implementation discipline: use focused regression/reproduction and current-run results. Any task requiring immutable hosted RED, frozen test authoring or approval receipts merely to implement this feature is superseded; tests of an explicitly selected optional seal feature remain in scope. + All tasks below are future implementation work. This planning change completes none of them and creates no `TDD_EVIDENCE.md`. ## 1. Dedicated session, worktree, and readiness @@ -12,7 +20,7 @@ All tasks below are future implementation work. This planning change completes n - [ ] 2.1 Finalize the delta scenarios and exact public interface names for role-classified scope, component ownership, per-input influence/no-impact disposition, risk disposition, execution stages, and Requirements-plan references without widening into CLI, persistence, validators, skills, or adapters. - [ ] 2.2 Add tests mapped to every contract, influence/no-impact completeness, risk-matrix, planned-to-test-authored Requirements lifecycle, selector reconciliation, implementation-lineage/predecessor seal and monotonic sequence, canonicalization, seal, and verifier scenario. -- [ ] 2.3 Run the targeted tests before production edits, capture expected failures, and create `TDD_EVIDENCE.md` with the red evidence. +- [ ] 2.3 Run targeted tests before production edits and summarize meaningful failures briefly using existing local or CI output; no authored TDD ledger is required under the effective lean policy. ## 3. Minimal core implementation @@ -25,10 +33,10 @@ All tasks below are future implementation work. This planning change completes n - [ ] 4.1 Re-run mapped tests and capture passing evidence after implementation. - [ ] 4.2 Run required format, type, lint, contract, smart-test, test, and SpecFact code-review gates for the touched scope; resolve all findings. - [ ] 4.3 Run `openspec status --change preflight-01-design-contract-core --json` and `openspec validate preflight-01-design-contract-core --strict`. -- [ ] 4.4 Update documentation and `TDD_EVIDENCE.md` only with observed commands and results. +- [ ] 4.4 Update documentation and concise validation notes only with observed commands and results. ## 5. Delivery and post-merge cleanup -- [ ] 5.1 Reconfirm the diff contains only issue-approved scope and that downstream modules contracts reference the released core interface identity. -- [ ] 5.2 Open the implementation PR to `dev` as the final pre-merge task, linking the issue and evidence. +- [ ] 5.1 Reconfirm issue-approved scope and prepare the downstream interface handoff against the candidate; do not claim it is released yet. +- [ ] 5.2 Open and integrate the reviewed implementation PR to `dev`, then promote through protected `main` and publish the core interface through the existing release workflow after required checks. - [ ] 5.3 Before archive, verify the released core identity was handed to modules #431 and that modules-owned signing/publication remains assigned to #432; then, from the repository root after merge, run `openspec archive preflight-01-design-contract-core`, update ordering/source mirrors, and remove the dedicated worktree and merged branch. diff --git a/openspec/changes/preflight-03-dogfood-hardening-and-release/CHANGE_VALIDATION.md b/openspec/changes/preflight-03-dogfood-hardening-and-release/CHANGE_VALIDATION.md index ef9049914..c7f92f831 100644 --- a/openspec/changes/preflight-03-dogfood-hardening-and-release/CHANGE_VALIDATION.md +++ b/openspec/changes/preflight-03-dogfood-hardening-and-release/CHANGE_VALIDATION.md @@ -33,3 +33,7 @@ ## Decision The proposal is ready for review and a planning-only PR. Dogfood and implementation remain explicitly unstarted and must begin later in a dedicated issue-linked session. + +## Planning review follow-up — 2026-09-20 + +Bound both C14 #680 and exact modules #431 runtime/workflow/validator identities inside the normative dogfood requirement, including UNKNOWN/no-go on mismatch without a circular stable-release prerequisite. Strict OpenSpec and scoped Markdown/whitespace pass. diff --git a/openspec/changes/preflight-03-dogfood-hardening-and-release/design.md b/openspec/changes/preflight-03-dogfood-hardening-and-release/design.md index d7c21b78c..5b6f3a44a 100644 --- a/openspec/changes/preflight-03-dogfood-hardening-and-release/design.md +++ b/openspec/changes/preflight-03-dogfood-hardening-and-release/design.md @@ -1,5 +1,11 @@ ## Context +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. The dogfood exercise requires both core C14 #680 and modules runtime #431; record both native prerequisites now that C14 no longer depends on #431. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + Core C14 adoption is a useful first dogfood subject because it is cross-repository, evidence-sensitive, and explicitly bounded by signed module handoff and native dependency metadata. The dogfood protocol tests whether the preflight loop detects planning drift before implementation without silently becoming a second source of truth. ## Goals / Non-Goals @@ -21,7 +27,7 @@ Core C14 adoption is a useful first dogfood subject because it is cross-reposito ### 1. C14 is the first mandatory target -The protocol runs against the current accepted core C14 change and linked issue #680 using immutable repository and GitHub identities. It records the starting artifacts before any authorized refinement. Existing C14/C15 worktrees remain untouched by this planning setup; future dogfood occurs only in the C14 owner session or a read-only snapshot. +The protocol replays an immutable C14 planning snapshot after independent C14 delivery, using recorded repository and issue #680 identities. It records starting artifacts before authorized fixture refinements. Shipped C14 history and existing C14/C15 worktrees remain untouched; a real product defect requires a separately scoped owner-authorized follow-up, never retroactive preflight gating. ### 2. Evidence separates four defect owners @@ -36,7 +42,7 @@ Ambiguous classification is recorded as unknown and cannot authorize hardening s ### 3. Before/after evidence is identity-bound -Each run records source revisions, contract/result digests, validator identities, findings, user decisions, approved artifact edits, rerun identities, and seal verification. A narrative summary may explain results but cannot replace the machine-readable identities. +Each run binds both independently delivered C14 #680 and the selected #431 runtime commit/tree, workflow and validator digests; #431 may still be the unpublished dogfood candidate. Missing or mismatched identity yields UNKNOWN/no-go. Each run records source revisions, contract/result digests, validator identities, findings, user decisions, approved artifact edits, rerun identities, and seal verification. A narrative summary may explain results but cannot replace the machine-readable identities. ### 4. Readiness requires observed usefulness and safety diff --git a/openspec/changes/preflight-03-dogfood-hardening-and-release/proposal.md b/openspec/changes/preflight-03-dogfood-hardening-and-release/proposal.md index d2786edf0..6cc3bc836 100644 --- a/openspec/changes/preflight-03-dogfood-hardening-and-release/proposal.md +++ b/openspec/changes/preflight-03-dogfood-hardening-and-release/proposal.md @@ -1,16 +1,22 @@ # Change: Preflight Dogfood Evidence and Readiness Decision +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. The dogfood exercise requires both core C14 #680 and modules runtime #431; record both native prerequisites now that C14 no longer depends on #431. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + ## Why The preflight loop should not be stabilized or distributed based only on its own specification. It must first be used against the kind of dense, cross-repository change that exposed scope, dependency, ownership, and evidence drift. Core C14 adoption [#680](https://github.com/nold-ai/specfact-cli/issues/680) is the first bounded dogfood target and provides a concrete readiness gate before module hardening and publication. ## What Changes -- **NEW**: A core-owned dogfood evidence protocol for running the exact modules preflight loop against the current C14 core adoption change before implementation or further scope changes. +- **NEW**: A core-owned dogfood evidence protocol for running the exact modules preflight loop against an immutable C14 planning snapshot after independent C14 delivery, replaying the pre-implementation review without gating that delivery. - **NEW**: Before/after records for input identities, findings, user-approved refinements, reruns, approval state, seal verification, elapsed operator steps, and false-positive/false-negative observations. - **NEW**: Readiness criteria that distinguish contract defects, runtime defects, source-artifact defects, and documentation/instruction defects. - **NEW**: A decision record that either authorizes evidence-backed modules hardening or blocks it with reproducible findings. -- **CLARIFY**: Dogfood may propose refinements to C14 artifacts, but only the C14 owner may authorize and apply them in its dedicated issue-linked session. +- **CLARIFY**: Dogfood may propose owner-authorized fixture refinements; real C14 defects require separately scoped follow-ups, without rewriting shipped planning history. ## Capabilities @@ -32,7 +38,7 @@ The preflight loop should not be stabilized or distributed based only on its own - Parent Feature: core [#681](https://github.com/nold-ai/specfact-cli/issues/681). - Upstream prerequisites are core `preflight-01-design-contract-core` [#682](https://github.com/nold-ai/specfact-cli/issues/682) and modules `preflight-02-assurance-runtime` [#431](https://github.com/nold-ai/specfact-cli-modules/issues/431). -- Blocked by core C14 adoption [#680](https://github.com/nold-ai/specfact-cli/issues/680), which consumes those prerequisites in the ordering recorded by `openspec/CHANGE_ORDER.md`; operators verify the exact issue identities and native blocker readback before dogfood. +- Blocked by core C14 adoption [#680](https://github.com/nold-ai/specfact-cli/issues/680), delivered independently of #431; #683 requires both the C14 snapshot and the #431 runtime. Operators verify both native prerequisites before dogfood. - Blocks the paired modules `preflight-03-dogfood-hardening-and-release` story [#432](https://github.com/nold-ai/specfact-cli-modules/issues/432). ## Explicit Non-Goals diff --git a/openspec/changes/preflight-03-dogfood-hardening-and-release/specs/preflight-dogfood-readiness/spec.md b/openspec/changes/preflight-03-dogfood-hardening-and-release/specs/preflight-dogfood-readiness/spec.md index 6449ae185..8abe3cf46 100644 --- a/openspec/changes/preflight-03-dogfood-hardening-and-release/specs/preflight-dogfood-readiness/spec.md +++ b/openspec/changes/preflight-03-dogfood-hardening-and-release/specs/preflight-dogfood-readiness/spec.md @@ -1,19 +1,41 @@ +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. The dogfood exercise requires both core C14 #680 and modules runtime #431; record both native prerequisites now that C14 no longer depends on #431. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + ## ADDED Requirements ### Requirement: Identity-bound C14 dogfood run -The dogfood protocol SHALL run the exact preflight workflow against core C14 issue #680 and its current proposal artifacts using recorded repository, issue, dependency, validator, and source identities. +These assurance obligations SHALL apply only when an explicitly selected +preflight assurance policy requests them; they SHALL NOT gate ordinary MEB delivery. + +The dogfood protocol SHALL require both independently delivered core C14 #680 and the exact selected modules preflight runtime #431. It SHALL bind the immutable C14 planning snapshot, #431 runtime commit/tree, workflow and validator digests, repository and issue identities before execution. The #431 runtime may be an unpublished candidate under the existing dogfood protocol; this does not require its later #432 stable release. Missing or mismatched required identities SHALL yield UNKNOWN and a no-go readiness decision. + +#### Scenario: Ordinary delivery has not selected preflight assurance + +- **GIVEN** ordinary MEB delivery has not selected a preflight assurance policy +- **WHEN** implementation or delivery readiness is evaluated +- **THEN** no preflight seal, checkpoint, successor approval, or historical chronology is required +- **AND** required current checks still apply, without claiming that unevaluated optional assurance passed. #### Scenario: Initial dogfood snapshot is captured -- **GIVEN** C14 is ready for a pre-implementation review and no concurrent ownership ambiguity exists +- **GIVEN** C14 has been independently delivered, its immutable planning snapshot and exact #431 runtime/workflow/validator identities are selected, and ownership is unambiguous - **WHEN** dogfood begins - **THEN** the starting source identities and expected-risk inventory are recorded before refinement - **AND** the C14 implementation worktree is not modified by the dogfood setup. +#### Scenario: Required modules runtime identity is missing or stale + +- **GIVEN** the selected #431 runtime, workflow or validator identity is absent or does not match the executing inputs +- **WHEN** dogfood readiness is evaluated +- **THEN** the result is UNKNOWN and the hardening decision is no-go, even if C14 identity matches. + ### Requirement: User-authorized refinement only -The dogfood protocol SHALL separate tool findings from source changes and SHALL require the owning user/session to authorize every material refinement. +The dogfood protocol SHALL separate tool findings from source changes and SHALL require the owning user/session to authorize every material refinement. Replay refinements SHALL change only the selected fixture; real product corrections require separately scoped follow-ups and SHALL NOT rewrite shipped C14 history. #### Scenario: Finding requires C14 scope clarification @@ -24,7 +46,7 @@ The dogfood protocol SHALL separate tool findings from source changes and SHALL #### Scenario: Authorized C14 artifact changes -- **GIVEN** the C14 owner applies an approved refinement +- **GIVEN** the C14 owner authorizes a refinement of the selected replay fixture - **WHEN** dogfood resumes - **THEN** all prior readiness and approval state is discarded - **AND** a new snapshot and complete validator run are required. @@ -42,6 +64,9 @@ Every dogfood observation SHALL be assigned to core contract, modules runtime, s ### Requirement: Readiness decision criteria +These assurance obligations SHALL apply only when an explicitly selected +preflight assurance policy requests them; they SHALL NOT gate ordinary MEB delivery. + The core dogfood change SHALL emit a go/no-go decision for modules hardening based on deterministic completion, approval safety, stale-input invalidation, renderer parity, and evidence-backed issue classification. #### Scenario: Readiness criteria fail diff --git a/openspec/changes/preflight-03-dogfood-hardening-and-release/tasks.md b/openspec/changes/preflight-03-dogfood-hardening-and-release/tasks.md index 4ca977956..7ef1b9cf5 100644 --- a/openspec/changes/preflight-03-dogfood-hardening-and-release/tasks.md +++ b/openspec/changes/preflight-03-dogfood-hardening-and-release/tasks.md @@ -1,5 +1,13 @@ # Tasks: preflight-03-dogfood-hardening-and-release (core dogfood) +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. The dogfood exercise requires both core C14 #680 and modules runtime #431; record both native prerequisites now that C14 no longer depends on #431. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + +Implementation discipline: use focused regression/reproduction and current-run results. Any task requiring immutable hosted RED, frozen test authoring or approval receipts merely to implement this feature is superseded; tests of an explicitly selected optional seal feature remain in scope. + All tasks below are future dogfood work. This planning change completes none of them and creates no dogfood or TDD evidence. ## 1. Dedicated session, worktree, and readiness @@ -12,13 +20,13 @@ All tasks below are future dogfood work. This planning change completes none of - [ ] 2.1 Finalize the C14 dogfood protocol, expected-risk inventory, defect classes, evidence schema, and readiness thresholds before running the tool. - [ ] 2.2 Add protocol/fixture tests for evidence completeness, stale identity rejection, classification, and decision aggregation. -- [ ] 2.3 Run those tests before any behavior edit, capture failing-first evidence, and create `TDD_EVIDENCE.md` only if implementation support is required. +- [ ] 2.3 Run those tests before any behavior edit and summarize meaningful failures briefly using existing local or CI results; no authored TDD ledger is required under the effective lean policy. ## 3. Read-only C14 dogfood and approved refinement -- [ ] 3.1 Capture the immutable starting C14/OpenSpec/GitHub/repository snapshot without editing the existing C14 worktree. +- [ ] 3.1 Capture the delivered C14 planning snapshot for read-only pre-implementation review replay; never edit shipped history or make #680 wait for dogfood. - [ ] 3.2 Run the exact installed preflight loop and retain normalized result identities plus human output. -- [ ] 3.3 Classify every observation and present exact source-owned refinements to the C14 owner. +- [ ] 3.3 Classify every observation and present fixture corrections or separately scoped product follow-ups to the C14 owner. - [ ] 3.4 After any separately authorized refinement, capture a new snapshot and rerun the complete loop; never reuse prior approval. ## 4. Readiness decision and verification diff --git a/openspec/changes/preflight-05-implementation-conformance/design.md b/openspec/changes/preflight-05-implementation-conformance/design.md index d94572c80..35b43f082 100644 --- a/openspec/changes/preflight-05-implementation-conformance/design.md +++ b/openspec/changes/preflight-05-implementation-conformance/design.md @@ -1,5 +1,11 @@ ## Context +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. This contract and modules #434 no longer block generic skill installation #251/#253 or native review support. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + The preflight seal binds an approved plan and source snapshot. Implementation necessarily changes repository state, so the original seal becomes a reference contract rather than a claim about the resulting code. This change defines the core data boundary for repeatable local checkpoints and a later immutable-range comparison. ## Goals / Non-Goals diff --git a/openspec/changes/preflight-05-implementation-conformance/proposal.md b/openspec/changes/preflight-05-implementation-conformance/proposal.md index 0576f6673..8a8c8555e 100644 --- a/openspec/changes/preflight-05-implementation-conformance/proposal.md +++ b/openspec/changes/preflight-05-implementation-conformance/proposal.md @@ -1,5 +1,11 @@ # Change: Seal-Bound Implementation Assurance Core +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. This contract and modules #434 no longer block generic skill installation #251/#253 or native review support. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + ## Why Pre-implementation assurance freezes what was reviewed and approved, but it deliberately does not prove that later code matches that contract. Core needs one reusable comparison boundary for cheap local implementation checkpoints and final immutable-range conformance without allowing local evidence to masquerade as pull-request authority. @@ -26,7 +32,7 @@ Pre-implementation assurance freezes what was reviewed and approved, but it deli ## Impact - Planning artifacts only in this phase. No production or test code, runtime command, generated snapshot/result, module, skill, adapter, manifest, signature, version, or dependency is created. -- This change starts after the stable preflight module handoff and before generic skill installation, generated instructions, or harness adapters. +- This optional change starts after the stable preflight module handoff. Its paired runtime #434 precedes optional adapters #433, which also need core #253. Generic #251/#253 can proceed independently. - Modules owns checkpoint/conformance execution, evidence extraction, rendering, bounded agent handoff, persistence, and release in the paired change. ## Dependencies diff --git a/openspec/changes/preflight-05-implementation-conformance/specs/preflight-implementation-conformance-contracts/spec.md b/openspec/changes/preflight-05-implementation-conformance/specs/preflight-implementation-conformance-contracts/spec.md index 6858be8e2..20f79c5fb 100644 --- a/openspec/changes/preflight-05-implementation-conformance/specs/preflight-implementation-conformance-contracts/spec.md +++ b/openspec/changes/preflight-05-implementation-conformance/specs/preflight-implementation-conformance-contracts/spec.md @@ -1,13 +1,29 @@ +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. This contract and modules #434 no longer block generic skill installation #251/#253 or native review support. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + ## ADDED Requirements ### Requirement: Versioned implementation snapshot +These assurance obligations SHALL apply only when an explicitly selected +preflight assurance policy requests them; they SHALL NOT gate ordinary MEB delivery. + The system SHALL define a versioned implementation snapshot whose kind is `worktree`, `index`, or `range` and which contains repository identity, exact kind-specific Git identity, complete changed-path manifest, public-interface records, test/evidence references, and producer, policy, toolchain, and extractor identities. The `worktree` kind SHALL bind a full base commit ID and worktree-manifest digest and include staged, unstaged, and untracked state relative to that base. The `index` kind SHALL bind a full base commit ID and exact index tree ID and exclude untracked paths unless staged as additions. The `range` kind SHALL bind full base/head commit IDs and base/head tree IDs and SHALL NOT represent untracked paths. Every snapshot base SHALL equal the seal-bound implementation-lineage origin repository/base commit/base tree, including after refinement or reapproval. A range head SHALL be proven to descend from that origin by a producer/policy/toolchain-bound ancestry attestation, SHALL equal a separately supplied policy-authorized current delivery-target commit/tree identity, and its manifest SHALL cover the complete lineage-origin-to-delivery-head range. Every kind SHALL preserve additions, deletions, both rename endpoints, before/after modes, symlink target identity, and byte-preserving path identity where those states exist. Rename interpretation SHALL be bound to producer, policy, and toolchain identity. Every governed manifest path that policy classifies as capable of defining a public interface SHALL carry normalized base/current public-interface observations regardless of whether its sealed role is `source`, `test`, `docs`, `generated`, or `evidence`. A side that does not exist by construction for an addition, deletion, or rename endpoint SHALL be represented by a normalized `absent` record/tombstone rather than omitted. The tombstone SHALL bind the same path, role, exact snapshot, extractor identity/version/configuration, policy/toolchain, and Git-transition provenance required of a present observation and SHALL contain no fabricated interface members. Deterministic comparison of `absent` with `present` SHALL derive interface additions/removals, including both rename endpoints. Missing, unsupported, incomplete, stale, ambiguous, wrong-snapshot, wrong-extractor, or unverifiable-absence observations SHALL be `unverifiable`; an omitted record, an `absent` record for an existing path, or a caller-supplied empty set SHALL NOT prove no changed interface. +#### Scenario: Ordinary delivery has not selected preflight assurance + +- **GIVEN** ordinary MEB delivery has not selected a preflight assurance policy +- **WHEN** implementation or delivery readiness is evaluated +- **THEN** no preflight seal, checkpoint, successor approval, or historical chronology is required +- **AND** required current checks still apply, without claiming that unevaluated optional assurance passed. + #### Scenario: Snapshot preserves complete Git path semantics - **GIVEN** an implementation adds, deletes, renames, changes mode, symlinks, or introduces an untracked path supported by its snapshot kind @@ -52,6 +68,9 @@ Every governed manifest path that policy classifies as capable of defining a pub ### Requirement: Sealed obligation mapping +These assurance obligations SHALL apply only when an explicitly selected +preflight assurance policy requests them; they SHALL NOT gate ordinary MEB delivery. + The system SHALL map approved scope roles, component ownership, interfaces, acceptance criteria, risk rows, Requirements-plan references, test intent, verification stages, and exclusions from the canonical latest valid preflight seal to normalized implementation evidence. The verifier SHALL accept a separately supplied policy-authorized canonical lineage-tip identity binding the change/lineage, latest seal digest and monotonic sequence, complete predecessor-chain digest, and authority/source identity; the selected seal SHALL equal that tip. A checkpoint MAY carry the deterministic affected subset for its sealed stage/profile. A final range result SHALL bind the obligation-map digest and SHALL require the exhaustive transitive closure for every changed governed path/interface and every applicable sealed component, acceptance criterion, risk row, Requirements case, component target, stage including `ci`, and exclusion. Every evidence record SHALL carry a producer authority class and verifiable provenance bound to its exact snapshot or range. An obligation whose earliest stage is `ci` SHALL be satisfiable only by evidence from a seal/policy-authorized protected-CI producer with authenticated provenance bound to the exact immutable range; local or caller-asserted producer identity SHALL NOT satisfy it. #### Scenario: Selected seal is not the canonical lineage tip @@ -84,6 +103,9 @@ The system SHALL map approved scope roles, component ownership, interfaces, acce ### Requirement: Separate local checkpoint result +These assurance obligations SHALL apply only when an explicitly selected +preflight assurance policy requests them; they SHALL NOT gate ordinary MEB delivery. + The system SHALL define a `DevelopmentCheckpointResult` with `PASS`, `FAIL`, `UNKNOWN`, or `NOT_APPLICABLE` status and authority limited to `local_worktree` or `local_index`. #### Scenario: Local result cannot become PR authority @@ -102,6 +124,9 @@ The system SHALL define a `DevelopmentCheckpointResult` with `PASS`, `FAIL`, `UN ### Requirement: Immutable implementation conformance result +These assurance obligations SHALL apply only when an explicitly selected +preflight assurance policy requests them; they SHALL NOT gate ordinary MEB delivery. + The system SHALL define an `ImplementationConformanceResult` that accepts only an explicit immutable range identity containing repository identity, full base and head commit IDs, base and head tree identities, the policy-authorized canonical seal-lineage tip identity, and the policy-authorized current delivery-target identity. Its complete path manifest and tree attestations SHALL bind to that exact repository, lineage-origin/current-delivery-head range, and selected latest seal. #### Scenario: Worktree evidence is supplied as final conformance @@ -113,6 +138,9 @@ The system SHALL define an `ImplementationConformanceResult` that accepts only a ### Requirement: Closed implementation assurance finding classes +These assurance obligations SHALL apply only when an explicitly selected +preflight assurance policy requests them; they SHALL NOT gate ordinary MEB delivery. + Checkpoint and conformance results SHALL distinguish mutually exclusive `missing`, `unexpected`, `modified`, `violated`, `stale`, and `unverifiable` findings with stable source and evidence identities. Classification precedence SHALL be `stale`, `unverifiable`, `unexpected`, `missing`, `modified`, then `violated`: stale identifies changed seal-bound inputs; unverifiable identifies absent, ambiguous, unsupported, or unreconciled required identity/evidence; unexpected identifies implementation without a sealed mapping; missing identifies a sealed required counterpart with no implementation/evidence; modified identifies a counterpart whose structural identity differs; violated identifies reconciled identities and executed evidence whose semantic observable differs from the sealed expectation. `stale` and `unverifiable` SHALL be blocking uncertainty, `unexpected` SHALL be blocking failure, and required `missing`, `modified`, and `violated` SHALL be blocking failure. Only sealed policy that already marks an obligation non-required may make `missing`, `modified`, or `violated` advisory. Aggregation SHALL return `FAIL` when any determinate blocking failure exists, otherwise `UNKNOWN` when blocking uncertainty exists, otherwise `PASS`. Caller-owned applicability MAY construct `NOT_APPLICABLE`; the verifier SHALL NOT infer it. diff --git a/openspec/changes/preflight-05-implementation-conformance/tasks.md b/openspec/changes/preflight-05-implementation-conformance/tasks.md index 315cd8e0a..c5fc2f9a8 100644 --- a/openspec/changes/preflight-05-implementation-conformance/tasks.md +++ b/openspec/changes/preflight-05-implementation-conformance/tasks.md @@ -1,5 +1,13 @@ # Tasks: preflight-05-implementation-conformance (core contracts) +## Scope rescope — 2026-09-20 + +Seal, checkpoint, frozen mapping, successor approval, and historical RED/GREEN requirements in this issue apply only when an explicitly selected assurance policy requests them. They are not prerequisites for ordinary implementation, Code Review, release promotion, skill installation, or generated instructions. Keep the internal optional-feature dependency chain and source/signature integrity. Missing optional chronology is not a failed current-execution claim. No runtime policy changes in this planning update. This contract and modules #434 no longer block generic skill installation #251/#253 or native review support. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + +Implementation discipline: use focused regression/reproduction and current-run results. Any task requiring immutable hosted RED, frozen test authoring or approval receipts merely to implement this feature is superseded; tests of an explicitly selected optional seal feature remain in scope. + All tasks below are future implementation work. This planning change completes none of them and creates no `TDD_EVIDENCE.md`. ## 1. Dedicated session, worktree, and readiness @@ -21,7 +29,7 @@ All tasks below are future implementation work. This planning change completes n path; checkpoint-subset and exhaustive final-obligation closure; protected-CI producer authorization and exact-range provenance; finding-class/precedence, including the reconciled semantic `violated` scenario; stale-seal; evidence; and assurance-limit scenarios. -- [ ] 2.3 Run targeted tests before production edits and record failing-first results in a newly created `TDD_EVIDENCE.md`. +- [ ] 2.3 Run relevant regression tests before production edits, summarize meaningful failures briefly, and reference ordinary CI artifacts where available; no authored TDD ledger is required. ## 3. Minimal core implementation @@ -39,6 +47,6 @@ All tasks below are future implementation work. This planning change completes n ## 5. Delivery and post-merge cleanup -- [ ] 5.1 Hand the exact released core implementation-assurance interface identity to paired modules #434 before #251/#253/#433 begin. -- [ ] 5.2 Open the implementation PR to `dev` as the final pre-merge task, linking the paired modules issue and evidence. +- [ ] 5.1 Prepare the candidate core implementation-assurance interface handoff for paired modules #434; the released identity is supplied only after 5.2. Optional adapters #433 require both its signed release and core #253; generic #251/#253 do not wait for this handoff. +- [ ] 5.2 Open and integrate the reviewed implementation PR to `dev`, then promote through protected `main` and publish the core interface through the existing release workflow after required checks. - [ ] 5.3 Before archive, verify the released core identity was handed to modules #434 and that modules-owned signing/stable publication remains assigned there; then, from the repository root after merge, run `openspec archive preflight-05-implementation-conformance`, update ordering/source mirrors, and remove the dedicated worktree and merged branch. diff --git a/openspec/changes/requirements-07-runtime-proof-delivery/design.md b/openspec/changes/requirements-07-runtime-proof-delivery/design.md index 59876a5dd..b2c03a842 100644 --- a/openspec/changes/requirements-07-runtime-proof-delivery/design.md +++ b/openspec/changes/requirements-07-runtime-proof-delivery/design.md @@ -1,5 +1,11 @@ ## Context +## Scope rescope — 2026-09-20 + +Unimplemented corrected R07 delivery ownership transfers to and . Keep this issue open solely to reconcile the replacement after delivery; do not implement a competing R07 pipeline or wait for abandoned R08. Replace its old native blocker on closed modules #368 with . Existing shipped behavior remains historical context. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + The Requirements module owns mappings, maturity, plan construction, and reconciliation semantics. Core owns immutable module selection, Git snapshot selection, safe subprocess execution, environment limits, artifact retention, workflow ordering, and branch-protection integration. The previous R07 design treated current-run execution and historical failing-first chronology as one maturity ladder. This forced core to decide which repository inputs could change after a retained red result. For arbitrary Python and pytest, that becomes an open-ended execution model rather than a bounded evidence protocol. @@ -70,4 +76,3 @@ The implementation must not cherry-pick PR #671 or add static rules for Python i 4. Regenerate evidence and enable strict current-run enforcement. 5. Implement R08 separately. 6. Roll back by reverting the core adaptation and fixture pin; retained artifacts remain auditable. - diff --git a/openspec/changes/requirements-07-runtime-proof-delivery/proposal.md b/openspec/changes/requirements-07-runtime-proof-delivery/proposal.md index c854f5426..1c17e083a 100644 --- a/openspec/changes/requirements-07-runtime-proof-delivery/proposal.md +++ b/openspec/changes/requirements-07-runtime-proof-delivery/proposal.md @@ -1,32 +1,39 @@ # Change: Deliver Current-Run Requirements Evidence in Local and CI Gates +## Scope rescope — 2026-09-20 + +Unimplemented corrected R07 delivery ownership transfers to and . Keep this issue open solely to reconcile the replacement after delivery; do not implement a competing R07 pipeline or wait for abandoned R08. Replace its old native blocker on closed modules #368 with . Existing shipped behavior remains historical context. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + ## Why Issue #662 asks SpecFact to execute exact tests linked to changed requirement scenarios and to report whether those tests were collected and passed in the current delivery run. The existing R07 implementation mixed that bounded observation with a stronger historical claim: proving that the tests failed earlier and remained unchanged through arbitrary Python and pytest dependency behavior. That stronger claim caused the gate to grow into a static approximation of pytest execution. It also made a current-run Requirements result depend on historical evidence that issue #662 did not require. The two claims need independent contracts and independent statuses. -## What Changes +## Current scope -- Keep R07 responsible for lifecycle-derived planning maturity, accepted mappings, exact selector plans, safe current-run execution, JUnit reconciliation, explicit no-impact decisions, artifact publication, and the independent Code Review handoff. -- Define a current-run pass precisely: every required selector was collected exactly once and passed at the evaluated source revision in the pinned execution environment. -- Remove Git-bound failing-first proof, legacy-ledger migration, and static pytest/Python dependency-closure inference from R07. -- Move historical red-to-green proof to `requirements-08-bounded-red-green-proof`, where the red and final commits are replayed under an explicit bounded Git policy. -- Preserve Requirements and Code Review as independent verdicts. Requirements evidence is review context and provenance; it does not rewrite review findings or scores. +R07 is reconciliation-only. #740 and paired modules #481 own the corrected +current-run implementation, signed handoff and rollout. Do not implement the +former R07 correction or wait for abandoned R08. -## Capabilities +After R09 delivery, verify that #662's useful current-run obligations are +covered and retire the superseded unimplemented deltas without applying them +to canonical specs. Preserve shipped behavior and historical evidence; do not +mark abandoned implementation tasks complete or archive them as implemented. -### Modified Capabilities +## Historical capability context -- `requirements-runtime-proof-delivery`: Plan and safely execute exact Requirements selectors, then report current-run observations without implying historical chronology. -- `requirements-evidence-delivery-gate`: Pin the reviewed module release, retain artifacts, and enforce the current-run Requirements and Code Review decisions independently. +The former correction concerned `requirements-runtime-proof-delivery` and +`requirements-evidence-delivery-gate`. Their historical design/spec files are +comparison context only; R09 owns any reconciled canonical-spec changes. ## Impact -- Planning scope: OpenSpec artifacts only in this commit. No runtime, workflow, test, fixture, or report-schema implementation changes are included. -- Later implementation may simplify or replace prior-red provenance code after R08 is ready; it must not add more AST rules for imports, plugins, configuration, data reads, aliases, mutation, or dynamic execution. -- The module-side R07 contract must first separate current execution from historical chronology and publish a signed release before core adopts the corrected report. -- Rollback: retain the current R07 runtime behind its existing branch while the corrected contract is implemented; the planning commit is reversible by reverting one commit. +Planning and replacement reconciliation only. No independent R07 runtime, +workflow, fixture or report-schema implementation remains queued. Historical +evidence is unchanged. Reverting this planning edit restores the prior text. ## Explicit Non-Goals @@ -43,16 +50,11 @@ That stronger claim caused the gate to grow into a static approximation of pytes - **GitHub Issue**: [#662](https://github.com/nold-ai/specfact-cli/issues/662) - **Repository**: `nold-ai/specfact-cli` - **Issue Type**: User Story -- **Last Synced Status**: open, live-read 2026-08-13T21:49:59Z -- **Assignee**: `djm81` -- **Labels**: `enhancement`, `openspec`, `change-proposal` -- **Project Assignment / Project Status**: not exposed by the current connector; implementation is blocked until a fresh hierarchy-cache/project check verifies both and rules out concurrent work. +- **Last Synced Status**: open / replacement reconciliation only, 2026-09-20 - **Parent Feature**: [#374](https://github.com/nold-ai/specfact-cli/issues/374) - **Parent Epic**: [#258](https://github.com/nold-ai/specfact-cli/issues/258) -- **Blocked By**: [specfact-cli-modules#368](https://github.com/nold-ai/specfact-cli-modules/issues/368), closed/completed; the corrected signed release planned by modules PR #412 is the remaining implementation dependency. -- **Concurrency**: no current project status was available to prove whether #662 is already in progress; task B.3 is a mandatory stop condition before implementation. -- **Extends**: `requirements-06-evidence-enforcement`; archive R06 first, then apply this exact-name `MODIFIED` delta. -- **Paired Modules Change**: corrected `requirements-07-scenario-runtime-proof` and [modules PR #412](https://github.com/nold-ai/specfact-cli-modules/pull/412) -- **Follow-up**: `requirements-08-bounded-red-green-proof` -- **Planning correction date**: 2026-08-13 - +- **Blocked By**: replacement delivery [#740](https://github.com/nold-ai/specfact-cli/issues/740) +- **Paired Implementation**: [modules #481](https://github.com/nold-ai/specfact-cli-modules/issues/481) +- **Historical references only**: closed modules #368 and superseded correction PR #412; neither is an active release dependency. +- **R08**: abandoned, not a follow-up prerequisite. +- **Readiness**: refresh hierarchy/project/concurrency state before reconciliation work. diff --git a/openspec/changes/requirements-07-runtime-proof-delivery/specs/requirements-evidence-delivery-gate/spec.md b/openspec/changes/requirements-07-runtime-proof-delivery/specs/requirements-evidence-delivery-gate/spec.md index 469e777cb..54bbcd48b 100644 --- a/openspec/changes/requirements-07-runtime-proof-delivery/specs/requirements-evidence-delivery-gate/spec.md +++ b/openspec/changes/requirements-07-runtime-proof-delivery/specs/requirements-evidence-delivery-gate/spec.md @@ -1,3 +1,9 @@ +## Scope rescope — 2026-09-20 + +Unimplemented corrected R07 delivery ownership transfers to and . Keep this issue open solely to reconcile the replacement after delivery; do not implement a competing R07 pipeline or wait for abandoned R08. Replace its old native blocker on closed modules #368 with . Existing shipped behavior remains historical context. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + ## MODIFIED Requirements ### Requirement: Requirements evidence delivery enforcement diff --git a/openspec/changes/requirements-07-runtime-proof-delivery/specs/requirements-runtime-proof-delivery/spec.md b/openspec/changes/requirements-07-runtime-proof-delivery/specs/requirements-runtime-proof-delivery/spec.md index 1e7fc725d..e2fb596ae 100644 --- a/openspec/changes/requirements-07-runtime-proof-delivery/specs/requirements-runtime-proof-delivery/spec.md +++ b/openspec/changes/requirements-07-runtime-proof-delivery/specs/requirements-runtime-proof-delivery/spec.md @@ -1,3 +1,9 @@ +## Scope rescope — 2026-09-20 + +Unimplemented corrected R07 delivery ownership transfers to and . Keep this issue open solely to reconcile the replacement after delivery; do not implement a competing R07 pipeline or wait for abandoned R08. Replace its old native blocker on closed modules #368 with . Existing shipped behavior remains historical context. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + ## ADDED Requirements ### Requirement: Lifecycle-Derived Requirements Gate @@ -119,4 +125,3 @@ Every governed pull request SHALL publish selected, failed, or deterministic no- - **WHEN** the terminal decision is produced - **THEN** strict policy exits non-zero after retaining diagnostics - **AND** no summary says all validations passed. - diff --git a/openspec/changes/requirements-07-runtime-proof-delivery/tasks.md b/openspec/changes/requirements-07-runtime-proof-delivery/tasks.md index 2dfc8c3d2..34d1d5abe 100644 --- a/openspec/changes/requirements-07-runtime-proof-delivery/tasks.md +++ b/openspec/changes/requirements-07-runtime-proof-delivery/tasks.md @@ -1,104 +1,13 @@ -# Tasks: Correct R07 to Current-Run Requirements Evidence +# Tasks: R07 replacement reconciliation -## 0. Planning-only scope reset +Implementation ownership moved to core [#740](https://github.com/nold-ai/specfact-cli/issues/740) +and modules [#481](https://github.com/nold-ai/specfact-cli-modules/issues/481). +The former implementation queue is superseded, not completed. Its +[historical task list](https://github.com/nold-ai/specfact-cli/blob/d306d2a8e855cba493c8e5b07093565be75e89cc/openspec/changes/requirements-07-runtime-proof-delivery/tasks.md) +is retained in Git history. Existing shipped evidence files remain unchanged. -- [x] 0.1 Reframe R07 around issue #662 current-run acceptance criteria. -- [x] 0.2 Move historical failing-first chronology to R08. -- [x] 0.3 Record explicit non-goals prohibiting Python/pytest dependency-closure inference. -- [x] 0.4 Create this planning commit without runtime, workflow, fixture, test, or schema implementation changes. +## Remaining reconciliation -## Implementation-session checklist — before any unchecked implementation task - -- [ ] B.1 Create a dedicated implementation worktree and feature branch from current `origin/dev`; do not implement from the primary, `dev`, or `main` checkout. -- [ ] B.2 Run `hatch env create`, then `hatch run smart-test-status` and `hatch run contract-test-status` in the worktree; record and resolve unexpected baseline failures before edits. -- [ ] B.3 Read and apply `openspec/config.yaml`, `docs/agent-rules/60-github-change-governance.md`, and their artifact/readiness rules. Refresh and consult `.specfact/backlog/github_hierarchy_cache.md` when available; otherwise use a fresh live GitHub read and record why the cache path was unavailable. Verify issue #662 exists, remains a User Story under Feature #374, has required labels/assignee/project assignment, has complete blocker relationships, and is not concurrently in progress elsewhere. Stop before implementation when project/status/concurrency metadata is unavailable or ambiguous. If the sibling `specfact-cli-internal` checkout exists, consult its applicable wiki guidance. Record all sources, timestamped facts, and constraints in `CHANGE_VALIDATION.md`. Then run exactly `openspec validate requirements-07-runtime-proof-delivery --strict`; require success before tests or source edits and rerun it after every validation-artifact fix. Confirm the accepted specs, dependencies, signed-module prerequisites, and closed file allowlist still match repository reality. -- [ ] B.4 Follow `spec -> tests -> failing evidence -> code -> passing evidence`; in `TDD_EVIDENCE.md`, record separate failing-before and passing-after sections with exact commands, timestamps, actual results, behavioral summaries, environment limitations, and artifact identities. Record passing evidence only after implementation. -- [ ] B.5 Before PR finalization, rerun exactly `openspec validate requirements-07-runtime-proof-delivery --strict` plus the required format, type, lint, YAML, contract, focused/full test, workflow, independent-analysis, signature, and explicit base/head Code Review gates. Update `CHANGE_VALIDATION.md` with validation scope/impact, affected files, exact commands, actual results and test counts, focused/full tests, skipped or unavailable tests/dependencies with reasons, artifact locations/identities, environment limitations, and release hygiene. Keep planning evidence, failing-before evidence, and passing-after evidence separate; rerun affected validation after every artifact fix and resolve every finding or document an approved exception. -- [ ] B.6 After each merge, remove the implementation worktree, delete its local feature branch, run `git worktree prune`, and complete an explicit `AGENTS.md` worktree/policy self-check. Archive only from a dedicated follow-up worktree when the stated release and observation gates are complete. - -## 1. Modules dependency — each task at most two hours - -- [ ] 1.1 In modules PR #412, add failing model tests separating `current_execution` from `red_green_chronology` using only the companion paths listed below. -- [ ] 1.2 In modules PR #412, update the Requirements reconciliation/report schema so current-run pass/fail can be final without historical proof; version the accepted report shape using only the companion paths listed below. -- [ ] 1.3 In modules PR #412, update the Code Review context adapter and its focused tests so `current_execution` is retained without requiring `proof_basis: "red-junit"`; preserve optional `red_green_chronology` as a separate field that cannot rewrite the review verdict. -- [ ] 1.4 Publish the paired module payloads through the repository release generators. Version module manifests, update `CHANGELOG.md`, `registry/index.json`, generated archives/checksums/signatures, and record immutable commit/tree, package/report-schema versions, manifest integrity, approved signing-key fingerprint or trust-root identity, signer/signature identities, and passing package/signature/registry verification evidence. - -## 2. Core failing tests — tests before implementation, each task at most two hours - -- [ ] 2.1 Add `test_current_run_pass_is_not_labelled_passing_after_red`. Allowed file: `tests/unit/workflows/test_requirements_evidence_delivery_workflow.py`. -- [ ] 2.2 Add `test_missing_historical_proof_retains_current_run_observation`. Allowed file: `tests/unit/scripts/test_requirements_evidence_delivery_gate.py`. -- [ ] 2.3 Add `test_unresolved_current_run_cannot_be_pass_or_no_impact`, covering missing scope, missing JUnit, timeout, and tool failure. Allowed file: `tests/unit/scripts/test_requirements_evidence_delivery_gate.py`. -- [ ] 2.4 Add `test_unverified_fixture_identity_fails_before_module_execution` in `tests/unit/workflows/test_requirements_evidence_delivery_workflow.py` and `test_local_adapter_rejects_untrusted_fixture_before_module_execution` in `tests/unit/scripts/test_requirements_evidence_delivery_gate.py`. Both tests reject mismatched repository, commit, tree, package, report-schema, manifest-integrity, signature, clean-state identity, missing/unknown approved public-key fingerprint, or missing/unknown trust-root reference with field-specific diagnostics before module import. -- [ ] 2.5 Before any production edit, update `requirements-evidence.yaml` under the paired released mapping schema: remap `R07-CORE-008-S01` to `tests/unit/workflows/test_requirements_evidence_delivery_workflow.py::test_unverified_fixture_identity_fails_before_module_execution`; convert planned `R07-CORE-008-S02` to `tests/unit/scripts/test_requirements_evidence_delivery_gate.py::test_local_adapter_rejects_untrusted_fixture_before_module_execution`; convert `R07-CORE-009-S01` from inspection to `tests/unit/workflows/test_requirements_evidence_delivery_workflow.py::test_current_run_pass_is_not_labelled_passing_after_red`; convert `R07-CORE-009-S02` from inspection to `tests/unit/scripts/test_requirements_evidence_delivery_gate.py::test_missing_historical_proof_retains_current_run_observation`; convert planned `R07-CORE-009-S03` to `tests/unit/scripts/test_requirements_evidence_delivery_gate.py::test_unresolved_current_run_cannot_be_pass_or_no_impact`; verify all five exact selectors collect once; then freeze the accepted mapping and plan digests. Do not invent selectors on this planning-only branch. -- [ ] 2.6 Record the exact failing commands, timestamps, actual results, behavioral summaries, environment limitations, and artifact identities in the failing-before section of `TDD_EVIDENCE.md` before production edits. - -## 3. Core implementation — each task at most two hours - -- [ ] 3.1 Pin and verify the signed corrected modules repository commit/tree, package version, report-schema version, manifest integrity, signature, clean-state identity, and approved public-key fingerprint or trust-root reference before any module import or execution. Validate the key/trust reference against the repository's configured approved key set and fail closed when it is missing or unknown; retain field-specific mismatch diagnostics. Allowed files: `ci/module-fixture.lock.json`, the exact pre-execution fixture/signature assertion in `.github/workflows/requirements-evidence.yml`, and the exact pre-module local fixture/signature/trust assertion in `scripts/requirements_evidence_delivery_gate.py` only. -- [ ] 3.2 Adapt current-run report handling without implementing chronology. Allowed files: `.github/workflows/requirements-evidence.yml` and `scripts/requirements_evidence_delivery_gate.py`; change `scripts/requirements_proof_executor.py` only when a named failing test proves field incompatibility. -- [ ] 3.3 Remove the R07 legacy-ledger/prior-red requirement from current-run enforcement in `.github/workflows/requirements-evidence.yml` and `scripts/requirements_evidence_delivery_gate.py`. Do not add replacement AST inference. -- [ ] 3.4 In `.github/workflows/requirements-evidence.yml`, keep Requirements and Code Review verdicts independent and publish both available artifacts before terminal enforcement. - -## 4. Verification and delivery - -- [ ] 4.1 Run focused tests, exactly `openspec validate requirements-07-runtime-proof-delivery --strict`, workflow lint, type/lint/contract checks, and full Code Review with explicit base/head scope; record the exact results under task B.5 and rerun the affected commands after every validation or evidence-artifact fix. -- [ ] 4.2 After implementation, add a separate passing-after section to `TDD_EVIDENCE.md` with timestamps, exact commands, actual results, behavioral summaries, environment limitations, and artifact identities. -- [ ] 4.3 Create or update `docs/guides/requirements-evidence.md` to distinguish current-run evidence from bounded historical proof; update `docs/index.md` and `docs/_layouts/default.html` only when needed to expose the guide, and regenerate `llms.txt` through the existing docs generator rather than hand-editing it. -- [ ] 4.4 Merge the completed implementation to `dev`; do not archive R07 until the correction is released and observed for one delivery cycle. -- [ ] 4.5 Before archiving R07, from the repository root in a disposable verification worktree run exactly `openspec archive requirements-06-evidence-enforcement`, then verify this exact-name `MODIFIED` delta leaves one canonical delivery-gate requirement rather than a duplicate and preserves both runtime-smoke registry scenarios plus their existing exact selectors. Do not move an OpenSpec directory manually. -- [ ] 4.6 After the correction is released and observed for one delivery cycle, from the repository root in a dedicated follow-up worktree run exactly `openspec archive requirements-07-runtime-proof-delivery` and verify the canonical delivery-gate requirement remains singular and still contains the bundle-dependency and Semantic-Versioning runtime-smoke scenarios. Do not move a directory into `openspec/changes/archive/` manually. -- [ ] 4.7 Merge checklist follow-up: update `wiki/sources/requirements-07-runtime-proof-delivery.md` (`depends-on`, `blocks`, `external-deps`, `status`, and summary) and run `python3 scripts/wiki_rebuild_graph.py` from the `specfact-cli-internal` repository root. -- [ ] 4.8 After each implementation or archive PR merges, remove its worktree, delete its local feature branch, run `git worktree prune`, and record the worktree-policy self-check. - -## Prohibited shortcuts - -- Do not cherry-pick or merge PR #671. -- Do not add AST rules for imports, plugins, configuration, data files, aliases, mutations, namespaces, symlinks, or dynamic execution. -- Do not mark a skipped, failed, unresolved, or missing tool result as pass/no-impact. -- Do not modify unrelated security, dependency-trust, safe-write, or smart-coverage tooling. -- Do not manually move directories into `openspec/changes/archive/`; use the exact repository-root `openspec archive ` commands above. - -## Closed implementation allowlist - -Anything not listed here is prohibited unless this OpenSpec change is updated and accepted first. - -OpenSpec mapping and evidence records: - -- `openspec/changes/requirements-07-runtime-proof-delivery/requirements-evidence.yaml`: before production edits only, remap `R07-CORE-008-S01`, convert planned `R07-CORE-008-S02`, and convert `R07-CORE-009-S01`/`R07-CORE-009-S02` plus planned `R07-CORE-009-S03` to the five exact selectors named in task 2.5, verify collection, and freeze the accepted mapping/plan digests. -- `openspec/changes/requirements-07-runtime-proof-delivery/TDD_EVIDENCE.md`: add failing-before evidence after the named tests fail and before production edits; add a separate passing-after section only after implementation passes. -- `openspec/changes/requirements-07-runtime-proof-delivery/CHANGE_VALIDATION.md`: pre-implementation readiness section required by task B.3 before tests/source edits, plus a separate final corrected implementation-validation section only after implementation and all required gates complete. - -Production/configuration: - -- `ci/module-fixture.lock.json`: signed modules repository/commit/tree/package/report-schema identities plus the approved public-key fingerprint or trust-root reference only. -- `scripts/requirements_evidence_delivery_gate.py`: verify the full signed fixture and approved key/trust identity before local module import, and adapt request/report fields for independent `current_execution`; no chronology or Git-history logic. -- `.github/workflows/requirements-evidence.yml`: remove retained-run discovery/download and legacy-ledger/prior-red branches; verify the fixture's approved signing-key/trust-root reference before module import; consume, publish, and enforce current-run evidence plus explicit PR review paths. -- `scripts/requirements_proof_executor.py`: conditional only when a named failing test proves schema incompatibility; plan/result field adaptation only. - -Tests: - -- `tests/unit/workflows/test_requirements_evidence_delivery_workflow.py`. -- `tests/unit/scripts/test_requirements_evidence_delivery_gate.py`. -- `tests/unit/scripts/test_requirements_proof_executor.py`: conditional only when the executor changes. -- No new R07 test module. - -Adoption documentation: - -- `docs/guides/requirements-evidence.md`: canonical current-execution versus chronology guide; create if absent. -- `docs/index.md` and `docs/_layouts/default.html`: link/navigation only when the new guide is otherwise undiscoverable. -- `llms.txt`: generated from accepted docs only through the existing generator; never hand-edit. - -Paired modules dependency allowlist — separate repository `nold-ai/specfact-cli-modules`, implemented and reviewed in PR #412 rather than on this core branch: - -- Requirements sources: `packages/specfact-requirements/src/specfact_requirements/requirements/lifecycle.py`, `evidence.py`, `commands.py`, new `replay_proof.py`, and `__init__.py` only for intentional exports. -- Code Review handoff sources: `packages/specfact-code-review/src/specfact_code_review/run/commands.py` and `findings.py` only. -- Focused tests: `tests/unit/specfact_requirements/test_requirements_lifecycle.py`, new `test_requirements_replay_proof.py`, `test_requirements_evidence.py`, `tests/integration/specfact_requirements/test_command_apps.py`, `tests/unit/specfact_code_review/run/test_commands.py`, and `test_findings.py`. -- Release/docs sources: `docs/bundles/requirements/overview.md`, `packages/specfact-requirements/module-package.yaml`, and `packages/specfact-code-review/module-package.yaml` only when its proof-context payload changes. -- Generated publication outputs, only through existing generators after behavior passes: `registry/index.json`, `registry/modules/specfact-requirements-.tar.gz`, its `.sha256`, `registry/signatures/specfact-requirements-.tar.sig`, and equivalent Code Review artifacts when that payload changes; include `CHANGELOG.md` and generated command docs required by module policy. -- No core implementation file is authorized by this companion allowlist, and no companion file is authorized by the core allowlist above. - -Explicitly forbidden: - -- `scripts/requirements_proof_provenance.py`, `scripts/requirements_proof_pytest_plugin.py`, and `scripts/pre-commit-quality-checks.sh`; -- all `src/**`, `tools/**`, security, dependency, safe-write, and smart-coverage paths; -- `pyproject.toml`, `uv.lock`, and unrelated tests. +- [ ] 1. After #740/#481 delivery, verify #662's current-run outcome requirements against the delivered behavior and tests. +- [ ] 2. Reconcile canonical specs through R09 and retire superseded unimplemented R07 deltas without applying or marking them implemented. Preserve shipped history; R08 remains abandoned. +- [ ] 3. Update #662, change ordering and internal mirrors with the replacement release and disposition. Refresh hierarchy/concurrency state first; use a dedicated worktree. diff --git a/openspec/changes/requirements-09-minimal-evidence/.openspec.yaml b/openspec/changes/requirements-09-minimal-evidence/.openspec.yaml new file mode 100644 index 000000000..cbd245e43 --- /dev/null +++ b/openspec/changes/requirements-09-minimal-evidence/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-09-20 diff --git a/openspec/changes/requirements-09-minimal-evidence/DEPENDENCY_REVIEW.md b/openspec/changes/requirements-09-minimal-evidence/DEPENDENCY_REVIEW.md new file mode 100644 index 000000000..ba041544b --- /dev/null +++ b/openspec/changes/requirements-09-minimal-evidence/DEPENDENCY_REVIEW.md @@ -0,0 +1,179 @@ +# Dependency and scope review + +Reviewed 2026-09-20 against freshly fetched core origin/dev and modules dev, all open issue bodies, native blocked-by/blocking relationships, parent hierarchy and project status. Scope is planning only. All affected implementation issues were Todo at inspection; no concurrent In Progress state was observed. + +## Dispositions and dependency edits + +- Core #740 owns coordinated default delivery/governance; modules #481 owns current-run product semantics/signing. Native edge: core #740 is blocked by modules #481. Policy preparation can proceed first. +- Core #662 transfers its unfinished R07 scope to #740 and waits for replacement reconciliation. Remove its stale blocker on closed modules #368. Modules #368 remains closed historical delivery; do not reopen it to imply the corrected contract shipped. +- R08 core #675/modules #414 remains closed Not Planned. No replay capsule/checkpoint protocol is reintroduced. +- Remove four optional-assurance prerequisites: core #680 <- modules #431; modules #417 <- modules #432; core #251 <- modules #434; modules #460 <- modules #434. +- Preserve the internal optional chain. Add core #683 <- modules #431 because that dependency is no longer inherited through C14; add modules #433 <- modules #434 because generic installation no longer supplies that transitive prerequisite. +- Keep C14/C15 real producer/runtime/layout/profile/policy/exception dependencies and native-platform regression requirements. Generic skill installation uses bounded fixtures and authentic signed assets when applicable; optional preflight assets wait for their own signed release. +- Scope instruction generation to lean defaults and explicit optional-assurance references. Governance emitters, graph consumers and dogfood retain useful observations without becoming prerequisites for MEB. +- Core #736 and docs16/docs17, required-check triggers, security/provenance/promotion repair changes are merged or historical records awaiting their separate acceptance/archive reconciliation. Preserve real security fixes and historical evidence; do not reopen them as proof prerequisites. +- Core #680/#679 and modules #417 were recovered from uncommitted planning-only feature worktrees into local `dev` and the R09 planning worktrees on 2026-09-20. Remote integration remains pending. All three remain planned; no implementation was imported. Modules C14 #416 shipped and is closed, but its existing proposal remains unarchived on dev; it is distinct from core C14 adoption #680. +- Broader graph backlog has pre-existing prose/native dependency differences (for example core #241/#247). Do not rewrite that unrelated graph here or add it to the MEB critical path. + +## Open issue inventory + +The inventory below covers all 74 pre-existing open issues (47 core, 27 modules), not only keyword matches. The two new stories are additional. A retained issue is not asserted complete or free of defects; it has no new hard dependency on this migration. + +| Repository | Issue | Disposition | +|---|---|---| +| specfact-cli | [#186 [Epic] specfact backlog](https://github.com/nold-ai/specfact-cli/issues/186) | Retain scope; no new MEB blocker | +| specfact-cli | [#194 [Epic] Architecture (CLI structure, modularity, performance)](https://github.com/nold-ai/specfact-cli/issues/194) | Retain scope; no new MEB blocker | +| specfact-cli | [#240 Architecture Boundary Validation Records](https://github.com/nold-ai/specfact-cli/issues/240) | Retain scope; no new MEB blocker | +| specfact-cli | [#241 Validation Evidence Graph Engine](https://github.com/nold-ai/specfact-cli/issues/241) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#244 Backlog Drift Evidence Adapter](https://github.com/nold-ai/specfact-cli/issues/244) | Retain scope; no new MEB blocker | +| specfact-cli | [#247 Evidence & Audit Output for CI/CD Pipelines](https://github.com/nold-ai/specfact-cli/issues/247) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#248 Exception Management — Time-Bound, Tracked Policy Exceptions](https://github.com/nold-ai/specfact-cli/issues/248) | Retain scope; no new MEB blocker | +| specfact-cli | [#251 [User Story] Shared Module-Owned Skill Discovery, Installation, and Export](https://github.com/nold-ai/specfact-cli/issues/251) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#252 Thin MCP Adapter For Validation Tools](https://github.com/nold-ai/specfact-cli/issues/252) | Retain scope; no new MEB blocker | +| specfact-cli | [#253 [User Story] Generated AGENTS, OpenSpec, Spec Kit, and Harness Instructions](https://github.com/nold-ai/specfact-cli/issues/253) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#254 Integration Umbrella for Cross-Change Contracts and Ownership](https://github.com/nold-ai/specfact-cli/issues/254) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#255 AI-Bloat Defense Dogfooding Proof](https://github.com/nold-ai/specfact-cli/issues/255) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#256 Validation evidence and context adapters](https://github.com/nold-ai/specfact-cli/issues/256) | Retain scope; no new MEB blocker | +| specfact-cli | [#257 AI IDE validation integration](https://github.com/nold-ai/specfact-cli/issues/257) | Retain scope; no new MEB blocker | +| specfact-cli | [#258 Evidence dogfooding and governance](https://github.com/nold-ai/specfact-cli/issues/258) | Retain scope; no new MEB blocker | +| specfact-cli | [#281 [Change] Misuse Safety Proof](https://github.com/nold-ai/specfact-cli/issues/281) | Retain scope; no new MEB blocker | +| specfact-cli | [#282 [Change] Acceptance Test Runner](https://github.com/nold-ai/specfact-cli/issues/282) | Retain scope; no new MEB blocker | +| specfact-cli | [#285 CLI validation trust](https://github.com/nold-ai/specfact-cli/issues/285) | Retain scope; no new MEB blocker | +| specfact-cli | [#353 [Feature] Marketplace Module Distribution](https://github.com/nold-ai/specfact-cli/issues/353) | Retain scope; no new MEB blocker | +| specfact-cli | [#354 [Feature] Module Migration & CLI Reorganization](https://github.com/nold-ai/specfact-cli/issues/354) | Retain scope; no new MEB blocker | +| specfact-cli | [#356 [Feature] Documentation & Discrepancy Remediation](https://github.com/nold-ai/specfact-cli/issues/356) | Retain scope; no new MEB blocker | +| specfact-cli | [#357 [Feature] Backlog Core Commands](https://github.com/nold-ai/specfact-cli/issues/357) | Retain scope; no new MEB blocker | +| specfact-cli | [#360 [Feature] Kanban Flow Metrics](https://github.com/nold-ai/specfact-cli/issues/360) | Retain scope; no new MEB blocker | +| specfact-cli | [#361 [Feature] SAFe & PI Planning](https://github.com/nold-ai/specfact-cli/issues/361) | Retain scope; no new MEB blocker | +| specfact-cli | [#365 [Feature] Configuration Profiles](https://github.com/nold-ai/specfact-cli/issues/365) | Retain scope; no new MEB blocker | +| specfact-cli | [#366 [Feature] Requirements Layer](https://github.com/nold-ai/specfact-cli/issues/366) | Retain scope; no new MEB blocker | +| specfact-cli | [#367 [Feature] Solution Architecture Layer](https://github.com/nold-ai/specfact-cli/issues/367) | Retain scope; no new MEB blocker | +| specfact-cli | [#368 [Feature] Full-Chain Validation & Traceability](https://github.com/nold-ai/specfact-cli/issues/368) | Retain scope; no new MEB blocker | +| specfact-cli | [#370 [Feature] Governance, Policy Packs & Evidence](https://github.com/nold-ai/specfact-cli/issues/370) | Retain scope; no new MEB blocker | +| specfact-cli | [#371 [Feature] OpenSpec Bridge Integration](https://github.com/nold-ai/specfact-cli/issues/371) | Retain scope; no new MEB blocker | +| specfact-cli | [#372 [Feature] Agent Skills & Instruction Files](https://github.com/nold-ai/specfact-cli/issues/372) | Retain scope; no new MEB blocker | +| specfact-cli | [#373 [Feature] MCP Server](https://github.com/nold-ai/specfact-cli/issues/373) | Retain scope; no new MEB blocker | +| specfact-cli | [#374 [Feature] End-to-End Integration Proof](https://github.com/nold-ai/specfact-cli/issues/374) | Retain scope; no new MEB blocker | +| specfact-cli | [#375 [Feature] CLI Behavior Validation Suite](https://github.com/nold-ai/specfact-cli/issues/375) | Retain scope; no new MEB blocker | +| specfact-cli | [#514 [Feature] Architecture Review & Interface Governance](https://github.com/nold-ai/specfact-cli/issues/514) | Retain scope; no new MEB blocker | +| specfact-cli | [#518 Opt-In Validation Outcome Telemetry](https://github.com/nold-ai/specfact-cli/issues/518) | Retain scope; no new MEB blocker | +| specfact-cli | [#524 Architecture-Boundary Review Findings (Gated)](https://github.com/nold-ai/specfact-cli/issues/524) | Retain scope; no new MEB blocker | +| specfact-cli | [#662 [Change] Deliver empirical requirements proof in local and CI gates](https://github.com/nold-ai/specfact-cli/issues/662) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#679 [Change] Adopt the signal-calibrated Code Review gate](https://github.com/nold-ai/specfact-cli/issues/679) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#680 [Change] Adopt C14 protected PR-range assurance](https://github.com/nold-ai/specfact-cli/issues/680) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#681 [Feature] Deterministic Pre-Implementation Change Assurance](https://github.com/nold-ai/specfact-cli/issues/681) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#682 [User Story] Deterministic Pre-Implementation Design Contract Core](https://github.com/nold-ai/specfact-cli/issues/682) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#683 [User Story] Preflight Dogfood Evidence and Readiness Decision](https://github.com/nold-ai/specfact-cli/issues/683) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#684 [User Story] Seal-Bound Development Checkpoint and Conformance Core](https://github.com/nold-ai/specfact-cli/issues/684) | Rescoped; see authoritative issue amendment | +| specfact-cli | [#718 [Change] Block category-less module command squatting](https://github.com/nold-ai/specfact-cli/issues/718) | Retain scope; no new MEB blocker | +| specfact-cli | [#720 [Change] Confine IDE prompt exports to the repository](https://github.com/nold-ai/specfact-cli/issues/720) | Retain scope; no new MEB blocker | +| specfact-cli | [#736 [Bug] Close release workflow authentication and evidence gaps](https://github.com/nold-ai/specfact-cli/issues/736) | Rescoped; see authoritative issue amendment | +| specfact-cli-modules | [#124 [Change] docs-14 - Publish-Driven Module Release History And Docs Rendering](https://github.com/nold-ai/specfact-cli-modules/issues/124) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#144 specfact project validation context adapters](https://github.com/nold-ai/specfact-cli-modules/issues/144) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#145 specfact backlog policy and evidence inputs](https://github.com/nold-ai/specfact-cli-modules/issues/145) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#147 Sync Safety For Validation Context Adapters](https://github.com/nold-ai/specfact-cli-modules/issues/147) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#148 [Feature] Policy Engine & Enforcement Modes](https://github.com/nold-ai/specfact-cli-modules/issues/148) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#157 Sync Safety Kernel For Validation Context Adapters](https://github.com/nold-ai/specfact-cli-modules/issues/157) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#158 Policy Packs & Enforcement Modes (Advisory/Mixed/Hard)](https://github.com/nold-ai/specfact-cli-modules/issues/158) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#161 Context Adapters For Validation Evidence](https://github.com/nold-ai/specfact-cli-modules/issues/161) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#162 specfact code / AI-bloat defense](https://github.com/nold-ai/specfact-cli-modules/issues/162) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#163 Validation Evidence And Governance Runtime Follow-Ups](https://github.com/nold-ai/specfact-cli-modules/issues/163) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#164 Architecture Boundary Runtime Input Follow-Up](https://github.com/nold-ai/specfact-cli-modules/issues/164) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#166 Backlog Drift Evidence Runtime Follow-Up](https://github.com/nold-ai/specfact-cli-modules/issues/166) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#167 [Change] Runtime Exception Management And Enforcement Follow-Up](https://github.com/nold-ai/specfact-cli-modules/issues/167) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#169 [Change] Governance Evidence Emitters Follow-Up](https://github.com/nold-ai/specfact-cli-modules/issues/169) | Rescoped; see authoritative issue amendment | +| specfact-cli-modules | [#170 [Change] Traceability Runtime Queries And Orphan Detection Follow-Up](https://github.com/nold-ai/specfact-cli-modules/issues/170) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#171 Validation Evidence Graph Runtime Engine Follow-Up](https://github.com/nold-ai/specfact-cli-modules/issues/171) | Rescoped; see authoritative issue amendment | +| specfact-cli-modules | [#219 [Feature] Architecture Review & Interface Governance](https://github.com/nold-ai/specfact-cli-modules/issues/219) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#230 Architecture Boundary Review Module (Gated)](https://github.com/nold-ai/specfact-cli-modules/issues/230) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#275 [Feature] Code Review Simplification Feedback Loop](https://github.com/nold-ai/specfact-cli-modules/issues/275) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#305 [Feature] Backlog CLI Contract Reliability](https://github.com/nold-ai/specfact-cli-modules/issues/305) | Retain scope; no new MEB blocker | +| specfact-cli-modules | [#417 [Change] Calibrate Code Review into a truthful blocking gate](https://github.com/nold-ai/specfact-cli-modules/issues/417) | Rescoped; see authoritative issue amendment | +| specfact-cli-modules | [#431 [User Story] Pre-Implementation Assurance Runtime and Bundled Workflow](https://github.com/nold-ai/specfact-cli-modules/issues/431) | Rescoped; see authoritative issue amendment | +| specfact-cli-modules | [#432 [User Story] Evidence-Backed Preflight Hardening and Stable Publication](https://github.com/nold-ai/specfact-cli-modules/issues/432) | Rescoped; see authoritative issue amendment | +| specfact-cli-modules | [#433 [User Story] Plug-and-Play Preflight Harness Adapters](https://github.com/nold-ai/specfact-cli-modules/issues/433) | Rescoped; see authoritative issue amendment | +| specfact-cli-modules | [#434 [User Story] Seal-Bound Development Checkpoint and Conformance Runtime](https://github.com/nold-ai/specfact-cli-modules/issues/434) | Rescoped; see authoritative issue amendment | +| specfact-cli-modules | [#460 [Change] Native local Code Review on macOS, Linux, and Windows](https://github.com/nold-ai/specfact-cli-modules/issues/460) | Rescoped; see authoritative issue amendment | +| specfact-cli-modules | [#480 P2 Bug Post-0.50.1: preserve explicit pytest file selection in portable reviews](https://github.com/nold-ai/specfact-cli-modules/issues/480) | Retain scope; no new MEB blocker | + +## Active change inventory + +| Repository | Change | Disposition | +|---|---|---| +| specfact-cli | `ai-integration-01-agent-skill` | Rescoped planning; existing evidence untouched | +| specfact-cli | `ai-integration-02-mcp-server` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `ai-integration-03-instruction-files` | Rescoped planning; existing evidence untouched | +| specfact-cli | `architecture-01-solution-layer` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `architecture-02-well-architected-review` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `audit-01-reproducible-delivery` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `ci-02-release-workflow-review` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `cli-removed-flat-alias-diagnostics` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `cli-val-03-misuse-safety-proof` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `cli-val-04-acceptance-test-runner` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `cli-val-05-ci-integration` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `docs-16-code-review-runtime-parity` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `docs-17-code-review-pytest-fixture-parity` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `dogfooding-01-full-chain-e2e-proof` | Rescoped planning; existing evidence untouched | +| specfact-cli | `fix-ci-test-environment-isolation` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `fix-final-requirements-review-artifact` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `fix-release-promotion-requirements-parity` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `fix-release-promotion-security-gates` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `governance-01-evidence-output` | Rescoped planning; existing evidence untouched | +| specfact-cli | `governance-02-exception-management` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `module-scope-02-preserve-user-installs` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `openspec-01-intent-trace` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `preflight-01-design-contract-core` | Rescoped planning; existing evidence untouched | +| specfact-cli | `preflight-03-dogfood-hardening-and-release` | Rescoped planning; existing evidence untouched | +| specfact-cli | `preflight-05-implementation-conformance` | Rescoped planning; existing evidence untouched | +| specfact-cli | `profile-01-config-layering` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `required-check-workflow-trigger-parity` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `requirements-03-backlog-sync` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `requirements-04-upstream-source-readiness` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `requirements-06-evidence-enforcement` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `requirements-07-runtime-proof-delivery` | Rescoped planning; existing evidence untouched | +| specfact-cli | `requirements-09-minimal-evidence` | New paired MEB scope | +| specfact-cli | `telemetry-01-opentelemetry-default-on` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `tooling-spaced-env-pythonpath` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `traceability-01-index-and-orphans` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli | `validation-02-full-chain-engine` | Rescoped planning; existing evidence untouched | +| specfact-cli-modules | `architecture-01-solution-layer` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `architecture-02-module-well-architected` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `ci-02-codeql-cache-scope-isolation` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `code-review-14-scope-truth-and-differential-enforcement` | Shipped #416; unarchived historical follow-up tasks need reconciliation; distinct from core C14 #680 | +| specfact-cli-modules | `code-review-16-portable-project-runtime` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `code-review-capsule-customer-execution` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `code-review-installed-payload-layout` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `code-review-native-platform-execution` | Rescoped planning; existing evidence untouched | +| specfact-cli-modules | `docs-14-module-release-history` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `governance-01-evidence-output` | Rescoped planning; existing evidence untouched | +| specfact-cli-modules | `governance-02-exception-management` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `governance-05-hierarchy-cache-freshness` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `openspec-01-intent-trace` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `policy-02-packs-and-modes` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `preflight-02-assurance-runtime` | Rescoped planning; existing evidence untouched | +| specfact-cli-modules | `preflight-03-dogfood-hardening-and-release` | Rescoped planning; existing evidence untouched | +| specfact-cli-modules | `preflight-04-harness-adapters` | Rescoped planning; existing evidence untouched | +| specfact-cli-modules | `preflight-05-implementation-conformance` | Rescoped planning; existing evidence untouched | +| specfact-cli-modules | `requirements-03-backlog-sync` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `requirements-06-evidence-enforcement` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `requirements-07-scenario-runtime-proof` | Rescoped planning; existing evidence untouched | +| specfact-cli-modules | `requirements-09-minimal-evidence` | New paired MEB scope | +| specfact-cli-modules | `sync-01-unified-kernel` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `traceability-01-index-and-orphans` | Reviewed; retain existing scope/status; not a new MEB prerequisite | +| specfact-cli-modules | `validation-02-full-chain-engine` | Rescoped planning; existing evidence untouched | + +| specfact-cli | `code-review-14-protected-range-adoption` | Recovered and rescoped planning for #680; implementation not started | +| specfact-cli | `cli-val-07-code-review-gate-adoption` | Recovered and rescoped planning for #679; implementation not started | +| specfact-cli-modules | `code-review-15-signal-calibrated-blocking-gate` | Recovered and rescoped planning for #417; implementation not started | + +## Limits and implementation readiness + +This audit changes future scope, not deployed guarantees. Required checks, signing, registry data, source code and tests are untouched. New stories remain Todo; metadata and native relations are read back after mutation. Actual protection/ruleset changes need the reviewed cutover task, not a planning-time bypass. + +No exact token cost or causal fraction of PR waiting time is known. Evidence-artifact churn is measured separately from useful test/source changes. No archive action is performed before verified completion. + +## C14/C15 source recovery + +Verified 2026-09-20: each source worktree had only its untracked change directory and a change-order edit; no associated open pull request. Source branches: core `feature/code-review-14-protected-range-adoption` at `e3a20f20`; core `feature/cli-val-07-code-review-gate-adoption` at `31d945e0`; modules `feature/code-review-15-signal-calibrated-blocking-gate` at `c3eda08c`. Recovered documents include their existing design/tasks/specs and dated validation context; the new scope amendment replaces default historical-proof duties. Original worktrees were preserved. + +Modules C14 implementation [PR #418](https://github.com/nold-ai/specfact-cli-modules/pull/418) and release [PR #420](https://github.com/nold-ai/specfact-cli-modules/pull/420) merged, and [#416](https://github.com/nold-ai/specfact-cli-modules/issues/416) closed on 2026-08-27. Its proposal is still in the active tree rather than the archive. Do not mark its accumulated later tasks complete or promote its full delta without a separate implementation-to-spec reconciliation. diff --git a/openspec/changes/requirements-09-minimal-evidence/design.md b/openspec/changes/requirements-09-minimal-evidence/design.md new file mode 100644 index 000000000..ed0931349 --- /dev/null +++ b/openspec/changes/requirements-09-minimal-evidence/design.md @@ -0,0 +1,99 @@ +## Context and evidence + +Issue #662 requested exact linked tests collected and passing in the current CI run. PRs #663/#666/#667/#668 expanded retained chronology into fixture/import freshness. Issue #689 records a real producer/consumer mismatch that blocked a security update. PR #674 separated the claims on paper, but live delivery retained the historical machinery. + +Measured additions plus deletions in run transcripts, TDD ledgers, mappings, and review receipts: #730 2,780/4,220 lines (65.9%); #737 460/1,041 (44.2%); #738 614/886 (69.3%). Tests/verifier code are excluded. Successful current Requirements runs took minutes, so PR lifetime is not attributed entirely to CI, and no token-cost estimate is claimed. Inspection date: 2026-09-20. + +## Decisions + +1. MEB means current observed behavior and relevant validation context. It does not mean complete requirements coverage, absence of defects, or historical TDD compliance. +2. Keep development order: specification -> useful regression tests -> observe the relevant failure where feasible -> implementation -> passing tests. No test-only Git commit or hosted RED run is required. An impractical reproduction needs a brief reason and alternative negative control; setup/import failures do not prove sensitivity to the bug. +3. Reuse normal CI test outputs once per candidate/environment. Bind the receipt to the tested commit/tree, runner/environment identity, selection or command, counts, and artifact references. Keep detailed JUnit/logs in CI artifacts (14 days); retain release integrity records with releases. A PR validation summary normally needs five to ten authored lines, without a line-count gate. +4. A required selected acceptance case must be collected exactly once and produce an ordinary pass without `wasxfail` or equivalent xfail outcome metadata, including empty-valued markers. Failure/error/skip/XFAIL/XPASS/missing/duplicate is not proof. Ordinary full-suite skips remain separately visible under existing suite policy. Missing or malformed required output and wrong revision remain non-passing. +5. The trusted orchestration layer verifies candidate identity and required job/result provenance; project-controlled metadata cannot authorize itself. Preserve existing source/module authentication, least privilege, parser/selector bounds, signing, security, contracts, tests, and independent review. Do not run PR code with signing or repository-write credentials. +6. Build one current report and let Code Review consume it as context without verdict fusion. On dev-to-main, validate the actual promotion candidate; do not reconstruct source-PR history or reuse legacy promotion capsules. +7. Scenario associations are optional for ordinary test context. If supplied, validate them exactly. Without them, requirement coverage is not evaluated; do not report complete intent/coverage. Explicit stronger traceability/chronology policy stays opt-in and retains its stricter failure semantics. +8. Existing preflight/seal/checkpoint work is an optional product capability. Its guarantees remain meaningful inside that explicit mode. It is not a prerequisite to ship C14/C15, native execution, generic skills, or lean generated instructions. +9. R09 replaces the unimplemented R07 correction rather than duplicating its workflow. R08 remains abandoned. No new capsule, seal service, checkpoint tag protocol, AST/import closure inference, or proof-specific caching framework. + +## Configured gate migration + +| Existing gate | Ordinary MEB handling | +| --- | --- | +| Spec-first and useful failing-before/passing-after work | Retain development order and relevant regression reproduction; note an impractical reproduction and alternative negative control. | +| `TDD_EVIDENCE.md`, committed transcripts and hosted RED proof | Replace the mandatory authored ledger with concise validation notes and current CI artifacts. Preserve historical records and explicit legacy chronology semantics; do not fabricate missing history. | +| `.specfact/code-review.json` | Where the existing code-review gate applies, retain its fresh report for the reviewed candidate before OpenSpec completion, with each finding fixed, rejected with reasons, or covered by an individually authorized documented exception. Reuse the existing review output; no duplicate suite or new committed receipt. | +| Applicable test, contract, lint/type, independent security, signed-module and delivery checks | Retain their actual outcomes and source/authentication checks. Missing required output is not success. | +| OpenSpec validation and archival | Retain strict validation and native archival of completed changes; never apply superseded unimplemented deltas. | + +This mapping adds no review run or JSON obligation to planning-only amendments. +It is enacted through the governance preparation below; +planning text alone does not change active hooks, required checks or repository +rules. Independently runnable changes use the effective governance at session +start, or an explicit owner-authorized scope exception, until that migration is +merged. They do not acquire a dependency on the complete R09 runtime rollout. + +## Ownership and interfaces + +Modules #481 owns current reconciliation, v3 report claims, legacy compatibility, and review-context consumption. Core owns revision selection, safe execution/original job outputs, artifact collection, trusted enforcement and platform integration. The same change ID is paired across repositories; each story owns only its repository's implementation. + +Modules add `current` as the default reconciliation stage; explicit legacy `red`/`final` remains supported. V3 separates `current_execution` and `red_green_chronology`; current-only operation reports chronology not evaluated. V2 is read through the explicit legacy path, never inferred from missing v3 fields. No new general profile framework is required for this rollout. + +## Bootstrap and rollout + +First update and review contributor governance, agent rules, templates and +pre-commit guidance under the owner-authorized MEB migration, before behavior +tests or code. Do not make the migration prove the historical process it removes. Implement and review regression tests under existing code/security checks; use the owner-authorized MEB scope for these changes. If the old required authority gate prevents integration, prepare and review the exact organization/core policy change first and apply the coordinated policy cutover as an explicit migration step. Never add a per-PR bypass or unconditional success job. + +The delivery order is reviewed governance preparation -> signed modules #481 +publication -> core adoption and trusted pilot -> coordinated repository/organization +cutover -> ten-PR observation/rollback window -> obsolete helper/test removal, +with the module/workflow/policy rollback kept together. Open and integrate +bounded PRs as each slice becomes reviewable; trusted pilot execution uses that +reviewed integrated source. Finalization records those PRs rather than opening +the first PR after deployment. + +Run the replacement non-blocking on representative ordinary, bugfix, executable-doc, dependency, fork, and promotion candidates. Verify current checks and trust boundaries before switching both repository and organization enforcement. Do not leave duplicate old/new test execution running for every PR after cutover. + +Rollback restores prior module pin, workflow, and required-policy configuration together, using matching helpers and their relevant tests retained through the ten-PR observation/rollback window. Delete those obsolete helpers/tests only after that window closes and cutover defects are resolved. Reader incompatibility, missing required checks, and incorrect current-result acceptance are rollback triggers. Historical records remain readable; rollback cannot relabel current-only evidence as chronology. + +## Verification and value check + +Keep the real defects visible: missing-JUnit (#666), producer/consumer mismatch (#689), skipped/XFAIL/XPASS/coverage acceptance (#737), and independent source authentication (#738). Add integration coverage for a normal PR completing without historical records or authority comments and for independent verdict propagation. + +Across ten representative PRs inspect existing GitHub data: proof-only reruns, duplicate execution, manual approvals, artifact-only churn, and detected regressions. Target aggregation under 60 seconds excluding tests/queueing, as an optimization target only. No mandatory cost telemetry or new dashboard. + +## Research basis + +- [DORA test automation](https://dora.dev/capabilities/test-automation/): useful, reliable automated tests and feedback within ten minutes. +- [NIST SSDF 1.1, PW.8](https://nvlpubs.nist.gov/nistpubs/specialpublications/nist.sp.800-218.pdf): risk-based testing, retained regressions and documented outcomes. +- [GitHub artifact attestations](https://docs.github.com/en/actions/concepts/security/artifact-attestations): provenance describes origin/build, not correctness or security. + +Accessed 2026-09-20. The MEB policy is a repository design decision informed by these sources, not a claim that they prescribe this exact implementation. + +## Execution-unit selection + +Exactly once is per canonical selector within the current selected candidate/environment/logical suite-or-shard/job-attempt unit, not globally across the matrix. Core derives the required unit set and current designated attempts from trusted CI metadata; each unit must satisfy its expected selector set. Do not cherry-pick older passing attempts when the current designated attempt is pending, failed, cancelled or unavailable. Ambiguous selection or duplicates inside one unit remain non-passing. Modules only compares supplied unit identities alongside the existing plan/source/environment binding. Reuse existing CI metadata and outputs; no historical transcript, extra suite or new approval protocol is introduced. + +The shared execution-unit identity SHALL be a compact UTF-8 JSON array with +this fixed field order: +`[candidate_binding, environment_binding, matrix_lane, suite_or_shard, provider, run_id, run_attempt, job_id]`. +Candidate and environment bindings SHALL preserve their existing report/plan +values, types and normalization; the remaining six fields SHALL be strings. +The tuple SHALL NOT replace the existing binding checks. +Matrix lane and suite/shard SHALL use stable configured identifiers, never +ambiguous display names; an absent matrix lane is the empty string. For GitHub, +provider is `github-actions` and run ID, run attempt and actual job execution ID +are canonical decimal strings from trusted metadata (job ID is the actual +execution ID, not the logical `GITHUB_JOB` name); reruns retain distinct +attempt/job identities. Local execution uses provider `local`, the current +invocation ID, attempt `1` and the selected command's configured job ID, without +claiming CI authority. Core selection and module comparison SHALL use exact +ordered-field equality after parsing this same representation, using existing +binding normalization and ignoring object-member order and JSON whitespace. Missing or ambiguous required identity SHALL remain non-passing. +This reuses existing bindings and execution metadata; no new hash, registry, +receipt, extra execution or approval protocol is required. + +## Current-run consumer boundary + +Current reconciliation requires no prior session receipt and preserves independent producer outcomes. Local progress cannot establish chronology or protected CI authority. Executors remain outside the pure Requirements reconciler, which performs no Git, test or network operations. diff --git a/openspec/changes/requirements-09-minimal-evidence/proposal.md b/openspec/changes/requirements-09-minimal-evidence/proposal.md new file mode 100644 index 000000000..c018a1a6f --- /dev/null +++ b/openspec/changes/requirements-09-minimal-evidence/proposal.md @@ -0,0 +1,57 @@ +# Change: Minimal Evidence for Reliable Delivery + +## Why + +Current Requirements delivery proves development chronology beyond the original current-run goal of #662. Frozen mappings, retained RED provenance, exact-head authority comments, and promotion reuse impose substantial maintenance and authoring overhead. Retain evidence that detects defects and identifies the tested revision while removing that history protocol from normal delivery. + +## What Changes + +- Adopt a compact, automatically generated minimum evidence bundle (MEB) from existing current-candidate tests and independent quality/security checks. +- Keep spec/test-first work and meaningful bug reproduction; replace mandatory committed transcripts and hosted RED checkpoints with short validation notes and CI artifacts. +- Consume the paired signed modules current-run contract, without approval receipts, prior RED artifacts, repeated suites, or custom release-promotion ancestry reuse. +- Coordinate the core required check and organization-required workflow cutover without unconditional-success placeholders or weakening source/module authentication. +- Rescope optional preflight/seal/checkpoint dependencies away from C14/C15, generic skills/instructions, and native execution. Preserve the optional feature's internal dependencies. +- Supersede the unimplemented R07 correction; leave shipped evidence and abandoned R08 history unchanged. + +## Capabilities + +### New Capabilities + +- `minimum-evidence-delivery`: Lean current-run MEB, independent gates, bounded storage, coordinated enforcement, and default-policy isolation. + +### Modified Capabilities + +- `requirements-runtime-proof-delivery`: Restrict retained RED obligations to explicit legacy historical-proof requests, removing them from the ordinary delivery prerequisite. + +## Impact + +Planning only now: OpenSpec artifacts, dependency/scope tracking, and wiki mirrors. No runtime, hooks, CI, rulesets, signatures, release metadata, or canonical specs change in this planning work. + +Implementation later touches core Requirements orchestration, pre-commit policy, agent/OpenSpec templates, and nold-ai/.github trusted workflow policy. Keep module-owned semantics and offline local operation. No new service, global evidence graph, telemetry product, or mandatory LLM review is introduced. + +Update contributor governance and Requirements/CI reference documentation at implementation time; retain existing navigation unless new pages are necessary. Paired module command/help documentation belongs to #481. + +## Dependencies and rollout + +Product handoff: modules #481 signed release -> core adoption/pilot -> coordinated dev/main and organization policy cutover -> ten-PR observation/rollback window -> obsolete default-path removal. Governance preparation does not wait for a new runtime release: update and review +agent rules, templates and contributor guidance before behavior tests or code. +Runtime enforcement changes remain coordinated with the verified pilot. Neither new story depends on the preflight or full-chain graph roadmap. + +Core #662 becomes replacement-reconciliation tracking; do not implement its old R07 tasks independently. See [dependency review](DEPENDENCY_REVIEW.md) for all open issues and change dispositions, including recovered C14/C15 proposals and the distinction between shipped modules C14 and pending core adoption. + +Rollback restores the previous signed fixture, workflow, and ruleset together, using matching helpers and their relevant tests retained until the observation/rollback window closes. Preserve explicit legacy readers and historical artifacts; never claim that old evidence validates a new revision. + +## Source Tracking + + +- **GitHub Issue**: #740 +- **Issue URL**: +- **Repository**: `nold-ai/specfact-cli` +- **Parent Feature**: #374 (End-to-End Integration Proof), under #258 +- **Paired Modules Story**: +- **Last Synced Status**: proposed / Todo, 2026-09-20 +- **Scope authority**: Owner-requested rescope before implementation; no runtime adoption claimed. + +## Current-run consumer boundary + +Current reconciliation requires no prior session receipt and preserves independent producer outcomes. Local progress cannot establish chronology or protected CI authority. Executors remain outside the pure Requirements reconciler, which performs no Git, test or network operations. diff --git a/openspec/changes/requirements-09-minimal-evidence/requirements-evidence.yaml b/openspec/changes/requirements-09-minimal-evidence/requirements-evidence.yaml new file mode 100644 index 000000000..9e3d604a0 --- /dev/null +++ b/openspec/changes/requirements-09-minimal-evidence/requirements-evidence.yaml @@ -0,0 +1,74 @@ +schema_version: "2" +x-planning-context: &planning-context + rationale: "Make this requirement reviewable before implementation begins." + stakeholder_refs: ["nold-ai/specfact-cli#740"] +x-planning-inspection: &planning-inspection + method: inspection + observable: >- + Strict OpenSpec validation retains the requirement and its scenarios + without claiming implementation or test execution. +x-spec-1: &spec-1 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/requirements-09-minimal-evidence/specs/minimum-evidence-delivery/spec.md" +x-spec-2: &spec-2 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/requirements-09-minimal-evidence/specs/requirements-runtime-proof-delivery/spec.md" +requirements: + openspec:requirements-09-minimal-evidence:minimum-evidence-delivery:minimum-current-run-delivery-evidence: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: R09-001 + scenario_id: minimum-current-run-delivery-evidence + intent: "Inspect the minimum current-run delivery evidence planning contract and its scenarios." + openspec:requirements-09-minimal-evidence:minimum-evidence-delivery:independent-gates-and-bounded-collection: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: R09-002 + scenario_id: independent-gates-and-bounded-collection + intent: "Inspect the independent gates and bounded collection planning contract and its scenarios." + openspec:requirements-09-minimal-evidence:minimum-evidence-delivery:optional-assurance-is-not-a-default-prerequisite: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: R09-003 + scenario_id: optional-assurance-is-not-a-default-prerequisite + intent: "Inspect the optional assurance is not a default prerequisite planning contract and its scenarios." + openspec:requirements-09-minimal-evidence:minimum-evidence-delivery:coordinated-trusted-enforcement-cutover: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: R09-004 + scenario_id: coordinated-trusted-enforcement-cutover + intent: "Inspect the coordinated trusted enforcement cutover planning contract and its scenarios." + openspec:requirements-09-minimal-evidence:minimum-evidence-delivery:workflow-state-is-not-delivery-evidence: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: R09-005 + scenario_id: workflow-state-is-not-delivery-evidence + intent: "Inspect the workflow state is not delivery evidence planning contract and its scenarios." + openspec:requirements-09-minimal-evidence:minimum-evidence-delivery:acceptance-uniqueness-is-per-selected-execution-unit: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: R09-006 + scenario_id: acceptance-uniqueness-is-per-selected-execution-unit + intent: "Inspect the acceptance uniqueness is per selected execution unit planning contract and its scenarios." + openspec:requirements-09-minimal-evidence:requirements-runtime-proof-delivery:producer-bound-retained-red-proof: + <<: *planning-context + touchpoints: *spec-2 + verification_cases: + - <<: *planning-inspection + case_id: R09-007 + scenario_id: producer-bound-retained-red-proof + intent: "Inspect the producer-bound retained red proof planning contract and its scenarios." diff --git a/openspec/changes/requirements-09-minimal-evidence/specs/minimum-evidence-delivery/spec.md b/openspec/changes/requirements-09-minimal-evidence/specs/minimum-evidence-delivery/spec.md new file mode 100644 index 000000000..679026b00 --- /dev/null +++ b/openspec/changes/requirements-09-minimal-evidence/specs/minimum-evidence-delivery/spec.md @@ -0,0 +1,129 @@ +## ADDED Requirements + +### Requirement: Minimum Current-Run Delivery Evidence + +Ordinary delivery SHALL derive a compact current-run result from existing required validation outputs at the candidate source identity. It SHALL NOT require historical RED commits, frozen mappings, approval receipts, retained-history replay or authority comments. Historical chronology and current execution SHALL remain independent claims. A required selected acceptance case SHALL be collected exactly once and produce an ordinary pass without `wasxfail` or equivalent xfail outcome metadata, including empty-valued markers; XFAIL and XPASS SHALL NOT satisfy acceptance. + +#### Scenario: Ordinary implementation has current passing results + +- **GIVEN** required validation ran against the candidate revision +- **AND** every selected acceptance test was collected exactly once and produced an ordinary pass without xfail outcome metadata +- **WHEN** ordinary delivery evaluates its MEB +- **THEN** current execution may pass without prior RED evidence +- **AND** chronology remains not evaluated and no correctness or complete-coverage claim is inferred. + +#### Scenario: Required current results are incomplete + +- **GIVEN** required output is missing, empty, malformed, wrong-revision, or a selected case is missing, duplicated, failed, errored, skipped, XFAIL or XPASS +- **WHEN** delivery reconciles the result +- **THEN** acceptance proof SHALL remain non-passing with actionable diagnostics. + +#### Scenario: Non-strict XPASS resembles a passed call + +- **GIVEN** a selected case has a passed call outcome with `wasxfail` or equivalent xfail metadata, even if the marker is empty +- **WHEN** current-run reconciliation evaluates acceptance +- **THEN** the case SHALL remain non-passing evidence rather than an ordinary pass +- **AND** a zero pytest exit code SHALL NOT override that result. + +### Requirement: Independent Gates and Bounded Collection + +Core SHALL preserve independent test, contract, security, module-authentication and review outcomes. It SHALL execute no duplicate suite solely for evidence, retain detailed run artifacts for 14 days by default, and keep release integrity records with releases. Source identity declared by candidate code alone SHALL NOT establish trusted CI authority. + +#### Scenario: Evidence exists but a security check fails + +- **GIVEN** current selected tests pass and an applicable security check fails +- **WHEN** the delivery decision is made +- **THEN** test evidence SHALL NOT override the failing check. + +#### Scenario: Promotion is validated + +- **GIVEN** a dev-to-main candidate +- **WHEN** required validation executes +- **THEN** it SHALL evaluate the promotion candidate without historical source-PR replay or bespoke ancestry reuse. + +### Requirement: Optional Assurance Is Not a Default Prerequisite + +Preflight seals, checkpoints and historical chronology SHALL apply only under explicit assurance selection. Generic review, skill distribution and generated default instructions SHALL remain usable without those optional capabilities. Explicit legacy requests SHALL retain their integrity requirements. + +#### Scenario: Optional seal capability is absent + +- **GIVEN** ordinary validation policy and no installed seal/checkpoint capability +- **WHEN** ordinary delivery or default generated guidance is used +- **THEN** missing optional assurance SHALL NOT block the operation or be described as a failed current-execution result. + +### Requirement: Coordinated Trusted Enforcement Cutover + +Core SHALL migrate repository-required checks and organization-required workflow policy together after a representative pilot, preserving independent workflow trust and required-check emission without unconditional-success placeholders. + +#### Scenario: Repository workflow changes but organization policy is stale + +- **GIVEN** the organization still requires historical authority +- **WHEN** rollout readiness is assessed +- **THEN** cutover SHALL remain incomplete until the reviewed policies agree +- **AND** no per-PR bypass SHALL substitute for that coordinated change. + +### Requirement: Workflow State Is Not Delivery Evidence + +Ordinary current-run consumers SHALL remain usable without prior local workflow receipts or session phases. Workflow execution SHALL remain separate from Requirements reconciliation and SHALL preserve independent producer outcomes. Local progress SHALL NOT establish historical proof or protected CI authority. + +#### Scenario: A harness starts in a fresh checkout + +- **GIVEN** current required validation inputs and no local turn receipt +- **WHEN** ordinary verification or current reconciliation is requested +- **THEN** the missing session history does not block the operation +- **AND** only current identity-matching producer outputs determine their respective claims. + +#### Scenario: A local receipt declares success over a failing producer + +- **GIVEN** a local workflow receipt marked successful and a required failed or unknown producer result +- **WHEN** ordinary delivery evaluates current evidence +- **THEN** the producer remains non-passing and local state cannot override it or grant CI authority. + +### Requirement: Acceptance Uniqueness Is Per Selected Execution Unit + +Exactly-once acceptance SHALL apply per canonical selector within a selected execution unit: candidate source identity, environment/matrix lane, logical suite or shard and designated job attempt. Distinct required matrix units SHALL remain separate; each unit SHALL satisfy its own complete expected selector set. Core SHALL select the current authoritative units/attempts from trusted workflow metadata, not whichever artifact passes. A newer designated attempt that is pending, failed, cancelled or unavailable SHALL NOT fall back to an older passing attempt. Ambiguous attempt selection or duplicate selected outcomes inside one unit SHALL remain non-passing. The module SHALL compare supplied unit identity without performing Git, test or network operations; local declarations alone SHALL NOT establish CI authority. + +The shared execution-unit identity SHALL be a compact UTF-8 JSON array with +this fixed field order: +`[candidate_binding, environment_binding, matrix_lane, suite_or_shard, provider, run_id, run_attempt, job_id]`. +Candidate and environment bindings SHALL preserve their existing report/plan +values, types and normalization; the remaining six fields SHALL be strings. +The tuple SHALL NOT replace the existing binding checks. +Matrix lane and suite/shard SHALL use stable configured identifiers, never +ambiguous display names; an absent matrix lane is the empty string. For GitHub, +provider is `github-actions` and run ID, run attempt and actual job execution ID +are canonical decimal strings from trusted metadata (job ID is the actual +execution ID, not the logical `GITHUB_JOB` name); reruns retain distinct +attempt/job identities. Local execution uses provider `local`, the current +invocation ID, attempt `1` and the selected command's configured job ID, without +claiming CI authority. Core selection and module comparison SHALL use exact +ordered-field equality after parsing this same representation, using existing +binding normalization and ignoring object-member order and JSON whitespace. Missing or ambiguous required identity SHALL remain non-passing. +This reuses existing bindings and execution metadata; no new hash, registry, +receipt, extra execution or approval protocol is required. + +#### Scenario: A selector runs in two required matrix environments + +- **GIVEN** one ordinary passing outcome for a selected selector in each of two distinct required execution units +- **WHEN** current results are reconciled +- **THEN** the matrix outcomes are evaluated separately rather than rejected as one duplicated global test +- **AND** every required unit must satisfy its own expected selection. + +#### Scenario: A newer designated attempt has not passed + +- **GIVEN** an older passing attempt and a newer designated attempt that is pending, failed, cancelled or missing +- **WHEN** current delivery selects results +- **THEN** it does not select the older pass or merge attempts into a passing result. + +#### Scenario: A selector appears twice inside one selected unit + +- **GIVEN** two outcomes for one canonical selected selector in the same execution unit +- **WHEN** reconciliation evaluates acceptance +- **THEN** the duplicate is rejected even when both outcomes say passed. + +#### Scenario: Different runs or job retries contain the same selector + +- **GIVEN** results share a selector, candidate and environment but differ in run, attempt or actual job execution ID +- **WHEN** the supplied execution-unit identities are compared +- **THEN** they remain distinct and only the currently designated trusted unit can satisfy its selection +- **AND** malformed or ambiguous identity cannot collapse them into one passing unit. diff --git a/openspec/changes/requirements-09-minimal-evidence/specs/requirements-runtime-proof-delivery/spec.md b/openspec/changes/requirements-09-minimal-evidence/specs/requirements-runtime-proof-delivery/spec.md new file mode 100644 index 000000000..c456cade9 --- /dev/null +++ b/openspec/changes/requirements-09-minimal-evidence/specs/requirements-runtime-proof-delivery/spec.md @@ -0,0 +1,39 @@ +## MODIFIED Requirements + +### Requirement: Producer-Bound Retained Red Proof + +When a caller explicitly requests legacy historical RED/final reconciliation, core SHALL preserve immutable source, test, JUnit and toolchain bindings and reject missing, inconsistent, stale or tampered historical proof. These obligations SHALL NOT apply to ordinary current-run validation. No new change-specific bootstrap authority exception SHALL be introduced. + +#### Scenario: Red execution publishes complete immutable bindings + +- **GIVEN** an explicitly selected legacy historical RED run +- **WHEN** its report is published for legacy final reconciliation +- **THEN** source tree, merge base, selected test bytes, JUnit digest and actual runner/toolchain bindings SHALL remain complete and consistent +- **AND** the report SHALL NOT be required by ordinary current-run delivery. + +#### Scenario: Missing or inconsistent producer evidence fails closed + +- **GIVEN** an explicitly requested legacy historical report lacks valid required bindings +- **WHEN** legacy reconciliation evaluates it +- **THEN** historical verification SHALL fail without falling back to current-only evidence. + +#### Scenario: Existing tamper and chronology checks remain enforced + +- **GIVEN** an explicitly requested legacy artifact is tampered, stale or chronologically invalid +- **WHEN** the legacy reader evaluates it +- **THEN** the historical claim SHALL be rejected +- **AND** no current pass SHALL be substituted for that claim. + +#### Scenario: Producer repair bootstraps from exact failing ledger + +- **GIVEN** the historical producer-repair bootstrap is encountered as an existing record +- **WHEN** a new producer repair is planned under ordinary delivery +- **THEN** the existing record SHALL remain historical only +- **AND** the new repair SHALL use meaningful regression tests and current-candidate results without a new expiring authority or ledger exception. + +#### Scenario: Ordinary current-run validation has no historical record + +- **GIVEN** ordinary delivery with valid current-candidate results and no retained RED artifact +- **WHEN** core evaluates current execution +- **THEN** it SHALL NOT request historical artifacts, frozen test ancestry or bootstrap approval +- **AND** it SHALL make no historical chronology claim. diff --git a/openspec/changes/requirements-09-minimal-evidence/tasks.md b/openspec/changes/requirements-09-minimal-evidence/tasks.md new file mode 100644 index 000000000..e7115223c --- /dev/null +++ b/openspec/changes/requirements-09-minimal-evidence/tasks.md @@ -0,0 +1,42 @@ +# Implementation tasks + +Planning only; all implementation tasks remain unchecked. Each numbered task is a bounded work slice, split further if it exceeds two hours. Development follows spec -> relevant failing test/reproduction -> code -> passing verification, with concise notes and CI artifacts rather than immutable hosted RED history. + +## 1. Establish implementation scope + +- [ ] 1.1 Fetch current refs and create a fresh issue-linked worktree from origin/dev; retain the primary checkout and verify branch/worktree ownership. +- [ ] 1.2 Refresh public hierarchy and read back #740/#481, dependencies, project state, and the dependency review; resolve concurrent In Progress ownership before implementation. +- [ ] 1.3 Revalidate these spec deltas against current runtime and the selected signed module release; reconcile superseded R07 without applying its historical task list. +- [ ] 1.4 Prepare the exact coordinated repository/organization policy migration and rollback, including required checks, trusted workflow source, and absence of per-PR authority bypasses. +- [ ] 1.5 Before behavior tests or implementation, update and review agent rules, `openspec/config.yaml`, OpenSpec templates and pre-commit guidance using the configured-gate mapping in design.md for the owner-authorized MEB migration. Remove mandatory authored run transcripts, hosted RED and proof receipts from normal work while retaining useful regression order and independent checks. Runtime enforcement changes remain in phase 4. + +- [ ] 1.6 Open linked implementation PRs as bounded governance, signed-contract adoption and orchestration slices become reviewable. Integrate prerequisite slices through the reviewed migration policy before trusted pilot or enforcement; do not defer the first PR until after cutover. + +## 2. Specify and demonstrate regression boundaries + +- [ ] 2.1 Add focused cases for missing/empty/malformed/wrong-revision JUnit, duplicate selected outcomes within one execution unit, separate required matrix units/current attempts, exact selected outcomes (ordinary pass versus XFAIL, strict/non-strict XPASS, and empty `wasxfail` markers) and shared execution-unit identity comparisons (separate runs, matrix lanes, shards and job retries; malformed identity), and no promotion of local/self-declared metadata to CI authority; observe meaningful failures before code changes. +- [ ] 2.2 Add ordinary PR, documentation, dependency, fork and dev-to-main cases that need no retained RED, frozen mapping or authority comment; retain the real #737/#738 acceptance and authentication controls. +- [ ] 2.3 Add independent-verdict and module report compatibility cases; use current fixture data without copying historical logs into Git. + +- [ ] 2.4 Add consumer-boundary cases for fresh-session current reconciliation, rejected local authority and independent failed/unknown producer results; keep executor I/O outside this reconciler. + +## 3. Implement lean delivery + +- [ ] 3.1 Adopt the signed #481 contract after verifying its manifest/payload and supported core compatibility. +- [ ] 3.2 Consume existing required suite outputs once per candidate/environment and emit compact current-run results plus always-published diagnostics. +- [ ] 3.3 Replace the default multi-stage historical proof path and promotion reuse with current-candidate validation; retain explicit legacy behavior only where supported. +- [ ] 3.4 Update contributor/Requirements/CI documentation and verify public command examples. Keep optional assurance instructions out of default generated guidance. + +## 4. Verify and activate + +- [ ] 4.1 Run focused/full affected tests and applicable format/type/lint/contracts, independent security, code review, OpenSpec, frozen delivery, and signature checks. Keep a fresh `.specfact/code-review.json` for the reviewed candidate and disposition every finding by fix, reasoned rejection, or individually authorized documented exception before completion; do not restart historical proof ceremonies. +- [ ] 4.2 Pilot representative PR paths from the reviewed, integrated trusted source in non-blocking mode and verify evidence identity, job conclusion propagation, artifact retention and required-check emission. +- [ ] 4.3 Apply the reviewed coordinated repository and organization enforcement cutover and verify readback on dev/main. Retain the matching rollback helpers and their relevant tests through the observation/rollback window in 4.4. +- [ ] 4.4 Review ten representative PRs using existing CI data; record concise overhead/regression observations and resolve cutover defects. Only after this observation/rollback window closes, remove obsolete default-path proof helpers and their relevant tests that only implement retired policy. Do not add a metrics gate. + +## 5. Finalize + +- [ ] 5.1 Reconcile #662 and merged-awaiting-archive records without rewriting historical evidence; update CHANGE_ORDER and internal mirrors. +- [ ] 5.2 Finalize the implementation PRs opened in 1.6 with short validation/rollback notes and #740/#481 links; complete remaining reviewed integration and confirm all migration slices are accounted for. + +After protected integration, archive completed changes using `openspec archive`; never manually move superseded or unimplemented deltas into canonical specs. Remove the implementation worktree only after merge. diff --git a/openspec/changes/requirements-planning-stack-boundary/CHANGE_VALIDATION.md b/openspec/changes/requirements-planning-stack-boundary/CHANGE_VALIDATION.md new file mode 100644 index 000000000..f361a5297 --- /dev/null +++ b/openspec/changes/requirements-planning-stack-boundary/CHANGE_VALIDATION.md @@ -0,0 +1,19 @@ +# Validation: authenticated planning stack boundary + +Date: 2026-10-12 (Europe/Berlin). Current protected dev: 4e7c38c. +Issue #760 is under parent #355, assigned to djm81, labeled bug/security/change-proposal and assigned +SpecFact CLI project/Todo; native dependency lists were verified empty at scope creation. Private source +mirror is maintained and graph rebuilt. Owner authorized protective fixes and forbids weakening gates. + +Root #719 is implemented while #743/#751 remain verified planning under #481/#483 upstream readiness. +The original aggregate gate applies one runtime mapping acceptance to unrelated planning sources. +The proposed closed boundary keeps protected validator bytes, unchanged root runtime/tests/source +folders, exact signed root identity, independent member authority and complete plan/context parity. +Root proof CI changes, runtime/test/delivery changes and non-regular files retain the existing full path. +Missing or mismatched inherited proof fails closed. No fixture/provider/threshold/authority parser changes. + +Strict OpenSpec, genuine RED, focused/full tests, independent SAST and all applicable local delivery, +security, signature and version gates are recorded in TDD_EVIDENCE.md. Live prototype resolves both +current child heads and the unchanged provider validates all one/nine planning sources. This does not +claim trusted hosted adoption. Exact human mapping acceptance, signed native authored proof, final-head +CI/review, reviewed protected integration and subsequent fresh stack proof remain mandatory. diff --git a/openspec/changes/requirements-planning-stack-boundary/TDD_EVIDENCE.md b/openspec/changes/requirements-planning-stack-boundary/TDD_EVIDENCE.md new file mode 100644 index 000000000..684630e2d --- /dev/null +++ b/openspec/changes/requirements-planning-stack-boundary/TDD_EVIDENCE.md @@ -0,0 +1,182 @@ +# TDD evidence: authenticated planning stack boundaries + +Work began 2026-10-11; final local validation 2026-10-12 (Europe/Berlin). +Protected baseline: dev 4e7c38c2830f4bc53ffe78eeb0a2bfbabfc711e2. Dedicated worktree, +CPython 3.12.13, frozen uv dependencies. Owner djm81 approved exact mapping 61488846 and prerequisite exact-head grants +on 2026-10-12; native publication remains pending. + +## Specification, tests and genuine failing-before evidence + +Specify closed planning paths, immutable file modes, inherited-source protection, exact signed root +proof, unchanged protected proof workflow/local actions and independent three-stage selection first. +Tests invoke the real CLI with owned synthetic Git objects and independent GitHub route fixtures. +Every fixture strips inherited GIT_* selectors/configuration before initialization and mutations. +The runtime check metadata uses live verified workflow IDs, paths, app and repository identities. +Nested pull-request head SHA is deliberately different: that API field follows live branch state; +the immutable run's top-level head_sha binds proof. + +The original unchanged CLI/workflow yields six genuine behavior/assertion failures and one passing +existing root-workflow restriction control (2.83 seconds). Every one of the five exact mapped selectors +also fails against an isolated immutable protected-baseline archive with current authored test bytes. +No MissingFunction/ImportError, test-stage flag, fabricated assertion, waived acceptance or fixture pin +substitutes for failing evidence. An initial passing-after mode-fixture invocation could not switch its +owned Git index after executable staging; the fixture now synchronizes owned mode/index before commit. +That fixture error is discarded as proof and all real mode controls pass. + +## Implementation and passing-after + +The protected helper authenticates exact live native ancestry and chooses root-to-child planning only +for regular non-executable OpenSpec document/sidecar changes. Root-covered source directories and all +runtime/test/workflow/delivery bytes stay under the complete existing proof path. Root proof workflow +and local action changes cannot self-authorize an exemption. Unsupported layers fall back to the full +existing path; eligible layers with missing/stale/foreign/failed proof fail before evidence consumption. + +Every consumer independently compares deterministic exact check/run/attempt context and the full +post-root planning plan. Validator and frozen fixture materialization stays at protected dev/main; +FINAL_BASE_COMMIT and promotion authority are not moved to candidate objects. Candidate helper bytes +cannot grant their own scope: all consumers archive only the protected origin/dev helper pair. + +Current focused ancestry, proof, archive, promotion and workflow controls pass: 64 tests, zero skips, +8.44 seconds. The first full suite exposed four stale protected-base selection literals; the assertions +now cover authenticated planning selection with protected-base fallback and retain existing protected +materialization, plan equality, promotion attestation and failure propagation. A new direct-root control +proves protected/promotion heads never receive a planning override. Fresh full suite passes 3,299 tests, +ten disclosed skips, two existing dependency deprecation warnings, 179.80 seconds, 64% coverage. + +Read-only execution against live stack 759 authenticates both child heads against signed root a63156e1 +and exact successful Requirements run 38177085272 / authority run 38177085268 attempt 2. The unchanged +released provider validates #743 at planned maturity (one source) and #751 at planned maturity (nine +sources). These are prototype/local evidence, not trusted hosted adoption or permission to merge. + +## Independent gates and review + +Frozen uv lock/export parity, all three advisory audits, dependency-trust register, normal full license +scan, strict module signatures, synchronized 0.55.7 version sources, strict PyPI-ahead, repository +lint/type-error threshold, Actionlint/Yamllint, blocking Bandit and independent Semgrep all pass. +Semgrep has zero current findings and zero baseline findings. No dependency payload or signed module +asset changes. The coherent prerequisite uses 0.55.7; #719 must move to a later available patch after +protected integration, then regenerate native proof rather than carrying stale evidence forward. + +Both new helper and test scopes have zero new review findings after the complexity/typing corrections. +Broad final review and independently measured protected-baseline review each contain the same nine +findings, with no new findings. Two MISSING_ICONTRACT advisories concern the unchanged stdlib CLI entry points; +the remaining unchanged findings concern existing workflow and release-fixture styles. Explicit narrow exception: these +execute with -I -S before dependency installation; +adding third-party decorators to that trust boundary would break the isolated bootstrap. Existing +contracts elsewhere, required gates and review thresholds remain unchanged. Current-head hosted/native evidence remains +required before integration. + +## Mapping and authority boundary + +Exact source mapping: sha256:61488846c84633e69d586fb818aed1be6cca123cf05e821406fdbdbfdbbb91b4. +It maps both Requirements to five exact executable selectors. Owner djm81 accepted this exact mapping on 2026-10-12; +the genuine acceptance record is included. +Normal publication is blocked by the repository's exact mapping/index-bound Requirements review gate. +The owner authorized prerequisite exact signed-head grants after verification; native authored RED/final +publication remains mandatory. Normal prerequisite merge authorization must follow a concrete green reviewed PR. +Stack PRs #719, #743 and #751 remain unmerged. Rollback restores the coherent protected helper/workflow +pair and captured branch heads with force-with-lease, returning children to the original blocking gate. + +## Final immutable mode review + +An additional existing-document-to-symlink fixture reproduces a real Git T status. The original raw +parser returns a context error instead of retaining full proof; the recorded failing assertion precedes +the parser fix. The final parser recognizes T and rejects non-regular modes through the existing full +proof fallback. Git object reads explicitly disable external diff and text conversion. All nineteen +planning/native-ancestry controls pass in 2.97 seconds, without skipped controls. The five exact mapping +selectors also fail genuinely against an isolated immutable protected-baseline archive with final test +bytes: zero passes, errors or skips, and exact JUnit selector identities. Mapping 61488846 is unchanged. + +## Final independent isolated-CLI authored phase + +This head intentionally contains specification and authored tests before implementation. PR #761 +remains Draft throughout this phase; passing-after claims below describe named historical candidates, +not missing production code at this authored head. Required final execution/review gates stay enforced. + +First direct-import authored 436bd0a3 proves real RED, but reuse is correctly rejected as stale when +production CLI changes. The harness now invokes the public Python -I -S script with caller-owned +Git/GitHub response fixtures. Signed isolated authored 880b5adb has genuine native RED run 38181748242, +and signed implementation 46e475df passes all three native Requirements stages in run 38182008657 +plus exact-head authority. These are historical for the final strengthened test-file bytes. + +Review strengthens two independent negative controls: both required check records remain present when +a duplicate page record is appended, with coherent total_count=103; Unicode paths each start from the +same immutable child checkout. Both pass against the implementation. A controlled unpublished probe +removing duplicate-ID enforcement makes the duplicate assertion fail; that guard mutation is restored +and never published. Existing runtime/mode loops already reset to their immutable child per case. + +The Unicode regression reproduces context rejection on 2c4348d1 before filesystem-decoding correction. +The fixed prototype preserves full-proof fallback for unsupported paths without relaxing strict Git +metadata, inherited source protection, signature or root check identities. Eleven isolated controls +passed on the previous candidate; the two strengthened controls pass in 1.29 seconds. +The approved exact mapping 61488846 and genuine owner acceptance remain unchanged. New authored native +RED must precede restoration and publication of the implementation with these exact test bytes. + +## Final strengthened native authored proof + +Signed authored head 684b8f92fca1c514183d7b809415d8ce32eb8df4 (tree bc5d142191aaa42ab08b3c8c7016f3cb11210691) has +native run 38182359051, artifact 11717743992. The exact approved mapping reconciles at RED with no findings: five +genuine failures, zero passes, errors or skips. Exact-head authority succeeds. Test file SHA-256 is +476a54c9170618d0f491babd5bf628f80957a76bea3afa583a0ad10ba337fa6a. + +The implementation from reviewed historical candidate 46e475df is restored only after this proof; strengthened test +bytes, accepted mapping and owner record remain unchanged. Final native execution, required CI and review remain +pending on the newly signed implementation head. + +## Current protected-control regression: failing before code + +Review identifies that root.base.sha can advance beyond the root branch's common ancestor. The existing specification +already requires root proof workflow/local-action Git objects to match the protected base. Two independent regression +cases use a newer protected workflow and a newly protected local action while retaining the exact successful old-root +checks. Both genuinely fail on immutable candidate 2af0b8af: planning reuse is incorrectly enabled. An independent +protected docs update is the positive control and passes. No imported production, mapping changes, weakened freshness +or additional acceptance claims are involved. The fix must compare current protected control objects while retaining +the original merge-base proof boundary. + +Historical pre-correction full suite: 3301 passed, 10 disclosed skips, 2 dependency warnings in 188.89 seconds; +timeout/failure log lines are deliberately injected by passing negative runner tests, not failed delivery execution. +Final corrected head still requires fresh execution. + +The corrected helper records the current live protected base before deriving the proof merge base. Root workflow and +local-action object comparison uses that current protected commit; OpenSpec source closure and full-proof fallback keep +the original proof merge base. Both security regressions and the independent unchanged-control positive case pass in +the isolated prototype; the normal three-stage context digest includes the preserved protected identity. + +## Both proof-producing workflow controls: failing before code + +Final review checks the authority producer as well as the Requirements producer. The existing unchanged-proof contract +applies to both. New current-protected-authority drift and root-authored-authority cases genuinely fail on signed +0c682d4d: both receive planning reuse despite changed authority objects. The other three existing protected-control +cases pass; there are no collection errors or skips. The correction must include both named proof-producing workflow +paths while preserving local-action rejection, current protected identity, complete-proof fallback and every exact +signature/run binding. Approved mapping 61488846 and immutable mapped test bytes stay unchanged. + +After signed authored authority-control tests, the guard derives both producer paths from the existing immutable +WORKFLOWS definitions; no new workflow identities or caller-controlled paths are introduced. The same current protected +object comparison now rejects edits to either producer and every local action. Passing focused controls and final +exact-head native/CI review must be regenerated before integration. + +## Authenticated check-read capability: failing before configuration + +GitHub's installation-token check-runs API documents Checks read access; explicitly declaring any Actions permission +leaves unspecified permissions at none. Official references accessed 2026-10-12 (Europe/Berlin): + and +. Public-resource +unauthenticated access also exists; this workflow deliberately uses its scoped GITHUB_TOKEN to authenticate root proof +and should declare its exact read capability. No live authorization failure is asserted from a local user token. + +Three independent consumer permission assertions genuinely fail against signed 11342fd3 because checks read is absent. +An initial incorrect execution-job fixture key is corrected to the verified requirements-evidence job; that KeyError is +discarded as proof. The final three failures are configuration assertions with no collection errors or skips. The fix +adds checks: read only to the three consumers, keeps every existing permission read-only, and adds no write scope, +broad token or authority exemption. Approved mapping and exact native selected test bytes stay unchanged. + +After signed authored permission controls, all three existing consumers declare checks: read alongside their unchanged +actions/contents/issues read capabilities. Unspecified scopes stay none; no top-level broad grant, write capability, +candidate policy override or proof identity relaxation is introduced. Focused workflow/ancestry/control verification +and final native/CI/review remain required on the newly signed head. + +The first passing-after focus run catches one obsolete three-scope allow-list literal in the existing +independent-consumer test. It now requires the precise four read-only capabilities, including Checks read; the equality +assertion and all protected materialization, ordering and context/plan checks remain enforced. The other 54 focus cases +already pass. This is capability parity with authenticated root verification, not a write-scope or gate exemption. diff --git a/openspec/changes/requirements-planning-stack-boundary/design.md b/openspec/changes/requirements-planning-stack-boundary/design.md new file mode 100644 index 000000000..862e408cc --- /dev/null +++ b/openspec/changes/requirements-planning-stack-boundary/design.md @@ -0,0 +1,29 @@ +## Decision + +Keep existing protected proof_base_commit and native chain authentication. Add planning_base_commit only +for closed planning layers above an authenticated successful exact root. The changed-path modes and both +source/destination paths are read from Git objects, not submitted labels. Root-covered active source +prefixes are derived from the original protected-base-to-root source diff; every child byte under those +prefixes remains protected. All post-root runtime/test/support bytes are excluded by the document allowlist. + +Require the root Requirements workflow and local CI action bytes to match protected-base objects before +allowing inherited proof. Candidate CI cannot establish its own planning exemption. Unsupported changed +proof controls retain the existing complete proof path. + +Authenticate the latest matching GitHub Actions consumer and authority checks, then their immutable run +metadata and the exact signed root commit. A failed or in-progress latest run blocks consumption. Store +check/run identities in deterministic context so producer, execution and verdict reject intervening changes. +The planning report covers all sources from root to child, including earlier planning layers, at planned +maturity. Existing exact child member authority remains independent. This adds no execution claim for future +proposals and does not copy root RED into a different aggregate plan. + +Materialize both helpers from protected origin/dev. Candidate helper bytes cannot authorize their own +scope. The new prerequisite itself uses the existing full implementation/RED path. Do not broaden the +allowlist to fix a failed gate; unsupported layers retain the existing path and require actual proof. + +## Rollback and limits + +Revert the coherent workflow/helper prerequisite and restack; planning children return to the existing +blocking aggregate gate. No requirements fixture, grant semantics, review gate or protected rule changes. +The root-to-child allowlist is deliberately limited to current OpenSpec planning paths. Exact-root successful +CI is inherited proof, not child acceptance, and subsequent runtime edits still require full independent proof. diff --git a/openspec/changes/requirements-planning-stack-boundary/proposal.md b/openspec/changes/requirements-planning-stack-boundary/proposal.md new file mode 100644 index 000000000..6770dce4f --- /dev/null +++ b/openspec/changes/requirements-planning-stack-boundary/proposal.md @@ -0,0 +1,46 @@ +# Change: Validate planning layers above a verified native stack root + +## Why + +Stack #759 contains an implemented security root followed by unrelated planning proposals. Selecting the +entire stack at one implementation maturity applies a single accepted mapping to all sources and blocks +legitimate deferred planning. Accepting or claiming execution for those proposals would be incorrect. + +## What Changes + +- Authenticate the existing native ancestry and an exact-root successful Requirements execution and + member-authority run before selecting an additional planning-only boundary. +- Allow that boundary only when every root-to-child path is a regular non-executable OpenSpec planning + file and no root-covered OpenSpec source changes. Runtime, tests, workflow, delivery, canonical specs, + executables, symlinks and changes to inherited security sources retain the full existing proof path. +- Preserve protected dev/main as the trust boundary for validator materialization. Select the root head + only as the planning diff boundary, independently and consistently in producer, execution and verdict. +- Keep the inherited verified implementation and child planned maturity distinct; do not relabel RED, + alter provider semantics, infer acceptance, skip checks or change fixture pins. + +## Capabilities + +### New Capabilities + +- `requirements-planning-stack-evidence`: authenticated closed-scope planning above verified runtime. + +### Modified Capabilities + +None. Existing full-plan RED reconciliation remains mandatory for implementation layers. + +## Impact + +Affected code: protected stack context helper, one stdlib planning-boundary helper and Requirements +workflow selection. Existing exact-head authority, frozen delivery, independent checks and review remain. +Requires a reviewed prerequisite merge before candidates can use protected helper bytes, then restacking +and fresh proof. Existing upstream modules #481/#483 contracts and deferred roadmap implementation are +unaffected. One bugfix version bump will be synchronized at final implementation publication. + +## Source Tracking + + +- GitHub issue: #760, . +- Parent feature: #355; project: SpecFact CLI/Todo; labels: bug, security, change-proposal. +- Owner authorized protective fixes for stack #759, preserving required gates and leaving its PRs unmerged. +- Native blocked-by/blocking lists are empty. Prerequisite merge and fresh exact-head grants require owner + authorization after a concrete validated result is ready for review. diff --git a/openspec/changes/requirements-planning-stack-boundary/requirements-evidence.yaml b/openspec/changes/requirements-planning-stack-boundary/requirements-evidence.yaml new file mode 100644 index 000000000..e6104afe9 --- /dev/null +++ b/openspec/changes/requirements-planning-stack-boundary/requirements-evidence.yaml @@ -0,0 +1,60 @@ +schema_version: '2' +requirements: + ? openspec:requirements-planning-stack-boundary:requirements-planning-stack-evidence:authenticate-unchanged-inherited-proof-before-planning-selection + : rationale: Preserve implementation proof and member authority while separately validating deferred planning. + stakeholder_refs: + - nold-ai/specfact-cli#760 + touchpoints: + - id: planning-boundary + kind: code_symbol + locator: scripts/requirements_stack_context.py::main + verification_cases: + - case_id: PLANNING-PROOF + scenario_id: authenticate-unchanged-inherited-proof-before-planning-selection + method: test + intent: Authenticate signed exact runtime root and immutable checks before separating planning. + observable: Planning diff and protected validator base remain distinct with exact run attempts. + selector: + runner: pytest + node_id: tests/unit/scripts/test_requirements_planning_stack_context.py::test_verified_planning_child_preserves_protected_trust_and_maturities + - case_id: PLANNING-FULL + scenario_id: authenticate-unchanged-inherited-proof-before-planning-selection + method: test + intent: Change real runtime, test, CI, delivery and inherited source Git objects. + observable: Unsupported mutations retain complete protected-base proof. + selector: + runner: pytest + node_id: tests/unit/scripts/test_requirements_planning_stack_context.py::test_runtime_and_inherited_source_changes_keep_complete_proof + - case_id: PLANNING-FORGERY + scenario_id: authenticate-unchanged-inherited-proof-before-planning-selection + method: test + intent: Exercise failed, pending, foreign or mismatched root check, run, issuer and signature. + observable: Evidence consumption fails closed. + selector: + runner: pytest + node_id: tests/unit/scripts/test_requirements_planning_stack_context.py::test_forged_or_stale_root_checks_fail_before_evidence + - case_id: PLANNING-MODES + scenario_id: authenticate-unchanged-inherited-proof-before-planning-selection + method: test + intent: Stage executable, symlink, payload, deleted and moved inherited sources. + observable: OpenSpec names cannot exempt unsafe changes. + selector: + runner: pytest + node_id: tests/unit/scripts/test_requirements_planning_stack_context.py::test_non_regular_planning_paths_keep_complete_proof + ? openspec:requirements-planning-stack-boundary:requirements-planning-stack-evidence:reconcile-planning-evidence-independently-of-verifier-trust + : rationale: Preserve implementation proof and member authority while separately validating deferred planning. + stakeholder_refs: + - nold-ai/specfact-cli#760 + touchpoints: + - id: independent-consumers + kind: config_file + locator: .github/workflows/requirements-evidence.yml + verification_cases: + - case_id: PLANNING-CONSUMERS + scenario_id: reconcile-planning-evidence-independently-of-verifier-trust + method: test + intent: Check all independent consumers use protected helpers and common planning selection. + observable: Protected materialization and final plan/context equality remain enforced. + selector: + runner: pytest + node_id: tests/unit/scripts/test_requirements_planning_stack_context.py::test_all_consumers_separate_planning_selection_from_protected_verifier_trust diff --git a/openspec/changes/requirements-planning-stack-boundary/requirements-proof/review-evidence.json b/openspec/changes/requirements-planning-stack-boundary/requirements-proof/review-evidence.json new file mode 100644 index 000000000..87d83f08f --- /dev/null +++ b/openspec/changes/requirements-planning-stack-boundary/requirements-proof/review-evidence.json @@ -0,0 +1,9 @@ +{ + "schema_version": "1", + "decision": "accepted", + "reviewer_id": "djm81", + "reviewer_role": "repository-owner", + "recorded_at": "2026-10-11T22:30:56.434662+00:00", + "reference": "https://github.com/nold-ai/specfact-cli/issues/760", + "mapping_digest": "sha256:61488846c84633e69d586fb818aed1be6cca123cf05e821406fdbdbfdbbb91b4" +} diff --git a/openspec/changes/requirements-planning-stack-boundary/specs/requirements-planning-stack-evidence/spec.md b/openspec/changes/requirements-planning-stack-boundary/specs/requirements-planning-stack-evidence/spec.md new file mode 100644 index 000000000..d2570263f --- /dev/null +++ b/openspec/changes/requirements-planning-stack-boundary/specs/requirements-planning-stack-evidence/spec.md @@ -0,0 +1,55 @@ +## ADDED Requirements + +### Requirement: Authenticate unchanged inherited proof before planning selection + +Producer, independent execution and final verdict SHALL authenticate the same live native ancestry. +A planning child MAY select its stack root head as a proposal diff boundary only when every changed path +is a regular non-executable OpenSpec planning document or sidecar, and no source directory covered by +the root's changed OpenSpec sources is modified. All root runtime, selected tests, test support, mappings +and delivery bytes SHALL remain unchanged. Protected dev/main SHALL remain the trusted validator source. +The root proof workflow and its local CI actions SHALL match protected-base Git objects; a root that changes +those controls SHALL retain the complete proof path. The system SHALL authenticate successful exact-root +Requirements and member-authority runs from their +expected GitHub Actions app, repository, workflow IDs, paths, event, branch and SHA, plus root signature. + +#### Scenario: Closed planning layer preserves both maturities + +- **GIVEN** a live native child containing only eligible planning changes above an unchanged verified root +- **WHEN** its current-root Requirements and authority checks and signed head authenticate successfully +- **THEN** planning selection SHALL use that root head for every post-root proposal source +- **AND** protected-base trust and the root's complete verified implementation proof SHALL remain unchanged +- **AND** child proposals SHALL report planned maturity without implementation or RED claims + +#### Scenario: Runtime or inherited source change retains full proof + +- **GIVEN** a child changes code, tests, workflows, delivery, canonical specs or root-covered source files +- **WHEN** its boundary is resolved +- **THEN** it SHALL NOT receive a planning boundary +- **AND** the existing full-stack implementation and exact mapping/plan/RED checks SHALL remain mandatory + +#### Scenario: Forged or stale upstream proof is rejected + +- **GIVEN** eligible planning paths but missing, failed, pending, unsigned, foreign or mismatched root proof +- **WHEN** the boundary is resolved +- **THEN** resolution SHALL fail before proposal evidence is consumed +- **AND** no stack membership or child grant SHALL substitute for inherited proof + +#### Scenario: Symlink or executable document is not planning + +- **GIVEN** a child supplies an executable, symlink or non-document payload under an OpenSpec path +- **WHEN** the boundary is resolved +- **THEN** it SHALL NOT receive a planning boundary +- **AND** root-covered document deletions or moves SHALL NOT escape source protection + +### Requirement: Reconcile planning evidence independently of verifier trust + +All three Requirements stages SHALL derive the same authenticated planning selection and context digest. +They SHALL continue materializing validator, runtime fixtures and delivery controls from protected-base +objects. Final planning plan equality SHALL remain enforced independently of the inherited root verdict. + +#### Scenario: Every consumer retains protected validator materialization + +- **GIVEN** an eligible authenticated planning child +- **WHEN** producer, independent execution and final verdict select its post-root proposals +- **THEN** all three SHALL use the same planning diff boundary +- **AND** protected validator materialization and complete plan/context equality SHALL remain enforced diff --git a/openspec/changes/requirements-planning-stack-boundary/tasks.md b/openspec/changes/requirements-planning-stack-boundary/tasks.md new file mode 100644 index 000000000..322ff891b --- /dev/null +++ b/openspec/changes/requirements-planning-stack-boundary/tasks.md @@ -0,0 +1,26 @@ +## 1. Scope and readiness + +- [x] 1.1 Reuse a clean attached worktree on a dedicated branch from refreshed dev. +- [x] 1.2 Verify live issue #760 parent, labels, project/Todo and empty native dependencies. +- [x] 1.3 Read repository rules, existing stack protocol and private strategic context. +- [x] 1.4 Mirror scope privately and validate OpenSpec strictly. + +## 2. TDD and proof + +- [x] 2.1 Add tests for authenticated planning selection and all rejection/fallback boundaries. +- [x] 2.2 Capture real failing-before evidence without implementation edits. +- [x] 2.3 Review mapping 61488846 and obtain owner djm81 acceptance before authored native proof. +- [x] 2.4 Publish signed authored tests with genuine native RED and exact-head authority. + +## 3. Implementation and validation + +- [x] 3.1 Implement protected closed-scope selection and exact-root check/run authentication. +- [x] 3.2 Integrate selection independently in producer, execution and final verdict; preserve trust base. +- [ ] 3.3 Run security, quality, review, frozen-delivery, signature and version gates plus native final proof. +- [ ] 3.4 Create a reviewable prerequisite PR and obtain integration authorization only after required checks pass. + +## 4. Stack and cleanup + +- [ ] 4.1 Restack #719/#743/#751 after reviewed integration and regenerate actual final-head proof. +- [ ] 4.2 Resolve only verified findings and leave those three PRs unmerged. +- [ ] 4.3 After merge, archive via OpenSpec CLI, update private wiki, and clean the worktree. diff --git a/openspec/changes/security-stack-gate-readiness/.openspec.yaml b/openspec/changes/security-stack-gate-readiness/.openspec.yaml new file mode 100644 index 000000000..a36921962 --- /dev/null +++ b/openspec/changes/security-stack-gate-readiness/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-11 diff --git a/openspec/changes/security-stack-gate-readiness/CHANGE_VALIDATION.md b/openspec/changes/security-stack-gate-readiness/CHANGE_VALIDATION.md new file mode 100644 index 000000000..5c6470506 --- /dev/null +++ b/openspec/changes/security-stack-gate-readiness/CHANGE_VALIDATION.md @@ -0,0 +1,23 @@ +# Prerequisite validation + +Validated against dev 553b46f01c6581c96dc92133b6e77404c8df7cd9 and immutable +module fixture 69f075819be5e1ceca1446b026b0417f19e584ca. The owner explicitly +requested this gate-repair scope on 2026-10-11 Europe/Berlin. + +The changes add a data-only validator and isolate a test harness. Production CLI +interfaces, released Requirements full-plan reconciliation, immutable module +fixture, signatures and dependency versions retain their existing contracts. +No new runtime dependency is needed. Native ancestry and missing frozen packages +fail closed. The organization companion changes accepted base contexts only +after native API verification; it preserves all existing grant checks. + +`openspec validate security-stack-gate-readiness --strict` passes. Two meaningful +regressions fail before production edits; their frozen parent assertions use +bounded child processes. No breaking public interface is identified. The published portable-fixture head aae60949 passes ordinary hosted checks and +all three native evidence stages against the retained original RED. Its full local +smart run passes 3,238 tests with unchanged coverage. These are historical for +the subsequent import-isolation repair: the repaired source now passes 3,242 full smart tests, zero-diagnostic changed-file +typing, contracts, frozen parity, three audits, trust, licenses, signatures, version +and static security gates. Fresh exact-head native and hosted checks are required. Exact-head member authority and normal gated prerequisite integration +remain pending; neither is replaced by implementation-agent evidence. +Confidence: Medium until the final heads and complete restacked chain pass. diff --git a/openspec/changes/security-stack-gate-readiness/TDD_EVIDENCE.md b/openspec/changes/security-stack-gate-readiness/TDD_EVIDENCE.md new file mode 100644 index 000000000..37c64d8dc --- /dev/null +++ b/openspec/changes/security-stack-gate-readiness/TDD_EVIDENCE.md @@ -0,0 +1,409 @@ +# Gate readiness evidence + +## Authored failures — 2026-10-11 Europe/Berlin + +Specs precede tests; production files remain unchanged against dev +553b46f01c6581c96dc92133b6e77404c8df7cd9. The two selected regressions fail: + +- Native stack resolution: the missing adapter prevents resolving an authentic + root boundary; the parent assertion records a real failure. +- Direct smoke isolation: querying the actual launcher reports the running + pytest environment prefix, outside the smoke workspace. + +Command: `.venv/bin/python -m pytest -q -o addopts=''` with the two mapped +selectors. Result: **2 failed in 0.67 seconds**. Raw log retained at +`/private/tmp/specfact-security-stack-20261011/prerequisite-red.log`. +The child probes never mutate a shared installation. Their test bytes and +fixture are to remain frozen through implementation and final reconciliation. + +Strict OpenSpec validation passes. The new-test self-review has zero findings. +The first planned mapping diagnostic identified the released provider's +requirement-slug scenario identity constraint. Correct the mapping metadata +before authored publication; this does not change test behavior or production. +No member authority is inferred from the implementation-agent acceptance record. + +## Native and implementation validation + +Native authored run [38091288977](https://github.com/nold-ai/specfact-cli/actions/runs/38091288977) +retains `failing-first-proven` for both unchanged selectors, with no findings. +The report and JUnit remain original artifacts; no passing control was labeled RED. +Implementation focused suite: **29 passed in 1.70 seconds**. Direct and console +real marketplace smoke both pass; the invoking frozen pytest environment remains +unchanged. The auxiliary lock input-tamper control failed before its checker was +implemented; it now passes with the lock/export parity checks. + +Organization policy: original protected-base-only verifier failed the valid native +stack test (**1 failed, 25 passed**); the implemented adapter passes all **26** +controls, including missing member authority, unrelated ancestry and stale events. +Policy tests and immutable stdlib-only adapter retain the protected execution +boundary; no PR content is imported and no required rule is disabled. + +Stdlib-only stack parsing is required in the immutable policy and `python -I -S` +consumer boundary. Its contract is enforced by bounded schema validation and +negative tests rather than importing project contract decorators into that trust +boundary. This is a runtime boundary choice, not a quality-gate exclusion. + +## Explicit narrow review dispositions + +The self-review retains two `MISSING_ICONTRACT` warnings for +`requirements_stack_context.resolve_stack_context` and `main`. These two functions +must run under `python -I -S` in an immutable organization policy with no dependency +installation. Importing project `icontract` would break that execution boundary. +Their schema, bounded traversal, exact identity/ancestry and stable fail-closed +contracts are executable stdlib checks, covered by positive/negative regressions. +This is the rare explicit exception permitted by the agent quality rules; no +review rule, scan, or required check is disabled, and warnings remain visible. + +Two existing informational readability suggestions concern unchanged functions +`_create_rootless_monorepo_demo` and `_build_local_registry`. Preserve their expanded +versioned demo/manifest fixtures: separate explicit files and fields explain the +domain contract, while a collapsed string/comprehension obscures it. Neither +function is modified by this repair. This records their inspection disposition +without suppressing findings or introducing unrelated refactors. + +The first full smart run found ten failures: one leaked parent `pip-compile` +entry point and nine stale workflow/version contracts. The child PATH now +excludes the invoking interpreter's tool directory and clears its virtualenv +selection variables; normal child pip dependency installation is still exercised. +Existing workflow tests now assert the independently authenticated root base, +consumer context digest parity and retained early-failure reports. Their isolated +shell fixtures supply the new authenticated context variable. These repairs pass +**59 tests in 25.42 seconds**, including the real smoke launched through Hatch. +The new PATH control has separate genuine RED evidence; frozen native selector +files and their fixture remain byte-identical to the authored commit. + +The next full run passed 3,221 tests and exposed one remaining bootstrap +archive-count expectation. The materializers now conditionally archive the frozen +smoke lock from the same authenticated base, preserving legacy bases that lack +that input. The contract test asserts that exact protected source and conditional +ordering; it still rejects invalid legacy identity/ancestry substitutions. + +The existing naming advisory on +`test_invalid_utf8_authority_is_stable_metadata_failure` is a regex substring match +on **metadata**, not a generic API identifier. Its test body and selector are +unchanged and remain referenced by completed #692 evidence. Preserve that exact +selector rather than invalidate prior proof to satisfy an overbroad substring +rule. The unchanged expanded security/artifact fixture suggestions are similarly +retained for their explicit reviewed data and attack boundaries. These dispositions +remain visible in the raw review; no rule or report is suppressed. + +## Completed local gates — 2026-10-11 Europe/Berlin + +`hatch run smart-test-force --level full`: **3,222 passed, 13 skipped**, exit 0, +169.49 seconds. Coverage 64% meets the unchanged configured 50% line threshold; +`smart-test-status` reports no relevant changes and no full run required. Do not +confuse the separate 80% public-API contract-density threshold with line coverage. + +Explicit touched-script contract validation: pass, zero violations. Changed-file +BasedPyright: zero errors and zero warnings. Full repository typing retains its +existing advisory warnings, with zero errors. Ruff, changed YAML, Actionlint and +strict OpenSpec pass. Semgrep SAST: zero findings outside the unchanged baseline; +Bandit: zero medium/high findings. All three frozen advisory audits pass. License +scans: 172 parent packages, seven Code Review smoke packages and four core module +manifests, zero violations; the exact existing Pylint Code Review-only exception +remains visible and is not widened. PyPI-ahead confirms 0.55.5 > 0.55.4. + +Organization policy prerequisite [nold-ai/.github#3](https://github.com/nold-ai/.github/pull/3) +has a verified signed head and passing pinned policy CI (26 controls). Neither +prerequisite has been merged; final-head native CI and member authority remain +required before integration and dependency restacking. + +The final full self-review passes (score 97), with zero errors. A new cohesive +assertion-count warning was resolved by separating smoke-lock source validation +from legacy bootstrap identities; the exact assertions remain exercised. Remaining +warnings/informational suggestions have the explicit dispositions above. + +The normal commit hook rejected generated documentation drift produced with the +execution fixture. Regenerate with the separate immutable documentation fixture +046d366c190af7a188d6f48953a96edb65e766db; preserve the documented runtime/review +commands instead of committing an execution-fixture downgrade. No hook is skipped. +Live GitHub metadata validation succeeds for all five unchanged heads in #757: +each resolves root #744, protected dev 553b46f..., and consecutive parent ancestry. + +## Policy review repair + +Review of signed head 198f4326 identified a hard policy violation: the older +Code Review fixture declares Python BasedPyright, whose wheel requires an +unofficial Node runtime. Artifact inspection and passing checks cannot override +`docs/agent-rules/55-dependency-hygiene.md`. The prior seven-package seed and its +passing results are historical diagnostics, not merge readiness. + +Six new smoke policy/fixture controls failed before this repair. A separate trust +control also failed because the auxiliary graph was not checked. The repair uses +the already reviewed portable snapshot 046d366c (Code Review 0.50.1), with its +real unmodified manifests, separately from the unchanged 69f0758 Requirements +provider. CI authenticates the exact commit/tree and cleanliness before execution. +The seed now contains only Lz4 4.4.5; the existing npm BasedPyright runner stays +in use. Existing wheel-prohibition and security-floor policy now checks the +auxiliary lock as well. No native mapped selector/fixture bytes are changed. +Fresh full smart, frozen, security, review and native checks are required. + +Portable fixture validation: real direct/console marketplace smoke passes; +19 initial policy/trust controls and 114 workflow controls pass. The complete +smart run passes **3,238 tests, nine skipped**, 163.06 seconds, with unchanged +64% coverage and configured threshold. All three audits, lock/export parity, +normal parent plus Lz4-child license scans, module signatures and 0.55.5 > 0.55.4 +version gates pass. Full typing has zero errors and the same 1,526 baseline +warnings; all seven changed Python files have zero diagnostics. Independent +Semgrep has zero findings outside the unchanged baseline. + +The review's new parameter-count warning is resolved by retaining the trust +checker's original five-parameter API: the auxiliary graph always uses its +committed authoritative paths. The focused trust controls still reject the +prohibited wheel. The remaining three contract warnings in the pre-existing +stdlib-only trust checker are the same pre-dependency boundary as documented in +its module header and dependency-hygiene rules; `python -S` positive/negative +controls pass. Importing third-party decorators before frozen bootstrap would +break that boundary. Existing `test_*process*` naming warnings describe actual +pytest subprocess invariants; their bodies/selectors are unchanged and preserve +prior test evidence. The existing version-parser information item retains an +explicit invalid-version branch for fail-closed security-floor diagnosis. +No rule or finding is suppressed and no new clean-code regression is accepted. + +Two mocked launcher-lifecycle tests now model the fixture-verification seam; +the separate real Git commit/tree/dirty rejection tests and real marketplace +smoke remain active. The stale blanket license assertion is narrowed to retain +the original Code Review-only GPL boundary while requiring normal scanning of +the sole Lz4 smoke seed. An interrupted intermediate rerun is retained as +incomplete evidence, not a pass. + +Final interface verification: **3,238 passed, nine skipped**, exit 0, +160.91 seconds; unchanged coverage 64%. Absolute-path changed-script contract +validation passes with zero violations. Final SpecFact self-review scores 94, +zero errors and no new clean-code regression; all remaining raw advisories have +the explicit stdlib-boundary or unchanged-selector dispositions above. + +## Complete child import isolation + +A second external review correctly identified that read-only parent site-packages +and inherited PYTHONPATH can mask missing or incompatible child dependencies. +Two new controls fail before implementation: the import path includes parent/ +inherited directories, and the seed omits the complete frozen CI graph. The +repair installs the full hash-protected CI graph plus the Lz4 seed into each +owned child, uses only the explicit CLI source PYTHONPATH, and builds the console +entry point with its child-installed frozen backend. Normal marketplace +resolution remains enabled with no index. The direct native prefix probe and +all original mapped test/fixture bytes remain unchanged. Earlier local/hosted +passes remain historical; fresh complete validation is required on this repair. + +The initial pip-only installation exposes the existing source-only CommentJSON/ +Lark packages: applying wheel-only installation to the whole CI graph correctly +fails. Preserve that diagnostic. Materialize the exact full graph with the same +frozen uv protocol as CI, offline into the owned child; unavailable cached source/ +build artifacts fail, with no resolver/network fallback. Then install the reviewed +Lz4 wheel through its exact hashes. This reuses frozen artifact data rather than +parent import directories and preserves all normal marketplace dependency checks. + +The child cache path assertion also fails before repair (KeyError: UV_CACHE_DIR). +The implementation queries uv's original artifact-cache path before changing HOME; +offline frozen sync copies packages into the child and never adds cache/parent +directories to PYTHONPATH. See the official uv cache-directory precedence + (accessed 2026-10-11). + +Normal marketplace resolution needs candidate artifacts even when packages are +already installed. Two additional controls fail before wheelhouse implementation: +the frozen transitive hash selector is absent and PIP_FIND_LINKS is missing. The +repair selects only the authenticated module closure from uv.lock and exact +committed hash exports, downloads wheels with require-hashes/no-deps, and supplies +that owned directory with no index to normal pip-tools resolution. Missing graph +entries or hashes fail. The smoke project's ordinary pip-tools configuration emits +pins without duplicating transport options already supplied by its environment; +this leaves dependency validation and installation active. No force or skip-deps +flag is used. Ten positive/negative controls pass. Real direct and console smoke +both pass after full child installation and marketplace resolution. Earlier pip +network, source-only wheel, cache-miss and option-emission failures are retained as +diagnostics rather than passing evidence. + +Final owned-child smart gate: **3,242 passed, nine skipped, two warnings**, +171.93 seconds, exit 0; coverage remains 64% and its existing threshold is +unchanged. Absolute-path contract validation passes with zero violations. Both +changed Python files have zero typing diagnostics. Frozen lock/export parity, +three advisory audits, dependency trust, normal license scans, signatures, +0.55.5 > PyPI 0.55.4 and zero-new-findings Semgrep gates pass. Negative-test +log messages from smart-test regression fixtures are expected captured output, +not failures of this complete run. Original native mapped selector/support bytes +remain unchanged from 0dbcc8e3. Protected exact-head authority and prerequisite +integration remain pending; these executable passes cannot replace member approval. + +The final broad self-review covers all **20** prerequisite Python files (including +existing workflow regression fixtures), scores **92, PASS_WITH_ADVISORY**, zero +errors. The new CC16 selector error and new seed-length information item are +resolved with separate manifest selection, frozen graph traversal and sync helpers. +The remaining nine warnings are the five stdlib/pre-dependency contract boundaries +and four unchanged naming false positives already disposed above. The nine +information items concern the unchanged invalid-version branch and explicit +security/workflow fixture setup. Collapsing those setup assertions would reduce +reviewability of independently tested attack boundaries; no rule is disabled, +no finding is hidden, and no new clean-code regression is accepted. +An initial focused invocation without the portable fixture skipped its real smoke; +that invocation is not used as real-smoke evidence. Full smart and the separately +executed explicit-fixture console probe exercise the real authenticated fixture. + +Post-review complete rerun: **3,242 passed, nine skipped, two warnings**, +168.26 seconds, exit 0, coverage 64%. Final changed-file typing has zero +diagnostics and the reviewed helper contracts pass with zero violations. The +explicit-fixture console smoke passes independently. The protected/core native +adapter remains byte-identical (SHA-256 6cc6c72de461980aba67c3c144364d7ac4f8f53fb351e77b31bfc8fcdc450d89). + +## Git refs and pinned source staging review corrections + +Fresh review identifies two valid issues. The stdlib branch allowlist rejects +valid plus/at-sign/Unicode characters and accepts hidden or lock-suffix path +components. Thirty-one literal branch cases compare with Git: eleven fail before +repair. The ignored-file archive control also fails before repair: an ignored +Python file enters a module archive despite a clean porcelain status. +Git ref rules: and ignored status +behavior: (accessed 2026-10-11). + +Ref validation now follows literal Git branch rules while rejecting option-leading +names, reserved/control characters, traversal, reflog expressions and invalid +components. Both stdlib adapters remain byte-identical: SHA-256 +86ab97f88c1857eb881a5c36ff848763aa8afa339ae76cd50f88c1bd24b7e314. +The protected policy retains all original 26 controls and passes all 57 after +the eleven additional genuine RED failures. + +Archive staging reads immutable object bytes from the authenticated pinned commit +and extracts with the standard data filter. It excludes ignored/untracked files +and avoids checkout races. A further real bootstrap control fails before the +import-path repair: the child reads an ignored raw-checkout source. Child bundle +bootstrap now points exclusively to the owned staged Git tree, with inherited +bundle roots removed and raw metadata retained solely as the authenticated seed +input. The 61 focused controls pass, including real direct marketplace smoke. +Existing semantic-version unit fixtures model the staging seam; the separate +real Git archive/ignored-file control and actual runtime smoke exercise it. +The interrupted intermediate full run is incomplete, not passing evidence. +Fresh complete smart/native/hosted validation is required on the final head. + +The broad 21-file self-review remains PASS_WITH_ADVISORY, zero errors, with the +same nine warning and nine information dispositions above. Five changed files +have zero typing diagnostics. The original mapped native selectors and support +bytes are unchanged. No required policy, dependency check or signature gate is +relaxed. + +Final staged-source full gate: **3,275 passed, nine skipped, two warnings**, +170.57 seconds, exit 0; coverage remains 64% with the existing threshold. The +first staged-source full run caught the new bootstrap control in the wrong +migration surface (one failure, 3,274 passed). Relocating the same assertion to +the existing retained bootstrap test file passes all four migration controls; +the migration rule and allowlist are unchanged. Its existing fixture annotation +is corrected to pytest.MonkeyPatch for mandatory changed-file typing. All six +changed Python files now have zero diagnostics. Broad 22-file review passes with +zero errors and the same documented dispositions. Real direct and console smoke +exercise pinned archive and bundle-import bytes. The original native cases stay +byte-identical; final-head hosted/native and member authority remain required. + +## Ordinary forks and mocked installer lookup (2026-10-11) + +The ordinary fork regression fails before repair: one failed, three passed in +both core and protected policy copies. A non-stacked fork targeting dev/main +retains its own proof boundary. Its head repository identity is bound to live +GitHub data; a native fork, foreign base or stale event identity still fails. +Native parent and promotion repository checks remain unchanged. All 61 protected +policy controls and 55 focused core/smoke controls pass after repair. + +The fully mocked child-seed control fails when PATH omits uv. Stubbing its +installer lookup makes that same test pass with PATH=/usr/bin:/bin, without +executing an installer or changing real runtime behavior. + +The downstream-trigger finding does not reproduce: Requirements run +[38088538194](https://github.com/nold-ai/specfact-cli/actions/runs/38088538194) +was a pull_request event for #727 head 44d18861, based on #744 branch +dependabot/pip/development-security-updates-e660df2248 at 76a42fc8. All three +Requirements jobs executed; they failed at missing retained root RED, which this +prerequisite repairs. The native stack API reports stack #757 position 2. The +existing protected-root trigger filter stays unchanged. + +At aabcefe3 all ordinary required CI and all three native proof jobs pass; the +external member authority fails without a grant. Fresh validation is required +for the subsequent fork/mock correction head. + +## Advanced protected root (2026-10-11) + +Two real Git-history controls (ordinary root and native child) fail before repair +in both copies: the declared protected tip differs from the branches' common +ancestor. The CLI now computes a unique merge base from the exact authenticated +root head/base Git objects before writing proof context. Missing, unrelated or +ambiguous objects fail closed. The protected authority continues to import only +the data resolver and never executes Git or PR content. Original native mapped +selectors/support remain byte-identical. All 63 policy and 57 focused core/smoke +controls pass; forced contract validation reports zero violations. + +The claimed missing stack response does not reproduce. A fresh authenticated +GET /repos/nold-ai/specfact-cli/pulls/727 includes the native stack object +(id 1987468, number 757, position 2, size 5, protected root dev/553b46f0). +No additional synthetic membership or weaker ancestry path is added. + +The preceding full fork/mock gate passed 3,278 tests in 184.75 seconds, ten +skipped, two warnings, 64% coverage. The additional skip is the existing +clean-index version-source test because changes were staged; the actual +version-source script passes independently. Fresh complete validation follows +the merge-base correction. + +Final merge-base full smart gate: **3,280 passed, ten skipped, two warnings**, +183.53 seconds, exit 0, unchanged 64% coverage threshold. The additional +staged-index version test skip is covered by the actual passing version-source +script; strict network PyPI gate confirms 0.55.5 > 0.55.4. All four newly changed +Python files have zero diagnostics; full repository typing has zero errors and +1,522 existing advisory warnings. Forced contracts report zero violations. +Semgrep has zero current/new findings, and Bandit has zero medium/high findings. +Broad 24-file SpecFact review scores 92 with zero errors and the same documented +nine warning/nine information dispositions. Its newly reported fork-test CC13 +warning is fixed by separating fixture arrangement from assertions; all four +core fork cases and all 63 policy controls pass after that simplification. +Original native mapped files remain byte-identical. Final-head hosted checks, +member capabilities and normal prerequisite integration remain required. + +## Producer verifier boundary ordering (2026-10-11) + +Final review finds a valid trust-order defect for stacked children: delivery +verification selected a merge base against the immediate unmerged parent before +resolving the protected proof root. Two controls fail before repair: step +ordering and real Git archive-byte provenance. The byte control archives +marker data only and executes no verifier. The producer now authenticates +context before frozen dependency setup and selects its verifier from +PROOF_BASE_COMMIT. All 52 targeted delivery/promotion/security controls pass, +retaining promotion identities and all three independent proof consumers. +Original native mapped files remain byte-identical. Final full smart, workflow +lint and exact-head hosted validation follow this additional correction; +e29b1a2e's successful native run is historical for the subsequent head. + +Final producer-boundary local validation: **3,282 passed, ten existing skips, +two warnings**, 170.03 seconds, exit 0, unchanged 64% coverage. Plain Actionlint +and changed-workflow Yamllint pass; changed-file typing has zero diagnostics. +Strict OpenSpec passes. Broad 25-file SpecFact review scores 92 with zero errors +and the same documented nine warning/nine information dispositions; the new +workflow tests add no findings. No source graph, native mapping, promotion +condition or required gate is weakened. Fresh signed-head hosted/native +validation and explicit member authority remain required. + +## Owned Git fixture environment (2026-10-11) + +Six read-only controls in core and three in policy fail before repair when +GIT_DIR, GIT_COMMON_DIR or GIT_WORK_TREE selects a foreign temporary repository. +The reproduction executes no checkout or commit. Fixture commands and child +probes now clear inherited GIT_* variables and verify their absolute owned .git +before changing refs. Related smoke-policy Git fixtures use file-local isolation +and ownership checks. All 21 targeted core controls and all 66 policy controls +pass, with zero changed typing diagnostics. Running the actual 21 mutation +controls with all three selectors poisoned passes and leaves the foreign +repository's complete file hashes unchanged. Primary hooks, signed publication +and required gates keep their normal environment. + +Git environment semantics: (accessed 2026-10-11). +The review's additional generic CWE-78 note does not identify a request-input +path: helper arguments are test-owned literals/fixture SHAs, passed as argv, +without a shell. The valid repository-selection finding is corrected. A rejected +whole-file restore was replaced by a scoped patch preserving existing edits. +Fresh complete smart/review and signed-head CI follow this fixture correction; +7ce75de5 native success is historical for the next head. + +Complete fixture-isolation smart gate: **3,288 passed, ten existing skips, two +warnings**, 184.75 seconds, exit 0, unchanged 64% coverage. Broad 25-file review +scores 92, zero errors, same documented nine warning/nine information +dispositions. The new fixture naming/duplicate ownership-check findings are +repaired with an idiomatic public autouse fixture and one shared owned-init +helper, preserving every assertion. All 21 affected controls pass again, with +zero changed typing diagnostics. The protected policy's signed d1ea845c head +passes all 66 controls in hosted run 38101673411. Original mapped native inputs +remain byte-identical. Fresh signed-head hosted/native checks and member +authority remain required; no PR is merged and no capability is posted. diff --git a/openspec/changes/security-stack-gate-readiness/design.md b/openspec/changes/security-stack-gate-readiness/design.md new file mode 100644 index 000000000..71f2dc4c8 --- /dev/null +++ b/openspec/changes/security-stack-gate-readiness/design.md @@ -0,0 +1,61 @@ +# Gate-preserving stack readiness + +Keep full native plan equality. A native stack context identifies its live root +PR and protected dev/main base; it does not rewrite a RED report or add a passing +case to RED. Authenticate every parent edge against GitHub native stack identity, +position, size, repository, exact branch/head and commit ancestry. Revalidate live +metadata in all consumers; missing, ambiguous, changed or forked metadata fails. +The existing provenance validator then uses the authenticated immutable root base, +original RED artifact and current source. Test/support changes remain stale proof. + +Author the complete dependency scope before replaying its implementation after a +normal prerequisite integration. Mappings, executable selectors and acceptance +must stay unchanged across the five layers. A different mapping or plan remains a +hard reconciliation failure; no scoped digest relaxation is introduced. + +Direct/console smoke runs receive workspace-owned virtual environments with the +complete frozen CI graph installed offline into each child. Inherited Python paths +and parent site-packages are excluded; only the explicit CLI source path is exposed. Runtime smoke independently +authenticates the reviewed portable 046d366c module snapshot; native Requirements +keeps its 69f0758 provider and original proof unchanged. Only reviewed Lz4 is +seeded from an exact hash-protected wheel lock. BasedPyright keeps the approved +npm runner; unofficial Node wheels are prohibited in every frozen graph. +Marketplace pip sees only child-installed packages with union version +constraints and no index. This +prevents upgrades in the running pytest environment and fails on missing pins. Other launcher modes retain their existing delivery +checks. Existing isolated virtualenv fallback cache controls remain intact. + +Organization authority executes only immutable organization policy, never PR +bytes. Native parent branches are accepted only after API ancestry validation; +all existing unedited member-comment, exact commit/tree, seven-day expiry and +live collaborator-permission checks remain required. A prerequisite policy PR +must be reviewed and pass its own tests before integration. + +Risks: parent pushes invalidate context; missing frozen module dependencies fail +closed; rebases invalidate head-bound grants and proof. Mitigate with final-head +validation, reviewed frozen inputs and new grants. Rollback uses captured heads, +explicit leases and normal revert of prerequisites; never disable required gates. + +The normal pip-tools marketplace resolver also receives a child-owned wheelhouse. Its inputs are the transitive closure of authenticated, unmodified module manifests, selected from the committed frozen graph and exact hash-protected exports. Downloads require the committed hashes and wheels; resolution keeps its existing constraints and no-index policy. Missing or unreviewed graph entries fail closed. No resolver or install validation is skipped. + +Ref compatibility follows Git branch naming rules in the standard-library-only adapter, while retaining traversal, reserved-sequence and option-boundary rejection. Smoke archives stage bytes from the authenticated pinned Git object instead of copying untracked or ignored checkout files. Original mapped native selectors stay frozen. + +Child bundle bootstrap also points only to the owned staging tree. Raw checkout metadata remains a separate authenticated seed input; inherited bundle-root overrides are removed. This closes ignored-file imports in addition to archive contents. + +Ordinary fork PRs with no native stack retain the protected-base single-PR proof path and bind the live/event head repository identity. Native membership and parent traversal continue to require this repository; same-named forks cannot enter promotion reuse, whose existing identity guards remain unchanged. + +The data-only resolver authenticates the declared live protected root. The CLI +uses those exact SHA-addressed Git objects to derive the root merge base before +writing proof context. Ordinary and native root branches may be behind an +advanced protected tip; their original proof baseline remains an ancestor. The +protected authority imports only the data resolver and executes no Git/PR bytes. + +Producer authentication must precede trusted verifier selection and execution. +The delivery verifier is archived from the authenticated protected proof base, +never the immediate unmerged parent. A stacked child's parent verifier bytes +remain candidate input and cannot supply the trusted implementation. + +Git-history fixtures must clear inherited repository-selecting GIT_* settings +from fixture commands and child probes. Before changing refs they verify the +absolute Git directory is their owned temporary .git. Primary repository hooks, +commit signing and required gates retain their normal environment and behavior. diff --git a/openspec/changes/security-stack-gate-readiness/proposal.md b/openspec/changes/security-stack-gate-readiness/proposal.md new file mode 100644 index 000000000..95157d0c4 --- /dev/null +++ b/openspec/changes/security-stack-gate-readiness/proposal.md @@ -0,0 +1,54 @@ +# Change: Resolve security stack readiness gates + +## Why + +On 2026-10-11 Europe/Berlin the owner explicitly requested resolving Requirements +member authority, native parent-proof reuse and local smart-test failures for +stack #757, preserving required gates. The existing dependency slice excludes +these workflow repairs, so this prerequisite has its own scope and evidence. + +## What Changes + +- Isolate direct and console runtime smoke launchers from the running test + interpreter. Borrow the already frozen runtime through an explicit child path, + constrain marketplace pip resolution to committed versions and prohibit new + package downloads in those launchers. +- Authenticate same-repository native GitHub stack ancestry through live API data + before selecting the root branch and immutable base for retained RED lookup. +- Revalidate stack context independently in producer, execution and final verdict; + retain original Git/JUnit/digest, immutable fixture and test-support checks. +- Add organization-policy support for authenticated native stack bases while + retaining the exact head/tree member grant, expiry and live write permission. +- Rebase the dependency PRs onto the reviewed prerequisite and author their entire + unchanged five-layer plan before replaying dependency implementation. Obtain + fresh RED/final evidence; keep historical evidence and all five PR numbers. + +No inferred member approval, fabricated proof, mutable fixture, waiver or ignored +failure is permitted. No CLI dependency upgrade beyond the approved stack or +change to the released Requirements reconciliation semantics is included. + +## Capabilities + +### New Capabilities + +- `requirements-stack-evidence`: authenticated native-stack proof selection. +- `runtime-smoke-isolation`: isolated direct/console marketplace smoke execution. + +### Modified Capabilities + +None. Existing authority, frozen delivery and native reconciliation remain gates. + +## Impact + +Affected code: runtime smoke harness, data-only stack context validator and +Requirements workflow; companion organization policy. One prepared 0.55.5 +bugfix remains shared with the dependency stack. A prerequisite merge changes +proof bases and requires signed rebases and new final-head evidence. + +## Source Tracking + + +- GitHub issue: #747, +- Parent Epic: #194; project: SpecFact CLI #1; owner explicitly resumed this work. +- Related stack: #757; preserved PRs: #744, #727, #752, #754, #753. +- Dependency consolidation remains open for broader accepted work. diff --git a/openspec/changes/security-stack-gate-readiness/requirements-evidence.yaml b/openspec/changes/security-stack-gate-readiness/requirements-evidence.yaml new file mode 100644 index 000000000..b09ce6377 --- /dev/null +++ b/openspec/changes/security-stack-gate-readiness/requirements-evidence.yaml @@ -0,0 +1,55 @@ +schema_version: '2' +requirements: + openspec:security-stack-gate-readiness:requirements-stack-evidence:authenticate-native-stack-proof-ancestry: + rationale: Preserve exact native proof while authenticating parent relationships. + stakeholder_refs: + - nold-ai/specfact-cli#747 + touchpoints: + - id: stack-validator + kind: code_symbol + locator: scripts/requirements_stack_context.py::resolve_stack_context + verification_cases: + - case_id: STACK-PROOF-001 + scenario_id: authenticate-native-stack-proof-ancestry + method: test + intent: Retain the actual root proof branch and base after checking native ancestry. + observable: The child resolves the original root without changing artifact identities. + selector: + runner: pytest + node_id: tests/unit/scripts/test_requirements_stack_context.py::test_native_stack_uses_original_root_proof_boundary + openspec:security-stack-gate-readiness:requirements-stack-evidence:preserve-independent-member-authority: + rationale: Stack membership cannot substitute for a valid member grant. + stakeholder_refs: + - nold-ai/specfact-cli#747 + touchpoints: + - id: organization-policy + kind: config_file + locator: .github/workflows/requirements-evidence.yml + verification_cases: + - case_id: STACK-AUTH-001 + scenario_id: preserve-independent-member-authority + method: inspection + intent: Run independent organization policy tests for valid and absent grants. + observable: Exact head/tree, expiry, edit state and current permission remain mandatory. + openspec:security-stack-gate-readiness:runtime-smoke-isolation:isolate-marketplace-smoke-installations: + rationale: Marketplace installation must preserve the active test interpreter. + stakeholder_refs: + - nold-ai/specfact-cli#747 + touchpoints: + - id: direct-launcher + kind: code_symbol + locator: scripts/runtime_discovery_smoke.py::_launcher_command + verification_cases: + - case_id: STACK-SMOKE-001 + scenario_id: isolate-marketplace-smoke-installations + method: test + intent: Query the real child interpreter prefix without modifying shared files. + observable: The installation prefix is workspace-owned and differs from pytest's prefix. + selector: + runner: pytest + node_id: tests/integration/scripts/test_runtime_smoke_isolation.py::test_direct_smoke_uses_workspace_owned_interpreter + - case_id: STACK-SMOKE-002 + scenario_id: isolate-marketplace-smoke-installations + method: inspection + intent: Run direct/console marketplace smoke and full smart testing from frozen inputs. + observable: Parent package files and frozen versions are unchanged and all tests pass. diff --git a/openspec/changes/security-stack-gate-readiness/requirements-proof/review-evidence.json b/openspec/changes/security-stack-gate-readiness/requirements-proof/review-evidence.json new file mode 100644 index 000000000..cab58e7a7 --- /dev/null +++ b/openspec/changes/security-stack-gate-readiness/requirements-proof/review-evidence.json @@ -0,0 +1,9 @@ +{ + "schema_version": "1", + "decision": "accepted", + "reviewer_id": "codex:/root", + "reviewer_role": "implementation-agent", + "recorded_at": "2026-10-10T22:22:56.008362+00:00", + "reference": "https://github.com/nold-ai/specfact-cli/issues/747", + "mapping_digest": "sha256:445bec8cc79359eebedec98629c0a4f22d94e4c7d6429536cfd08dcacab7f7ab" +} diff --git a/openspec/changes/security-stack-gate-readiness/specs/requirements-stack-evidence/spec.md b/openspec/changes/security-stack-gate-readiness/specs/requirements-stack-evidence/spec.md new file mode 100644 index 000000000..b72c6265c --- /dev/null +++ b/openspec/changes/security-stack-gate-readiness/specs/requirements-stack-evidence/spec.md @@ -0,0 +1,33 @@ +## ADDED Requirements + +### Requirement: Authenticate native stack proof ancestry + +A stacked PR SHALL reuse retained RED only from its live same-repository native +stack root, with exact full plan equality, immutable artifacts and unchanged +selected tests/support. Producer, execution and final verdict SHALL independently +validate the same context. Root base SHALL be protected dev or main. + +#### Scenario: Valid parent proof remains usable + +- **GIVEN** live native stack members form an exact same-repository branch and commit chain +- **WHEN** a child selects retained RED from its root branch +- **THEN** the original root base and branch are returned without relabeling proof +- **AND** the existing Git/JUnit and full mapping/plan checks remain mandatory + +#### Scenario: Invalid or stale ancestry is rejected + +- **GIVEN** a parent is ambiguous, forked, changed, missing or outside the native stack +- **WHEN** stack proof context is resolved +- **THEN** resolution fails before proof consumption + +### Requirement: Preserve independent member authority + +The organization policy SHALL require an unedited member grant for the exact +current head/tree, expiry and current write/admin permission. A stack base SHALL +be accepted only after native ancestry validation and SHALL NOT grant authority. + +#### Scenario: Stack membership does not grant approval + +- **GIVEN** a valid native chain without a matching member comment +- **WHEN** the authority gate validates the current PR +- **THEN** the gate fails diff --git a/openspec/changes/security-stack-gate-readiness/specs/runtime-smoke-isolation/spec.md b/openspec/changes/security-stack-gate-readiness/specs/runtime-smoke-isolation/spec.md new file mode 100644 index 000000000..a65e076ab --- /dev/null +++ b/openspec/changes/security-stack-gate-readiness/specs/runtime-smoke-isolation/spec.md @@ -0,0 +1,20 @@ +## ADDED Requirements + +### Requirement: Isolate marketplace smoke installations + +Direct and console smoke launchers SHALL use a workspace-owned interpreter, +leaving the invoking test environment unchanged. Their marketplace resolution +SHALL use committed version constraints and SHALL not download new packages. + +#### Scenario: Direct launcher owns its installation prefix + +- **GIVEN** smoke is called from the running pytest interpreter +- **WHEN** the direct launcher is prepared +- **THEN** its prefix is inside the smoke workspace and differs from pytest's prefix + +#### Scenario: Frozen marketplace resolution preserves parent imports + +- **GIVEN** reviewed runtime dependencies are installed in the invoking environment +- **WHEN** direct or console marketplace modules are installed +- **THEN** frozen package versions satisfy module requirements without upgrading parent files +- **AND** missing or incompatible dependencies fail visibly diff --git a/openspec/changes/security-stack-gate-readiness/tasks.md b/openspec/changes/security-stack-gate-readiness/tasks.md new file mode 100644 index 000000000..1db63c2a4 --- /dev/null +++ b/openspec/changes/security-stack-gate-readiness/tasks.md @@ -0,0 +1,21 @@ +## 1. Authored prerequisite + +- [x] 1.1 Create isolated worktrees, capture current stack heads and refresh metadata. +- [x] 1.2 Author specs/tests and retain real failures before implementation. +- [x] 1.3 Validate strict OpenSpec and actual agent acceptance; mirror scope internally. + +## 2. Gate repairs + +- [x] 2.1 Isolate direct/console smoke interpreter and frozen marketplace resolution using the authenticated portable fixture and approved npm tool path. +- [x] 2.2 Authenticate native stack context and use it in all proof consumers. +- [x] 2.3 Extend immutable organization policy with tested stack ancestry checks. +- [ ] 2.4 Pass native, smart, contract, quality/review, frozen/security and signature gates. + +## 3. Integration and final heads + +- [ ] 3.1 Publish signed prerequisite PRs and obtain required member grants/review. +- [ ] 3.2 Integrate prerequisites through normal protected workflows. +- [ ] 3.3 Rebase all five PRs sequentially; author the complete plan before dependency replay. +- [ ] 3.4 Capture fresh RED/final, grants and full final-head CI; resolve review feedback. +- [ ] 3.5 Deliver merge-ready stack #757 and updated promotion/rollback evidence. +- [ ] 3.6 Archive this prerequisite with OpenSpec after reviewed integration. diff --git a/openspec/changes/validation-02-full-chain-engine/CHANGE_VALIDATION.md b/openspec/changes/validation-02-full-chain-engine/CHANGE_VALIDATION.md index 6624c2058..4c2aff3f5 100644 --- a/openspec/changes/validation-02-full-chain-engine/CHANGE_VALIDATION.md +++ b/openspec/changes/validation-02-full-chain-engine/CHANGE_VALIDATION.md @@ -1,6 +1,6 @@ # Change Validation: validation-02-full-chain-engine -- **Validated on (UTC):** 2026-03-22T22:28:26+00:00 +- **Historical validation snapshot (UTC):** 2026-03-22T22:28:26+00:00 - **Workflow:** /wf-validate-change (proposal-stage dry-run validation) - **Strict command:** `openspec validate validation-02-full-chain-engine --strict` - **Result:** PASS @@ -26,3 +26,9 @@ - Required artifacts are present and parseable. - Strict OpenSpec validation passed. - Change is ready to consume clean-code review output as a parallel quality dimension. + +## Current planning amendment — 2026-09-20 + +The normative deltas now preserve independent current execution, optional chronology, unassessed coverage and original producer verdicts. This is planning-only; no runtime readiness follows from the historical snapshot above. Strict OpenSpec and scoped Markdown/whitespace checks passed for this amendment. Serialization uses distinct ADDED requirements rather than replacing producer-owned Full Chain Validation or Policy Engine, so archival cannot delete those owners' scenarios or require an absent target requirement. Core retains the envelope contract; modules retains emission and file persistence. The earlier broad dependency observations are historical, not new blockers on R09 or workflow adoption. + +The sidecar extension also uses a distinct ADDED requirement, preserving existing spec-code behavior without replacing an absent generic Sidecar Validation header. Strict validation reports no archive incompatibility for these two amended changes. diff --git a/openspec/changes/validation-02-full-chain-engine/design.md b/openspec/changes/validation-02-full-chain-engine/design.md index c69c5e4d3..3ddf1e593 100644 --- a/openspec/changes/validation-02-full-chain-engine/design.md +++ b/openspec/changes/validation-02-full-chain-engine/design.md @@ -1,5 +1,11 @@ ## Context +## Scope rescope — 2026-09-20 + +Consume current_execution independently from optional chronology. No global graph, full-chain completeness, or historical proof prerequisite for . Missing mappings remain unassessed coverage, not fabricated completeness. Preserve graph validation for users who request that capability. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + This change implements the narrowed validation evidence graph scope for `validation-02-full-chain-engine`. It is proposal-stage only and defines implementation strategy without changing runtime code. diff --git a/openspec/changes/validation-02-full-chain-engine/proposal.md b/openspec/changes/validation-02-full-chain-engine/proposal.md index 7c7beb9f2..0913286b9 100644 --- a/openspec/changes/validation-02-full-chain-engine/proposal.md +++ b/openspec/changes/validation-02-full-chain-engine/proposal.md @@ -1,5 +1,11 @@ # Change: Validation Evidence Graph Engine +## Scope rescope — 2026-09-20 + +Consume current_execution independently from optional chronology. No global graph, full-chain completeness, or historical proof prerequisite for . Missing mappings remain unassessed coverage, not fabricated completeness. Preserve graph validation for users who request that capability. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + ## Why SpecFact should not own the full upstream planning lifecycle. Spec Kit, diff --git a/openspec/changes/validation-02-full-chain-engine/requirements-evidence.yaml b/openspec/changes/validation-02-full-chain-engine/requirements-evidence.yaml new file mode 100644 index 000000000..c17616918 --- /dev/null +++ b/openspec/changes/validation-02-full-chain-engine/requirements-evidence.yaml @@ -0,0 +1,34 @@ +schema_version: "2" +x-planning-context: &planning-context + rationale: "Make this requirement reviewable before implementation begins." + stakeholder_refs: ["nold-ai/specfact-cli#241"] +x-planning-inspection: &planning-inspection + method: inspection + observable: >- + Strict OpenSpec validation retains the requirement and its scenarios + without claiming implementation or test execution. +x-spec-1: &spec-1 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/validation-02-full-chain-engine/specs/full-chain-validation/spec.md" +x-spec-2: &spec-2 + - id: openspec-source-spec + kind: config_file + locator: "openspec/changes/validation-02-full-chain-engine/specs/sidecar-validation/spec.md" +requirements: + openspec:validation-02-full-chain-engine:full-chain-validation:full-chain-validation: + <<: *planning-context + touchpoints: *spec-1 + verification_cases: + - <<: *planning-inspection + case_id: VAL02-001 + scenario_id: full-chain-validation + intent: "Inspect the full chain validation planning contract and its scenarios." + openspec:validation-02-full-chain-engine:sidecar-validation:full-chain-sidecar-inputs: + <<: *planning-context + touchpoints: *spec-2 + verification_cases: + - <<: *planning-inspection + case_id: VAL02-002 + scenario_id: full-chain-sidecar-inputs + intent: "Inspect the full chain sidecar inputs planning contract and its scenarios." diff --git a/openspec/changes/validation-02-full-chain-engine/specs/full-chain-validation/spec.md b/openspec/changes/validation-02-full-chain-engine/specs/full-chain-validation/spec.md index 9cb91564f..31d6b7e3e 100644 --- a/openspec/changes/validation-02-full-chain-engine/specs/full-chain-validation/spec.md +++ b/openspec/changes/validation-02-full-chain-engine/specs/full-chain-validation/spec.md @@ -1,9 +1,17 @@ +## Scope rescope — 2026-09-20 + +Consume current_execution independently from optional chronology. No global graph, full-chain completeness, or historical proof prerequisite for . Missing mappings remain unassessed coverage, not fabricated completeness. Preserve graph validation for users who request that capability. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + ## ADDED Requirements ### Requirement: Full Chain Validation The system SHALL validate Requirement -> Architecture -> Spec -> Code -> Test transitions and emit layered evidence. +The system SHALL represent `current_execution` independently from optional historical chronology. Current passing tests SHALL NOT establish historical failing-before evidence, full requirement coverage or protected CI authority. Missing requirement/architecture/spec mappings SHALL remain unassessed coverage or explicit findings under the selected graph policy, never fabricated completeness. Independent producer verdicts SHALL retain their original status and source reference. Ordinary current-run verification SHALL NOT require full-chain execution, chronology, transcripts, approval receipts or duplicate proof execution. + #### Scenario: Full-chain command emits transition-level results - **GIVEN** `specfact validate --full-chain --output json --evidence-dir .specfact/evidence/` @@ -31,3 +39,25 @@ The system SHALL validate Requirement -> Architecture -> Spec -> Code -> Test tr - **WHEN** the validation run completes - **THEN** the evidence output includes a `code_quality` summary sourced from `specfact review` - **AND** the traceability layers remain limited to `req_to_arch`, `arch_to_spec`, `spec_to_code`, and `code_to_tests` + +#### Scenario: Current tests pass while graph coverage is incomplete + +- **GIVEN** ordinary current tests pass and required graph mappings are absent +- **WHEN** evidence is evaluated or serialized +- **THEN** `current_execution` remains separate from incomplete graph coverage +- **AND** missing mappings retain their own findings or unassessed status rather than becoming complete coverage +- **AND** no historical chronology or protected CI authority is inferred. + +#### Scenario: Independent producer failure survives current test success + +- **GIVEN** current tests pass but an independently required review or policy producer fails +- **WHEN** evidence is evaluated or serialized +- **THEN** both original verdicts and source references remain available +- **AND** the passing test result does not turn the failed required gate into acceptance. + +#### Scenario: Current-only evidence has no historical receipt + +- **GIVEN** a valid current-run result with no historical chronology or prior session receipt +- **WHEN** ordinary current-run verification consumes it +- **THEN** absence of optional history does not invalidate the current-execution claim +- **AND** no full-chain run, transcript, approval receipt or duplicate proof execution is required. diff --git a/openspec/changes/validation-02-full-chain-engine/specs/sidecar-validation/spec.md b/openspec/changes/validation-02-full-chain-engine/specs/sidecar-validation/spec.md index ab2586669..a620b4f69 100644 --- a/openspec/changes/validation-02-full-chain-engine/specs/sidecar-validation/spec.md +++ b/openspec/changes/validation-02-full-chain-engine/specs/sidecar-validation/spec.md @@ -1,6 +1,12 @@ -## MODIFIED Requirements +## Scope rescope — 2026-09-20 -### Requirement: Sidecar Validation +Consume current_execution independently from optional chronology. No global graph, full-chain completeness, or historical proof prerequisite for . Missing mappings remain unassessed coverage, not fabricated completeness. Preserve graph validation for users who request that capability. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../../../requirements-09-minimal-evidence/proposal.md). + +## ADDED Requirements + +### Requirement: Full Chain Sidecar Inputs The sidecar validation capability SHALL support full-chain payload checks in addition to spec-code checks. diff --git a/openspec/changes/validation-02-full-chain-engine/tasks.md b/openspec/changes/validation-02-full-chain-engine/tasks.md index a35b21f61..458c6f6f1 100644 --- a/openspec/changes/validation-02-full-chain-engine/tasks.md +++ b/openspec/changes/validation-02-full-chain-engine/tasks.md @@ -1,5 +1,13 @@ # Tasks: validation-02-full-chain-engine +## Scope rescope — 2026-09-20 + +Consume current_execution independently from optional chronology. No global graph, full-chain completeness, or historical proof prerequisite for . Missing mappings remain unassessed coverage, not fabricated completeness. Preserve graph validation for users who request that capability. + +This owner-requested scope amendment takes precedence over conflicting default-workflow or dependency wording below. It changes planning only; runtime policy is unchanged. [Replacement policy](../requirements-09-minimal-evidence/proposal.md). + +Implementation discipline: use focused regression/reproduction and current-run results. Any task requiring immutable hosted RED, frozen test authoring or approval receipts merely to implement this feature is superseded; tests of an explicitly selected optional seal feature remain in scope. + ## 1. Branch and dependency guardrails - [ ] 1.1 Create dedicated worktree branch `feature/validation-02-full-chain-engine` from `dev` before implementation work: `scripts/worktree.sh create feature/validation-02-full-chain-engine`. @@ -10,7 +18,7 @@ - [ ] 2.1 Finalize `specs/` deltas for all listed capabilities and cross-check scenario completeness. - [ ] 2.2 Add/update tests mapped to new and modified scenarios. -- [ ] 2.3 Run targeted tests to capture failing-first behavior and record results in `TDD_EVIDENCE.md`. +- [ ] 2.3 Run targeted tests before behavior edits and summarize relevant failures and subsequent passes in concise validation notes with existing local or CI references; no authored TDD ledger is required under the effective lean policy. ## 3. Implementation @@ -29,3 +37,8 @@ - [ ] 5.1 Update `openspec/CHANGE_ORDER.md` status/dependency notes if implementation sequencing changed. - [ ] 5.2 Open a PR from `feature/validation-02-full-chain-engine` to `dev` with spec/test/code/docs evidence. + +## Current-evidence boundary acceptance + +- [ ] Test current-only verification without history, incomplete mappings alongside passing tests, and independent failed producer gates; retain producer statuses and references. +- [ ] Verify native archival preserves the normative boundaries and adds serialization requirements without replacing producer-owned validation or policy requirements. diff --git a/openspec/changes/workflow-01-turn-orchestration/CHANGE_VALIDATION.md b/openspec/changes/workflow-01-turn-orchestration/CHANGE_VALIDATION.md new file mode 100644 index 000000000..5c4fe2437 --- /dev/null +++ b/openspec/changes/workflow-01-turn-orchestration/CHANGE_VALIDATION.md @@ -0,0 +1,115 @@ +## Planning validation + +Date: 2026-09-20 Europe/Berlin. Scope: standalone workflow proposal and issue metadata; runtime implementation is unstarted and all implementation tasks remain unchecked. + +This proposal was separated from the combined lean-evidence planning PR at the owner's request. Its branch stacks on the cleaned lean-planning branch, so the PR diff contains harness-owned planning only. R09 issues #740/#481 and their plans retain their separate ownership; no changes to those issues or files are part of this proposal. + +The prior combined plan passed strict OpenSpec and scoped documentation validation. Strict OpenSpec 1.13.0 validation, scoped Markdown and whitespace checks passed on the separated branch. The signed #481/v3 compatibility prerequisite is explicit in proposal/design/spec/tasks; runtime acceptance awaits implementation and actual signed releases. + +Native metadata is User Story, djm81, enhancement/openspec/change-proposal, SpecFact CLI project 1 / Todo, core #742 parent #372 or modules #483 parent #163. Native direction remains modules #481 -> modules #483 -> core #742 runtime adoption; independent projection is not blocked on signed runtime delivery. Refresh/read back this metadata before implementation. + +Normal hooks on the same combined planning artifacts rejected Requirements with unsupported-sidecar-schema. The existing owner-authorized local planning-only Block2 exception is retained for separation commits; it does not change runtime, protected CI or remote policy and is not a passing Requirements/review claim. Other applicable hooks and scoped documentation checks must pass. Workflow receipts remain disposable local progress, separate from Requirements, ReviewReport and governance envelopes. + +Separated branch base: `b84644491fd6f8a7c9b6dbf6245d352c271242a5` on `codex/lean-requirements-evidence`. Proposal scope is limited to this change and the workflow sections of INTEGRATION/CHANGE_ORDER. + +## Risk-first planning creation validation — 2026-10-08 + +Scope: this existing paired change and its change-order section only. The owner explicitly authorized the supplied risk-first plan. This is documentation/planning work; executable assumption probes, TDD RED/GREEN, runtime adoption and bounded trials are inapplicable to creation and remain future unchecked tasks. No new skill/runtime command is activated. The 2026-10-04 amendment, independent producer authority, effective finding policy, source/index preservation, ownership and exact signed #481/v3 compatibility gates are retained. + +### Baseline and prepared artifacts + +- Isolated branch: `codex/plan-workflow-risk-first`; Git HEAD at creation validation `fc8188e37008476a55fb6117267d51db80866bce`. Modules starts from refreshed `origin/dev` after merged PR #484. Core starts at draft PR #743's inspected head, with its base `codex/lean-requirements-evidence` at `c8635ed53965c51a1b06f4c2b1cc6a07dcf85f67`; its original branch/base is preserved. +- Proposal/design, existing ADDED capability deltas, unchecked tasks, planned evidence mappings and change ordering were reconciled. No duplicate change/capability, competing stage/command, report/receipt schema, verdict, ledger or automatic collection was introduced. Public artifacts contain adapted guidance and public sources; private research stays local. +- Delivery order: skills/projection -> deterministic verification -> bounded repair -> opt-in PR automation. Two engineer-days is an initial investment checkpoint, not a full-runtime estimate. Trials await a subsequently authorized nontrivial change in each repository. + +### Executed checks + +- OpenSpec CLI 1.13.2: `openspec validate workflow-01-turn-orchestration --strict --json --no-interactive` passed with no findings. +- `openspec validate --changes --strict --json --no-interactive`: 38/39 passed on the creation worktree based on inspected draft PR #743 head `fc8188e37008476a55fb6117267d51db80866bce`; this is not the dev-based publication result. The existing unrelated `governance-01-evidence-output` failure was reproduced before editing and remained identical afterward; no new all-change failure was introduced. +- Native Requirements evidence on the actual amended index snapshot, `--staged --required-maturity planned`, passed: schema 2, observed maturity `planned`, delivery status `proposal-only`, no source/mapping findings. Index staging was temporary and restored; no commit was made. Inspection cases use the existing native requirement identity convention with individual scenario intent/observable text; no executable selectors or historical proof were fabricated. +- Mapping file SHA-256 at validation: `85adb25bab04be8e43dd62641488105360d7020aff681e2b3555a92d0e556f9a`. Existing producer-generated evidence is ignored under `.specfact/reports/risk-first/`; it is planning evidence, not signed runtime compatibility acceptance. +- Scoped Markdown lint (repository core configuration), local planning-link integrity and `git diff --check` passed. Two pre-existing extra blank lines in the already touched modules change order were normalized. Every implementation/trial checkbox remains unchecked; all touched public paths are OpenSpec Markdown/YAML. +- Existing core documentation command validation passed (393 command prefixes); documentation-accountability passed against the paired modules worktree; enforced documentation frontmatter passed (24 documents). +- Native evidence used the repository-owned `requirements_evidence_delivery_gate.py` and verified clean immutable fixture `69f075819be5e1ceca1446b026b0417f19e584ca` with locked tree `5d0b8e66c6cd467e6b1ad9d582e24c66b907e205`. This is the existing planning fixture, not a claim that future #481/v3 runtime integration has shipped. +- The historical unsupported-sidecar result above is retained as historical context; this creation's native planned-maturity run passed. It does not claim accepted, test-authored, RED or verified maturity, or modify runtime/protected policy. + +### Tracking, ownership and limits + +- [Existing issue #742](https://github.com/nold-ai/specfact-cli/issues/742) synchronized with a dated, idempotent risk-first section, unchecked acceptance criteria, non-goals, verification/rollback and reciprocal public baseline proposal links. At creation completion the amendment was locally prepared/unpublished at those baseline links; subsequent publication is authorized below. +- Live readback: native User Story; native parent Feature #372; assignee `djm81`; organization project SpecFact CLI/1; Status Todo; labels `QA, architecture, change-proposal, enhancement, openspec`. Titles, unrelated body content, milestone/project date fields and both native dependency directions were preserved. Modules QA is absent and was not created. Modules #483 remains blocked by #481 and blocks core #742/#492; core #742 remains blocked by modules #483. +- Both prescribed GitHub hierarchy cache refreshes succeeded. Core's required private wiki mirror/index and generated graph were updated without copying private content into public artifacts. Scoped mirror health has no findings; unrelated full-wiki health failures are recorded locally. Its prior modified graph was backed up, and unrelated source files were preserved. +- Python analyzer/code-review JSON, runtime tests, security/contract execution, signing/version bumps and release acceptance are inapplicable to this Markdown/YAML planning surface; no analyzer/runtime PASS or completed OpenSpec delivery is claimed. The existing modules planning-only analyzer applicability rationale remains in `openspec/AGENTIC_SDLC_VALIDATION.md`. These gates remain mandatory for their applicable implementation slices. +- During creation: no commits, pushes, new issues/PRs, PR retargeting, runtime implementation, trial execution or archival. Current policy remains effective; calibrated warnings and minimal-evidence cutover retain their separate owners. + +Rollback: revert/disable new guidance/projection planning while retaining evidence and ordinary checks. Runtime rollout still uses coordinated configuration/module-pin rollback. Risks are added ceremony, narrow probes implying too much and mistaken defect dismissal; mitigate with limited assumptions, explicit proof boundaries and producer/current-input authority. Confidence: high in scoped planning/tracking consistency; medium in defect/overhead improvements. Monetary and causal savings remain unverified. + +## Publication follow-up — 2026-10-08 + +The owner subsequently authorized committing/pushing both amendments, creating review PRs and monitoring CI/review with the CodeRabbit autofix skill. Both publication PRs target dev. Core incorporates only the reconciled workflow artifacts inspected at PR #743 head into refreshed origin/dev; draft PR #743, its branch and codex/lean-requirements-evidence base are preserved. This publication does not implement runtime, complete trial tasks, close either story or archive the changes. Applicable pre-commit checks and signed commit provenance remain required. Remote PR/check/review outcomes are reported separately; no green/review-complete result is inferred from local planning validation. + +Core publication baseline is refreshed origin/dev at 5db1f213f1cd6dca283b86c6710379deb61bad94. The earlier creation validation above applies to the inspected draft-PR-head snapshot. Publication copies only this existing workflow change and its change-order sections onto dev so normal CI/review triggers run; no unrelated lean-planning artifacts are imported. This is the same planned capability/story, not a second OpenSpec change. The original draft PR is retained as provenance and does not become an implementation prerequisite. Publication revalidation ran in the prepared workflow worktree based on `5db1f213f1cd6dca283b86c6710379deb61bad94`: strict selected validation passed and strict all-change validation passed 39/40. The sole failure remained the unrelated `governance-01-evidence-output` delta; the publication worktree contains 40 direct active change folders. These counts describe the dev-based publication run, separately from the 38/39 draft-head creation run above. + +Publication hooks passed on the actual prepared candidates. Scoped mapping YAML lint passed with parsed-value equality after indentation adjustment. Local GPG signing is unavailable (No secret key); the publication route uses GitHub createCommitOnBranch with expected-head protection, then requires verified signature and exact tree equality with git write-tree before accepting the remote commit. No signing configuration or protected CI policy is changed. Runtime review/contract checks were skipped by their owning hooks for this planning-only scope, not claimed as runtime PASS. + +## Merged baseline integration (2026-10-09) + +Focused repair PR [#756](https://github.com/nold-ai/specfact-cli/pull/756) merged into `dev` as `cb417b5dd5d58fbe9ce50b62cb8a75868caa5ef2` after its current-head required checks and completed reviews passed. The planning branch incorporates that signed baseline through signed merge `cc801ce532d6c88dc0584d97a7bb0eb1a9040bbd`; its diff against `dev` remains this existing workflow change and its ordering entry. Both approved ordering entries are preserved, with separate insertion anchors to avoid their original content conflict. The integrated tree has 41 direct active change folders, so its ordering count is 41. Earlier creation/publication counts above remain bound to their own historical snapshots. + +Integration revalidation: strict selected validation passed; strict all-change validation passed 40/41, retaining the same unrelated `governance-01-evidence-output` delta filename failure. The count-only staged inspection reported `no-impact`/`skipped`; it is not proof of broader planned maturity. Native Requirements separately passed for the complete planning diff against the merged `origin/dev` baseline: required/observed `planned`, `proposal-only` delivery, `pass` gate, no findings. Markdown lint and whitespace checks cover the changed planning documents. No behavior, planned runtime, implementation/trial task completion or policy exception is introduced. Hosted current-head required checks, completed review and a fresh member grant bound to the final planning commit/tree remain necessary; the earlier grant for `29637d9d` does not cover a new head. + +## Refreshed stack disposition — 2026-10-11 (Europe/Berlin) + +The amended proposal, design, scenarios, tasks and planned mapping are already present on refreshed dev +`4e7c38c2830f4bc53ffe78eeb0a2bfbabfc711e2`. Replaying the old proposal must retain those newer amendments. +No workflow runtime implementation is claimed by that integration. + +The owner selected verification/planning for this layer and conditioned core implementation on its paired +modules work being underway. Live project readback on 2026-10-11: Requirements #481 In Progress, workflow +Modules #483 Todo. No signed #483 publication is established; core #742 runtime adoption therefore remains deferred. +Standalone projection remains independently deliverable in its owning implementation story, with all existing +unchecked tasks and compatibility gates retained. This stack completes no runtime checkbox or trial. + +Validate the actual planning layer with strict OpenSpec, staged planned Requirements and applicable normal +hooks; validate inherited #719 execution and protected exact-head authority independently through stack CI. +The historical local planning-only exception above applies only to its dated original creation and is not used +for this publication. Required gates, source/index preservation, producer verdicts and signatures remain enforced. + +## Protected stack revalidation — 2026-10-12 (Europe/Berlin) + +Merged prerequisite #761 is protected dev c4ee5be8457f69848bf9bc2150ebc3df56b52d4f. This existing planning layer is +restacked onto signed security parent f6481418597d772d144203e530f932d145537236 after its native Requirements +producer, execution, final verdict and trusted authority pass. Its complete delta remains only INTEGRATION.md and +this workflow validation document; the newer proposal, design, scenarios, planned mapping and unchecked runtime +tasks on dev are preserved. + +The owner conditioned core implementation on paired module work already underway. Current readback retains modules +`#483` Todo; runtime adoption remains deferred. Ongoing modules #481 owns its signed Requirements contract. Core #740 separately owns the R09 policy +cutover already present on the protected parent; its files are absent from this PR delta. No implementation task is completed or future signed-release +compatibility asserted. + +Applicable normal signed hooks, strict selected OpenSpec, native local planned-maturity selection and documentation +checks are required. Hosted producer/execution/final verdict must independently authenticate current root proof and +scope using the merged protected helper and scoped tokens; a local planning pass cannot substitute for those checks. +A new owner-authorized exact-head grant follows actual GitHub signature verification. Leave #719/#743/#751 unmerged. + +## Corrected native lineage validation — 2026-10-12 (Europe/Berlin) + +Protected prerequisite #767 merged as c6af68a0fdf2ea798dd22c6750f077e217c938f5. Its independently tested +correction preserves protected review trust and checks planning-parent ancestry against the candidate. +This layer now follows signed security root 57f51829a0899afc3bc413c3ed21099d261b8ab8, whose fresh native +Requirements run 38306990823 and exact-head authority pass. The earlier consumer failure on 98a932c3 +is retained as historical evidence and is not reported as a passing run. + +The complete own delta remains two Markdown planning documents. Deferred module workflow `#483` remains +Todo. Ongoing modules `#481` owns the signed Requirements contract; core `#740` separately owns the +R09 policy cutover already on the protected parent and outside this delta. +Local planned Requirements, strict selected OpenSpec, documentation and normal signed hooks must pass. +Current-head hosted producer, independent execution, final verdict, authority and completed review remain +required before readiness. No runtime task, future release compatibility or inherited execution is fabricated. + +Independent execution on 2ee53305/run 38303860784 passed planned reconciliation, then the immutable +Code Review provider correctly rejected the child planning receipt as non-final execution proof. +Protective `fix-planning-review-proof-context` preserves full protected-dev review and authentic root +final proof separately; exact mapping acceptance, native proof and protected integration are pending. +This branch aligns to the current signed root and fixes ownership wording; hosted consumer readiness +remains blocked until that protection is integrated and validated. No review gate is skipped. diff --git a/openspec/changes/workflow-01-turn-orchestration/design.md b/openspec/changes/workflow-01-turn-orchestration/design.md new file mode 100644 index 000000000..e6409a2c2 --- /dev/null +++ b/openspec/changes/workflow-01-turn-orchestration/design.md @@ -0,0 +1,59 @@ +## Owner-approved agentic SDLC amendment — 2026-10-04 + +Include optional decision-context drift and advisory external findings in #483 producer adapters and #742 adoption. Retain original producer identity, severity/rule, artifact digest, snapshot identity and verification basis. Producer trust and finding epistemic basis are separate fields: a trusted producer can emit heuristic advice. Present affected obligations, missing evidence, source/test/configuration changes and next actions. Context absence does not block ordinary verification. Use structured pytest runner identity first; certify its selected-suite/current-attempt semantics before additional runners. Deterministic verification precedes repair; one controller owns a shared elapsed/attempt budget across optional preflight and PR loops, with exhaustion, no-progress, oscillation and required-human-decision stops. An advisory converged result never overrides a failed or unavailable required producer. + +This planning amendment supersedes conflicting scope and prerequisite wording below. It changes no runtime behavior and completes no implementation task. See the [paired modules proposal](https://github.com/nold-ai/specfact-cli-modules/blob/dev/openspec/changes/workflow-01-turn-orchestration/proposal.md). + +## Ownership and integration + +Use the existing module registry to expose the paired signed workflow package. Do not register a new built-in workflow engine beside init/module/upgrade. Repository configuration supplies its own gate commands and applicability; portable runtime does not hardcode Hatch, internal wiki paths or these two repositories. The core slice also maintains repository-local skill projection; module-owned reusable skill bodies are referenced, not hand-copied into a competing source of truth. + +Canonical repository wrappers use `.agents/skills/specfact-{pre-validate,implement,verify,fix,autofix}` and the existing OpenSpec workflow skill. Supported projections follow observed harness discovery conventions with an explicit manifest; do not create duplicate active Codex registrations. A deterministic renderer owns generated files only, rejects unmanaged collisions and escaping/symlink destinations, and preserves unrelated custom files. Known AGENTS/CLAUDE differences are explicit transformations. Check mode writes nothing and runs for canonical, destination, renderer or manifest changes, including deletions, both locally and in CI. Fix parent-directory ignore rules so canonical files are genuinely tracked. This repository mechanism is distinct from generic customer installation/export #251. + +## Check-only adapters + +The current quality shell script is not a read-only verifier: it can format, generate command docs and stage files. Factor or call check-only commands for the workflow adapter. Declare gate scope and relevant input/config identities; use explicit index/worktree/immutable-range selection rather than inferring all modes from staged filenames. Verification may write ignored result/cache files, but must preserve source and index byte contents and must not regenerate tracked outputs. Explicit preparation/fix commands perform requested mutations and invalidate prior results. + +Reuse test/contract/security/review outputs from the same candidate, environment and scope only when their identity and required producer completeness match. No duplicate evidence-only suite. Required missing tools, malformed output or unavailable producer versions remain non-passing with remediation diagnostics. Never turn a green review score into a pytest/security PASS. A required warning under effective contributor governance remains unresolved until handled by its owning policy; the workflow cannot invent a severity exception. C15 owns future calibrated policy. + +Pre-validation treats changed referenced paths or upstream observations as drift signals requiring disposition, not proof that intent is invalid. ADDED capabilities may legitimately be absent; completed blockers can improve readiness. Bind the reviewed proposal/spec/task inputs explicitly rather than using the folder's last commit as approval. Existing backlog readiness does not cover every parent/label/project/concurrency check, so the repository governance adapter checks the uncovered native fields. Optional internal-wiki presence is repository configuration; absence produces a follow-up, not a product-wide dependency. + +## Evidence and policy boundaries + +Standalone verify and ordinary CI start from current inputs even in a fresh checkout without `.specfact/turn`. Workflow receipts support local resumption only. Core #740 owns authenticated current-run CI enforcement; R09 modules remains a pure reconciler supplied with outputs, without Git, test execution or network I/O. The workflow executes or references checks separately and preserves Requirements, review and security claims. Optional assurance selects the existing seal/conformance consumer; this adoption does not implement a substitute seal or upgrade local authority. + +## Rollout and verification + +Projection can be reviewed and merged independently. Then select an immutable compatible signed workflow release, check actual installed command discovery and pilot read-only verification on both repositories. Open bounded implementation PRs as each slice becomes reviewable, integrate before trusted pilot/policy activation, and retain existing required checks. Enable local repair after the scope and identity fixtures pass; opt-in PR automation is last. Do not merge or publish from the agent loop automatically. + +Pilot cases: fresh session, partially staged file, unstaged/untracked changes, immutable PR range, missing tool, independent failed gate, generated-skill drift and explicitly selected assurance. Use existing CI timings and result artifacts to inspect duplicate work and failure detection; do not add a telemetry framework or promise measured savings. A wrong scope, silent failure acceptance or unexpected mutation disables adoption and restores its prior configuration/module pin. + +## Research basis + +Reviewed 2026-09-20: [Anthropic's long-running harness guidance](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents) supports explicit progress/handoff and acceptance checks; [pstack babysit](https://github.com/cursor/plugins/blob/main/pstack/skills/poteto-mode/playbooks/babysit.md) separates CI and review completion. These inform the design, not a claim that framework prompts enforce repository policy. Local inspection of the mutating quality script and schema 1.6 report parser motivates check-only adapters and independent producer reports. No framework dependency is added. + +## Signed Requirements compatibility gate + +Before selecting or adopting workflow #483 for current-run Requirements integration, validate the exact immutable signed Requirements #481 publication and its schema-v3 `current_execution` contract against the actual core and workflow versions. Verify archive/manifest/payload signatures and identities, then exercise the exact installed pair with existing representative current-result fixtures. An absent, incompatible, unsigned or v2-only producer leaves runtime integration/adoption not ready; do not substitute a passing receipt or silently downgrade the contract. Repository skill projection remains independently deliverable, and core #740 retains policy cutover ownership. + +## Risk-first guidance in the existing skills + +This is a guidance amendment, not an extension to runtime/report/receipt interfaces. Keep canonical reusable guidance in modules and repository wrappers/projection in core. Reference `docs/agent-rules/INDEX.md`, `40-openspec-and-tdd.md`, `50-quality-gates-and-review.md` and `60-github-change-governance.md` rather than copying their policy. Each repository owns applicability and actual commands. Core projection references the modules-owned content; do not hand-copy a second canonical body. + +| Existing skill | Added responsibility | +| --- | --- | +| `specfact-pre-validate` | Identify one or two approach-invalidating assumptions for nontrivial behavior changes; record planned observable checks, obligations and exclusions in existing sections. Use a brief applicability rationale for documentation-only work. | +| `specfact-implement` | Execute the smallest relevant existing probe before work that depends on its assumption. Record inputs/evidence and the exercised boundary. A disproved assumption returns dependent work to design; an unsupported probe leaves it unresolved. Continue independent work without presenting the dependent approach as ready. Preserve spec -> tests -> failing evidence -> code -> passing evidence. | +| `specfact-verify` | Run required verification on current bound inputs, preserving source/index bytes and independent producer outcomes. A narrow passing probe proves only its exercised boundary; failed/unavailable required producers remain non-passing. Fresh-session verification requires no earlier receipts. | +| `specfact-fix` | Triage with the violated requirement or concrete invariant, reachable failure path and supporting evidence; distinguish confirmed defects, disproved concerns, suggestions and unresolved claims. Model agreement is supporting context, never proof. Preserve producer authority and effective remediation policy. | +| `specfact-autofix` | Keep one writer, batch compatible corrections and pass compact delta/evidence handoffs within existing authority/budgets. Rebind affected verification after changes and broaden review for boundary drift or uncertain impact. Preserve exact-head external-write safeguards. | + +Supply assumptions, observed evidence, affected obligations and exclusions to the applicable existing review. No additional default review lane is introduced. A confirmed required defect stays actionable; a preference may receive an evidenced disposition, but disposition cannot waive a required finding under effective governance or alter its producer's verdict. Unsupported claims remain unresolved until evidence or the owning policy supplies a decision. + +Correction handoffs reuse existing notes/producer artifacts: delta, affected assumptions, remaining findings and original evidence references. They are not a new packet format. Broaden review when a public/assurance/persistence/signing/compatibility boundary changes or dependency impact is uncertain. Compact handoffs do not reduce final candidate coverage: recheck every affected gate or use identity-matching valid outputs; select the complete lane when applicability is uncertain. One controller retains consumed elapsed/attempt budgets across implementation, optional preflight and PR repair; a handoff/session cannot reset them. + +## Bounded evaluation of delivery overhead + +Use existing CI timings, producer artifacts and available provider/session records; introduce no ledger, report fields or automatic collection. Token counts by available category (for example input/output/cache) and actual billed charges are different measures. Elapsed time, active engineer effort and waiting are also different measures; record units, source and unavailable values as unknown. Do not convert elapsed time or tokens into labor or money without defensible data. + +After the skills/projection slice, review investment at two engineer-days. Run one bounded trial on the next subsequently authorized nontrivial change per repository under existing budgets; compare early discoveries, later escapes, correction batches and observed overhead. Trials are operational evidence, not causal savings proof. Stop/reassess if added ceremony outweighs the observed benefit. Disable/revert guidance/projection if needed while preserving evidence and ordinary checks; existing runtime configuration/module-pin rollback and signed acceptance remain unchanged. diff --git a/openspec/changes/workflow-01-turn-orchestration/proposal.md b/openspec/changes/workflow-01-turn-orchestration/proposal.md new file mode 100644 index 000000000..7c4c9060c --- /dev/null +++ b/openspec/changes/workflow-01-turn-orchestration/proposal.md @@ -0,0 +1,74 @@ +# Change: Adopt the Bounded Turn Workflow + +## Owner-approved agentic SDLC amendment — 2026-10-04 + +Include optional decision-context drift and advisory external findings in #483 producer adapters and #742 adoption. Retain original producer identity, severity/rule, artifact digest, snapshot identity and verification basis. Producer trust and finding epistemic basis are separate fields: a trusted producer can emit heuristic advice. Present affected obligations, missing evidence, source/test/configuration changes and next actions. Context absence does not block ordinary verification. Use structured pytest runner identity first; certify its selected-suite/current-attempt semantics before additional runners. Deterministic verification precedes repair; one controller owns a shared elapsed/attempt budget across optional preflight and PR loops, with exhaustion, no-progress, oscillation and required-human-decision stops. An advisory converged result never overrides a failed or unavailable required producer. + +This planning amendment supersedes conflicting scope and prerequisite wording below. It changes no runtime behavior and completes no implementation task. See the [paired modules proposal](https://github.com/nold-ai/specfact-cli-modules/blob/dev/openspec/changes/workflow-01-turn-orchestration/proposal.md). + +## Why + +Agents currently reconstruct repository gate order from prose. Repeated skill copies and mutating verification helpers make that flow inconsistent. Adopt a reproducible local validation/remediation flow while preserving the current-run evidence policy of R09 and independent producer authority. + +## What Changes + +- Adopt the paired module-owned `specfact workflow` surface; core does not gain a built-in orchestration engine or duplicate analyzers. +- Add repository-owned check-only gate configuration and thin, prefixed skill references. Project canonical repository skills into supported harness directories with a drift check covering source, destination and generator edits. +- Keep standalone verification independent of earlier turn receipts. Local resumable state is operational context, never historical proof or protected CI authority. +- Pilot the signed module against worktree/index/range inputs before enabling contributor guidance; retain normal required checks and effective governance. + +## Capabilities + +### New Capabilities + +- `turn-workflow-adoption`: check-only repository integration, canonical skill projection, standalone verification and bounded adoption. + +### Modified Capabilities + +None. This proposal consumes R09 without editing its change or issues; generic skill installation, review policy, optional preflight and evidence schemas retain their existing owners. + +## Impact + +Planning artifacts only in this delivery. Later implementation touches repository scripts, selected hooks/CI drift checks, agent guidance, command reference generation and documentation. Regeneration/staging are explicit preparation steps, never verification side effects. No current runtime, CI/ruleset, signature, registry or version changes are made here. + +Core owns repository configuration, projection and adoption; modules owns reusable workflow content and runtime. Use existing registry/discovery interfaces, with an actionable install hint if the module is absent. Generic module skill distribution remains #251; generated general instructions remain #253. Repository-local projection does not require those features to ship first. + +Document usage and limitations in contributor/agent guidance and the existing CLI reference on docs.specfact.io; validate real cross-site permalinks for module help. Update generated command references only when the module is actually adopted. No new navigation is needed at proposal stage. + +## Dependencies and rollout + +Signed modules #483 precedes core runtime adoption; independent repository skill projection can land first. Modules #481 supplies the workflow's current-run Requirements integration. Core #740 continues to own the R09 required-policy cutover; this story neither duplicates it nor makes independent tooling wait for its complete rollout. Before behavior work, follow effective governance or an explicit owner-authorized exception. + +Optional preflight, #251/#253, the full-chain graph and parked #522 are related capabilities, not blanket prerequisites. A C15-specific adapter consumes the signed C15 policy contract only after its existing prerequisites. No new producer verdict semantics or policy exceptions are introduced here. + +Roll out projection -> signed deterministic verification -> bounded local repair -> opt-in PR automation. Roll back repository invocation/configuration and its module pin together if scope selection or gate outcomes regress; ordinary standalone checks remain available. + +## Source Tracking + + +- **GitHub Issue**: #742 +- **Issue URL**: +- **Repository**: `nold-ai/specfact-cli` +- **Parent Feature**: #372 +- **Paired Modules Story**: +- **Paired Modules Proposal (public baseline)**: ; the risk-first amendment is prepared for a separate review PR. +- **Last Synced Status**: proposed / Todo, 2026-10-08 (live readback; planning creation complete, separate publication authorized) + +## Signed Requirements compatibility gate + +Before selecting or adopting workflow #483 for current-run Requirements integration, validate the exact immutable signed Requirements #481 publication and its schema-v3 `current_execution` contract against the actual core and workflow versions. Verify archive/manifest/payload signatures and identities, then exercise the exact installed pair with existing representative current-result fixtures. An absent, incompatible, unsigned or v2-only producer leaves runtime integration/adoption not ready; do not substitute a passing receipt or silently downgrade the contract. Repository skill projection remains independently deliverable, and core #740 retains policy cutover ownership. + +## Risk-first execution amendment — 2026-10-08 + +The owner selected a minimal adaptation within this existing paired change: discover invalid assumptions before dependent work and reduce avoidable correction overhead. Reuse `specfact-pre-validate`, `specfact-implement`, `specfact-verify`, `specfact-fix` and `specfact-autofix`; add no stage aliases or commands. This amendment preserves the 2026-10-04 decision, current assurance/review policy and signed release dependencies. It adds guidance and acceptance scenarios, not report schemas, receipt formats, producer verdicts or automatic collection. + +- For nontrivial behavior changes, select one or two assumptions that could invalidate the approach and run the smallest relevant existing CLI/API/dependency probe before dependent implementation. Record the observed boundary, limitations and result in existing planning/evidence sections. Documentation-only work records applicability; a probe supplements required checks. +- Give the applicable existing review the assumptions, evidence references, affected obligations and exclusions. Triage findings against a violated requirement or concrete invariant, a reachable failure path and evidence. Preserve confirmed defects, disproved concerns, suggestions and unresolved claims as distinct dispositions; no disposition waives required findings. +- Keep one writer and batch compatible repairs. Handoffs carry the delta, affected assumptions, remaining findings and original evidence references. Boundary drift or uncertain impact broadens review and affected-gate rechecks; retain the shared controller and consumed budgets across handoffs. +- Reuse existing records for a bounded trial on the next subsequently authorized nontrivial change in each repository. Report early discoveries, later escapes, correction batches and overhead. Separate token categories, billed charges, elapsed time, active effort and waiting; unavailable values remain unknown. Operational observations do not prove causal or monetary savings. + +Non-goals: full pstack installation, default Arena/reviewer panels, a new ledger, importing another project's capability, warning-policy calibration or minimal-evidence cutover. No runtime command becomes active because skills exist. Keep reusable modules content concise, reference existing rules, and let each repository supply its own commands/governance and core-owned wrappers/projection. + +Deliver skills/projection -> deterministic verification -> bounded repair -> opt-in PR automation. Review the initial skills/projection slice after two engineer-days as an investment checkpoint, not an estimate for the full paired runtime. Added ceremony, weak probes and incorrect dismissals are mitigated by limiting assumptions, stating proof boundaries and preserving producer authority/current-input binding. Roll back new guidance/projection while retaining evidence and ordinary checks; runtime adoption retains coordinated configuration/module-pin rollback. + +Public research context (accessed 2026-10-08): [Cursor pstack](https://github.com/cursor/plugins/tree/main/pstack) and [Flavio Copes' overview, updated 2026-09-29](https://flaviocopes.com/pstack/). These inform adapted practices, not a framework dependency or savings claim. Confidence: high in architectural fit; medium in reducing defects/overhead. Planning creation leaves runtime implementation and trials unchecked. diff --git a/openspec/changes/workflow-01-turn-orchestration/requirements-evidence.yaml b/openspec/changes/workflow-01-turn-orchestration/requirements-evidence.yaml new file mode 100644 index 000000000..cab99961c --- /dev/null +++ b/openspec/changes/workflow-01-turn-orchestration/requirements-evidence.yaml @@ -0,0 +1,212 @@ +schema_version: '2' +requirements: + openspec:workflow-01-turn-orchestration:turn-workflow-adoption:module-owned-workflow-adoption: + rationale: Inspect approved planning acceptance without claiming runtime or signed release evidence. + stakeholder_refs: + - nold-ai/specfact-cli#742 + touchpoints: + - id: turn-workflow-adoption-spec + kind: config_file + locator: openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md + verification_cases: + - case_id: module-owned-workflow-adoption-workflow-module-is-unavailable-inspection + scenario_id: module-owned-workflow-adoption + method: inspection + intent: 'Inspect planned scenario: Workflow module is unavailable.' + observable: an actionable installation/compatibility diagnostic is returned without a synthetic passing + result. Planning inspection only; runtime implementation, execution and signed acceptance remain pending. + openspec:workflow-01-turn-orchestration:turn-workflow-adoption:verification-preserves-repository-inputs: + rationale: Inspect approved planning acceptance without claiming runtime or signed release evidence. + stakeholder_refs: + - nold-ai/specfact-cli#742 + touchpoints: + - id: turn-workflow-adoption-spec + kind: config_file + locator: openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md + verification_cases: + - case_id: verification-preserves-repository-inputs-partially-staged-file-requires-a-formatting-change-inspection + scenario_id: verification-preserves-repository-inputs + method: inspection + intent: 'Inspect planned scenario: Partially staged file requires a formatting change.' + observable: it reports the index violation without formatting, staging, regenerating tracked files or substituting + worktree contents. Planning inspection only; runtime implementation, execution and signed acceptance remain + pending. + - case_id: verification-preserves-repository-inputs-fresh-session-verification-inspection + scenario_id: verification-preserves-repository-inputs + method: inspection + intent: 'Inspect planned scenario: Fresh-session verification.' + observable: required checks execute or reuse identity-matching current outputs without requiring prior session + phases or RED history. Planning inspection only; runtime implementation, execution and signed acceptance + remain pending. + - case_id: verification-preserves-repository-inputs-required-independent-gate-fails-inspection + scenario_id: verification-preserves-repository-inputs + method: inspection + intent: 'Inspect planned scenario: Required independent gate fails.' + observable: the failing producer remains non-passing and cannot be hidden by a score or findings projection. + Planning inspection only; runtime implementation, execution and signed acceptance remain pending. + openspec:workflow-01-turn-orchestration:turn-workflow-adoption:canonical-repository-skill-projection: + rationale: Inspect approved planning acceptance without claiming runtime or signed release evidence. + stakeholder_refs: + - nold-ai/specfact-cli#742 + touchpoints: + - id: turn-workflow-adoption-spec + kind: config_file + locator: openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md + verification_cases: + - case_id: canonical-repository-skill-projection-only-a-generated-skill-is-edited-or-removed-inspection + scenario_id: canonical-repository-skill-projection + method: inspection + intent: 'Inspect planned scenario: Only a generated skill is edited or removed.' + observable: it fails with the affected destination and a regeneration instruction without writing files. + Planning inspection only; runtime implementation, execution and signed acceptance remain pending. + - case_id: canonical-repository-skill-projection-a-destination-contains-unrelated-custom-content-inspection + scenario_id: canonical-repository-skill-projection + method: inspection + intent: 'Inspect planned scenario: A destination contains unrelated custom content.' + observable: it preserves unrelated files and rejects unsafe or colliding writes. Planning inspection only; + runtime implementation, execution and signed acceptance remain pending. + openspec:workflow-01-turn-orchestration:turn-workflow-adoption:readiness-uses-current-governance: + rationale: Inspect approved planning acceptance without claiming runtime or signed release evidence. + stakeholder_refs: + - nold-ai/specfact-cli#742 + touchpoints: + - id: turn-workflow-adoption-spec + kind: config_file + locator: openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md + verification_cases: + - case_id: readiness-uses-current-governance-a-planned-capability-is-new-and-its-blocker-completed-inspection + scenario_id: readiness-uses-current-governance + method: inspection + intent: 'Inspect planned scenario: A planned capability is new and its blocker completed.' + observable: neither observation alone causes stale-plan failure; current metadata and remaining applicable + prerequisites determine readiness. Planning inspection only; runtime implementation, execution and signed + acceptance remain pending. + - case_id: readiness-uses-current-governance-linked-issue-governance-cannot-be-verified-inspection + scenario_id: readiness-uses-current-governance + method: inspection + intent: 'Inspect planned scenario: Linked issue governance cannot be verified.' + observable: it reports the unmet check without treating a cached backlog-only PASS as complete governance + verification. Planning inspection only; runtime implementation, execution and signed acceptance remain + pending. + openspec:workflow-01-turn-orchestration:turn-workflow-adoption:pilot-precedes-adoption-activation: + rationale: Inspect approved planning acceptance without claiming runtime or signed release evidence. + stakeholder_refs: + - nold-ai/specfact-cli#742 + touchpoints: + - id: turn-workflow-adoption-spec + kind: config_file + locator: openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md + verification_cases: + - case_id: pilot-precedes-adoption-activation-only-a-candidate-workflow-build-exists-inspection + scenario_id: pilot-precedes-adoption-activation + method: inspection + intent: 'Inspect planned scenario: Only a candidate workflow build exists.' + observable: candidate testing may proceed but the signed publication prerequisite remains unresolved. Planning + inspection only; runtime implementation, execution and signed acceptance remain pending. + openspec:workflow-01-turn-orchestration:turn-workflow-adoption:signed-current-run-producer-compatibility-precedes-adoption: + rationale: Inspect approved planning acceptance without claiming runtime or signed release evidence. + stakeholder_refs: + - nold-ai/specfact-cli#742 + touchpoints: + - id: turn-workflow-adoption-spec + kind: config_file + locator: openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md + verification_cases: + - case_id: signed-current-run-producer-compatibility-precedes-adoption-workflow-package-is-signed-but-its-requirements-producer-is-incompatible-inspection + scenario_id: signed-current-run-producer-compatibility-precedes-adoption + method: inspection + intent: 'Inspect planned scenario: Workflow package is signed but its Requirements producer is incompatible.' + observable: the combination is rejected as not ready with an actionable diagnostic independent repository + projection can proceed without claiming runtime readiness. Planning inspection only; runtime implementation, + execution and signed acceptance remain pending. + - case_id: signed-current-run-producer-compatibility-precedes-adoption-installed-signed-producer-and-workflow-match-the-actual-core-inspection + scenario_id: signed-current-run-producer-compatibility-precedes-adoption + method: inspection + intent: 'Inspect planned scenario: Installed signed producer and workflow match the actual core.' + observable: the compatibility prerequisite is satisfied for that exact combination this does not activate + or replace the separate R09 required-policy cutover. Planning inspection only; runtime implementation, + execution and signed acceptance remain pending. + openspec:workflow-01-turn-orchestration:turn-workflow-adoption:optional-context-and-advisory-producer-feedback: + rationale: Inspect approved planning acceptance without claiming runtime or signed release evidence. + stakeholder_refs: + - nold-ai/specfact-cli#742 + touchpoints: + - id: turn-workflow-adoption-spec + kind: config_file + locator: openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md + verification_cases: + - case_id: optional-context-and-advisory-producer-feedback-heuristic-finding-from-trusted-producer-inspection + scenario_id: optional-context-and-advisory-producer-feedback + method: inspection + intent: 'Inspect planned scenario: Heuristic finding from trusted producer.' + observable: producer trust and heuristic basis remain distinct and the original finding is available. Planning + inspection only; runtime implementation, execution and signed acceptance remain pending. + - case_id: optional-context-and-advisory-producer-feedback-advisory-convergence-with-failed-tests-inspection + scenario_id: optional-context-and-advisory-producer-feedback + method: inspection + intent: 'Inspect planned scenario: Advisory convergence with failed tests.' + observable: the test remains failed and the result cannot pass. Planning inspection only; runtime implementation, + execution and signed acceptance remain pending. + - case_id: optional-context-and-advisory-producer-feedback-context-drift-during-verification-inspection + scenario_id: optional-context-and-advisory-producer-feedback + method: inspection + intent: 'Inspect planned scenario: Context drift during verification.' + observable: reuse is rejected for affected obligations and source/index bytes are preserved. Planning inspection + only; runtime implementation, execution and signed acceptance remain pending. + - case_id: optional-context-and-advisory-producer-feedback-nested-repair-would-exceed-budget-inspection + scenario_id: optional-context-and-advisory-producer-feedback + method: inspection + intent: 'Inspect planned scenario: Nested repair would exceed budget.' + observable: it consumes the same controller budget rather than starting a new independent attempt allowance. + Planning inspection only; runtime implementation, execution and signed acceptance remain pending. + openspec:workflow-01-turn-orchestration:turn-workflow-adoption:risk-first-repository-guidance: + rationale: Inspect approved planning acceptance without claiming runtime or signed release evidence. + stakeholder_refs: + - nold-ai/specfact-cli#742 + touchpoints: + - id: turn-workflow-adoption-spec + kind: config_file + locator: openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md + verification_cases: + - case_id: risk-first-repository-guidance-routine-work-requires-no-extra-lane-inspection + scenario_id: risk-first-repository-guidance + method: inspection + intent: 'Inspect planned scenario: Routine work requires no extra lane.' + observable: existing checks and review lanes remain sufficient and documentation-only work records applicability + without invented runtime proof. Planning inspection only; runtime implementation, execution and signed + acceptance remain pending. + - case_id: risk-first-repository-guidance-risk-boundary-has-a-critical-assumption-inspection + scenario_id: risk-first-repository-guidance + method: inspection + intent: 'Inspect planned scenario: Risk boundary has a critical assumption.' + observable: the selected assumption, smallest existing observable probe, result and limitations are recorded + before dependent work. Planning inspection only; runtime implementation, execution and signed acceptance + remain pending. + - case_id: risk-first-repository-guidance-assumption-cannot-establish-readiness-inspection + scenario_id: risk-first-repository-guidance + method: inspection + intent: 'Inspect planned scenario: Assumption cannot establish readiness.' + observable: disproved assumptions return to design and unsupported ones remain unresolved; independent work + may continue without a dependent-readiness claim. Planning inspection only; runtime implementation, execution + and signed acceptance remain pending. + - case_id: risk-first-repository-guidance-triage-respects-the-owning-policy-inspection + scenario_id: risk-first-repository-guidance + method: inspection + intent: 'Inspect planned scenario: Triage respects the owning policy.' + observable: the defect stays actionable, the preference receives an evidenced disposition and the claim + remains unresolved; no disposition waives a required finding or substitutes model agreement for proof. + Planning inspection only; runtime implementation, execution and signed acceptance remain pending. + - case_id: risk-first-repository-guidance-compact-correction-encounters-boundary-drift-inspection + scenario_id: risk-first-repository-guidance + method: inspection + intent: 'Inspect planned scenario: Compact correction encounters boundary drift.' + observable: review broadens and affected gates rebind to current inputs, with complete-lane fallback and + consumed budgets preserved across handoffs. Planning inspection only; runtime implementation, execution + and signed acceptance remain pending. + - case_id: risk-first-repository-guidance-trial-overhead-has-missing-measurements-inspection + scenario_id: risk-first-repository-guidance + method: inspection + intent: 'Inspect planned scenario: Trial overhead has missing measurements.' + observable: token categories, billed charges, elapsed time, active effort and waiting remain separate with + units/sources; missing values remain unknown and causal savings are not claimed. Planning inspection only; + runtime implementation, execution and signed acceptance remain pending. diff --git a/openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md b/openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md new file mode 100644 index 000000000..6620ad0e8 --- /dev/null +++ b/openspec/changes/workflow-01-turn-orchestration/specs/turn-workflow-adoption/spec.md @@ -0,0 +1,161 @@ +## ADDED Requirements + +### Requirement: Module-Owned Workflow Adoption + +Core SHALL adopt workflow execution through its existing module interfaces and SHALL retain repository configuration and projection ownership. It SHALL NOT introduce a competing built-in orchestration engine or redefine producer verdict policy. + +#### Scenario: Workflow module is unavailable + +- **GIVEN** an invocation reference and no compatible enabled workflow module +- **WHEN** a contributor invokes the workflow +- **THEN** an actionable installation/compatibility diagnostic is returned without a synthetic passing result. + +### Requirement: Verification Preserves Repository Inputs + +Workflow verification SHALL use check-only adapters with explicit index, worktree or resolved range scope. It SHALL preserve source files and index contents; ignored outputs and caches MAY be written. Mutation operations SHALL remain explicit and invalidate affected results. + +#### Scenario: Partially staged file requires a formatting change + +- **GIVEN** different index and worktree contents and a format violation +- **WHEN** index verification runs +- **THEN** it reports the index violation without formatting, staging, regenerating tracked files or substituting worktree contents. + +#### Scenario: Fresh-session verification + +- **GIVEN** current source and configured checks but no earlier turn receipt +- **WHEN** standalone verification runs +- **THEN** required checks execute or reuse identity-matching current outputs without requiring prior session phases or RED history. + +#### Scenario: Required independent gate fails + +- **GIVEN** a passing review report and failed tests or security checks +- **WHEN** verification presents results +- **THEN** the failing producer remains non-passing and cannot be hidden by a score or findings projection. + +### Requirement: Canonical Repository Skill Projection + +Repository skill projections SHALL be reproducible from canonical tracked sources and an explicit supported-destination manifest. Drift checks SHALL cover source, destination, renderer and manifest changes in local hooks and CI. Generation SHALL preserve unmanaged files and reject unsafe destinations or collisions. + +#### Scenario: Only a generated skill is edited or removed + +- **GIVEN** unchanged canonical skills and a changed or deleted managed destination +- **WHEN** projection checking runs +- **THEN** it fails with the affected destination and a regeneration instruction without writing files. + +#### Scenario: A destination contains unrelated custom content + +- **GIVEN** unmanaged content or a path escaping the configured destination root +- **WHEN** generation is requested +- **THEN** it preserves unrelated files and rejects unsafe or colliding writes. + +### Requirement: Readiness Uses Current Governance + +The adoption SHALL check uncovered native GitHub metadata where linked-issue governance applies and SHALL keep drift observations distinct from semantic invalidity. Optional assurance and internal wiki access SHALL NOT become universal product prerequisites. + +#### Scenario: A planned capability is new and its blocker completed + +- **GIVEN** a valid ADDED capability absent from canonical specs and a genuinely completed blocker +- **WHEN** pre-validation checks planning drift +- **THEN** neither observation alone causes stale-plan failure; current metadata and remaining applicable prerequisites determine readiness. + +#### Scenario: Linked issue governance cannot be verified + +- **GIVEN** required current parent, project or concurrency metadata is missing or unavailable +- **WHEN** readiness for linked public implementation is evaluated +- **THEN** it reports the unmet check without treating a cached backlog-only PASS as complete governance verification. + +### Requirement: Pilot Precedes Adoption Activation + +Core runtime adoption SHALL consume a compatible signed workflow publication and SHALL pass representative scope, no-mutation and independent-gate checks before activating contributor guidance. Repository projection MAY ship independently. R09 and C15 SHALL retain their own policy activation and trust requirements. + +#### Scenario: Only a candidate workflow build exists + +- **GIVEN** an unpublished workflow candidate +- **WHEN** stable repository runtime adoption is evaluated +- **THEN** candidate testing may proceed but the signed publication prerequisite remains unresolved. + +### Requirement: Signed Current-Run Producer Compatibility Precedes Adoption + +Runtime Requirements integration and adoption SHALL validate the exact immutable signed #481 publication, schema-v3 `current_execution` capability, archive/manifest/payload identities and compatibility with the actual core and selected #483 workflow versions before selecting that combination for use. Existing representative current-result fixtures SHALL exercise the installed combination. Missing, unsigned, incompatible or v2-only producers SHALL leave runtime adoption not ready; receipts and legacy-schema fallbacks SHALL NOT satisfy this gate. Repository projection MAY proceed independently, and core #740 SHALL retain its policy cutover ownership. + +#### Scenario: Workflow package is signed but its Requirements producer is incompatible + +- **GIVEN** a valid signed #483 workflow package and an absent, unsigned, incompatible or v2-only #481 Requirements producer +- **WHEN** runtime integration or core adoption readiness is checked +- **THEN** the combination is rejected as not ready with an actionable diagnostic +- **AND** independent repository projection can proceed without claiming runtime readiness. + +#### Scenario: Installed signed producer and workflow match the actual core + +- **GIVEN** exact signed #481/#483 identities compatible with the actual core candidate and the v3 current-execution contract +- **WHEN** the installed combination passes the existing representative acceptance fixtures +- **THEN** the compatibility prerequisite is satisfied for that exact combination +- **AND** this does not activate or replace the separate R09 required-policy cutover. + +### Requirement: Optional context and advisory producer feedback + +Producer adapters SHALL retain original severity/rule, producer identity, artifact digest, snapshot identity and verification basis separately from producer trust. Optional decision-context drift SHALL present affected obligations, test/configuration changes and missing evidence. R09 SHALL remain pure reconciliation; execution SHALL remain in workflow adapters. Structured pytest identity SHALL be certified first. One controller SHALL bound total attempts/elapsed time across selected loops and SHALL stop on exhaustion, no progress, oscillation or required human decisions. + +#### Scenario: Heuristic finding from trusted producer + +- **GIVEN** an authenticated producer emitting a heuristic finding +- **WHEN** feedback is normalized +- **THEN** producer trust and heuristic basis remain distinct and the original finding is available. + +#### Scenario: Advisory convergence with failed tests + +- **GIVEN** a converged advisory and a failed required test +- **WHEN** verification is aggregated +- **THEN** the test remains failed and the result cannot pass. + +#### Scenario: Context drift during verification + +- **GIVEN** a bound context, test or configuration changing during execution +- **WHEN** result reuse is evaluated +- **THEN** reuse is rejected for affected obligations and source/index bytes are preserved. + +#### Scenario: Nested repair would exceed budget + +- **GIVEN** an optional checkpoint requesting repair within an active workflow loop +- **WHEN** the request is handled +- **THEN** it consumes the same controller budget rather than starting a new independent attempt allowance. + +### Requirement: Risk-First Repository Guidance + +Core wrappers SHALL reference modules-owned risk-first guidance in the existing five workflow skills and supply repository-specific commands/governance. Nontrivial changes SHALL identify one or two critical assumptions and exercise the smallest relevant existing probe before dependent implementation. Documentation-only changes SHALL record applicability. Review/triage/handoffs SHALL preserve independent producer authority, current-input coverage and shared budgets, without new stage names, commands, report schemas, receipt formats, ledgers, automatic collection or default reviewer panels. + +#### Scenario: Routine work requires no extra lane + +- **GIVEN** a routine change and applicable repository checks +- **WHEN** the wrapper selects guidance +- **THEN** existing checks and review lanes remain sufficient and documentation-only work records applicability without invented runtime proof. + +#### Scenario: Risk boundary has a critical assumption + +- **GIVEN** a nontrivial assurance, persistence, signing or compatibility change +- **WHEN** implementation depends on an assumption +- **THEN** the selected assumption, smallest existing observable probe, result and limitations are recorded before dependent work. + +#### Scenario: Assumption cannot establish readiness + +- **GIVEN** a disproved assumption or an unsupported probe +- **WHEN** dependent readiness is summarized +- **THEN** disproved assumptions return to design and unsupported ones remain unresolved; independent work may continue without a dependent-readiness claim. + +#### Scenario: Triage respects the owning policy + +- **GIVEN** a confirmed reachable invariant violation, a preference and an unsupported claim +- **WHEN** the existing review is given assumptions, evidence, obligations and exclusions +- **THEN** the defect stays actionable, the preference receives an evidenced disposition and the claim remains unresolved; no disposition waives a required finding or substitutes model agreement for proof. + +#### Scenario: Compact correction encounters boundary drift + +- **GIVEN** a one-writer correction batch with delta, affected assumptions, remaining findings and original evidence references +- **WHEN** a boundary changes or impact is uncertain +- **THEN** review broadens and affected gates rebind to current inputs, with complete-lane fallback and consumed budgets preserved across handoffs. + +#### Scenario: Trial overhead has missing measurements + +- **GIVEN** a bounded trial on a subsequently authorized nontrivial change with existing records +- **WHEN** early discoveries, later escapes, correction batches and overhead are reported +- **THEN** token categories, billed charges, elapsed time, active effort and waiting remain separate with units/sources; missing values remain unknown and causal savings are not claimed. diff --git a/openspec/changes/workflow-01-turn-orchestration/tasks.md b/openspec/changes/workflow-01-turn-orchestration/tasks.md new file mode 100644 index 000000000..589cbb98d --- /dev/null +++ b/openspec/changes/workflow-01-turn-orchestration/tasks.md @@ -0,0 +1,52 @@ +## 1. Worktree and readiness + +- [ ] 1.1 Create a dedicated `codex/workflow-01-turn-orchestration` worktree from current `origin/dev`; keep the primary checkout on dev. +- [ ] 1.2 Refresh/read back issue type, parent, assignee, labels, project/status and both dependency directions; coordinate concurrent ownership before implementation. +- [ ] 1.3 Revalidate scope and strict OpenSpec; use effective governance or an explicit owner-authorized lean-evidence exception. No blanket full-R09 prerequisite. + +## 2. Specification and regression cases + +- [ ] 2.1 Confirm the adoption scenarios and paired module interfaces against current core discovery; preserve existing ownership in INTEGRATION. +- [ ] 2.2 Add projection tests for source/destination-only edits, deletion, renderer changes, tracked ignore exceptions, collisions and unsafe paths; observe relevant failures before implementation. +- [ ] 2.3 Add index/worktree/range, missing-tool, independent-gate and source/index no-mutation tests; observe meaningful failures before adapter edits. +- [ ] 2.4 Record concise failing/passing evidence under effective governance; use TDD_EVIDENCE.md if still required. Reuse normal test output, without adding a history protocol. + +## 2A. Approved context and skill alignment + +- [ ] 2A.1 Derive failing cases for optional decision context, producer trust distinct from epistemic basis, certified structured pytest identity and shared controller accounting from the preserved 2026-10-04 amendment before behavior changes. +- [ ] 2A.2 Reference modules-owned risk-first content from core wrappers and supply core commands/governance; review the paired initial skills/projection slice after two engineer-days as an investment checkpoint. Keep unavailable runtime commands inactive. + +## 3. Repository projection and check-only adapters + +- [ ] 3.1 Implement the canonical projection manifest/renderer and prefixed thin wrappers; ensure only one active native registration per harness. +- [ ] 3.2 Add check-mode local/CI coverage for source, destination, renderer and manifest changes, including deletion; repair ignore exceptions narrowly. +- [ ] 3.3 Implement repository gate configuration and check-only adapters; keep tracked generation, formatting and staging in explicit preparation/fix paths. +- [ ] 3.4 Add the uncovered current GitHub governance checks and optional wiki follow-up integration without widening product requirements. +- [ ] 3.5 Open a bounded projection/adapter PR once reviewable; run relevant gates and merge only through normal reviewed integration. + +## 4. Signed adoption and pilot + +- [ ] 4.1 Before selecting/adopting #483, validate exact signed #481 archive/manifest/payload identities, schema-v3 current_execution and compatibility with actual core/workflow versions; exercise the installed combination with existing representative fixtures. Missing/unsigned/incompatible/v2-only producers are not ready. Keep projection independent. +- [ ] 4.2 Integrate the reviewed adoption slice before trusted pilot execution; preserve existing required checks and keep R09 policy migration under core #740. +- [ ] 4.3 Run the representative pilot across both repositories; prove fresh-session verification, no source/index mutation and independent failure propagation. +- [ ] 4.4 Enable bounded local repair only after verification acceptance; enable opt-in PR automation only after its paired release/recovery scenarios pass. Test rollback of configuration and module pin together. + +## 5. Quality, documentation and delivery + +- [ ] 5.1 Run scenario-derived tests and applicable contract, lint/type, security, signature, review and strict OpenSpec gates; resolve findings under effective policy. New public APIs require icontract/beartype contracts. +- [ ] 5.2 Update contributor/agent guidance, CLI reference and generated command docs explicitly; check docs ownership, navigation and real cross-site permalinks. +- [ ] 5.3 Apply semver/changelog/signing only for implementation assets actually changed; keep registry/version compatibility truthful. +- [ ] 5.4 Update issue source tracking and paired wiki mirrors, then open/update the final implementation PR to dev with actual validation and remaining release prerequisites. +- [ ] 5.5 After reviewed integration and completion, archive natively with `openspec archive workflow-01-turn-orchestration`; clean the implementation worktree only after merge. Do not archive this planning proposal as implemented. + +Implementation is unstarted. Split each checkbox into bounded work sessions of at most two hours if its observed scope exceeds that size; keep test-before-code ordering within every slice. + +## Risk-first acceptance and bounded trials (unexecuted) + +- [ ] Apply the early-assumption guidance to pre-validation/implementation: one or two critical assumptions, observable boundaries/limitations, disproved-to-design and unsupported-as-unresolved outcomes, plus documentation-only applicability. +- [ ] Exercise routine and assurance/persistence/signing/compatibility cases through the existing review lane, with assumptions, evidence, affected obligations and exclusions; add no default panel. +- [ ] Validate evidence-based defect/preference/unresolved dispositions and one-writer compatible correction batches; broaden review on boundary drift, rebind affected gates and retain shared controller/budget accounting across handoffs. +- [ ] Verify narrow probes never imply broader PASS, advisory convergence cannot hide failed/unavailable required producers, fresh-session verification needs no earlier receipt and projection detects drift while preserving unmanaged content. +- [ ] After the relevant slice is separately authorized, run a bounded trial on the next subsequently authorized nontrivial change in this repository. Reuse existing records to report early discoveries, later escapes, correction batches and overhead, separating token categories, billed charges, elapsed time, active effort and waiting; leave missing values unknown and causal savings unproven. + +Delivery remains skills/projection -> deterministic verification -> bounded repair -> opt-in PR automation, with signed/versioned publication and exact released compatibility gates at their existing points. This planning amendment executes none of these implementation or trial tasks. diff --git a/pyproject.toml b/pyproject.toml index c1e810561..28407a94a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["hatchling==1.32.0"] +requires = ["hatchling==1.32.4"] build-backend = "hatchling.build" [project] name = "specfact-cli" -version = "0.55.4" +version = "0.55.11" description = "AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, and spec/contract evidence for AI-assisted and brownfield delivery." readme = "README.md" requires-python = ">=3.11" @@ -62,6 +62,7 @@ dependencies = [ "typing-extensions>=4.15.0", "PyYAML>=6.0.3", "requests>=2.32.3", + "certifi>=2026.10.19", "azure-identity>=1.17.1", "cryptography>=50.0.0", # Transitive through CFFI. The lock supplies reproducibility; this range @@ -87,7 +88,7 @@ dependencies = [ # Git operations # gitpython: CVE history (CVE-2022-24439, CVE-2023-41040, CVE-2023-40590). Phase 2: replace with dulwich. - "gitpython>=3.1.61", + "gitpython>=3.2.0", # YAML utilities "ruamel.yaml>=0.18.16", @@ -117,7 +118,7 @@ telemetry = [ ] dev = [ - "hatchling==1.32.0", # Frozen PEP 517 backend used by delivery wheel builds. + "hatchling==1.32.4", # Frozen PEP 517 backend used by delivery wheel builds. "setuptools>=83.0.0,<85", "pip>=26.2", # CVE-2026-13346 is fixed in pip 26.2; tooling-only, not a core runtime dependency. "pytest>=8.4.2", @@ -230,7 +231,7 @@ validate-prompts = "python tools/validate_prompts.py" bandit-scan = "bandit -r src/ -ll" license-check = "python scripts/check_license_compliance.py" # Wrap pip-audit so JSON parsing and CVSS threshold checks fail closed. -security-audit = "python scripts/security_audit_gate.py && python scripts/security_audit_gate.py --requirement requirements/code-review/locked.txt" +security-audit = "python scripts/security_audit_gate.py && python scripts/security_audit_gate.py --requirement requirements/code-review/locked.txt && python scripts/security_audit_gate.py --requirement requirements/runtime-smoke/locked.txt" refresh-frozen-delivery = "python scripts/refresh_reproducible_delivery.py" # Development scripts test = "pytest {args}" @@ -461,6 +462,7 @@ sources = ["src"] include = [ "/src", "/resources", + "/modules/bundle-mapper", "/README.md", "/LICENSE", "/pyproject.toml", diff --git a/requirements/ci/locked.txt b/requirements/ci/locked.txt index 4e9a1ce93..507487f83 100644 --- a/requirements/ci/locked.txt +++ b/requirements/ci/locked.txt @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv export --locked --all-extras --no-emit-project --format requirements-txt --no-annotate --output-file requirements/ci/.locked-export-an22olz1.txt +# uv export --locked --all-extras --no-emit-project --format requirements-txt --no-annotate --output-file requirements/ci/.locked-export-r0ianjn1.txt annotated-doc==0.0.4 \ --hash=sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320 \ --hash=sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4 @@ -51,9 +51,9 @@ cachecontrol==0.14.4 \ cattrs==26.1.0 \ --hash=sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096 \ --hash=sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40 -certifi==2026.7.22 \ - --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ - --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 +certifi==2026.10.19 \ + --hash=sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae \ + --hash=sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b cffi==2.1.0 ; platform_python_implementation != 'PyPy' \ --hash=sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc \ --hash=sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d \ @@ -407,9 +407,9 @@ filelock==3.32.0 \ gitdb==4.0.12 \ --hash=sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571 \ --hash=sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf -gitpython==3.1.61 \ - --hash=sha256:8ab28c9da863cdd9e7d7694ec46cf3e6c9a12d8a30a1acd3447aec11975d530c \ - --hash=sha256:f51c24d8c0f733a195447385f5774a5dfe8767f5acfd7994a33755644c6ecc95 +gitpython==3.2.0 \ + --hash=sha256:bd70c5ec05cd2b797423e7eb312147d2458d3cca92085888fba2213f85905537 \ + --hash=sha256:fb92310af6844d96adc95ca066ed2e617c00e1dbd146a326626c81e72e18cc2e glom==25.12.0 \ --hash=sha256:1ae7da88be3693df40ad27bdf57a765a55c075c86c971bcddd67927403eb0069 \ --hash=sha256:b9f21e77f71a6576a43864e85066b8cc3f0f778d0d50961563f8981377a6dcb1 @@ -422,9 +422,9 @@ graphviz==0.21 \ h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 -hatchling==1.32.0 \ - --hash=sha256:0bdbde4a52b06c37e3eca395f85a762bf0ef06fe374fd8ae429dc6be10230f5f \ - --hash=sha256:0e17c9c3b9aa7c625acc8d0f5b622f107d5049af9ecf5ada4de1aada5be7cdbc +hatchling==1.32.4 \ + --hash=sha256:08ecf7548fb48205e7f213d70c71e67b8271b7242093dc3f1da578b42c734a2c \ + --hash=sha256:c4468f73144c054d2aab4ef0f0378c43b9878bf07f8ffd6b79690e970d375f07 httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 @@ -985,9 +985,9 @@ pygls==2.1.1 \ pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 -pyjwt==2.13.0 \ - --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ - --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 +pyjwt==2.15.1 \ + --hash=sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193 \ + --hash=sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8 pyparsing==3.3.2 \ --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc @@ -1012,9 +1012,9 @@ pytest-timeout==2.4.0 \ pytest-xdist==3.8.0 \ --hash=sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88 \ --hash=sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1 -python-discovery==1.5.0 \ - --hash=sha256:3e014c6327154d3dda27939a9a0dc9c5c000439f1906d3f303b48f984bd2ecef \ - --hash=sha256:70c4fc61b4e7404e44f01d6fc44a715c4d685ca6cea83d295922f05891877c98 +python-discovery==1.6.0 \ + --hash=sha256:6393b4eae1be8b2182670635e7baff89ac21cb9f8e86fd1ff40c7b1144febb4c \ + --hash=sha256:d4e244cf17b8b29819ed78003d55fbacf86eda23425b075454fff9271b79377a python-dotenv==1.2.2 \ --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 @@ -1315,15 +1315,15 @@ secretstorage==3.5.0 ; platform_machine != 'ppc64le' and platform_machine != 's3 semantic-version==2.10.0 \ --hash=sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c \ --hash=sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177 -semgrep==1.175.0 \ - --hash=sha256:54ffeeaad2ed9428e9bd40ccf55b2bf7778ce0b59bf848862234aaf6ada2a889 \ - --hash=sha256:68a8d832eff6422c188f5d745e319ce36deccaac6effb77e3833b8930bc10425 \ - --hash=sha256:6bd6480637aea992cb8d41635329fd4e3bd6dc6e9c124a80341f150c5caeca2d \ - --hash=sha256:82b5015b5c215ab0dcada25ccbbc39d57167d09b077e06abbd450958f5c29f78 \ - --hash=sha256:ac787bcef4fd4563f8cdd179392c743bded89ea38c0c81ff99913883b6cde5ad \ - --hash=sha256:c54736be4dd41e269856695685203d676123c2e21145ecd6c26c438b8d1a41ca \ - --hash=sha256:e8b14c91558f765b9dd155a99b0071bfe64f61577cda8eb4964132155232c1af \ - --hash=sha256:e8ecd7ee8ef1033c9635111c6e162e778834d61416968c5c1c5e7b7fba35c34e +semgrep==1.179.0 \ + --hash=sha256:34b5e801e3f4325c93014124ccfab6da96857f74aae68458d2a07defd4727b0f \ + --hash=sha256:69bce348296374df6c51e72758801ce06dd18d6dfb307bc73d24b9de53bbe5f1 \ + --hash=sha256:6c6cf104ad0959c0b11de56d15c5d31f56949c620b4bd130c5d1fb899f2bb5a3 \ + --hash=sha256:9b57026f587a4bc7833bca2b6e4d6778072a79dcf0b9f2e5e01d3d5df39c7759 \ + --hash=sha256:b529c4db63f293918989b84ce4640f12f1b1e4680d9e7fcab5f3554f9113dfc5 \ + --hash=sha256:c23d79c4839540372acc39b59aad167c1c7f3dc21bacacb27f19aa2edd79d271 \ + --hash=sha256:c7e32b952990434b591003547f26b46c89a5bd56033715b00ff26327a8abe8c5 \ + --hash=sha256:dc6317fbfad49b1dbe396df7da7d9702cffc5ea9ea62cb3b4b2209824496b425 setproctitle==1.3.7 \ --hash=sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2 \ --hash=sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c \ @@ -1497,18 +1497,18 @@ typing-inspection==0.4.2 \ uc-micro-py==2.0.0 \ --hash=sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c \ --hash=sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811 -urllib3==2.7.0 \ - --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ - --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 +urllib3==2.8.0 \ + --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \ + --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63 userpath==1.9.2 \ --hash=sha256:2cbf01a23d655a1ff8fc166dfb78da1b641d1ceabf0fe5f970767d380b14e89d \ --hash=sha256:6c52288dab069257cc831846d15d48133522455d4677ee69a9781f11dbefd815 uvicorn==0.51.0 ; sys_platform != 'emscripten' \ --hash=sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b \ --hash=sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0 -virtualenv==21.7.0 \ - --hash=sha256:7f9519b9432ff11b6e1a3e94061664efc2ff99ea21780e3cf4f6bd0a5da8b37c \ - --hash=sha256:a8370c1c5530fbabf955e40b8fbbc68a431648b10f9433faa587db30a06e51dd +virtualenv==21.11.0 \ + --hash=sha256:15be2e0b4a5c35f2bec284b11ef025f2b80e0d907b0ab9bbae3c4f835f6bf1e8 \ + --hash=sha256:fa83254de82b83565013b2f111519cf056124dd068bdbb8c5e49c6b72c061f0f watchdog==6.0.0 \ --hash=sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a \ --hash=sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 \ diff --git a/requirements/runtime-smoke/README.md b/requirements/runtime-smoke/README.md new file mode 100644 index 000000000..ad0aaa3ff --- /dev/null +++ b/requirements/runtime-smoke/README.md @@ -0,0 +1,59 @@ +# Frozen runtime smoke seed + +This auxiliary lock supplies **Lz4 4.4.5** to workspace-owned direct and console +smoke interpreters. The complete frozen `uv.lock` graph, matching the CI export, is installed +offline from the reviewed cache into each child first; parent site-packages and inherited Python +paths are excluded. Marketplace pip has no index and its normal +manifest/dependency validation remains exercised. + +Runtime smoke uses the separately pinned portable module snapshot in +`ci/runtime-smoke-fixture.lock.json`: commit +`046d366c190af7a188d6f48953a96edb65e766db`, tree +`c5b44db3477d1c9ea8dae1dceab2af276d265353`. +This is the already reviewed documentation snapshot, with Code Review 0.50.1 +compatible with CLI 0.55.5 and only Packaging/Requests Python dependencies. +Its real module declarations and archives are preserved. No dependency is +removed from a staged manifest and no module dependency check is skipped. + +The native Requirements provider remains independently pinned at 69f0758 in +`ci/module-fixture.lock.json`. Its original RED artifacts and protocol are +unchanged. CI checks out both snapshots at full SHAs and verifies the portable +snapshot's tree and clean working directory before any module execution. +Locally set `SPECFACT_SMOKE_MODULES_REPO` to that clean portable checkout; +`SPECFACT_MODULES_REPO` continues to select the Requirements provider. + +BasedPyright uses the existing committed npm lock and SHA-pinned Node setup. +Neither BasedPyright's Python package nor an unofficial Node runtime wheel is +part of this seed. The dependency-trust gate applies its existing prohibited +release and minimum-version policy to this auxiliary graph too. + +Generate after artifact review: + +```bash +uv pip compile requirements/runtime-smoke/requirements.in --no-deps \ + --constraints requirements/runtime-smoke/locked.txt --python-version 3.12 \ + --only-binary :all: --generate-hashes --no-annotate --output-file /tmp/smoke-lock.txt +``` + +Copy the generated body with one `# input-sha256:` digest of `requirements.in`. +Frozen delivery recompiles it under committed constraints. The separate audit, +license and Socket checks remain required; pip installs only hash-pinned wheels. +The child wheelhouse contains only the authenticated manifests' transitive closure, +selected from exact committed hashes and downloaded with `--require-hashes`. +Normal marketplace pip-tools resolution uses that directory with no index. Its +project configuration emits pins without repeating transport options already in +the environment. Missing shared/transitive dependencies or frozen hashes fail. + +Artifact review on **2026-10-11 Europe/Berlin**: inspected Lz4 wheel metadata and +BSD license; macOS arm64 and Linux x86_64 hashes match the exact +[PyPI 4.4.5 metadata](https://pypi.org/pypi/lz4/4.4.5/json). + +| Inspected distribution | SHA-256 | +| --- | --- | +| `lz4-4.4.5-cp312-cp312-macosx_11_0_arm64.whl` | `6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a` | +| `lz4-4.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl` | `24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e` | + +Module archives and child bundle imports use only the owned staging tree read +from the authenticated Git commit. Ignored checkout files and inherited module +root overrides cannot enter that source tree. Raw checkout metadata is a separate +verified seed input. Normal marketplace dependency checks remain active. diff --git a/requirements/runtime-smoke/locked.txt b/requirements/runtime-smoke/locked.txt new file mode 100644 index 000000000..2e395f219 --- /dev/null +++ b/requirements/runtime-smoke/locked.txt @@ -0,0 +1,60 @@ +# Frozen module-only runtime smoke seed. +# input-sha256: 75f5008dcb0d4babbe9c04649dd486ddc495938f4ba91b73a299f99d0668d975 +lz4==4.4.5 \ + --hash=sha256:0846e6e78f374156ccf21c631de80967e03cc3c01c373c665789dc0c5431e7fc \ + --hash=sha256:0bba042ec5a61fa77c7e380351a61cb768277801240249841defd2ff0a10742f \ + --hash=sha256:12233624f1bc2cebc414f9efb3113a03e89acce3ab6f72035577bc61b270d24d \ + --hash=sha256:13254bd78fef50105872989a2dc3418ff09aefc7d0765528adc21646a7288294 \ + --hash=sha256:15551280f5656d2206b9b43262799c89b25a25460416ec554075a8dc568e4397 \ + --hash=sha256:1dd4d91d25937c2441b9fc0f4af01704a2d09f30a38c5798bc1d1b5a15ec9581 \ + --hash=sha256:214e37cfe270948ea7eb777229e211c601a3e0875541c1035ab408fbceaddf50 \ + --hash=sha256:216ca0c6c90719731c64f41cfbd6f27a736d7e50a10b70fad2a9c9b262ec923d \ + --hash=sha256:24092635f47538b392c4eaeff14c7270d2c8e806bf4be2a6446a378591c5e69e \ + --hash=sha256:28ccaeb7c5222454cd5f60fcd152564205bcb801bd80e125949d2dfbadc76bbd \ + --hash=sha256:2a2b7504d2dffed3fd19d4085fe1cc30cf221263fd01030819bdd8d2bb101cf1 \ + --hash=sha256:2c3ea562c3af274264444819ae9b14dbbf1ab070aff214a05e97db6896c7597e \ + --hash=sha256:33dd86cea8375d8e5dd001e41f321d0a4b1eb7985f39be1b6a4f466cd480b8a7 \ + --hash=sha256:3b84a42da86e8ad8537aabef062e7f661f4a877d1c74d65606c49d835d36d668 \ + --hash=sha256:451039b609b9a88a934800b5fc6ee401c89ad9c175abf2f4d9f8b2e4ef1afc64 \ + --hash=sha256:533298d208b58b651662dd972f52d807d48915176e5b032fb4f8c3b6f5fe535c \ + --hash=sha256:5f0b9e53c1e82e88c10d7c180069363980136b9d7a8306c4dca4f760d60c39f0 \ + --hash=sha256:609a69c68e7cfcfa9d894dc06be13f2e00761485b62df4e2472f1b66f7b405fb \ + --hash=sha256:61d0ee03e6c616f4a8b69987d03d514e8896c8b1b7cc7598ad029e5c6aedfd43 \ + --hash=sha256:66c5de72bf4988e1b284ebdd6524c4bead2c507a2d7f172201572bac6f593901 \ + --hash=sha256:67531da3b62f49c939e09d56492baf397175ff39926d0bd5bd2d191ac2bff95f \ + --hash=sha256:6bb05416444fafea170b07181bc70640975ecc2a8c92b3b658c554119519716c \ + --hash=sha256:6d0bf51e7745484d2092b3a51ae6eb58c3bd3ce0300cf2b2c14f76c536d5697a \ + --hash=sha256:713a777de88a73425cf08eb11f742cd2c98628e79a8673d6a52e3c5f0c116f33 \ + --hash=sha256:75419bb1a559af00250b8f1360d508444e80ed4b26d9d40ec5b09fe7875cb989 \ + --hash=sha256:7b62f94b523c251cf32aa4ab555f14d39bd1a9df385b72443fd76d7c7fb051f5 \ + --hash=sha256:7c4e7c44b6a31de77d4dc9772b7d2561937c9588a734681f70ec547cfbc51ecd \ + --hash=sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f \ + --hash=sha256:83bc23ef65b6ae44f3287c38cbf82c269e2e96a26e560aa551735883388dcc4b \ + --hash=sha256:8a842ead8ca7c0ee2f396ca5d878c4c40439a527ebad2b996b0444f0074ed004 \ + --hash=sha256:92159782a4502858a21e0079d77cdcaade23e8a5d252ddf46b0652604300d7be \ + --hash=sha256:9b5e6abca8df9f9bdc5c3085f33ff32cdc86ed04c65e0355506d46a5ac19b6e9 \ + --hash=sha256:a1acbbba9edbcbb982bc2cac5e7108f0f553aebac1040fbec67a011a45afa1ba \ + --hash=sha256:a2af2897333b421360fdcce895c6f6281dc3fab018d19d341cf64d043fc8d90d \ + --hash=sha256:a482eecc0b7829c89b498fda883dbd50e98153a116de612ee7c111c8bcf82d1d \ + --hash=sha256:a5f197ffa6fc0e93207b0af71b302e0a2f6f29982e5de0fbda61606dd3a55832 \ + --hash=sha256:a88cbb729cc333334ccfb52f070463c21560fca63afcf636a9f160a55fac3301 \ + --hash=sha256:b424df1076e40d4e884cfcc4c77d815368b7fb9ebcd7e634f937725cd9a8a72a \ + --hash=sha256:bd85d118316b53ed73956435bee1997bd06cc66dd2fa74073e3b1322bd520a67 \ + --hash=sha256:c1cfa663468a189dab510ab231aad030970593f997746d7a324d40104db0d0a9 \ + --hash=sha256:c216b6d5275fc060c6280936bb3bb0e0be6126afb08abccde27eed23dead135f \ + --hash=sha256:c8e71b14938082ebaf78144f3b3917ac715f72d14c076f384a4c062df96f9df6 \ + --hash=sha256:cdd4bdcbaf35056086d910d219106f6a04e1ab0daa40ec0eeef1626c27d0fddb \ + --hash=sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d \ + --hash=sha256:d64141085864918392c3159cdad15b102a620a67975c786777874e1e90ef15ce \ + --hash=sha256:d6da84a26b3aa5da13a62e4b89ab36a396e9327de8cd48b436a3467077f8ccd4 \ + --hash=sha256:d994b87abaa7a88ceb7a37c90f547b8284ff9da694e6afcfaa8568d739faf3f7 \ + --hash=sha256:da68497f78953017deb20edff0dba95641cc86e7423dfadf7c0264e1ac60dc22 \ + --hash=sha256:daffa4807ef54b927451208f5f85750c545a4abbff03d740835fc444cd97f758 \ + --hash=sha256:df5aa4cead2044bab83e0ebae56e0944cc7fcc1505c7787e9e1057d6d549897e \ + --hash=sha256:e099ddfaa88f59dd8d36c8a3c66bd982b4984edf127eb18e30bb49bdba68ce67 \ + --hash=sha256:e64e61f29cf95afb43549063d8433b46352baf0c8a70aa45e2585618fcf59d86 \ + --hash=sha256:e928ec2d84dc8d13285b4a9288fd6246c5cde4f5f935b479f50d986911f085e3 \ + --hash=sha256:f32b9e65d70f3684532358255dc053f143835c5f5991e28a5ac4c93ce94b9ea7 \ + --hash=sha256:f6538aaaedd091d6e5abdaa19b99e6e82697d67518f114721b5248709b639fad \ + --hash=sha256:f9b8bde9909a010c75b3aea58ec3910393b758f3c219beed67063693df854db0 \ + --hash=sha256:ff1b50aeeec64df5603f17984e4b5be6166058dcf8f1e26a3da40d7a0f6ab547 diff --git a/requirements/runtime-smoke/requirements.in b/requirements/runtime-smoke/requirements.in new file mode 100644 index 000000000..2764ae3fe --- /dev/null +++ b/requirements/runtime-smoke/requirements.in @@ -0,0 +1,3 @@ +# Module-only smoke dependency; shared packages use requirements/ci/locked.txt. +# The portable Code Review fixture uses the committed npm runner, never a Node wheel. +lz4==4.4.5 diff --git a/scripts/check_dependency_trust_exceptions.py b/scripts/check_dependency_trust_exceptions.py index 72c831c39..7be8eaebe 100644 --- a/scripts/check_dependency_trust_exceptions.py +++ b/scripts/check_dependency_trust_exceptions.py @@ -22,6 +22,8 @@ DEFAULT_SECURITY_TOOL_FLOORS = REPO_ROOT / "ci" / "security-tool-minimum-versions.json" DEFAULT_CODE_REVIEW_INPUT = REPO_ROOT / "requirements" / "code-review" / "requirements.in" DEFAULT_CODE_REVIEW_LOCK = REPO_ROOT / "requirements" / "code-review" / "locked.txt" +DEFAULT_RUNTIME_SMOKE_INPUT = REPO_ROOT / "requirements" / "runtime-smoke" / "requirements.in" +DEFAULT_RUNTIME_SMOKE_LOCK = REPO_ROOT / "requirements" / "runtime-smoke" / "locked.txt" CODE_REVIEW_INPUT_BINDING_PREFIX = "# input-sha256:" REQUIRED_FIELDS = frozenset( { @@ -424,7 +426,7 @@ def validate_frozen_dependency_policy( code_review_input_path: Path = DEFAULT_CODE_REVIEW_INPUT, code_review_lock_path: Path = DEFAULT_CODE_REVIEW_LOCK, ) -> list[str]: - """Reject blocked releases and reviews that do not bind to either frozen graph.""" + """Reject blocked releases and reviews across all three frozen dependency graphs.""" errors = validate_exception_register(register_path, today=today) locked_packages, lock_errors = _read_locked_packages(lock_path) errors.extend(lock_errors) @@ -441,6 +443,11 @@ def validate_frozen_dependency_policy( ) errors.extend(code_review_errors) errors.extend(_validate_locked_version_policy(code_review_packages, tool_floors)) + smoke_packages, smoke_errors = _read_code_review_locked_packages( + DEFAULT_RUNTIME_SMOKE_INPUT, DEFAULT_RUNTIME_SMOKE_LOCK + ) + errors.extend("Runtime smoke graph: " + error for error in smoke_errors) + errors.extend(_validate_locked_version_policy(smoke_packages, tool_floors)) return errors diff --git a/scripts/check_jekyll_dependencies.rb b/scripts/check_jekyll_dependencies.rb new file mode 100644 index 000000000..195675f27 --- /dev/null +++ b/scripts/check_jekyll_dependencies.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +# Execute with the frozen docs bundle. Fixtures and output stay in a temporary +# directory; generated JavaScript is inspected as text and never executed. +require "jekyll" +require "jekyll-redirect-from" +require "jekyll-feed" +require "jekyll-relative-links" +require "rexml/document" +require "tmpdir" +require "fileutils" +require "json" +require "yaml" + +module JekyllDependencyChecks + SITE_URL = "https://docs.example.test" + + def self.assert(condition, message) + raise message unless condition + end + + def self.write_page(source, name, data, content = "Original content") + path = File.join(source, name) + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, "#{YAML.dump(data)}---\n#{content}\n") + end + + def self.build_site(source, destination, baseurl, plugins) + config = Jekyll.configuration( + "source" => source, "destination" => destination, "url" => SITE_URL, + "baseurl" => baseurl, "plugins" => plugins, "quiet" => true, + "timezone" => "UTC", "title" => "Dependency probes" + ) + site = Jekyll::Site.new(config) + Dir.chdir(source) { site.process } + site + end + + def self.redirect(baseurl) + Dir.mktmpdir("jekyll-redirect-probe") do |source| + destination = File.join(source, "_site") + write_page(source, "target.md", {"permalink" => "/target/", "redirect_from" => "/legacy/"}) + write_page(source, "external.md", {"permalink" => "/external/", "redirect_to" => "https://example.test/safe?a=1&b=2"}) + rejected = ["javascript:alert(1)", "data:text/html,probe"] + rejected.each_with_index do |target, index| + write_page(source, "blocked-#{index}.md", {"permalink" => "/blocked-#{index}/", "redirect_to" => target}) + end + payload = 'https://example.test/"`{probe}?a=1&b=2' + write_page(source, "encoded.md", {"permalink" => "/encoded/", "redirect_to" => payload}) + build_site(source, destination, baseurl, ["jekyll-redirect-from"]) + rejected.each_index do |index| + output = File.read(File.join(destination, "blocked-#{index}/index.html")) + assert(output.include?("Original content") && !output.include?("!m)[1]) + assert(target.include?("%22%3C/script%3E%3Cimg%20src=x%3E%60%7Bprobe%7D"), "Unsafe redirect characters were not encoded") + assert(!encoded.include?(""), "Redirect target escaped into HTML") + end + puts "PASS redirect security and ordinary URLs: baseurl=#{baseurl.inspect}" + end + + def self.feed_language(baseurl) + Dir.mktmpdir("jekyll-feed-language-probe") do |source| + destination = File.join(source, "_site") + lang = 'en" probe="injected' + File.write(File.join(source, "_config.yml"), YAML.dump({"lang" => lang})) + write_page(source, "_posts/2026-10-01-probe.html", {"title" => "Probe", "lang" => lang}, "

ordinary content

") + build_site(source, destination, baseurl, ["jekyll-feed"]) + root = REXML::Document.new(File.read(File.join(destination, "feed.xml"))).root + assert(root.attributes["xml:lang"] == lang && !root.attributes["probe"], "Feed language injected an XML attribute") + entry = root.elements["entry"] + assert(entry.attributes["xml:lang"] == lang && !entry.attributes["probe"], "Post language injected an XML attribute") + alternate = root.elements.to_a("link").find { |link| link.attributes["rel"] == "alternate" } + assert(alternate.attributes["hreflang"] == lang && !alternate.attributes["probe"], "Alternate language was not escaped") + assert(alternate.attributes["href"] == "#{SITE_URL}#{baseurl}/", "Feed URL lost deployment baseurl") + end + puts "PASS feed language escaping and ordinary URLs: baseurl=#{baseurl.inspect}" + end + + def self.feed_cdata(baseurl) + Dir.mktmpdir("jekyll-feed-cdata-probe") do |source| + destination = File.join(source, "_site") + payload = "before]]>after" + write_page(source, "_posts/2026-10-01-probe.html", {"title" => "Probe", "description" => payload}, "

#{payload}

") + build_site(source, destination, baseurl, ["jekyll-feed"]) + entry = REXML::Document.new(File.read(File.join(destination, "feed.xml"))).root.elements["entry"] + assert(entry.elements["content"].texts.map(&:value).join == "

#{payload}

", "Content CDATA did not preserve text") + assert(entry.elements["summary"].texts.map(&:value).join == payload, "Summary CDATA did not preserve text") + end + puts "PASS feed content and summary CDATA: baseurl=#{baseurl.inspect}" + end + + + def self.relative(baseurl) + Dir.mktmpdir("jekyll-relative-probe") do |source| + destination = File.join(source, "_site") + FileUtils.mkdir_p(File.join(source, "_includes")) + File.write(File.join(source, "_includes/nav.html"), '') + write_page(source, "target.md", {"permalink" => "/target/"}, "## Section") + index_content = "{% include nav.html %}\n\n[Markdown](target.md#section)\n\n[External](https://example.test/file.md#section)" + write_page(source, "index.md", {"permalink" => "/"}, index_content) + write_page(source, "nested/page.md", {"permalink" => "/nested/"}, "[Nested](../target.md#section)") + site = build_site(source, destination, baseurl, ["jekyll-relative-links"]) + index = File.read(File.join(destination, "index.html")) + assert(index.include?("href=\"#{baseurl}/target/#section\" title=\"Target\""), "Included navigation lost rewritten URL/baseurl") + assert(index.include?("href=\"#{baseurl}/target/#section\">Markdown"), "Markdown link lost target/fragment/baseurl") + assert(index.include?("https://example.test/file.md#section"), "External Markdown URL changed") + nested = File.read(File.join(destination, "nested/index.html")) + assert(nested.include?("href=\"#{baseurl}/target/#section\""), "Nested Markdown link was not rewritten") + write_page(source, "target.md", {"permalink" => "/updated-target/"}, "## Section") + Dir.chdir(source) { site.process } + rebuilt = File.read(File.join(destination, "index.html")) + assert(rebuilt.include?("#{baseurl}/updated-target/#section") && !rebuilt.include?("#{baseurl}/target/#section"), "Rebuild retained stale link target cache") + end + puts "PASS relative links, included navigation and rebuild: baseurl=#{baseurl.inspect}" + end + +end + +modes = ARGV.empty? ? ["redirect", "feed_language", "feed_cdata", "relative"] : ARGV +abort "Unknown dependency probe" unless (modes - %w[redirect feed_language feed_cdata relative]).empty? +["", "/preview"].each do |baseurl| + modes.each { |mode| JekyllDependencyChecks.public_send(mode, baseurl) } +end +puts "Validated Jekyll #{Jekyll::VERSION}, JSON #{JSON::VERSION}, redirect #{JekyllRedirectFrom::VERSION}, feed #{Gem.loaded_specs.fetch("jekyll-feed").version}, relative #{Gem.loaded_specs.fetch("jekyll-relative-links").version}" diff --git a/scripts/check_reproducible_delivery.py b/scripts/check_reproducible_delivery.py index 6e04bd74f..ee3890958 100644 --- a/scripts/check_reproducible_delivery.py +++ b/scripts/check_reproducible_delivery.py @@ -23,6 +23,8 @@ LOCKED_EXPORT = REPO_ROOT / "requirements" / "ci" / "locked.txt" CODE_REVIEW_REQUIREMENTS_INPUT = REPO_ROOT / "requirements" / "code-review" / "requirements.in" CODE_REVIEW_LOCKED_EXPORT = REPO_ROOT / "requirements" / "code-review" / "locked.txt" +RUNTIME_SMOKE_REQUIREMENTS_INPUT = REPO_ROOT / "requirements/runtime-smoke/requirements.in" +RUNTIME_SMOKE_LOCKED_EXPORT = REPO_ROOT / "requirements/runtime-smoke/locked.txt" CODE_REVIEW_INPUT_BINDING_PREFIX = "# input-sha256:" COMMIT_SHA_PATTERN = re.compile(r"^[0-9a-f]{40}$") SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") @@ -140,18 +142,27 @@ def verify_code_review_lock() -> None: if "--hash=sha256:" not in contents: raise ValueError("Code Review lock must contain distribution hashes") _verify_code_review_input_binding(contents) - with tempfile.TemporaryDirectory(prefix="specfact-code-review-lock-") as temporary_directory: + _verify_compiled_lock(CODE_REVIEW_REQUIREMENTS_INPUT, CODE_REVIEW_LOCKED_EXPORT, contents, []) + + +@beartype +def _verify_compiled_lock( + requirements_input: Path, locked_export: Path, contents: str, extra_arguments: list[str] +) -> None: + """Compare exact input compilation under committed constraints without upgrading.""" + with tempfile.TemporaryDirectory(prefix="specfact-frozen-tooling-lock-") as temporary_directory: rendered_lock = Path(temporary_directory) / "locked.txt" completed = subprocess.run( [ "uv", "pip", "compile", - str(CODE_REVIEW_REQUIREMENTS_INPUT.relative_to(REPO_ROOT)), + str(requirements_input.relative_to(REPO_ROOT)), "--constraints", - str(CODE_REVIEW_LOCKED_EXPORT.relative_to(REPO_ROOT)), + str(locked_export.relative_to(REPO_ROOT)), "--python-version", "3.12", + *extra_arguments, "--generate-hashes", "--no-annotate", "--output-file", @@ -165,12 +176,37 @@ def verify_code_review_lock() -> None: ) if completed.returncode != 0: detail = (completed.stderr or completed.stdout).strip() - raise ValueError(f"Could not compile Code Review requirements: {detail}") + raise ValueError(f"Could not compile Isolated tooling requirements: {detail}") if not rendered_lock.is_file(): - raise ValueError("Code Review requirements compile did not create the expected lock") + raise ValueError("Isolated tooling requirements compile did not create the expected lock") rendered = rendered_lock.read_text(encoding="utf-8") if _without_generated_header(rendered) != _without_generated_header(contents): - raise ValueError("Code Review lock differs from requirements.in; regenerate the isolated lock") + raise ValueError("Isolated tooling lock differs from requirements.in; regenerate the isolated lock") + + +def _is_none(result: object) -> bool: + """Check the smoke verifier's explicit no-value postcondition.""" + return result is None + + +@beartype +@ensure(_is_none, "must return None") +def verify_runtime_smoke_lock() -> None: + """Bind the reviewed module-only seed to its exact input and artifact hashes.""" + contents = RUNTIME_SMOKE_LOCKED_EXPORT.read_text(encoding="utf-8") + bindings = [ + line.removeprefix(CODE_REVIEW_INPUT_BINDING_PREFIX).strip() + for line in contents.splitlines() + if line.startswith(CODE_REVIEW_INPUT_BINDING_PREFIX) + ] + expected = hashlib.sha256(RUNTIME_SMOKE_REQUIREMENTS_INPUT.read_bytes()).hexdigest() + if bindings != [expected]: + raise ValueError("Runtime smoke lock input SHA-256 binding does not match requirements.in") + if "--hash=sha256:" not in contents: + raise ValueError("Runtime smoke lock must contain distribution hashes") + _verify_compiled_lock( + RUNTIME_SMOKE_REQUIREMENTS_INPUT, RUNTIME_SMOKE_LOCKED_EXPORT, contents, ["--no-deps", "--only-binary", ":all:"] + ) @beartype @@ -189,6 +225,7 @@ def main(argv: list[str] | None = None) -> int: verify_uv_lock() verify_locked_export() verify_code_review_lock() + verify_runtime_smoke_lock() verify_module_fixture() except (OSError, ValueError, json.JSONDecodeError, subprocess.TimeoutExpired) as error: sys.stderr.write(f"reproducible delivery check failed: {error}\n") diff --git a/scripts/requirements_planning_context.py b/scripts/requirements_planning_context.py new file mode 100644 index 000000000..076539b33 --- /dev/null +++ b/scripts/requirements_planning_context.py @@ -0,0 +1,233 @@ +"""Authenticate closed planning layers above independently verified native roots.""" + +from __future__ import annotations + +import os +import re +import subprocess +from collections.abc import Callable +from typing import NamedTuple, cast + + +REPOSITORY = "nold-ai/specfact-cli" +REPOSITORY_ID = 1085706003 +ACTIONS_APP_ID = 15368 +MAX_CHECK_PAGES = 50 +RAW_ENTRY = re.compile(r":([0-7]{6}) ([0-7]{6}) [0-9a-f]{40} [0-9a-f]{40} [AMDT]") +SOURCE_PATH = re.compile(r"openspec/changes/([a-z0-9][a-z0-9-]*)/") +PLANNING_PATH = re.compile( + r"openspec/(?:[A-Za-z0-9][A-Za-z0-9_.-]*\.md|changes/[A-Za-z0-9_.-]+/" + r"(?:[A-Za-z0-9_.-]+/)*(?:[A-Za-z0-9_.-]+\.md|requirements-evidence\.yaml|\.openspec\.yaml))" +) +RUN_URL = re.compile(r"https://github\.com/nold-ai/specfact-cli/actions/runs/([1-9][0-9]*)/job/([1-9][0-9]*)") +ApiReader = Callable[[str], object] + + +class GitChange(NamedTuple): + """Both immutable file modes and a non-renamed path in a Git object diff.""" + + old_mode: str + new_mode: str + path: str + + +class RequiredWorkflow(NamedTuple): + """Protected, repository-specific workflow identity for inherited proof.""" + + check_name: str + name: str + identifier: int + path: str + + +WORKFLOWS = ( + RequiredWorkflow( + "Requirements evidence", "Requirements Evidence", 323253915, ".github/workflows/requirements-evidence.yml" + ), + RequiredWorkflow( + "Trusted Requirements Authority", + "Trusted Requirements Authority", + 348163848, + ".github/workflows/trusted-requirements-authority.yml", + ), +) + + +def _object(value: object) -> dict[str, object]: + if not isinstance(value, dict) or not all(isinstance(key, str) for key in value): + raise ValueError("planning-context-invalid") + return cast(dict[str, object], value) + + +def _positive_integer(value: object) -> int: + if type(value) is not int or cast(int, value) <= 0: + raise ValueError("planning-context-invalid") + return cast(int, value) + + +def _git_changes(base: str, head: str) -> list[GitChange]: + """Read all source/destination changes as independent NUL-delimited Git paths.""" + result = subprocess.run( + ["git", "diff", "--raw", "--no-renames", "--no-ext-diff", "--no-textconv", "--abbrev=40", "-z", base, head], + capture_output=True, + check=True, + timeout=20, + ) + entries = result.stdout.split(b"\x00") + if entries.pop() != b"" or len(entries) % 2: + raise ValueError("planning-context-invalid") + changes: list[GitChange] = [] + for index in range(0, len(entries), 2): + metadata = RAW_ENTRY.fullmatch(entries[index].decode("ascii")) + if metadata is None: + raise ValueError("planning-context-invalid") + path = os.fsdecode(entries[index + 1]) + changes.append(GitChange(metadata[1], metadata[2], path)) + return changes + + +def _eligible_planning_paths(context: dict[str, object]) -> bool: + root = cast(str, context["proof_head_commit"]) + head = cast(str, context["head_commit"]) + if root == head: + return False + root_changes = _git_changes(cast(str, context["proof_base_commit"]), root) + control_changes = _git_changes(cast(str, context["protected_base_commit"]), root) + proof_paths = {workflow.path for workflow in WORKFLOWS} + if any(change.path.startswith(".github/actions/") or change.path in proof_paths for change in control_changes): + return False + covered_sources = { + match[0] + for change in root_changes + if (match := SOURCE_PATH.match(change.path)) is not None and match[1] != "archive" + } + if not covered_sources: + return False + changes = _git_changes(root, head) + return bool(changes) and all(_planning_change(change, covered_sources) for change in changes) + + +def _planning_change(change: GitChange, covered_sources: set[str]) -> bool: + """A name alone cannot authorize an executable, symlink or inherited-source edit.""" + if change.old_mode not in {"000000", "100644"} or change.new_mode not in {"000000", "100644"}: + return False + if PLANNING_PATH.fullmatch(change.path) is None or any(part in {".", ".."} for part in change.path.split("/")): + return False + return not any(change.path.startswith(prefix) for prefix in covered_sources) + + +def _check_page(payload: dict[str, object], previous_total: int | None) -> tuple[int, list[dict[str, object]]]: + """Validate page shape and a stable listing count independently of pagination.""" + count, records = payload.get("total_count"), payload.get("check_runs") + if type(count) is not int or cast(int, count) < 0 or not isinstance(records, list): + raise ValueError("planning-context-invalid") + if previous_total is not None and count != previous_total: + raise ValueError("planning-context-invalid") + return cast(int, count), [_object(record) for record in records] + + +def _complete_checks(checks: list[dict[str, object]], total: int) -> list[dict[str, object]]: + """Reject overfull listings and duplicates before choosing any successful check.""" + if len(checks) != total or len({_positive_integer(check.get("id")) for check in checks}) != total: + raise ValueError("planning-context-invalid") + return checks + + +def _root_checks(root: str, get_json: ApiReader) -> list[dict[str, object]]: + """Read a complete check listing and fail closed on races or truncation.""" + checks: list[dict[str, object]] = [] + total = None + for page in range(1, MAX_CHECK_PAGES + 1): + payload = _object( + get_json(f"/repos/{REPOSITORY}/commits/{root}/check-runs?filter=all&per_page=100&page={page}") + ) + total, records = _check_page(payload, total) + checks.extend(records) + if len(checks) >= total: + return _complete_checks(checks, total) + if len(records) != 100: + raise ValueError("planning-context-invalid") + raise ValueError("planning-context-invalid") + + +def _latest_check(checks: list[dict[str, object]], workflow: RequiredWorkflow, root: str) -> dict[str, object]: + matching = [ + check + for check in checks + if check.get("name") == workflow.check_name + and _object(check.get("app")).get("id") == ACTIONS_APP_ID + and _object(check.get("app")).get("slug") == "github-actions" + ] + if not matching: + raise ValueError("planning-context-invalid") + check = max(matching, key=lambda record: _positive_integer(record.get("id"))) + if check.get("head_sha") != root or check.get("status") != "completed" or check.get("conclusion") != "success": + raise ValueError("planning-context-invalid") + return check + + +def _repository_matches(value: object) -> bool: + repository = _object(value) + return repository.get("id") == REPOSITORY_ID and repository.get("full_name") == REPOSITORY + + +def _verified_run( + check: dict[str, object], + workflow: RequiredWorkflow, + context: dict[str, object], + get_json: ApiReader, +) -> dict[str, object]: + details_url = check.get("details_url") + match = RUN_URL.fullmatch(details_url) if isinstance(details_url, str) else None + if match is None or int(match[2]) != _positive_integer(check.get("id")): + raise ValueError("planning-context-invalid") + run_id = int(match[1]) + run = _object(get_json(f"/repos/{REPOSITORY}/actions/runs/{run_id}")) + expected = { + "id": run_id, + "name": workflow.name, + "workflow_id": workflow.identifier, + "path": workflow.path, + "head_sha": context["proof_head_commit"], + "head_branch": context["proof_head_branch"], + "event": "pull_request", + "status": "completed", + "conclusion": "success", + } + if any(run.get(key) != value for key, value in expected.items()): + raise ValueError("planning-context-invalid") + if not _repository_matches(run.get("repository")) or not _repository_matches(run.get("head_repository")): + raise ValueError("planning-context-invalid") + if run.get("check_suite_id") != _positive_integer(_object(check.get("check_suite")).get("id")): + raise ValueError("planning-context-invalid") + pulls = run.get("pull_requests") + if not isinstance(pulls, list) or not any( + _object(pull).get("number") == context["proof_pull_request"] for pull in pulls + ): + raise ValueError("planning-context-invalid") + return { + "check_name": workflow.check_name, + "check_id": _positive_integer(check.get("id")), + "run_id": run_id, + "run_attempt": _positive_integer(run.get("run_attempt")), + } + + +def _resolve_planning_context(context: dict[str, object], get_json: ApiReader) -> dict[str, object]: + """Append a planning diff boundary only after closed scope and exact-root proof authenticate.""" + if not _eligible_planning_paths(context): + return context + root = cast(str, context["proof_head_commit"]) + commit = _object(get_json(f"/repos/{REPOSITORY}/commits/{root}")) + verification = _object(_object(commit.get("commit")).get("verification")) + if commit.get("sha") != root or verification.get("verified") is not True: + raise ValueError("planning-context-invalid") + checks = _root_checks(root, get_json) + proof = [ + _verified_run(_latest_check(checks, workflow, root), workflow, context, get_json) for workflow in WORKFLOWS + ] + return {**context, "planning_base_commit": root, "inherited_proof": {"head_commit": root, "checks": proof}} + + +# Explicit stdlib-only entry point loaded from the protected Git archive. +PLANNING_RESOLVER: Callable[[dict[str, object], ApiReader], dict[str, object]] = _resolve_planning_context diff --git a/scripts/requirements_review_context.py b/scripts/requirements_review_context.py new file mode 100644 index 000000000..161abefa4 --- /dev/null +++ b/scripts/requirements_review_context.py @@ -0,0 +1,204 @@ +"""Read authentic finalized root proof without relabeling child planning evidence.""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib.util +import io +import json +import stat +import subprocess +from collections.abc import Callable +from pathlib import Path +from typing import cast +from zipfile import ZipFile + + +REPOSITORY = "nold-ai/specfact-cli" +REPOSITORY_ID = 1085706003 +MAX_ARCHIVE_BYTES = 16 * 1024 * 1024 +MAX_REPORT_BYTES = 4 * 1024 * 1024 +MAX_ARTIFACT_PAGES = 50 +ApiReader = Callable[[str], object] +ArchiveReader = Callable[[str], bytes] + + +def _object(value: object) -> dict[str, object]: + """Require a dictionary with string keys before reading proof fields.""" + if not isinstance(value, dict) or not all(isinstance(key, str) for key in value): + raise ValueError("review-context-invalid") + return cast(dict[str, object], value) + + +def _integer(value: object) -> int: + """Accept positive integers while rejecting booleans and malformed identifiers.""" + if type(value) is not int or cast(int, value) <= 0: + raise ValueError("review-context-invalid") + return cast(int, value) + + +def _reauthenticate(context: dict[str, object], event: dict[str, object], get_json: ApiReader) -> dict[str, object]: + """Repeat live native resolution and protected selection before artifact retrieval.""" + helper = Path(__file__).with_name("requirements_stack_context.py") + spec = importlib.util.spec_from_file_location("_trusted_review_stack_context", helper) + if spec is None or spec.loader is None: + raise ValueError("review-context-invalid") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + resolve = cast(Callable[[dict[str, object], ApiReader], dict[str, object]], module.resolve_stack_context) + merge_base = cast(Callable[[str, str], str], module._proof_merge_base) + planning = cast(Callable[[dict[str, object], ApiReader], dict[str, object]], module._planning_context) + fresh = resolve(event, get_json) + fresh["protected_base_commit"] = fresh["proof_base_commit"] + fresh["proof_base_commit"] = merge_base( + cast(str, fresh["proof_base_commit"]), cast(str, fresh["proof_head_commit"]) + ) + fresh = planning(fresh, get_json) + if fresh != context or fresh.get("planning_base_commit") != fresh.get("proof_head_commit"): + raise ValueError("review-context-invalid") + return fresh + + +def _root_run(context: dict[str, object]) -> int: + """Select the single inherited Requirements run bound to the authenticated root.""" + proof = _object(context.get("inherited_proof")) + records = proof.get("checks") + if not isinstance(records, list) or proof.get("head_commit") != context.get("proof_head_commit"): + raise ValueError("review-context-invalid") + matching = [_object(record) for record in records if _object(record).get("check_name") == "Requirements evidence"] + if len(matching) != 1: + raise ValueError("review-context-invalid") + return _integer(matching[0].get("run_id")) + + +def _artifact_page(payload: object) -> tuple[int, list[dict[str, object]]]: + """Validate one nonempty page and its declared artifact inventory size.""" + record = _object(payload) + total = _integer(record.get("total_count")) + entries = record.get("artifacts") + if not isinstance(entries, list) or not entries: + raise ValueError("review-context-invalid") + return total, [_object(entry) for entry in entries] + + +def _unique_artifact(records: list[dict[str, object]], total: int) -> dict[str, object]: + """Reject incomplete or duplicate inventories and select the sole proof artifact.""" + identifiers = {_integer(record.get("id")) for record in records} + matching = [record for record in records if record.get("name") == "requirements-evidence"] + if len(identifiers) != total or len(records) != total or len(matching) != 1: + raise ValueError("review-context-invalid") + return matching[0] + + +def _artifact(run_id: int, get_json: ApiReader) -> dict[str, object]: + """Require a complete stable inventory with one exact named artifact.""" + records: list[dict[str, object]] = [] + total: int | None = None + for page in range(1, MAX_ARTIFACT_PAGES + 1): + count, entries = _artifact_page( + get_json(f"/repos/{REPOSITORY}/actions/runs/{run_id}/artifacts?per_page=100&page={page}") + ) + if total is not None and count != total: + raise ValueError("review-context-invalid") + total = count + records.extend(entries) + if len(records) >= total: + return _unique_artifact(records, total) + raise ValueError("review-context-invalid") + + +def _verify_archive(artifact: dict[str, object], context: dict[str, object], run_id: int, archive: bytes) -> None: + """Bind archive bytes to the exact service run, repository, head, size and digest.""" + expected = { + "id": run_id, + "repository_id": REPOSITORY_ID, + "head_repository_id": REPOSITORY_ID, + "head_sha": context["proof_head_commit"], + "head_branch": context["proof_head_branch"], + } + metadata = _object(artifact.get("workflow_run")) + if any(metadata.get(key) != value for key, value in expected.items()) or artifact.get("expired") is not False: + raise ValueError("review-context-invalid") + if _integer(artifact.get("size_in_bytes")) != len(archive) or len(archive) > MAX_ARCHIVE_BYTES: + raise ValueError("review-context-invalid") + if artifact.get("digest") != "sha256:" + hashlib.sha256(archive).hexdigest(): + raise ValueError("review-context-invalid") + + +def _json_member(archive: bytes) -> bytes: + """Read the sole bounded regular proof report without extracting archive paths.""" + with ZipFile(io.BytesIO(archive)) as zipped: + entries = zipped.infolist() + reports = [entry for entry in entries if entry.filename == "requirements-evidence.json"] + if len(entries) > 8 or len(reports) != 1: + raise ValueError("review-context-invalid") + member = reports[0] + if member.is_dir() or stat.S_IFMT(member.external_attr >> 16) not in {0, stat.S_IFREG}: + raise ValueError("review-context-invalid") + if member.file_size <= 0 or member.file_size > MAX_REPORT_BYTES: + raise ValueError("review-context-invalid") + return zipped.read(member) + + +def _report(artifact: dict[str, object], context: dict[str, object], run_id: int, archive: bytes) -> bytes: + """Read bounded regular JSON bytes after service provenance and ZIP integrity.""" + _verify_archive(artifact, context, run_id, archive) + data = _json_member(archive) + report = _object(json.loads(data)) + proof = _object(report.get("execution_proof")) + expected_report = { + "schema_version": "2", + "required_maturity": "verified", + "observed_maturity": "verified", + "gate_decision": "pass", + "findings": [], + } + if any(report.get(key) != value for key, value in expected_report.items()): + raise ValueError("review-context-invalid") + if proof.get("run_stage") != "final" or proof.get("source_ref") != context["proof_head_commit"]: + raise ValueError("review-context-invalid") + # The immutable review provider independently validates plans, selectors and complete proof structure. + return data + + +def _api(path: str) -> object: + """Read JSON through the fixed GitHub API executable and hostname with a timeout.""" + result = subprocess.run( + ["gh", "api", "--hostname", "github.com", path], capture_output=True, check=True, timeout=30 + ) + return json.loads(result.stdout) + + +def _archive(path: str) -> bytes: + """Retrieve artifact bytes through the fixed API command and reject oversized output.""" + result = subprocess.run( + ["gh", "api", "--hostname", "github.com", path], capture_output=True, check=True, timeout=30 + ) + if len(result.stdout) > MAX_ARCHIVE_BYTES: + raise ValueError("review-context-invalid") + return result.stdout + + +def _main() -> None: + """Authenticate live planning context and write the verified root report unchanged.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--context", type=Path, required=True) + parser.add_argument("--event", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + arguments = parser.parse_args() + context = _reauthenticate( + _object(json.loads(arguments.context.read_bytes())), _object(json.loads(arguments.event.read_bytes())), _api + ) + run_id = _root_run(context) + artifact = _artifact(run_id, _api) + if _integer(artifact.get("size_in_bytes")) > MAX_ARCHIVE_BYTES: + raise ValueError("review-context-invalid") + archive = _archive(f"/repos/{REPOSITORY}/actions/artifacts/{_integer(artifact.get('id'))}/zip") + data = _report(artifact, context, run_id, archive) + arguments.output.parent.mkdir(parents=True, exist_ok=True) + arguments.output.write_bytes(data) + + +if __name__ == "__main__": + _main() diff --git a/scripts/requirements_stack_context.py b/scripts/requirements_stack_context.py new file mode 100644 index 000000000..d3a9bebbd --- /dev/null +++ b/scripts/requirements_stack_context.py @@ -0,0 +1,282 @@ +"""Resolve native-stack proof boundaries using only authenticated GitHub data.""" + +from __future__ import annotations + +import argparse +import importlib.util +import json +import re +import subprocess +import sys +import urllib.parse +from collections.abc import Callable +from pathlib import Path +from typing import NamedTuple, NoReturn, cast + + +REPOSITORY = "nold-ai/specfact-cli" +REPOSITORY_ID = 1085706003 +PROTECTED_BASES = frozenset({"dev", "main"}) +SHA_PATTERN = re.compile(r"^[0-9a-f]{40}$") +REF_PATTERN = re.compile(r"^[^\x00-\x20\x7f~^:?*\[\\]+$") +INVALID_REF_COMPONENT_PATTERN = re.compile(r"(?:^|/)(?:\.|[^/]*\.lock(?:/|$))") +MAX_STACK_SIZE = 50 +MAX_INPUT_BYTES = 5_000_000 +ApiReader = Callable[[str], object] + + +class PullBoundary(NamedTuple): + """Exact branch and repository identities for one live pull request.""" + + number: int + head_ref: str + head_sha: str + base_ref: str + base_sha: str + head_repository: tuple[int, str] + + +class NativeStack(NamedTuple): + """GitHub-owned stack identity and protected root boundary.""" + + identifier: int + number: int + size: int + position: int + base_ref: str + base_sha: str + + +def _reject() -> NoReturn: + raise ValueError("stack-context-invalid") + + +def _object(value: object) -> dict[str, object]: + if not isinstance(value, dict) or not all(isinstance(key, str) for key in value): + _reject() + return cast(dict[str, object], value) + + +def _positive_integer(value: object) -> int: + if type(value) is not int or cast(int, value) <= 0: + _reject() + return cast(int, value) + + +def _sha(value: object) -> str: + if not isinstance(value, str) or SHA_PATTERN.fullmatch(value) is None: + _reject() + return value + + +def _ref(value: object) -> str: + if ( + not isinstance(value, str) + or REF_PATTERN.fullmatch(value) is None + or value == "HEAD" + or value.startswith(("-", "/")) + or ".." in value + or "//" in value + or "@{" in value + or value.endswith(("/", ".")) + or INVALID_REF_COMPONENT_PATTERN.search(value) is not None + ): + _reject() + return value + + +def _repository(value: object) -> None: + repository = _object(value) + if repository.get("full_name") != REPOSITORY or repository.get("id") != REPOSITORY_ID: + _reject() + + +def _head_repository(value: object) -> tuple[int, str]: + repository = _object(value) + name = repository.get("full_name") + if not isinstance(name, str) or not name: + _reject() + return _positive_integer(repository.get("id")), name + + +def _pull(value: object) -> PullBoundary: + pull = _object(value) + head, base = _object(pull.get("head")), _object(pull.get("base")) + head_repository = _head_repository(head.get("repo")) + _repository(base.get("repo")) + base_ref = _ref(base.get("ref")) + if pull.get("stack") is not None or base_ref not in PROTECTED_BASES: + _repository(head.get("repo")) + if pull.get("state") != "open": + _reject() + return PullBoundary( + _positive_integer(pull.get("number")), + _ref(head.get("ref")), + _sha(head.get("sha")), + base_ref, + _sha(base.get("sha")), + head_repository, + ) + + +def _stack(value: object) -> NativeStack: + stack = _object(value) + base = _object(stack.get("base")) + identity = NativeStack( + _positive_integer(stack.get("id")), + _positive_integer(stack.get("number")), + _positive_integer(stack.get("size")), + _positive_integer(stack.get("position")), + _ref(base.get("ref")), + _sha(base.get("sha")), + ) + if identity.position > identity.size or identity.size > MAX_STACK_SIZE or identity.base_ref not in PROTECTED_BASES: + _reject() + return identity + + +def _parent_payload(child: PullBoundary, get_json: ApiReader) -> dict[str, object]: + query = urllib.parse.urlencode({"state": "open", "head": "nold-ai:" + child.base_ref, "per_page": 100}) + candidates = get_json(f"/repos/{REPOSITORY}/pulls?{query}") + if not isinstance(candidates, list) or len(candidates) != 1: + _reject() + return _object(candidates[0]) + + +def _verified_parent(child_payload: dict[str, object], get_json: ApiReader) -> dict[str, object]: + child = _pull(child_payload) + child_stack = _stack(child_payload.get("stack")) + parent_payload = _parent_payload(child, get_json) + parent = _pull(parent_payload) + parent_stack = _stack(parent_payload.get("stack")) + if ( + child.base_ref != parent.head_ref + or child.base_sha != parent.head_sha + or child_stack.position != parent_stack.position + 1 + or child_stack[:3] != parent_stack[:3] + or child_stack[4:] != parent_stack[4:] + ): + _reject() + comparison = _object(get_json(f"/repos/{REPOSITORY}/compare/{parent.head_sha}...{child.head_sha}")) + if ( + comparison.get("status") not in {"ahead", "identical"} + or _object(comparison.get("merge_base_commit")).get("sha") != parent.head_sha + ): + _reject() + return parent_payload + + +def _member_record(payload: dict[str, object]) -> dict[str, object]: + member = _pull(payload) + return { + "pull_request": member.number, + "head_branch": member.head_ref, + "head_commit": member.head_sha, + "base_ref": member.base_ref, + "base_commit": member.base_sha, + } + + +def _root_payload(members: list[dict[str, object]], get_json: ApiReader) -> dict[str, object]: + while _pull(members[-1]).base_ref not in PROTECTED_BASES: + if len(members) >= MAX_STACK_SIZE: + _reject() + members.append(_verified_parent(members[-1], get_json)) + root = members[-1] + root_boundary = _pull(root) + if root.get("stack") is not None: + root_stack = _stack(root["stack"]) + if ( + root_stack.position != 1 + or root_stack.base_ref != root_boundary.base_ref + or root_stack.base_sha != root_boundary.base_sha + ): + _reject() + return root + + +def resolve_stack_context(event: dict[str, object], get_json: ApiReader) -> dict[str, object]: + """Fail closed unless live native ancestry matches the exact workflow event.""" + _repository(event.get("repository")) + expected = _pull(event.get("pull_request")) + live_payload = _object(get_json(f"/repos/{REPOSITORY}/pulls/{expected.number}")) + current = _pull(live_payload) + if current != expected: + _reject() + members = [live_payload] + root_payload = _root_payload(members, get_json) + root = _pull(root_payload) + return { + "schema_version": 1, + "repository": REPOSITORY, + "repository_id": REPOSITORY_ID, + **_member_record(live_payload), + "proof_base_ref": root.base_ref, + "proof_base_commit": root.base_sha, + "proof_head_branch": root.head_ref, + "proof_head_commit": root.head_sha, + "proof_pull_request": root.number, + "stack": live_payload.get("stack"), + "members": [_member_record(member) for member in reversed(members)], + } + + +def _github_json(path: str) -> object: + result = subprocess.run( + ["gh", "api", "--hostname", "github.com", path], capture_output=True, check=True, timeout=30 + ) + if len(result.stdout) > MAX_INPUT_BYTES: + _reject() + return cast(object, json.loads(result.stdout)) + + +def _proof_merge_base(base: str, head: str) -> str: + result = subprocess.run( + ["git", "merge-base", "--all", _sha(base), _sha(head)], + capture_output=True, + text=True, + check=True, + timeout=20, + ) + return _sha(result.stdout.strip()) + + +def _planning_context(context: dict[str, object], get_json: ApiReader) -> dict[str, object]: + """Load only the adjacent protected helper; isolated Python excludes local imports.""" + helper_path = Path(__file__).with_name("requirements_planning_context.py") + spec = importlib.util.spec_from_file_location("trusted_requirements_planning_context", helper_path) + if spec is None or spec.loader is None: + _reject() + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + resolver = cast(Callable[[dict[str, object], ApiReader], dict[str, object]], module.PLANNING_RESOLVER) + return resolver(context, get_json) + + +def main(argv: list[str] | None = None) -> int: + """Write a deterministic context without executing any pull-request bytes.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--event", type=Path, required=True) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args(argv) + try: + if args.event.is_symlink() or args.event.stat().st_size > MAX_INPUT_BYTES: + _reject() + event = _object(json.loads(args.event.read_text())) + context = resolve_stack_context(event, _github_json) + context["protected_base_commit"] = context["proof_base_commit"] + context["proof_base_commit"] = _proof_merge_base( + _sha(context["proof_base_commit"]), _sha(context["proof_head_commit"]) + ) + context = _planning_context(context, _github_json) + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(json.dumps(context, indent=2, sort_keys=True) + "\n") + except (OSError, ValueError, subprocess.SubprocessError): + sys.stderr.write("stack-context-invalid\n") + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/runtime_discovery_smoke.py b/scripts/runtime_discovery_smoke.py index 4a7c4c20e..07dc6e78c 100644 --- a/scripts/runtime_discovery_smoke.py +++ b/scripts/runtime_discovery_smoke.py @@ -5,6 +5,7 @@ import argparse import hashlib +import io import json import logging import os @@ -14,6 +15,7 @@ import sys import tarfile import tempfile +import tomllib import venv from pathlib import Path from typing import Any, cast @@ -21,6 +23,8 @@ import yaml from beartype import beartype from icontract import ensure +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name REPO_ROOT = Path(__file__).resolve().parents[1] @@ -156,6 +160,9 @@ def _resolve_modules_repo(configured: str | None) -> Path: candidates: list[Path] = [] if configured: candidates.append(Path(configured).expanduser()) + smoke_repo = os.environ.get("SPECFACT_SMOKE_MODULES_REPO", "").strip() + if smoke_repo: + candidates.append(Path(smoke_repo).expanduser()) env_repo = os.environ.get("SPECFACT_MODULES_REPO", "").strip() if env_repo: candidates.append(Path(env_repo).expanduser()) @@ -181,6 +188,19 @@ def _resolve_modules_repo(configured: str | None) -> Path: raise RuntimeError(f"Could not find specfact-cli-modules with required packages. Searched: {searched}") +def _verify_smoke_fixture(modules_repo: Path) -> None: + """Reject unreviewed fixture bytes before building or executing module archives.""" + identity = json.loads((REPO_ROOT / "ci/runtime-smoke-fixture.lock.json").read_text(encoding="utf-8")) + if identity.get("repository") != "nold-ai/specfact-cli-modules": + raise RuntimeError("Smoke fixture identity does not match the reviewed repository") + for key, revision in (("commit", "HEAD"), ("tree", "HEAD^{tree}")): + actual = subprocess.check_output(["git", "-C", str(modules_repo), "rev-parse", revision], text=True).strip() + if actual != identity.get(key): + raise RuntimeError("Smoke fixture identity does not match the reviewed " + key) + if subprocess.check_output(["git", "-C", str(modules_repo), "status", "--porcelain"], text=True).strip(): + raise RuntimeError("Smoke fixture identity has unreviewed working-tree bytes") + + def _module_payload_for_checksum(package_dir: Path) -> bytes: from specfact_cli.registry.module_installer import _module_artifact_payload_signed @@ -210,10 +230,24 @@ def _module_dependency_closure(modules_repo: Path) -> tuple[str, ...]: return tuple(module_ids) +@beartype +@ensure(lambda result: result is None, "tracked module staging must return None") +def _stage_tracked_module(modules_repo: Path, relative: Path, staged_dir: Path) -> None: + """Read archive bytes from the authenticated Git object, excluding ignored files.""" + identity = json.loads((REPO_ROOT / "ci/runtime-smoke-fixture.lock.json").read_text(encoding="utf-8")) + archive_bytes = subprocess.check_output( + ["git", "-C", str(modules_repo), "archive", "--format=tar", f"{identity['commit']}:{relative.as_posix()}"], + timeout=30, + ) + staged_dir.mkdir() + with tarfile.open(fileobj=io.BytesIO(archive_bytes), mode="r:") as archive: + archive.extractall(staged_dir, filter="data") + + def _build_local_registry(workspace: Path, modules_repo: Path) -> Path: registry = workspace / "local-registry" modules_dir = registry / "modules" - staging_dir = registry / "staging" + staging_dir = registry / "staging" / "packages" modules_dir.mkdir(parents=True) staging_dir.mkdir(parents=True) entries: list[dict[str, Any]] = [] @@ -222,7 +256,7 @@ def _build_local_registry(workspace: Path, modules_repo: Path) -> Path: bundle_name = module_id.split("/", 1)[1] source_dir = modules_repo / "packages" / bundle_name staged_dir = staging_dir / bundle_name - shutil.copytree(source_dir, staged_dir) + _stage_tracked_module(modules_repo, source_dir.relative_to(modules_repo), staged_dir) manifest_path = staged_dir / "module-package.yaml" manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) @@ -261,16 +295,31 @@ def _build_local_registry(workspace: Path, modules_repo: Path) -> Path: return index_path -def _create_pip_editable_launcher(workspace: Path) -> list[str]: - venv_dir = workspace / "pip-editable-venv" +def _create_runtime_venv(venv_dir: Path) -> Path: try: venv.EnvBuilder(with_pip=True).create(venv_dir) except subprocess.CalledProcessError: virtualenv = shutil.which("virtualenv") if virtualenv is None: raise - _run([virtualenv, str(venv_dir)], cwd=REPO_ROOT, env=os.environ.copy(), timeout=120) - python = venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + _run( + [ + virtualenv, + "--app-data", + str(venv_dir.parent / "virtualenv-app-data"), + "--no-periodic-update", + str(venv_dir), + ], + cwd=REPO_ROOT, + env=os.environ.copy(), + timeout=120, + ) + return venv_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + + +def _create_pip_editable_launcher(workspace: Path) -> list[str]: + venv_dir = workspace / "pip-editable-venv" + python = _create_runtime_venv(venv_dir) _run( [str(python), "-m", "pip", "install", "-e", str(REPO_ROOT)], cwd=REPO_ROOT, @@ -281,9 +330,51 @@ def _create_pip_editable_launcher(workspace: Path) -> list[str]: return [str(specfact)] -def _launcher_command(name: str, workspace: Path) -> list[str]: +def _frozen_runtime_env(env: dict[str, str], constraints: Path) -> dict[str, str]: + locks = (REPO_ROOT / "requirements/ci/locked.txt", REPO_ROOT / "requirements/runtime-smoke/locked.txt") + pins: list[str] = [] + for line in "\n".join(lock.read_text(encoding="utf-8") for lock in locks).splitlines(): + entry = line.strip().rstrip("\\").strip() + if not entry or entry.startswith(("#", "--hash=")): + continue + if re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*==[^;\s]+(?:\s*;.*)?", entry) is None: + raise RuntimeError("Frozen smoke requirements must contain exact package pins") + pins.append(entry) + if not pins: + raise RuntimeError("Frozen smoke requirements are empty") + _write(constraints, "\n".join(pins) + "\n") + env["PYTHONPATH"] = str(SRC_ROOT) + env["SPECFACT_CHILD_PYTHONPATH"] = env["PYTHONPATH"] + env["PIP_CONSTRAINT"] = str(constraints) + env["PIP_NO_INDEX"] = "1" + env.pop("VIRTUAL_ENV", None) + env.pop("PYTHONHOME", None) + env.pop("SPECFACT_MODULES_ROOTS", None) + env.pop("SPECFACT_CLI_MODULES_REPO", None) + return env + + +def _create_isolated_runtime_launcher(name: str, workspace: Path, modules_repo: Path | None = None) -> list[str]: + venv_dir = workspace / "runtime-venv" + python = _create_runtime_venv(venv_dir) if name == "direct": - return [sys.executable, "-m", "specfact_cli.cli"] + return [str(python), "-m", "specfact_cli.cli"] + env = _frozen_runtime_env(os.environ.copy(), workspace / "frozen-runtime-constraints.txt") + if modules_repo is not None: + env["SPECFACT_SMOKE_MODULES_REPO"] = str(modules_repo) + _seed_frozen_module_dependencies(workspace, env) + _run( + [str(python), "-m", "pip", "install", "--no-deps", "--no-build-isolation", "-e", str(REPO_ROOT)], + cwd=REPO_ROOT, + env=env, + timeout=180, + ) + return [str(venv_dir / ("Scripts/specfact.exe" if os.name == "nt" else "bin/specfact"))] + + +def _launcher_command(name: str, workspace: Path, modules_repo: Path | None = None) -> list[str]: + if name in {"direct", "console"}: + return _create_isolated_runtime_launcher(name, workspace, modules_repo) if name == "hatch-source": hatch_launcher = _hatch_launcher_python_and_site() if hatch_launcher is not None: @@ -307,8 +398,6 @@ def _launcher_command(name: str, workspace: Path) -> list[str]: str(REPO_ROOT), "specfact", ] - if name == "console": - return ["specfact"] if name == "uvx": return ["uvx", "--from", str(REPO_ROOT), "specfact"] raise ValueError(f"Unsupported launcher: {name}") @@ -355,6 +444,22 @@ def _build_launcher_env(name: str, home: Path, index_path: Path, modules_repo: P "PYTHONUNBUFFERED": "1", } ) + if name in {"direct", "console"}: + env = _frozen_runtime_env(env, home / "frozen-runtime-constraints.txt") + child_bin = home.parent / f"launcher-{name}" / "runtime-venv" / ("Scripts" if os.name == "nt" else "bin") + parent_prefix = Path(sys.prefix).resolve() + system_paths = [ + part + for part in env.get("PATH", os.defpath).split(os.pathsep) + if part and not Path(part).resolve().is_relative_to(parent_prefix) + ] + env["PATH"] = os.pathsep.join([str(child_bin), *system_paths]) + env["PIP_FIND_LINKS"] = str(child_bin.parent.parent / "frozen-wheels") + env["SPECFACT_SMOKE_MODULES_REPO"] = str(modules_repo) + env["SPECFACT_MODULES_REPO"] = str(index_path.parent / "staging") + env["SPECFACT_CLI_MODULES_REPO"] = env["SPECFACT_MODULES_REPO"] + env.pop("VIRTUAL_ENV", None) + env.pop("PYTHONHOME", None) hatch_launcher = _hatch_launcher_python_and_site() if name == "hatch-source" and hatch_launcher is not None: _python_executable, hatch_site_path = hatch_launcher @@ -408,12 +513,163 @@ def _assert_project_help(cli: list[str], demo: Path, env: dict[str, str]) -> Non raise AssertionError("`specfact sync bridge --help` still resolves as a root command") +@beartype +@ensure(lambda result: len(result) > 0, "frozen marketplace requirements must not be empty") +def _frozen_marketplace_requirements(modules_repo: Path) -> str: + """Select exact exported hashes for the authenticated manifests' dependency closure.""" + pending: set[str] = set() + for module_id in _module_dependency_closure(modules_repo): + manifest_path = modules_repo / "packages" / module_id.split("/", 1)[1] / "module-package.yaml" + manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) + pending.update(canonicalize_name(Requirement(dep).name) for dep in manifest.get("pip_dependencies", [])) + pending.update(canonicalize_name(dep["name"]) for dep in manifest.get("pip_dependencies_versioned", [])) + selected = _frozen_dependency_closure(pending) + contents = "\n".join( + (REPO_ROOT / path).read_text(encoding="utf-8") + for path in ("requirements/ci/locked.txt", "requirements/runtime-smoke/locked.txt") + ) + blocks: dict[str, str] = { + canonicalize_name(match[1]): match[0] + "\n" + for match in re.finditer( + r"(?m)^([A-Za-z0-9][A-Za-z0-9._-]*)==[^\n]*(?:\n[ \t]+--hash=sha256:[0-9a-f]{64}[^\n]*)*", + contents, + ) + } + if any(name not in blocks or "--hash=sha256:" not in blocks[name] for name in selected): + raise RuntimeError("Marketplace closure is missing frozen hashes") + return "".join(blocks[name] for name in sorted(selected)) + + +@beartype +@ensure(lambda result: len(result) > 0, "frozen dependency closure must not be empty") +def _frozen_dependency_closure(roots: set[str]) -> set[str]: + """Traverse only package identities present in the committed frozen graph.""" + pending = roots.copy() + packages = {package["name"]: package for package in tomllib.loads((REPO_ROOT / "uv.lock").read_text())["package"]} + selected: set[str] = set() + while pending: + name = pending.pop() + if name in selected: + continue + selected.add(name) + if name == "lz4": + continue # Sole reviewed auxiliary wheel; not part of the core project graph. + if name not in packages: + raise RuntimeError(f"Missing frozen hash graph entry for {name}") + pending.update(dependency["name"] for dependency in packages[name].get("dependencies", [])) + return selected + + +def _seed_frozen_module_dependencies(workspace: Path, env: dict[str, str]) -> None: + """Materialize the reviewed frozen graph inside the workspace-owned child.""" + python = workspace / "runtime-venv" / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + seed_env = env.copy() + seed_env.pop("PIP_NO_INDEX", None) + uv = shutil.which("uv") + if uv is None: + raise RuntimeError("uv is required to materialize the frozen smoke graph") + _sync_frozen_child(uv, workspace, python, seed_env) + _run( + [ + uv, + "pip", + "install", + "--python", + str(python), + "--require-hashes", + "--no-deps", + "--only-binary", + ":all:", + "--index-url", + "https://pypi.org/simple", + "-r", + str(REPO_ROOT / "requirements/runtime-smoke/locked.txt"), + ], + cwd=REPO_ROOT, + env=seed_env, + timeout=180, + ) + + _download_frozen_marketplace_wheels(workspace, python, seed_env) + + +@beartype +@ensure(lambda result: result is None, "frozen child synchronization must return None") +def _sync_frozen_child(uv: str, workspace: Path, python: Path, seed_env: dict[str, str]) -> None: + """Copy the complete frozen graph from artifact data into the owned child.""" + cache = subprocess.check_output( + [uv, "cache", "dir"], cwd=REPO_ROOT, env=os.environ.copy(), text=True, timeout=30 + ).strip() + if not cache or not Path(cache).is_absolute(): + raise RuntimeError("uv must report an absolute reviewed artifact cache path") + seed_env["UV_CACHE_DIR"] = cache + seed_env["UV_PROJECT_ENVIRONMENT"] = str((workspace / "runtime-venv").resolve()) + _run( + [ + uv, + "sync", + "--frozen", + "--all-extras", + "--no-install-project", + "--offline", + "--link-mode", + "copy", + "--python", + str(python), + ], + cwd=REPO_ROOT, + env=seed_env, + timeout=180, + ) + + +@beartype +@ensure(lambda result: result is None, "wheel download must return None") +def _download_frozen_marketplace_wheels(workspace: Path, python: Path, seed_env: dict[str, str]) -> None: + """Authenticate manifests and download only their exported, hash-verified wheels.""" + modules_repo = _resolve_modules_repo( + seed_env.get("SPECFACT_SMOKE_MODULES_REPO") or seed_env.get("SPECFACT_MODULES_REPO") + ) + _verify_smoke_fixture(modules_repo) + requirements = workspace / "frozen-marketplace-requirements.txt" + _write(requirements, _frozen_marketplace_requirements(modules_repo)) + wheels = workspace / "frozen-wheels" + wheels.mkdir() + seed_env["PIP_CACHE_DIR"] = str(workspace.parents[1] / "frozen-pip-cache") + seed_env["PIP_DEFAULT_TIMEOUT"] = "60" + _run( + [ + str(python), + "-m", + "pip", + "download", + "--require-hashes", + "--no-deps", + "--only-binary", + ":all:", + "--index-url", + "https://pypi.org/simple", + "--dest", + str(wheels), + "-r", + str(requirements), + ], + cwd=REPO_ROOT, + env=seed_env, + timeout=180, + ) + + def _smoke_launcher(name: str, workspace: Path, demo: Path, index_path: Path, modules_repo: Path) -> None: home = workspace / f"home-{name}" home.mkdir(parents=True) - cli = _launcher_command(name, workspace / f"launcher-{name}") + cli = _launcher_command(name, workspace / f"launcher-{name}", modules_repo) env = _build_launcher_env(name, home, index_path, modules_repo) + if name in {"direct", "console"}: + _write(demo / ".pip-tools.toml", "[tool.pip-tools]\nemit-options = false\n") + if name == "direct": + _seed_frozen_module_dependencies(workspace / f"launcher-{name}", env) _install_marketplace_modules(cli, demo, env) _assert_module_list_contains(_run([*cli, "module", "list"], cwd=demo, env=env)) @@ -454,6 +710,7 @@ def main() -> int: args = parser.parse_args() modules_repo = _resolve_modules_repo(args.modules_repo) + _verify_smoke_fixture(modules_repo) workspace_obj: tempfile.TemporaryDirectory[str] | None = None if args.keep_workspace: workspace = Path(tempfile.mkdtemp(prefix="specfact-runtime-discovery-smoke-")) diff --git a/setup.py b/setup.py index cb72ce82b..63676aded 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ if __name__ == "__main__": _setup = setup( name="specfact-cli", - version="0.55.4", + version="0.55.11", description=( "AI-bloat defense CLI for Python teams. Run deterministic code review, cleanup forecasts, " "and spec/contract evidence for AI-assisted and brownfield delivery." @@ -19,6 +19,7 @@ "typing-extensions>=4.15.0", "PyYAML>=6.0.3", "requests>=2.32.3", + "certifi>=2026.10.19", "azure-identity>=1.17.1", "cryptography>=50.0.0", "pycparser>=2.22,!=3.0.*", @@ -30,7 +31,7 @@ "jinja2>=3.1.6", "networkx>=3.4.2", "graphviz>=0.20.1", - "gitpython>=3.1.61", + "gitpython>=3.2.0", "ruamel.yaml>=0.18.16", "jsonschema>=4.23.0", "commentjson>=0.9.0", diff --git a/src/__init__.py b/src/__init__.py index 3a4f9f7f8..8209aa704 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -3,4 +3,4 @@ """ # Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py -__version__ = "0.55.4" +__version__ = "0.55.11" diff --git a/src/specfact_cli/__init__.py b/src/specfact_cli/__init__.py index dba634b13..6f5fbda97 100644 --- a/src/specfact_cli/__init__.py +++ b/src/specfact_cli/__init__.py @@ -76,6 +76,6 @@ def _install_progressive_disclosure() -> None: # keeps missing-command and missing-parameter UX consistent outside the root CLI too. _install_progressive_disclosure() -__version__ = "0.55.4" +__version__ = "0.55.11" __all__ = ["__version__"] diff --git a/src/specfact_cli/registry/bootstrap.py b/src/specfact_cli/registry/bootstrap.py index 2626fbf3a..e4f4853c8 100644 --- a/src/specfact_cli/registry/bootstrap.py +++ b/src/specfact_cli/registry/bootstrap.py @@ -13,9 +13,9 @@ ``project``, ``spec``, ``govern``) for bundles that are installed and enabled. Legacy flat aliases (for example ``analyze`` or ``plan`` at the root) are not registered. -- When ``category_grouping_enabled`` is False: the same modules register with - ``_register_command_flat_path``, exposing each declared command name at the root (flat topology). - This is a compatibility path for older layouts; grouped categories are the default. +- When ``category_grouping_enabled`` is False: eligible category-less project packages may use + unreserved legacy flat roots. Categorized bundles retain their category routing; removed flat bundle + shims stay absent. Permanent core and official category root reservations remain enforced. """ from __future__ import annotations diff --git a/src/specfact_cli/registry/module_installer.py b/src/specfact_cli/registry/module_installer.py index cdf7c5e97..dafd89891 100644 --- a/src/specfact_cli/registry/module_installer.py +++ b/src/specfact_cli/registry/module_installer.py @@ -3,8 +3,10 @@ from __future__ import annotations import hashlib +import importlib.util import logging import os +import py_compile import re import shutil import subprocess @@ -362,7 +364,10 @@ def get_bundled_module_metadata() -> dict[str, ModulePackageMetadata]: @beartype def _canonical_manifest_payload(manifest_path: Path) -> bytes: """Build deterministic manifest payload for checksum/signature verification.""" - parsed = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) + try: + parsed = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) + except yaml.YAMLError as error: + raise ValueError("Invalid module manifest YAML") from error if not isinstance(parsed, dict): raise ValueError("Invalid module manifest format") cast(dict[str, Any], parsed).pop("integrity", None) @@ -806,6 +811,179 @@ def verify_module_artifact( ) +@dataclass(frozen=True, slots=True) +class _FilesystemPayloadSnapshot: + """Retain the exact authenticated payload inputs for code and metadata binding.""" + + payload: bytes + sources: dict[Path, bytes] + canonical_manifests: dict[str, bytes] + file_digests: dict[Path, str] + + +def _filesystem_payload_snapshot(package_dir: Path, module_dir_resolved: Path) -> _FilesystemPayloadSnapshot: + """Capture code and manifest bytes in the same canonical payload later authenticated by the core key.""" + entries: list[str] = [] + sources: dict[Path, bytes] = {} + manifests: dict[str, bytes] = {} + file_digests: dict[Path, str] = {} + for path in _rglob_signed_module_files(package_dir, module_dir_resolved): + relative = path.resolve().relative_to(module_dir_resolved).as_posix() + if relative in {"module-package.yaml", "metadata.yaml"}: + data = _canonical_manifest_payload(path) + manifests[relative] = data + else: + data = path.read_bytes() + digest = hashlib.sha256(data).hexdigest() + entries.append(f"{relative}:{digest}") + file_digests[path.resolve()] = digest + if path.suffix.lower() in {".py", ".pyw"}: + sources[path.resolve()] = data + return _FilesystemPayloadSnapshot("\n".join(entries).encode("utf-8"), sources, manifests, file_digests) + + +def _metadata_matches_authenticated_manifest(meta: ModulePackageMetadata, manifests: dict[str, bytes]) -> bool: + """Bind every registration field to the same manifest bytes authenticated by the payload signature.""" + from specfact_cli.registry.module_grouping import ModuleManifestError + from specfact_cli.registry.module_packages import normalize_module_manifest + + canonical = manifests.get("module-package.yaml") + if canonical is None: + canonical = manifests.get("metadata.yaml") + if canonical is None: + return False + try: + raw = yaml.safe_load(canonical) + if not isinstance(raw, dict) or "name" not in raw or "commands" not in raw: + return False + expected = normalize_module_manifest(cast(dict[str, Any], raw), meta.source) + excluded = {"integrity", "source", "protocol_operations"} + return expected.model_dump(exclude=excluded) == meta.model_dump(exclude=excluded) + except (yaml.YAMLError, ModuleManifestError, KeyError, TypeError, ValueError): + return False + + +def _compiled_authenticated_source_body(source: Path, data: bytes) -> bytes: + """Derive a standard cache from captured authenticated bytes without executing module code.""" + with tempfile.TemporaryDirectory(prefix="specfact-source-cache-") as temporary: + input_file = Path(temporary) / "source.py" + output_file = Path(temporary) / "cache.pyc" + input_file.write_bytes(data) + py_compile.compile( + str(input_file), cfile=str(output_file), dfile=str(source), doraise=True, optimize=sys.flags.optimize + ) + return output_file.read_bytes()[16:] + + +def _authenticated_source_cache_body(path: Path, authenticated_sources: dict[Path, bytes]) -> bytes | None: + """Reproduce one canonical cache from authenticated bytes before the final inventory traversal.""" + try: + source = Path(importlib.util.source_from_cache(str(path))).resolve() + except ValueError: + return None + return ( + _compiled_authenticated_source_body(source, authenticated_sources[source]) + if source in authenticated_sources + and Path(importlib.util.cache_from_source(str(source))).resolve() == path.resolve() + else None + ) + + +def _authenticated_source_cache_bodies(package_dir: Path, snapshot: _FilesystemPayloadSnapshot) -> dict[Path, bytes]: + """Finish all standard compiler work before making a final live-file authority decision.""" + bodies: dict[Path, bytes] = {} + for path in package_dir.rglob("*"): + if path.is_file() and path.suffix.lower() == ".pyc": + body = _authenticated_source_cache_body(path, snapshot.sources) + if body is not None: + bodies[path.resolve()] = body + return bodies + + +def _source_cache_matches_authenticated_body(path: Path, bodies: dict[Path, bytes]) -> bool: + """Compare live candidate bytes without compilation, execution or deserialization.""" + expected = bodies.get(path.resolve()) + if expected is None: + return False + data = path.read_bytes() + return ( + len(data) >= 16 + and data[:4] == importlib.util.MAGIC_NUMBER + and int.from_bytes(data[4:8], "little") in (0, 1, 3) + and data[16:] == expected + ) + + +def _filesystem_file_is_executable(path: Path) -> bool: + """Recognize code suffixes, executable modes and standard headers without running artifacts.""" + suffixes = {".py", ".pyw", ".pyc", ".pyo", ".so", ".pyd", ".dll", ".dylib", ".bundle"} + if path.suffix.lower() in suffixes or path.stat().st_mode & 0o111: + return True + with path.open("rb") as stream: + header = stream.read(4) + return header.startswith((b"#!", b"MZ")) or header in { + b"\x7fELF", + b"\xfe\xed\xfa\xce", + b"\xce\xfa\xed\xfe", + b"\xfe\xed\xfa\xcf", + b"\xcf\xfa\xed\xfe", + b"\xca\xfe\xba\xbe", + b"\xbe\xba\xfe\xca", + b"\xca\xfe\xba\xbf", + b"\xbf\xba\xfe\xca", + importlib.util.MAGIC_NUMBER, + } + + +def _filesystem_executable_payload_is_covered( + package_dir: Path, module_dir_resolved: Path, snapshot: _FilesystemPayloadSnapshot +) -> bool: + """Check authenticated membership/content and unsigned executables after cache reproduction.""" + cache_bodies = _authenticated_source_cache_bodies(package_dir, snapshot) + covered: set[Path] = set() + for path in package_dir.rglob("*"): + if path.is_symlink(): + return False + if not path.is_file(): + continue + if _signed_payload_is_hashable(path, module_dir_resolved): + relative = path.resolve().relative_to(module_dir_resolved).as_posix() + data = _canonical_manifest_payload(path) if relative in snapshot.canonical_manifests else path.read_bytes() + if snapshot.file_digests.get(path.resolve()) != hashlib.sha256(data).hexdigest(): + return False + covered.add(path.resolve()) + continue + if _filesystem_file_is_executable(path) and ( + path.suffix.lower() != ".pyc" or not _source_cache_matches_authenticated_body(path, cache_bodies) + ): + return False + return covered == set(snapshot.file_digests) + + +@beartype +@require(lambda package_dir: isinstance(package_dir, Path), "package_dir must be a Path") +@require(lambda meta: isinstance(meta, ModulePackageMetadata), "meta must be a ModulePackageMetadata instance") +@ensure(lambda result: isinstance(result, bool), "Must return a bool") +def verify_module_artifact_from_filesystem(package_dir: Path, meta: ModulePackageMetadata, public_key_pem: str) -> bool: + """Verify every filesystem payload file with no unsigned, key-override or receipt fallback.""" + if meta.integrity is None or not meta.integrity.signature or not public_key_pem.strip(): + return False + try: + if package_dir.is_symlink() or any(path.is_symlink() for path in package_dir.rglob("*")): + return False + resolved = package_dir.resolve() + snapshot = _filesystem_payload_snapshot(package_dir, resolved) + verify_checksum(snapshot.payload, meta.integrity.checksum) + verify_signature(snapshot.payload, meta.integrity.signature, public_key_pem) + if not _metadata_matches_authenticated_manifest(meta, snapshot.canonical_manifests): + return False + return ( + _filesystem_executable_payload_is_covered(package_dir, resolved, snapshot) and not package_dir.is_symlink() + ) + except (OSError, ValueError, EOFError, TypeError, SyntaxError, py_compile.PyCompileError): + return False + + def _clear_reinstall_download_cache(module_id: str, logger: Any) -> None: from specfact_cli.registry.marketplace_client import get_modules_branch diff --git a/src/specfact_cli/registry/module_packages.py b/src/specfact_cli/registry/module_packages.py index e06536527..4d2743d82 100644 --- a/src/specfact_cli/registry/module_packages.py +++ b/src/specfact_cli/registry/module_packages.py @@ -39,10 +39,12 @@ VersionedModuleDependency, VersionedPipDependency, ) +from specfact_cli.registry import module_installer from specfact_cli.registry.bridge_registry import BridgeRegistry, SchemaConverter from specfact_cli.registry.extension_registry import get_extension_registry from specfact_cli.registry.metadata import CommandMetadata from specfact_cli.registry.module_grouping import ( + CATEGORY_TO_GROUP_COMMAND, ModuleManifestError, normalize_legacy_bundle_group_command, validate_module_category_manifest, @@ -82,6 +84,15 @@ class _ModuleIntegrityContext: skipped: list[tuple[str, str]] +@dataclass(frozen=True) +class _RegistrationSelection: + """Keep admitted packages, dependency availability and actual versions in one selection.""" + + packages: list[tuple[Path, ModulePackageMetadata]] + enabled_map: dict[str, bool] + module_versions: dict[str, str] + + @dataclass class _PackageRegistrationContext: enabled_map: dict[str, bool] @@ -248,11 +259,20 @@ def get_workspace_modules_root(base_path: Path | None = None) -> Path | None: @beartype @ensure(lambda result: isinstance(result, list), "Must return a list of (Path, metadata) tuples") -def discover_all_package_metadata(base_path: Path | None = None) -> list[tuple[Path, ModulePackageMetadata]]: - """Discover module package metadata across built-in/marketplace/custom roots.""" - from specfact_cli.registry.module_discovery import discover_all_modules, discover_all_modules_for_project +def discover_all_package_metadata( + base_path: Path | None = None, *, include_shadowed_duplicates: bool = False +) -> list[tuple[Path, ModulePackageMetadata]]: + """Discover metadata winners by default, or ordered candidates for authority-aware registration.""" + from specfact_cli.registry.module_discovery import ( + discover_all_modules, + discover_all_modules_for_project, + discover_all_modules_for_project_with_shadowed, + ) - discovered = discover_all_modules() if base_path is None else discover_all_modules_for_project(base_path) + if include_shadowed_duplicates: + discovered = discover_all_modules_for_project_with_shadowed(base_path) + else: + discovered = discover_all_modules() if base_path is None else discover_all_modules_for_project(base_path) return [(entry.package_dir, entry.metadata) for entry in discovered] @@ -356,7 +376,7 @@ def _apply_category_manifest_postprocess(meta: ModulePackageMetadata) -> ModuleP return meta logger = get_bridge_logger(__name__) logger.warning( - "Module '%s' has no category field; mounting as flat top-level command.", + "Module '%s' has no category field; registration follows source and grouping policy.", meta.name, ) return meta @@ -392,7 +412,11 @@ def _raw_schema_version_str(raw: dict[str, Any]) -> str | None: return str(raw["schema_version"]) -def _module_package_metadata_from_raw_dict(raw: dict[str, Any], source: str) -> ModulePackageMetadata: +@beartype +@require(lambda raw: "name" in raw and "commands" in raw, "Manifest must declare name and commands") +@ensure(lambda result: isinstance(result, ModulePackageMetadata), "Normalized manifest must produce module metadata") +def normalize_module_manifest(raw: dict[str, Any], source: str) -> ModulePackageMetadata: + """Normalize manifest registration fields and injected discovery provenance with canonical policy.""" raw_help = raw.get("command_help") command_help = {str(k): str(v) for k, v in raw_help.items()} if isinstance(raw_help, dict) else None meta = ModulePackageMetadata( @@ -436,10 +460,10 @@ def _try_discover_one_package(child: Path, source: str, yaml_mod: Any) -> tuple[ if not isinstance(raw, dict) or "name" not in raw or "commands" not in raw: return None try: - meta = _module_package_metadata_from_raw_dict(raw, source) - except ModuleManifestError: - raise - except Exception: + meta = normalize_module_manifest(raw, source) + except Exception as error: + if isinstance(error, ModuleManifestError): + raise return None return (child, meta) @@ -1195,6 +1219,18 @@ def merge_module_state( return merged +def _resolved_bundle_name(meta: Any) -> str | None: + """Resolve explicit bundle precedence and namespaced aliases for both mounts and authority checks.""" + bundle_name = getattr(meta, "bundle", None) + if isinstance(bundle_name, str) and bundle_name: + return bundle_name + module_name = getattr(meta, "name", None) + if not isinstance(module_name, str) or "/" not in module_name: + return None + tail = module_name.split("/", 1)[1] + return tail if tail.startswith("specfact-") else None + + @beartype @require(lambda packages: isinstance(packages, list), "packages must be a list") @ensure(lambda result: isinstance(result, list), "Must return a sorted list of bundle name strings") @@ -1204,22 +1240,12 @@ def get_installed_bundles( ) -> list[str]: """Return sorted list of bundle names from discovered packages that are enabled and have a bundle set.""" - def _resolved_bundle(meta: Any) -> str | None: - bundle_name = getattr(meta, "bundle", None) - if isinstance(bundle_name, str) and bundle_name: - return bundle_name - module_name = getattr(meta, "name", None) - if not isinstance(module_name, str) or "/" not in module_name: - return None - tail = module_name.split("/", 1)[1] - return tail if tail.startswith("specfact-") else None - return sorted( { resolved for _dir, meta in packages if enabled_map.get(str(getattr(meta, "name", "")), True) - and (resolved := _resolved_bundle(meta)) is not None + and (resolved := _resolved_bundle_name(meta)) is not None } ) @@ -1232,9 +1258,8 @@ def _build_bundle_to_group() -> dict[str, tuple[str, str, Any]]: from specfact_cli.groups.project_group import build_app as build_project_app from specfact_cli.groups.spec_group import build_app as build_spec_app - return { - "specfact-backlog": ( - "backlog", + builders = { + "backlog": ( "Backlog and policy commands.", lambda: build_member_group( name="backlog", @@ -1244,16 +1269,11 @@ def _build_bundle_to_group() -> dict[str, tuple[str, str, Any]]: install_hint_module="nold-ai/specfact-backlog", ), ), - "specfact-codebase": ( - "code", - "Codebase quality commands: analyze, drift, validate, repro.", - build_codebase_app, - ), - "specfact-project": ("project", "Project lifecycle commands.", build_project_app), - "specfact-spec": ("spec", "Spec and contract commands: contract, api, sdd, generate.", build_spec_app), - "specfact-govern": ("govern", "Governance and quality gates: enforce, patch.", build_govern_app), - "specfact-requirements": ( - "requirements", + "codebase": ("Codebase quality commands: analyze, drift, validate, repro.", build_codebase_app), + "project": ("Project lifecycle commands.", build_project_app), + "spec": ("Spec and contract commands: contract, api, sdd, generate.", build_spec_app), + "govern": ("Governance and quality gates: enforce, patch.", build_govern_app), + "requirements": ( "Requirements evidence commands.", lambda: build_member_group( name="requirements", @@ -1265,6 +1285,11 @@ def _build_bundle_to_group() -> dict[str, tuple[str, str, Any]]: ), } + return { + f"specfact-{category}": (CATEGORY_TO_GROUP_COMMAND[category], help_str, build_app) + for category, (help_str, build_app) in builders.items() + } + @beartype def _mount_installed_category_groups( @@ -1511,14 +1536,73 @@ def _register_command_flat_path(package_dir: Path, meta: Any, cmd_name: str, log CommandRegistry.register(cmd_name, loader, cmd_meta) +def _workspace_module_path_is_unlinked(package_dir: Path) -> bool: + store = package_dir.parent + return ( + store.name == "modules" + and store.parent.name == ".specfact" + and not store.is_symlink() + and not store.parent.is_symlink() + ) + + +def _verified_project_bundle_root(package_dir: Path, meta: Any) -> bool: + """Authenticate canonical project-scoped official roots independently of unsigned flags and workspace keys.""" + command = CATEGORY_TO_GROUP_COMMAND.get(meta.category) + publisher = getattr(getattr(meta, "publisher", None), "name", None) + if ( + command is None + or not _workspace_module_path_is_unlinked(package_dir) + or not meta.name.startswith("nold-ai/specfact-") + or publisher != "nold-ai" + or meta.tier != "official" + or meta.bundle_group_command != command + or set(meta.commands).intersection(CATEGORY_TO_GROUP_COMMAND.values()) != {command} + ): + return False + try: + public_key = module_installer._bundled_public_key_path().read_text(encoding="utf-8").strip() + return ( + bool(public_key) + and module_installer.verify_module_artifact_from_filesystem(package_dir, meta, public_key) + and _workspace_module_path_is_unlinked(package_dir) + ) + except (OSError, ValueError): + return False + + +def _package_registration_allowed(package_dir: Path, meta: Any, category_grouping_enabled: bool, logger: Any) -> bool: + """Reject root claims before package registration or lazy import side effects.""" + if any(command in CORE_NAMES for command in meta.commands) and not _is_under_directory( + package_dir, get_modules_root() + ): + logger.warning("Module '%s' cannot supply permanent core commands.", meta.name) + return False + if meta.source != "project": + return True + category_capability = ( + meta.category is not None + or _resolved_bundle_name(meta) in {f"specfact-{category}" for category in CATEGORY_TO_GROUP_COMMAND} + or any(command in CATEGORY_TO_GROUP_COMMAND.values() for command in meta.commands) + ) + if category_capability: + if _verified_project_bundle_root(package_dir, meta): + return True + logger.warning("Workspace module '%s' lacks verified official category-root authority.", meta.name) + return False + if category_grouping_enabled: + logger.warning("Skipping category-less workspace module '%s' in grouped mode.", meta.name) + return False + return True + + def _register_commands_for_package( package_dir: Path, meta: Any, category_grouping_enabled: bool, logger: Any, ) -> None: - """Register package commands. Categorized marketplace modules never use flat root registration.""" - _ = category_grouping_enabled # retained for API compatibility; grouping no longer selects flat vs category + """Register authorized package commands without restoring removed bundle shims.""" for cmd_name in meta.commands: if meta.category is not None: _register_command_category_path(package_dir, meta, cmd_name, logger) @@ -1526,44 +1610,28 @@ def _register_commands_for_package( _register_command_flat_path(package_dir, meta, cmd_name, logger) -def _register_one_package_if_eligible(package_dir: Path, meta: Any, reg: _PackageRegistrationContext) -> None: - if not reg.enabled_map.get(meta.name, True): - return - compatible = _check_core_compatibility(meta, cli_version) - if not compatible: - reg.skipped.append((meta.name, f"requires {meta.core_compatibility}, cli is {cli_version}")) - return - deps_ok, missing = _validate_module_dependencies(meta, reg.enabled_map, reg.module_versions) - if not deps_ok: - reg.skipped.append((meta.name, f"missing dependencies: {', '.join(missing)}")) - return - integrity_ctx = _ModuleIntegrityContext( - allow_unsigned=reg.allow_unsigned, - is_test_mode=reg.is_test_mode, - logger=reg.logger, - skipped=reg.skipped, - ) - if not _module_integrity_allows_load(package_dir, meta, integrity_ctx): - return +def _package_local_registration_eligible( + package_dir: Path, + meta: ModulePackageMetadata, + category_grouping_enabled: bool, + integrity_ctx: _ModuleIntegrityContext, +) -> bool: + """Apply independent admission gates without schema, import or command registration effects.""" + if not _check_core_compatibility(meta, cli_version): + integrity_ctx.skipped.append((meta.name, f"requires {meta.core_compatibility}, cli is {cli_version}")) + return False if not _check_schema_compatibility(meta.schema_version, CURRENT_PROJECT_SCHEMA_VERSION): - reg.skipped.append( - ( - meta.name, - f"schema version {meta.schema_version} required, current is {CURRENT_PROJECT_SCHEMA_VERSION}", - ) + integrity_ctx.skipped.append( + (meta.name, f"schema version {meta.schema_version} required, current is {CURRENT_PROJECT_SCHEMA_VERSION}") ) - reg.logger.debug( - "Module %s: Schema version %s required, but current is %s (skipped)", - meta.name, - meta.schema_version, - CURRENT_PROJECT_SCHEMA_VERSION, - ) - return - if meta.schema_version is None: - reg.logger.debug("Module %s: No schema version declared (assuming current)", meta.name) - else: - reg.logger.debug("Module %s: Schema version %s (compatible)", meta.name, meta.schema_version) + return False + if not _package_registration_allowed(package_dir, meta, category_grouping_enabled, integrity_ctx.logger): + return False + return _module_integrity_allows_load(package_dir, meta, integrity_ctx) + +def _register_selected_package(package_dir: Path, meta: Any, reg: _PackageRegistrationContext) -> None: + """Apply effects only after the entire selected set passes the final admission barrier.""" _register_schema_extensions_safe(meta, reg.logger) _register_service_bridges_safe(meta, reg.bridge_owner_map, reg.logger) _record_protocol_compliance_result(package_dir, meta, reg.logger, reg.counters) @@ -1596,6 +1664,105 @@ def _log_skipped_modules_debug(logger: Any, skipped: list[tuple[str, str]]) -> N logger.debug("Skipped module '%s': %s", module_id, reason) +def _selection_from_packages( + packages: list[tuple[Path, ModulePackageMetadata]], enabled_map: dict[str, bool] +) -> _RegistrationSelection: + """Derive dependency presence and versions from exactly the selected copies.""" + names = {meta.name for _path, meta in packages} + return _RegistrationSelection( + packages=packages, + enabled_map={name: enabled for name, enabled in enabled_map.items() if not enabled or name in names}, + module_versions={meta.name: meta.version for _path, meta in packages}, + ) + + +def _dependency_candidate_indices( + groups: dict[str, list[tuple[Path, ModulePackageMetadata]]], selection: _RegistrationSelection +) -> tuple[int | None, ...]: + """Choose each first dependency-eligible copy against one coherent snapshot.""" + return tuple( + next( + ( + index + for index, (_path, meta) in enumerate(candidates) + if _validate_module_dependencies( + meta, + selection.enabled_map | {meta.name: True}, + selection.module_versions | {meta.name: meta.version}, + )[0] + ), + None, + ) + for candidates in groups.values() + ) + + +def _selection_at_indices( + groups: dict[str, list[tuple[Path, ModulePackageMetadata]]], + indices: tuple[int | None, ...], + enabled_map: dict[str, bool], +) -> _RegistrationSelection: + """Build a selection without allowing absent copies to satisfy dependencies.""" + return _selection_from_packages( + [candidates[index] for candidates, index in zip(groups.values(), indices, strict=True) if index is not None], + enabled_map, + ) + + +def _dependency_registration_selection( + packages: list[tuple[Path, ModulePackageMetadata]], enabled_map: dict[str, bool], ctx: _ModuleIntegrityContext +) -> _RegistrationSelection: + """Reconsider priority copies, refusing only names that oscillate before any effects.""" + groups: dict[str, list[tuple[Path, ModulePackageMetadata]]] = {} + for path, meta in packages: + groups.setdefault(meta.name, []).append((path, meta)) + while groups: + current: tuple[int | None, ...] = tuple(0 for _ in groups) + history: list[tuple[int | None, ...]] = [] + seen: dict[tuple[int | None, ...], int] = {} + while current not in seen: + seen[current] = len(history) + history.append(current) + selection = _selection_at_indices(groups, current, enabled_map) + following = _dependency_candidate_indices(groups, selection) + if following == current: + return selection + current = following + cycle = history[seen[current] :] + unstable = [name for index, name in enumerate(groups) if len({state[index] for state in cycle}) > 1] + for name in unstable: + del groups[name] + ctx.skipped.append((name, "conflicting module dependency selection")) + ctx.logger.warning("Refusing conflicting module dependency selection: %s", ", ".join(unstable)) + return _selection_from_packages([], enabled_map) + + +def _registration_packages( + packages: list[tuple[Path, ModulePackageMetadata]], + enabled_map: dict[str, bool], + category_grouping_enabled: bool, + integrity_ctx: _ModuleIntegrityContext, +) -> _RegistrationSelection: + """Select fully eligible copies and revalidate all selected artifacts before any effects.""" + candidates = [ + (path, meta) + for path, meta in packages + if enabled_map.get(meta.name, True) + and _package_local_registration_eligible(path, meta, category_grouping_enabled, integrity_ctx) + ] + while candidates: + selection = _dependency_registration_selection(candidates, enabled_map, integrity_ctx) + rejected = { + path + for path, meta in selection.packages + if not _package_local_registration_eligible(path, meta, category_grouping_enabled, integrity_ctx) + } + if not rejected: + return selection + candidates = [(path, meta) for path, meta in candidates if path not in rejected] + return _selection_from_packages([], enabled_map) + + @beartype @require( lambda enable_ids, disable_ids: not (set(enable_ids or []) & set(disable_ids or [])), @@ -1612,8 +1779,11 @@ def register_module_package_commands( Call after register_builtin_commands(). enable_ids/disable_ids from CLI (--enable-module/--disable-module). allow_unsigned: If True, allow modules without integrity metadata. Default from SPECFACT_ALLOW_UNSIGNED env. - category_grouping_enabled: Ignored for registration (retained for API compatibility). Category groups are - always mounted for installed bundles; categorized modules never register flat root aliases. + category_grouping_enabled: Default True rejects category-less project commands; False retains their + legacy flat registration. Categorized modules always retain category routing. External package paths + cannot register or extend permanent core commands in either mode. Project category roots require complete + official artifact verification against the core-bundled key; unsigned/test flags or workspace keys cannot + grant this authority. Rejected packages contribute no registration or active-import side effects. """ enable_ids = enable_ids or [] disable_ids = disable_ids or [] @@ -1622,16 +1792,22 @@ def register_module_package_commands( _clear_active_module_src_dirs() _MODULE_LOAD_FAILURES.clear() is_test_mode = os.environ.get("TEST_MODE") == "true" or os.environ.get("PYTEST_CURRENT_TEST") is not None - packages = discover_all_package_metadata() - packages = sorted(packages, key=_package_sort_key) - if not packages: - return + packages = discover_all_package_metadata(include_shadowed_duplicates=True) + logger = get_bridge_logger(__name__) discovered_list: list[tuple[str, str]] = [(meta.name, meta.version) for _dir, meta in packages] - module_versions = {meta.name: meta.version for _dir, meta in packages} state = read_modules_state() enabled_map = merge_module_state(discovered_list, state, enable_ids, disable_ids) - logger = get_bridge_logger(__name__) skipped: list[tuple[str, str]] = [] + integrity_ctx = _ModuleIntegrityContext( + allow_unsigned=allow_unsigned, is_test_mode=is_test_mode, logger=logger, skipped=skipped + ) + selection = _registration_packages(packages, enabled_map, category_grouping_enabled, integrity_ctx) + if not selection.packages: + _log_skipped_modules_debug(logger, skipped) + return + packages = sorted(selection.packages, key=_package_sort_key) + enabled_map = selection.enabled_map + module_versions = selection.module_versions counters = _ProtocolComplianceCounters( protocol_full=[0], protocol_partial=[0], @@ -1654,7 +1830,7 @@ def register_module_package_commands( counters=counters, ) for package_dir, meta in packages: - _register_one_package_if_eligible(package_dir, meta, reg_ctx) + _register_selected_package(package_dir, meta, reg_ctx) _mount_installed_category_groups(packages, enabled_map) _log_protocol_compatibility_footer(logger, counters) _log_skipped_modules_debug(logger, skipped) diff --git a/tests/fixtures/native_stack_context.json b/tests/fixtures/native_stack_context.json new file mode 100644 index 000000000..e523eac6f --- /dev/null +++ b/tests/fixtures/native_stack_context.json @@ -0,0 +1,108 @@ +{ + "event": { + "repository": { + "id": 1085706003, + "full_name": "nold-ai/specfact-cli" + }, + "pull_request": { + "number": 727, + "state": "open", + "head": { + "ref": "dependency-child", + "sha": "cccccccccccccccccccccccccccccccccccccccc", + "repo": { + "id": 1085706003, + "full_name": "nold-ai/specfact-cli" + } + }, + "base": { + "ref": "dependency-root", + "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "repo": { + "id": 1085706003, + "full_name": "nold-ai/specfact-cli" + } + }, + "stack": { + "id": 1987468, + "number": 757, + "size": 2, + "base": { + "ref": "dev", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "position": 2 + } + } + }, + "routes": { + "/repos/nold-ai/specfact-cli/pulls/727": { + "number": 727, + "state": "open", + "head": { + "ref": "dependency-child", + "sha": "cccccccccccccccccccccccccccccccccccccccc", + "repo": { + "id": 1085706003, + "full_name": "nold-ai/specfact-cli" + } + }, + "base": { + "ref": "dependency-root", + "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "repo": { + "id": 1085706003, + "full_name": "nold-ai/specfact-cli" + } + }, + "stack": { + "id": 1987468, + "number": 757, + "size": 2, + "base": { + "ref": "dev", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "position": 2 + } + }, + "/repos/nold-ai/specfact-cli/pulls?state=open&head=nold-ai%3Adependency-root&per_page=100": [ + { + "number": 744, + "state": "open", + "head": { + "ref": "dependency-root", + "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "repo": { + "id": 1085706003, + "full_name": "nold-ai/specfact-cli" + } + }, + "base": { + "ref": "dev", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "repo": { + "id": 1085706003, + "full_name": "nold-ai/specfact-cli" + } + }, + "stack": { + "id": 1987468, + "number": 757, + "size": 2, + "base": { + "ref": "dev", + "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "position": 1 + } + } + ], + "/repos/nold-ai/specfact-cli/compare/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb...cccccccccccccccccccccccccccccccccccccccc": { + "status": "ahead", + "merge_base_commit": { + "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + } + } + } +} diff --git a/tests/integration/scripts/test_runtime_discovery_fallback.py b/tests/integration/scripts/test_runtime_discovery_fallback.py new file mode 100644 index 000000000..b4599f4fd --- /dev/null +++ b/tests/integration/scripts/test_runtime_discovery_fallback.py @@ -0,0 +1,51 @@ +"""Verify the fallback across a process boundary with frozen parent assertions.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +FALLBACK_PROBE = """ +import json, subprocess, sys +from pathlib import Path +sys.path.insert(0, sys.argv[1]) +import scripts.runtime_discovery_smoke as smoke +workspace = Path(sys.argv[2]) +def failed_builder(_self, _venv_dir): + raise subprocess.CalledProcessError(1, "ensurepip") +calls = [] +smoke.venv.EnvBuilder.create = failed_builder +smoke.shutil.which = lambda _name: "/controlled/virtualenv" +smoke._run = lambda command, **_kwargs: calls.append(command) +launcher = smoke._create_pip_editable_launcher(workspace) +print(json.dumps({"calls": calls, "launcher": launcher})) +""" + + +def test_pip_editable_fallback_uses_isolated_seed_cache(tmp_path: Path) -> None: + """Freeze parent assertions while exercising fallback isolation in a bounded child.""" + observed = subprocess.run( + [sys.executable, "-P", "-c", FALLBACK_PROBE, str(REPO_ROOT), str(tmp_path)], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=True, + timeout=10, + ) + result = json.loads(observed.stdout) + assert result["calls"][0] == [ + "/controlled/virtualenv", + "--app-data", + str(tmp_path / "virtualenv-app-data"), + "--no-periodic-update", + str(tmp_path / "pip-editable-venv"), + ] + assert result["calls"][1][1:] == ["-m", "pip", "install", "-e", str(REPO_ROOT)] + assert result["launcher"] == [ + str(tmp_path / "pip-editable-venv" / ("Scripts/specfact.exe" if os.name == "nt" else "bin/specfact")) + ] diff --git a/tests/integration/scripts/test_runtime_discovery_smoke.py b/tests/integration/scripts/test_runtime_discovery_smoke.py index 8f6a5a9f5..304267df6 100644 --- a/tests/integration/scripts/test_runtime_discovery_smoke.py +++ b/tests/integration/scripts/test_runtime_discovery_smoke.py @@ -31,7 +31,9 @@ def _write_module_manifest(modules_repo: Path, bundle_name: str, dependencies: l def _resolve_modules_repo() -> Path | None: - configured = os.environ.get("SPECFACT_MODULES_REPO", "").strip() + configured = ( + os.environ.get("SPECFACT_SMOKE_MODULES_REPO", "").strip() or os.environ.get("SPECFACT_MODULES_REPO", "").strip() + ) candidates: list[Path] = [] if configured: candidates.append(Path(configured).expanduser()) @@ -52,6 +54,16 @@ def _resolve_modules_repo() -> Path | None: return None +@pytest.fixture +def tracked_staging_seam(monkeypatch: pytest.MonkeyPatch) -> None: + """Version/registry unit fixtures model staging; real Git bytes have their own controls.""" + import scripts.runtime_discovery_smoke as smoke + + monkeypatch.setattr( + smoke, "_stage_tracked_module", lambda repo, relative, staged: shutil.copytree(repo / relative, staged) + ) + + @pytest.mark.integration @pytest.mark.timeout(180) def test_runtime_discovery_smoke_direct_launcher() -> None: @@ -80,7 +92,9 @@ def test_runtime_discovery_smoke_direct_launcher() -> None: assert "runtime-discovery smoke passed for launcher=direct" in result.stdout -def test_local_runtime_registry_includes_transitive_bundle_dependencies(tmp_path: Path) -> None: +def test_local_runtime_registry_includes_transitive_bundle_dependencies( + tmp_path: Path, tracked_staging_seam: None +) -> None: import scripts.runtime_discovery_smoke as smoke modules_repo = tmp_path / "modules" @@ -102,7 +116,9 @@ def test_local_runtime_registry_includes_transitive_bundle_dependencies(tmp_path assert all(entry["download_url"].startswith("modules/") for entry in registry["modules"]) -def test_local_runtime_registry_rejects_traversal_in_manifest_version(tmp_path: Path) -> None: +def test_local_runtime_registry_rejects_traversal_in_manifest_version( + tmp_path: Path, tracked_staging_seam: None +) -> None: import scripts.runtime_discovery_smoke as smoke modules_repo = tmp_path / "modules" @@ -117,7 +133,9 @@ def test_local_runtime_registry_rejects_traversal_in_manifest_version(tmp_path: smoke._build_local_registry(tmp_path / "workspace", modules_repo) -def test_semver_prerelease_and_suffix_are_valid_for_runtime_registry(tmp_path: Path) -> None: +def test_semver_prerelease_and_suffix_are_valid_for_runtime_registry( + tmp_path: Path, tracked_staging_seam: None +) -> None: import scripts.runtime_discovery_smoke as smoke modules_repo = tmp_path / "modules" @@ -133,7 +151,9 @@ def test_semver_prerelease_and_suffix_are_valid_for_runtime_registry(tmp_path: P @pytest.mark.parametrize("version", ["01.2.3", "1.2.3-01", "1.2.3-alpha..1"]) -def test_local_runtime_registry_rejects_malformed_semver(tmp_path: Path, version: str) -> None: +def test_local_runtime_registry_rejects_malformed_semver( + tmp_path: Path, version: str, tracked_staging_seam: None +) -> None: import scripts.runtime_discovery_smoke as smoke modules_repo = tmp_path / "modules" @@ -164,6 +184,7 @@ def smoke_launcher(_name: str, workspace: Path, _demo: Path, _index_path: Path, captured["workspace"] = workspace monkeypatch.setattr(smoke, "_resolve_modules_repo", lambda _configured: tmp_path) + monkeypatch.setattr(smoke, "_verify_smoke_fixture", lambda _modules_repo: None) monkeypatch.setattr(smoke, "_create_rootless_monorepo_demo", create_demo) monkeypatch.setattr(smoke, "_build_local_registry", lambda workspace, _modules_repo: workspace / "index.json") monkeypatch.setattr(smoke, "_smoke_launcher", smoke_launcher) @@ -194,6 +215,7 @@ def build_registry(workspace: Path, _modules_repo: Path) -> Path: return workspace / "index.json" monkeypatch.setattr(smoke, "_resolve_modules_repo", lambda _configured: tmp_path) + monkeypatch.setattr(smoke, "_verify_smoke_fixture", lambda _modules_repo: None) monkeypatch.setattr(smoke, "_create_rootless_monorepo_demo", create_demo) monkeypatch.setattr(smoke, "_build_local_registry", build_registry) monkeypatch.setattr(smoke, "_smoke_launcher", lambda _name, _workspace, _demo, _index_path, _modules_repo: None) diff --git a/tests/integration/scripts/test_runtime_smoke_isolation.py b/tests/integration/scripts/test_runtime_smoke_isolation.py new file mode 100644 index 000000000..53e2a616e --- /dev/null +++ b/tests/integration/scripts/test_runtime_smoke_isolation.py @@ -0,0 +1,40 @@ +"""Smoke launchers must not install into the running test interpreter.""" + +from __future__ import annotations + +import importlib.metadata +import json +import subprocess +import sys +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +CHILD_PROBE = """ +import json, pathlib, subprocess, sys +sys.path.append(sys.argv[1]) +sys.path.insert(0, sys.argv[2]) +from scripts.runtime_discovery_smoke import _launcher_command +workspace = pathlib.Path(sys.argv[3]) +command = _launcher_command("direct", workspace) +result = subprocess.run([command[0], "-c", "import sys; print(sys.prefix)"], capture_output=True, text=True, check=True) +print(json.dumps({"prefix": result.stdout.strip(), "command": command})) +""" + + +def test_direct_smoke_uses_workspace_owned_interpreter(tmp_path: Path) -> None: + """Check the real prefix without ever modifying a shared interpreter.""" + runtime_site = str(importlib.metadata.distribution("beartype").locate_file("")) + result = subprocess.run( + [sys.executable, "-I", "-S", "-c", CHILD_PROBE, runtime_site, str(REPO_ROOT), str(tmp_path)], + capture_output=True, + text=True, + timeout=120, + check=False, + ) + assert result.returncode == 0, result.stderr + observed = json.loads(result.stdout) + prefix = Path(observed["prefix"]).resolve() + assert prefix.is_relative_to(tmp_path.resolve()) + assert prefix != Path(sys.prefix).resolve() + assert observed["command"][1:] == ["-m", "specfact_cli.cli"] diff --git a/tests/integration/scripts/test_runtime_smoke_policy.py b/tests/integration/scripts/test_runtime_smoke_policy.py new file mode 100644 index 000000000..00eb3530f --- /dev/null +++ b/tests/integration/scripts/test_runtime_smoke_policy.py @@ -0,0 +1,198 @@ +"""Reject executable-wheel smoke inputs and authenticate the portable fixture.""" + +from __future__ import annotations + +import json +import os +import subprocess +import tarfile +from pathlib import Path + +import pytest + +import scripts.runtime_discovery_smoke as smoke + + +@pytest.fixture(autouse=True) +def isolated_git_fixture_env(monkeypatch: pytest.MonkeyPatch) -> None: + for variable in tuple(os.environ): + if variable.startswith("GIT_"): + monkeypatch.delenv(variable) + monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull) + monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1") + + +def _init_owned_git_fixture(repo: Path) -> None: + repo.mkdir() + subprocess.run(["git", "init", "-q", str(repo)], check=True) + actual = subprocess.check_output(["git", "-C", str(repo), "rev-parse", "--absolute-git-dir"], text=True).strip() + assert Path(actual).resolve() == repo.resolve() / ".git" + + +def test_smoke_seed_excludes_unofficial_node_and_python_type_runner() -> None: + lock = (smoke.REPO_ROOT / "requirements/runtime-smoke/locked.txt").read_text() + assert "nodejs-wheel-binaries==" not in lock + assert "basedpyright==" not in lock + + +def test_portable_smoke_fixture_precedes_legacy_requirements_fixture( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + portable, legacy = tmp_path / "portable", tmp_path / "legacy" + for repo in (portable, legacy): + for module_id in smoke.MODULE_IDS: + manifest = repo / "packages" / module_id.split("/", 1)[1] / "module-package.yaml" + manifest.parent.mkdir(parents=True) + manifest.write_text("name: fixture\n") + monkeypatch.setenv("SPECFACT_SMOKE_MODULES_REPO", str(portable)) + monkeypatch.setenv("SPECFACT_MODULES_REPO", str(legacy)) + assert smoke._resolve_modules_repo(None) == portable.resolve() + + +@pytest.mark.parametrize("mismatch", [None, "commit", "tree", "dirty"]) +def test_smoke_fixture_rejects_unreviewed_git_bytes( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, mismatch: str | None +) -> None: + repo = tmp_path / "modules" + _init_owned_git_fixture(repo) + subprocess.run( + [ + "git", + "-C", + str(repo), + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "commit", + "-q", + "--allow-empty", + "-m", + "fixture", + ], + check=True, + ) + identity = {"repository": "nold-ai/specfact-cli-modules"} + for key, revision in (("commit", "HEAD"), ("tree", "HEAD^{tree}")): + identity[key] = subprocess.check_output(["git", "-C", str(repo), "rev-parse", revision], text=True).strip() + if mismatch in {"commit", "tree"}: + identity[mismatch] = "a" * 40 + if mismatch == "dirty": + (repo / "unreviewed.py").write_text("raise RuntimeError('unreviewed')\n") + (tmp_path / "ci").mkdir() + (tmp_path / "ci/runtime-smoke-fixture.lock.json").write_text(json.dumps(identity)) + monkeypatch.setattr(smoke, "REPO_ROOT", tmp_path) + if mismatch is None: + smoke._verify_smoke_fixture(repo) + else: + with pytest.raises(RuntimeError, match="Smoke fixture identity"): + smoke._verify_smoke_fixture(repo) + + +def test_child_environment_cannot_import_parent_or_inherited_packages( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + unrelated = tmp_path / "unreviewed-python-path" + monkeypatch.setenv("PYTHONPATH", str(unrelated)) + env = smoke._frozen_runtime_env(dict(smoke.os.environ), tmp_path / "constraints.txt") + assert env["PYTHONPATH"] == str(smoke.SRC_ROOT) + assert env["SPECFACT_CHILD_PYTHONPATH"] == str(smoke.SRC_ROOT) + + +def test_child_seed_installs_the_entire_hash_locked_graph(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + commands: list[list[str]] = [] + environments: list[dict[str, str]] = [] + cache = tmp_path / "reviewed-artifact-cache" + monkeypatch.setattr(smoke.shutil, "which", lambda command: "/reviewed/uv" if command == "uv" else None) + monkeypatch.setattr(smoke.subprocess, "check_output", lambda *_args, **_kwargs: str(cache)) + + def capture( + command: list[str], *, cwd: Path, env: dict[str, str], timeout: int + ) -> subprocess.CompletedProcess[str]: + commands.append(command) + environments.append(env) + return subprocess.CompletedProcess(command, 0, "") + + monkeypatch.setattr(smoke, "_run", capture) + monkeypatch.setattr(smoke, "_resolve_modules_repo", lambda _configured: tmp_path) + monkeypatch.setattr(smoke, "_verify_smoke_fixture", lambda _repo: None) + monkeypatch.setattr(smoke, "_frozen_marketplace_requirements", lambda _repo: "reviewed==1.0\n") + smoke._seed_frozen_module_dependencies(tmp_path, {"PIP_NO_INDEX": "1"}) + assert len(commands) == 3 + assert all( + flag in commands[0] for flag in ("sync", "--frozen", "--all-extras", "--offline", "--no-install-project") + ) + assert str(smoke.REPO_ROOT / "requirements/runtime-smoke/locked.txt") in commands[1] + assert "--require-hashes" in commands[1] + assert "download" in commands[2] and "--require-hashes" in commands[2] and "--no-deps" in commands[2] + assert all(env["UV_CACHE_DIR"] == str(cache) for env in environments) + + +def test_marketplace_wheelhouse_uses_frozen_transitive_hashes(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + modules = tmp_path / "modules" + for module_id in smoke.MODULE_IDS: + manifest = modules / "packages" / module_id.split("/", 1)[1] / "module-package.yaml" + manifest.parent.mkdir(parents=True) + manifest.write_text("pip_dependencies: [reviewed-root]\nbundle_dependencies: []\n") + (tmp_path / "uv.lock").write_text( + '[[package]]\nname = "reviewed-root"\nversion = "1.0"\ndependencies = [{name = "reviewed-leaf"}]\n' + '[[package]]\nname = "reviewed-leaf"\nversion = "2.0"\n' + ) + for directory in ("ci", "runtime-smoke"): + (tmp_path / "requirements" / directory).mkdir(parents=True) + root = "reviewed-root==1.0 \\" + "\n --hash=sha256:" + "a" * 64 + "\n" + leaf = "reviewed-leaf==2.0 \\" + "\n --hash=sha256:" + "b" * 64 + "\n" + (tmp_path / "requirements/ci/locked.txt").write_text(root + leaf + "unrelated==3.0\n") + (tmp_path / "requirements/runtime-smoke/locked.txt").write_text("lz4==4.4.5\n") + monkeypatch.setattr(smoke, "REPO_ROOT", tmp_path) + assert smoke._frozen_marketplace_requirements(modules) == leaf + root + (tmp_path / "requirements/ci/locked.txt").write_text(root) + with pytest.raises(RuntimeError, match="frozen hash"): + smoke._frozen_marketplace_requirements(modules) + + +def test_marketplace_resolver_only_uses_child_wheelhouse(tmp_path: Path) -> None: + home = tmp_path / "home-direct" + env = smoke._build_launcher_env("direct", home, tmp_path / "index.json", tmp_path / "modules") + assert env["PIP_FIND_LINKS"] == str(tmp_path / "launcher-direct" / "frozen-wheels") + assert env["PIP_NO_INDEX"] == "1" + + +def test_smoke_archive_ignores_uncommitted_ignored_fixture_bytes( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + repo = tmp_path / "modules" + _init_owned_git_fixture(repo) + (repo / ".gitignore").write_text("ignored.py\n") + for module_id in smoke.MODULE_IDS: + name = module_id.split("/", 1)[1] + manifest = repo / "packages" / name / "module-package.yaml" + manifest.parent.mkdir(parents=True) + manifest.write_text(f"name: {name}\nversion: 0.1.0\ncommands: []\n") + subprocess.run(["git", "-C", str(repo), "add", "."], check=True) + subprocess.run( + [ + "git", + "-C", + str(repo), + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "commit", + "-q", + "-m", + "fixture", + ], + check=True, + ) + commit = subprocess.check_output(["git", "-C", str(repo), "rev-parse", "HEAD"], text=True).strip() + (tmp_path / "ci").mkdir() + (tmp_path / "ci/runtime-smoke-fixture.lock.json").write_text(json.dumps({"commit": commit})) + monkeypatch.setattr(smoke, "REPO_ROOT", tmp_path) + (repo / "packages/specfact-project/ignored.py").write_text("raise RuntimeError('unreviewed')\n") + assert subprocess.check_output(["git", "-C", str(repo), "status", "--porcelain"], text=True) == "" + index = smoke._build_local_registry(tmp_path / "workspace", repo) + for archive_path in (index.parent / "modules").glob("*.tar.gz"): + with tarfile.open(archive_path) as archive: + assert not any(member.name.endswith("ignored.py") for member in archive.getmembers()) diff --git a/tests/integration/scripts/test_runtime_smoke_tools.py b/tests/integration/scripts/test_runtime_smoke_tools.py new file mode 100644 index 000000000..01fb835df --- /dev/null +++ b/tests/integration/scripts/test_runtime_smoke_tools.py @@ -0,0 +1,19 @@ +"""The child smoke environment must not invoke parent package entry points.""" + +import os +import sys +from pathlib import Path + +import pytest + + +def test_isolated_smoke_does_not_use_parent_cli_tools(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Parent entry points cannot resolve/install packages for the child interpreter.""" + import scripts.runtime_discovery_smoke as smoke + + parent_bin = str(Path(sys.prefix) / ("Scripts" if sys.platform == "win32" else "bin")) + monkeypatch.setenv("PATH", parent_bin + os.pathsep + os.defpath) + home = tmp_path / "home-direct" + env = smoke._build_launcher_env("direct", home, tmp_path / "index.json", tmp_path / "modules") + assert parent_bin not in env["PATH"].split(os.pathsep) + assert "VIRTUAL_ENV" not in env diff --git a/tests/integration/test_distribution_build.py b/tests/integration/test_distribution_build.py new file mode 100644 index 000000000..c9c90f402 --- /dev/null +++ b/tests/integration/test_distribution_build.py @@ -0,0 +1,31 @@ +"""Check release source artifacts through the actual offline PEP 517 build.""" + +from __future__ import annotations + +import shutil +import subprocess +import sys +import zipfile +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] + + +def test_source_distribution_rebuild_preserves_bundle_mapper(tmp_path: Path) -> None: + """The sdist can produce a wheel containing the original bundled manifest.""" + uv = shutil.which("uv") + assert uv is not None + build = subprocess.run( + [uv, "build", "--offline", "--no-build-isolation", "--python", sys.executable, "--out-dir", str(tmp_path)], + cwd=REPO_ROOT, + capture_output=True, + text=True, + timeout=120, + check=False, + ) + assert build.returncode == 0, build.stderr + wheel = next(tmp_path.glob("*.whl")) + with zipfile.ZipFile(wheel) as archive: + manifest = archive.read("specfact_cli/modules/bundle-mapper/module-package.yaml") + assert manifest == (REPO_ROOT / "modules/bundle-mapper/module-package.yaml").read_bytes() diff --git a/tests/unit/cli/test_lean_help_output.py b/tests/unit/cli/test_lean_help_output.py index 24548f128..db27beaeb 100644 --- a/tests/unit/cli/test_lean_help_output.py +++ b/tests/unit/cli/test_lean_help_output.py @@ -170,7 +170,7 @@ def _coverage_requirements() -> None: from specfact_cli.registry import module_packages as mp CommandRegistry._clear_for_testing() - scoped_monkeypatch.setattr(mp, "discover_all_package_metadata", lambda: packages) + scoped_monkeypatch.setattr(mp, "discover_all_package_metadata", lambda **_kwargs: packages) scoped_monkeypatch.setattr(mp, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) scoped_monkeypatch.setattr(mp, "read_modules_state", dict) scoped_monkeypatch.setattr(mp, "_check_protocol_compliance_from_source", lambda *_args, **_kwargs: []) diff --git a/tests/unit/docs/test_docs_validation_scripts.py b/tests/unit/docs/test_docs_validation_scripts.py index 37f3febc6..8155f4f18 100644 --- a/tests/unit/docs/test_docs_validation_scripts.py +++ b/tests/unit/docs/test_docs_validation_scripts.py @@ -16,10 +16,11 @@ def test_docs_json_gem_uses_the_patched_security_floor() -> None: gemfile = (REPO_ROOT / "docs" / "Gemfile").read_text(encoding="utf-8") lockfile = (REPO_ROOT / "docs" / "Gemfile.lock").read_text(encoding="utf-8") - assert 'gem "json", ">= 2.19.9"' in gemfile + assert 'gem "json", ">= 2.21.2", "< 3"' in gemfile match = re.search(r"^ json \(([^)]+)\)$", lockfile, re.MULTILINE) assert match is not None - assert tuple(int(part) for part in match.group(1).split(".")) >= (2, 19, 9) + assert (2, 21, 2) <= tuple(int(part) for part in match.group(1).split(".")) < (3,) + assert " jekyll (4.4.1)" in lockfile def _load_check_docs_commands() -> object: diff --git a/tests/unit/modules/test_bundle_import.py b/tests/unit/modules/test_bundle_import.py index 9b237477b..52666d2f0 100644 --- a/tests/unit/modules/test_bundle_import.py +++ b/tests/unit/modules/test_bundle_import.py @@ -1,8 +1,12 @@ from __future__ import annotations +import subprocess import sys from pathlib import Path +import pytest + +import scripts.runtime_discovery_smoke as smoke from specfact_cli.modules._bundle_import import bootstrap_local_bundle_sources @@ -13,7 +17,9 @@ def _make_modules_repo(tmp_path: Path) -> Path: return modules_repo -def test_bootstrap_local_bundle_sources_honors_specfact_modules_repo_env(monkeypatch: object, tmp_path: Path) -> None: +def test_bootstrap_local_bundle_sources_honors_specfact_modules_repo_env( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: modules_repo = _make_modules_repo(tmp_path) anchor = tmp_path / "workspace" / "src" / "specfact_cli" / "commands" / "repro.py" anchor.parent.mkdir(parents=True) @@ -31,3 +37,26 @@ def test_bootstrap_local_bundle_sources_honors_specfact_modules_repo_env(monkeyp assert expected_src in sys.path finally: sys.path[:] = original_path + + +def test_child_bundle_bootstrap_only_imports_staged_git_bytes(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + raw = tmp_path / "unreviewed-checkout" + staged = tmp_path / "staging" + for repo, marker in ((raw, "unreviewed"), (staged, "reviewed")): + source = repo / "packages" / "probe" / "src" / "smoke_source_probe.py" + source.parent.mkdir(parents=True) + source.write_text(f"MARKER = {marker!r}\n") + monkeypatch.setenv("SPECFACT_CLI_MODULES_REPO", str(raw)) + monkeypatch.setenv("SPECFACT_MODULES_ROOTS", str(raw / "packages")) + env = smoke._build_launcher_env("direct", tmp_path / "home-direct", tmp_path / "index.json", raw) + probe = ( + "from specfact_cli.modules._bundle_import import bootstrap_local_bundle_sources; " + "bootstrap_local_bundle_sources(__file__); " + "import smoke_source_probe; print(smoke_source_probe.MARKER)" + ).replace("__file__", repr(str(smoke.SRC_ROOT / "specfact_cli/commands/repro.py"))) + result = subprocess.run( + [sys.executable, "-c", probe], env=env, capture_output=True, text=True, check=False, timeout=20 + ) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "reviewed" + assert "SPECFACT_MODULES_ROOTS" not in env diff --git a/tests/unit/registry/test_category_groups.py b/tests/unit/registry/test_category_groups.py index 490a78072..c3acd3833 100644 --- a/tests/unit/registry/test_category_groups.py +++ b/tests/unit/registry/test_category_groups.py @@ -54,12 +54,32 @@ def test_bootstrap_with_category_grouping_enabled_registers_group_commands() -> ) -def test_bootstrap_with_category_grouping_disabled_still_has_no_flat_shims() -> None: - """Flat bundle shims are not registered even when SPECFACT_CATEGORY_GROUPING_ENABLED is false.""" +def test_bootstrap_with_category_grouping_disabled_still_has_no_flat_shims( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Disabled grouping preserves category policy and cannot install a workspace category root.""" + from specfact_cli.registry import module_packages + + builtin_packages = module_packages.discover_package_metadata(module_packages.get_modules_root(), source="builtin") + workspace_root = tmp_path / ".specfact" / "modules" + hostile = workspace_root / "hostile" + (hostile / "src").mkdir(parents=True) + (hostile / "module-package.yaml").write_text( + "name: hostile\nversion: '0.1.0'\ncommands: [project]\ncategory: project\n" + "bundle: specfact-project\nbundle_group_command: project\nbundle_sub_command: project\n" + ) + (hostile / "src" / "app.py").write_text("import typer\napp = typer.Typer()\n") + project_packages = module_packages.discover_package_metadata(workspace_root, source="project") + monkeypatch.setattr( + module_packages, "discover_all_package_metadata", lambda **_kwargs: builtin_packages + project_packages + ) + monkeypatch.setattr(module_packages, "read_modules_state", dict) with patch.dict(os.environ, {"SPECFACT_CATEGORY_GROUPING_ENABLED": "false"}, clear=False): register_builtin_commands() rebuild_root_app_from_registry() names = [name for name, _ in CommandRegistry.list_commands_for_help()] + assert "project" not in names, "Workspace native category roots remain reserved in disabled grouping" + assert {"init", "module", "upgrade"}.issubset(names) forbidden_flat = { "analyze", "drift", diff --git a/tests/unit/registry/test_core_only_bootstrap.py b/tests/unit/registry/test_core_only_bootstrap.py index b58949990..626cca694 100644 --- a/tests/unit/registry/test_core_only_bootstrap.py +++ b/tests/unit/registry/test_core_only_bootstrap.py @@ -47,32 +47,24 @@ def _make_core_metadata(name: str, commands: list[str] | None = None): @pytest.fixture(autouse=True) -def _clear_registry(): +def clear_registry(): CommandRegistry._clear_for_testing() yield CommandRegistry._clear_for_testing() def test_register_builtin_commands_registers_only_three_core_when_discovery_returns_three( - monkeypatch: pytest.MonkeyPatch, tmp_path: Path + monkeypatch: pytest.MonkeyPatch, ) -> None: """After bootstrap with only 3 core modules discovered, list_commands has exactly init, module, upgrade.""" - from specfact_cli.registry.module_discovery import DiscoveredModule - - def _discover(*, builtin_root=None, user_root=None, **kwargs): - root = builtin_root or tmp_path - return [ - DiscoveredModule(root / "init", _make_core_metadata("init"), "builtin"), - DiscoveredModule(root / "module_registry", _make_core_metadata("module_registry", ["module"]), "builtin"), - DiscoveredModule(root / "upgrade", _make_core_metadata("upgrade"), "builtin"), - ] + from specfact_cli.registry.module_packages import BUILTIN_MODULES_ROOT monkeypatch.setattr( "specfact_cli.registry.module_packages.discover_all_package_metadata", - lambda: [ - (tmp_path / "init", _make_core_metadata("init")), - (tmp_path / "module_registry", _make_core_metadata("module_registry", ["module"])), - (tmp_path / "upgrade", _make_core_metadata("upgrade")), + lambda **_kwargs: [ + (BUILTIN_MODULES_ROOT / "init", _make_core_metadata("init")), + (BUILTIN_MODULES_ROOT / "module_registry", _make_core_metadata("module_registry", ["module"])), + (BUILTIN_MODULES_ROOT / "upgrade", _make_core_metadata("upgrade")), ], ) monkeypatch.setattr( @@ -99,7 +91,7 @@ def test_bootstrap_does_not_register_extracted_modules_when_only_core_discovered """Bootstrap with only 3 core does NOT register project, plan, backlog, code, spec, govern, etc.""" monkeypatch.setattr( "specfact_cli.registry.module_packages.discover_all_package_metadata", - lambda: [ + lambda **_kwargs: [ (tmp_path / "init", _make_core_metadata("init")), (tmp_path / "module_registry", _make_core_metadata("module_registry", ["module"])), (tmp_path / "upgrade", _make_core_metadata("upgrade")), @@ -142,7 +134,7 @@ def test_flat_shim_plan_produces_actionable_error_after_shim_removal( """Invoking 'plan' when shims are removed should produce an actionable not-found error.""" monkeypatch.setattr( "specfact_cli.registry.module_packages.discover_all_package_metadata", - lambda: [ + lambda **_kwargs: [ (tmp_path / "init", _make_core_metadata("init")), (tmp_path / "module_registry", _make_core_metadata("module_registry", ["module"])), (tmp_path / "upgrade", _make_core_metadata("upgrade")), @@ -194,7 +186,7 @@ def test_mount_installed_category_groups_does_not_mount_code_when_codebase_not_i """When get_installed_bundles returns [] (or no specfact-codebase), code group must not be registered.""" monkeypatch.setattr( "specfact_cli.registry.module_packages.discover_all_package_metadata", - lambda: [ + lambda **_kwargs: [ (tmp_path / "init", _make_core_metadata("init")), (tmp_path / "module_registry", _make_core_metadata("module_registry", ["module"])), (tmp_path / "upgrade", _make_core_metadata("upgrade")), diff --git a/tests/unit/registry/test_module_bridge_registration.py b/tests/unit/registry/test_module_bridge_registration.py index 95877b900..e4a8a4dbc 100644 --- a/tests/unit/registry/test_module_bridge_registration.py +++ b/tests/unit/registry/test_module_bridge_registration.py @@ -4,6 +4,8 @@ from pathlib import Path +import pytest + from specfact_cli.models.module_package import ModulePackageMetadata, ServiceBridgeMetadata from specfact_cli.registry import CommandRegistry, module_packages from specfact_cli.registry.bridge_registry import BridgeRegistry @@ -28,14 +30,16 @@ def _metadata_with_bridges(*, converter_class: str) -> ModulePackageMetadata: ) -def test_register_module_package_commands_registers_declared_bridges(monkeypatch, tmp_path: Path) -> None: +def test_register_module_package_commands_registers_declared_bridges( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: """Lifecycle registration should load and register manifest service bridges.""" CommandRegistry._clear_for_testing() registry = BridgeRegistry() converter_path = f"{__name__}._TestConverter" packages = [(tmp_path, _metadata_with_bridges(converter_class=converter_path))] - monkeypatch.setattr(module_packages, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(module_packages, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(module_packages, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(module_packages, "read_modules_state", dict) monkeypatch.setattr(module_packages, "_make_package_loader", lambda *_args: object) @@ -46,12 +50,12 @@ def test_register_module_package_commands_registers_declared_bridges(monkeypatch assert registry.get_converter("ado") is not None -def test_invalid_bridge_declaration_is_non_fatal(monkeypatch, tmp_path: Path) -> None: +def test_invalid_bridge_declaration_is_non_fatal(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: """Invalid bridge declarations should be skipped with warnings.""" CommandRegistry._clear_for_testing() registry = BridgeRegistry() packages = [(tmp_path, _metadata_with_bridges(converter_class="invalid.path.MissingConverter"))] - monkeypatch.setattr(module_packages, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(module_packages, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(module_packages, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(module_packages, "read_modules_state", dict) monkeypatch.setattr(module_packages, "_make_package_loader", lambda *_args: object) diff --git a/tests/unit/registry/test_module_lifecycle.py b/tests/unit/registry/test_module_lifecycle.py index c78273251..7918bcfb0 100644 --- a/tests/unit/registry/test_module_lifecycle.py +++ b/tests/unit/registry/test_module_lifecycle.py @@ -1,14 +1,16 @@ from pathlib import Path +import pytest + from specfact_cli.models.module_package import ModulePackageMetadata from specfact_cli.registry import module_lifecycle -def test_apply_module_state_update_persists_disabled_modules(monkeypatch) -> None: +def test_apply_module_state_update_persists_disabled_modules(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr( module_lifecycle, "discover_all_package_metadata", - lambda: [(Path("/tmp/mock-module"), ModulePackageMetadata(name="mock-module", version="0.1.0"))], + lambda **_kwargs: [(Path("/tmp/mock-module"), ModulePackageMetadata(name="mock-module", version="0.1.0"))], ) monkeypatch.setattr( module_lifecycle, diff --git a/tests/unit/scripts/test_dependency_trust_smoke.py b/tests/unit/scripts/test_dependency_trust_smoke.py new file mode 100644 index 000000000..33815917d --- /dev/null +++ b/tests/unit/scripts/test_dependency_trust_smoke.py @@ -0,0 +1,27 @@ +"""An auxiliary lock must not escape the existing executable-wheel prohibition.""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +import pytest + +import scripts.check_dependency_trust_exceptions as checker + + +def test_runtime_smoke_lock_rejects_unofficial_node_wheel(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + source = tmp_path / "requirements.in" + source.write_text("nodejs-wheel-binaries==24.19.0\n") + locked = tmp_path / "locked.txt" + locked.write_text( + "# input-sha256: " + + hashlib.sha256(source.read_bytes()).hexdigest() + + "\nnodejs-wheel-binaries==24.19.0 \\\n --hash=sha256:" + + "a" * 64 + + "\n" + ) + monkeypatch.setattr(checker, "DEFAULT_RUNTIME_SMOKE_INPUT", source) + monkeypatch.setattr(checker, "DEFAULT_RUNTIME_SMOKE_LOCK", locked) + errors = checker.validate_frozen_dependency_policy() + assert "nodejs-wheel-binaries==24.19.0 is prohibited in the frozen lock" in errors diff --git a/tests/unit/scripts/test_reproducible_delivery.py b/tests/unit/scripts/test_reproducible_delivery.py index da59371f7..64c552615 100644 --- a/tests/unit/scripts/test_reproducible_delivery.py +++ b/tests/unit/scripts/test_reproducible_delivery.py @@ -216,8 +216,8 @@ def test_reproducible_delivery_verifier_bounds_uv_commands_and_fails_closed_on_t def test_reproducible_delivery_wheel_build_uses_a_locked_backend() -> None: """The no-isolation wheel proof must use the backend pinned in delivery inputs.""" project = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) - assert project["build-system"]["requires"] == ["hatchling==1.32.0"] - assert "hatchling==1.32.0" in project["project"]["optional-dependencies"]["dev"] + assert project["build-system"]["requires"] == ["hatchling==1.32.4"] + assert "hatchling==1.32.4" in project["project"]["optional-dependencies"]["dev"] assert "twine>=7.0" in project["project"]["optional-dependencies"]["dev"] assert "core-metadata-version" not in project["tool"]["hatch"]["build"]["targets"]["wheel"] @@ -321,3 +321,17 @@ def test_locked_sbom_renderer_uses_pip_inspect_without_generator_dependency(tmp_ "referenceType": "purl", } ] + + +def test_smoke_lock_rejects_unreviewed_input_change(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """A changed smoke seed cannot reuse the old reviewed lock binding.""" + checker = REPO_ROOT / "scripts/check_reproducible_delivery.py" + spec = importlib.util.spec_from_file_location("check_reproducible_delivery", checker) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + altered = tmp_path / "requirements.in" + altered.write_text("lz4==0.0.0\n") + monkeypatch.setattr(module, "RUNTIME_SMOKE_REQUIREMENTS_INPUT", altered, raising=False) + with pytest.raises(ValueError, match="input SHA-256 binding does not match"): + module.verify_runtime_smoke_lock() diff --git a/tests/unit/scripts/test_requirements_fork_context.py b/tests/unit/scripts/test_requirements_fork_context.py new file mode 100644 index 000000000..da134a1bc --- /dev/null +++ b/tests/unit/scripts/test_requirements_fork_context.py @@ -0,0 +1,62 @@ +"""Ordinary forks retain their own protected-base proof without native parent privileges.""" + +from __future__ import annotations + +import copy +import json +import subprocess +import sys +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[3] +SCRIPT_DIR = ROOT / "scripts" +FIXTURE = ROOT / "tests/fixtures/native_stack_context.json" +PROBE = """ +import json,sys +sys.path.insert(0,sys.argv[1]) +from requirements_stack_context import resolve_stack_context +payload=json.load(sys.stdin) +print(json.dumps(resolve_stack_context(payload["event"],lambda path:payload["routes"][path]))) +""" + + +def _fork_payload(failure: str | None) -> dict[str, object]: + fixture = json.loads(FIXTURE.read_text()) + pull = copy.deepcopy(fixture["event"]["pull_request"]) + pull.update(state="open", stack=None) + pull["base"].update(ref="main", sha="a" * 40) + pull["head"].update(ref="dev", repo={"id": 9, "full_name": "outside/specfact-cli"}) + if failure == "native-fork": + pull["stack"] = fixture["event"]["pull_request"]["stack"] + if failure == "foreign-base": + pull["base"]["repo"] = {"id": 9, "full_name": "outside/specfact-cli"} + event = {"repository": fixture["event"]["repository"], "pull_request": copy.deepcopy(pull)} + if failure == "stale-head-repository": + pull["head"]["repo"]["full_name"] = "outside/other" + return {"event": event, "routes": {"/repos/nold-ai/specfact-cli/pulls/727": pull}} + + +@pytest.mark.parametrize("failure", [None, "native-fork", "foreign-base", "stale-head-repository"]) +def test_ordinary_fork_context_preserves_own_proof_boundary(failure: str | None) -> None: + result = subprocess.run( + [sys.executable, "-I", "-S", "-c", PROBE, str(SCRIPT_DIR)], + input=json.dumps(_fork_payload(failure)), + capture_output=True, + text=True, + check=False, + timeout=20, + ) + if failure is not None: + assert result.returncode != 0 + assert "stack-context-invalid" in result.stderr + else: + assert result.returncode == 0, result.stderr + context = json.loads(result.stdout) + assert context["stack"] is None + assert context["proof_pull_request"] == 727 + assert context["proof_base_ref"] == "main" + assert context["proof_head_branch"] == "dev" + assert len(context["members"]) == 1 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 000000000..e256fb05c --- /dev/null +++ b/tests/unit/scripts/test_requirements_late_cycle_workflow.py @@ -0,0 +1,357 @@ +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 {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") + + +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, + } diff --git a/tests/unit/scripts/test_requirements_merge_base.py b/tests/unit/scripts/test_requirements_merge_base.py new file mode 100644 index 000000000..34f5bc820 --- /dev/null +++ b/tests/unit/scripts/test_requirements_merge_base.py @@ -0,0 +1,131 @@ +"""Proof context retains the common ancestor when the protected base advances.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[3] +SCRIPT_DIR = ROOT / "scripts" +FIXTURE = ROOT / "tests/fixtures/native_stack_context.json" +PROBE = """ +import json,sys +sys.path.insert(0,sys.argv[1]) +import requirements_stack_context as context +routes=json.load(sys.stdin) +context._github_json=lambda path:routes[path] +raise SystemExit(context.main(["--event",sys.argv[2],"--output",sys.argv[3]])) +""" + + +def _fixture_git_env() -> dict[str, str]: + env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")} + env.update(GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM="1") + return env + + +def _git(repo: Path, *args: str) -> str: + env = _fixture_git_env() + if args[0] != "init": + actual = subprocess.check_output( + ["git", "-C", str(repo), "rev-parse", "--absolute-git-dir"], env=env, text=True, timeout=20 + ).strip() + if Path(actual).resolve() != repo.resolve() / ".git": + raise RuntimeError("git-fixture-outside-owned-root") + result = subprocess.run( + [ + "git", + "-C", + str(repo), + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "-c", + "commit.gpgsign=false", + *args, + ], + env=env, + capture_output=True, + text=True, + check=True, + timeout=20, + ) + return result.stdout.strip() + + +@pytest.mark.parametrize("native", [False, True]) +def test_proof_cli_uses_root_merge_base_after_protected_tip_advances(tmp_path: Path, native: bool) -> None: + _git(tmp_path, "init", "-b", "dev") + _git(tmp_path, "commit", "--allow-empty", "-m", "common") + common = _git(tmp_path, "rev-parse", "HEAD") + _git(tmp_path, "checkout", "-b", "dependency-root") + _git(tmp_path, "commit", "--allow-empty", "-m", "root") + root = _git(tmp_path, "rev-parse", "HEAD") + _git(tmp_path, "checkout", "-b", "dependency-child") + _git(tmp_path, "commit", "--allow-empty", "-m", "child") + child = _git(tmp_path, "rev-parse", "HEAD") + _git(tmp_path, "checkout", "dev") + _git(tmp_path, "commit", "--allow-empty", "-m", "advanced base") + tip = _git(tmp_path, "rev-parse", "HEAD") + payload = json.loads(FIXTURE.read_text().replace("a" * 40, tip).replace("b" * 40, root).replace("c" * 40, child)) + if not native: + parent_path = "/repos/nold-ai/specfact-cli/pulls?state=open&head=nold-ai%3Adependency-root&per_page=100" + pull = payload["routes"][parent_path][0] + pull["stack"] = None + payload["event"]["pull_request"] = pull + payload["routes"]["/repos/nold-ai/specfact-cli/pulls/744"] = pull + event = tmp_path / "event.json" + event.write_text(json.dumps(payload["event"])) + output = tmp_path / "context.json" + result = subprocess.run( + [sys.executable, "-I", "-S", "-c", PROBE, str(SCRIPT_DIR), str(event), str(output)], + input=json.dumps(payload["routes"]), + env=_fixture_git_env(), + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + timeout=20, + ) + assert result.returncode == 0, result.stderr + context = json.loads(output.read_text()) + assert tip != common + assert context["proof_base_commit"] == common + assert context["proof_head_commit"] == root + assert context["proof_pull_request"] == 744 + + +@pytest.mark.parametrize( + ("variable", "query", "target"), + [ + ("GIT_DIR", "--absolute-git-dir", ".git"), + ("GIT_COMMON_DIR", "--git-common-dir", ".git"), + ("GIT_WORK_TREE", "--show-toplevel", ""), + ], +) +def test_git_fixture_ignores_foreign_repository_selectors( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, variable: str, query: str, target: str +) -> None: + owned, foreign = tmp_path / "owned", tmp_path / "foreign" + for repo in (owned, foreign): + repo.mkdir() + subprocess.run( + ["git", "init", "-q", str(repo)], + env={"PATH": os.environ["PATH"], "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1"}, + check=True, + capture_output=True, + text=True, + timeout=20, + ) + monkeypatch.setenv(variable, str(foreign if variable == "GIT_WORK_TREE" else foreign / ".git")) + observed = Path(_git(owned, "rev-parse", query)) + if not observed.is_absolute(): + observed = owned / observed + assert observed.resolve() == (owned / target).resolve() diff --git a/tests/unit/scripts/test_requirements_planning_stack_context.py b/tests/unit/scripts/test_requirements_planning_stack_context.py new file mode 100644 index 000000000..4801cc447 --- /dev/null +++ b/tests/unit/scripts/test_requirements_planning_stack_context.py @@ -0,0 +1,471 @@ +"""Exercise planning boundaries through the real CLI and immutable Git objects.""" + +from __future__ import annotations + +import copy +import json +import os +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any, cast + +import pytest + + +REPOSITORY = "nold-ai/specfact-cli" +REPO_ROOT = Path(__file__).resolve().parents[3] +ROOT_SOURCE = "openspec/changes/root-security/" +PLANNING_PATH = "openspec/changes/deferred-feature/proposal.md" +CHECK_ROUTE = "/repos/" + REPOSITORY + "/commits/{root}/check-runs?filter=all&per_page=100&page=1" +WORKFLOWS = ( + ("Requirements evidence", "Requirements Evidence", 323253915, "requirements-evidence.yml", 1101, 2101), + ( + "Trusted Requirements Authority", + "Trusted Requirements Authority", + 348163848, + "trusted-requirements-authority.yml", + 1102, + 2102, + ), +) + + +@dataclass +class StackCase: + """Owned synthetic repository and independently supplied GitHub responses.""" + + repo: Path + baseline: str + root: str + child: str + event: dict[str, Any] + routes: dict[str, Any] + + +def _git(repo: Path, *arguments: str) -> str: + """Run Git only inside the caller-isolated fixture repository.""" + return subprocess.run( + ["git", "-C", str(repo), *arguments], + check=True, + capture_output=True, + text=True, + timeout=20, + ).stdout.strip() + + +def _commit(repo: Path, path: str, content: str) -> str: + """Create an unsigned synthetic fixture commit, never a publication commit.""" + target = repo / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(content) + _git(repo, "add", "--", path) + _git(repo, "-c", "commit.gpgsign=false", "commit", "-m", "fixture") + return _git(repo, "rev-parse", "HEAD") + + +def _run_record(root: str, workflow: tuple[str, str, int, str, int, int]) -> dict[str, Any]: + """Supply immutable run identity separately from check metadata.""" + _, run_name, workflow_id, workflow_path, run_id, _ = workflow + repository = {"id": 1085706003, "full_name": REPOSITORY} + return { + "id": run_id, + "run_attempt": 1, + "name": run_name, + "workflow_id": workflow_id, + "path": ".github/workflows/" + workflow_path, + "event": "pull_request", + "head_sha": root, + "head_branch": "dependency-root", + "status": "completed", + "conclusion": "success", + "repository": repository, + "head_repository": repository, + "check_suite_id": run_id + 10000, + "pull_requests": [{"number": 744, "head": {"sha": "f" * 40}}], + } + + +def _check_record(root: str, workflow: tuple[str, str, int, str, int, int]) -> dict[str, Any]: + """Keep check issuer, exact SHA and run/job binding explicit.""" + check_name, _, _, _, run_id, check_id = workflow + return { + "id": check_id, + "name": check_name, + "head_sha": root, + "app": {"id": 15368, "slug": "github-actions"}, + "status": "completed", + "conclusion": "success", + "details_url": f"https://github.com/{REPOSITORY}/actions/runs/{run_id}/job/{check_id}", + "check_suite": {"id": run_id + 10000}, + } + + +def _proof_routes(root: str) -> dict[str, Any]: + """Model GitHub-owned checks and runs without candidate evidence labels.""" + checks = [_check_record(root, workflow) for workflow in WORKFLOWS] + routes = {f"/repos/{REPOSITORY}/actions/runs/{workflow[4]}": _run_record(root, workflow) for workflow in WORKFLOWS} + routes[f"/repos/{REPOSITORY}/commits/{root}"] = { + "sha": root, + "commit": {"verification": {"verified": True}}, + } + routes[CHECK_ROUTE.format(root=root)] = {"total_count": len(checks), "check_runs": checks} + return routes + + +def _case(repo: Path, baseline: str, root: str, child: str) -> StackCase: + """Bind the existing native ancestry fixture to actual commit objects.""" + text = (REPO_ROOT / "tests/fixtures/native_stack_context.json").read_text() + for original, replacement in (("a" * 40, baseline), ("b" * 40, root), ("c" * 40, child)): + text = text.replace(original, replacement) + payload = json.loads(text) + routes = payload["routes"] + routes.update(_proof_routes(root)) + return StackCase(repo, baseline, root, child, payload["event"], routes) + + +@pytest.fixture +def planning_case(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> StackCase: + """Own every Git mutation and ignore inherited worktree/configuration selectors.""" + for name in tuple(os.environ): + if name.startswith("GIT_"): + monkeypatch.delenv(name) + monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull) + monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1") + repo = tmp_path / "owned-repository" + repo.mkdir() + _git(repo, "init", "--initial-branch=fixture") + _git(repo, "config", "user.email", "fixture@example.invalid") + _git(repo, "config", "user.name", "Fixture") + baseline = _commit(repo, ".github/workflows/requirements-evidence.yml", "protected proof workflow\n") + _commit(repo, "src/runtime.py", "baseline\n") + baseline = _git(repo, "rev-parse", "HEAD") + _commit(repo, "src/runtime.py", "security fix\n") + root = _commit(repo, ROOT_SOURCE + "specs/security/spec.md", "security contract\n") + child = _commit(repo, PLANNING_PATH, "deferred planning\n") + monkeypatch.chdir(repo) + return _case(repo, baseline, root, child) + + +def _resolve(case: StackCase, monkeypatch: pytest.MonkeyPatch) -> tuple[int, dict[str, Any]]: + """Execute the public isolated CLI with a caller-owned GitHub response executable.""" + executable_dir = case.repo / "github-fixture-bin" + executable_dir.mkdir(exist_ok=True) + (executable_dir / "api-routes.json").write_text(json.dumps(case.routes)) + github = executable_dir / "gh" + github.write_text( + f"#!{sys.executable}\n" + "import json, sys\nfrom pathlib import Path\n" + "routes = json.loads(Path(sys.argv[0]).with_name('api-routes.json').read_text())\n" + "print(json.dumps(routes[sys.argv[-1]]))\n" + ) + github.chmod(0o755) + event = case.repo / "event.json" + output = case.repo / "context.json" + output.unlink(missing_ok=True) + event.write_text(json.dumps(case.event)) + environment = os.environ.copy() + environment["PATH"] = str(executable_dir) + os.pathsep + environment.get("PATH", "") + result = subprocess.run( + [ + sys.executable, + "-I", + "-S", + str(REPO_ROOT / "scripts/requirements_stack_context.py"), + "--event", + str(event), + "--output", + str(output), + ], + cwd=case.repo, + env=environment, + capture_output=True, + text=True, + timeout=30, + ) + return result.returncode, json.loads(output.read_text()) if output.exists() else {} + + +def _planning_context(case: StackCase, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: + """Require separate planning selection while preserving validator and native ancestry.""" + result, context = _resolve(case, monkeypatch) + assert result == 0 + assert context.get("planning_base_commit") == case.root + assert context["proof_base_commit"] == case.baseline + assert context["proof_head_commit"] == case.root + assert context["head_commit"] == case.child + return context + + +def test_verified_planning_child_preserves_protected_trust_and_maturities( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A signed, independently verified runtime root permits only post-root planned evidence.""" + context = _planning_context(planning_case, monkeypatch) + proof = context["inherited_proof"] + assert proof["head_commit"] == planning_case.root + assert {record["check_id"] for record in proof["checks"]} == {2101, 2102} + assert {record["run_attempt"] for record in proof["checks"]} == {1} + assert [member["pull_request"] for member in context["members"]] == [744, 727] + + +def test_runtime_and_inherited_source_changes_keep_complete_proof( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Eligible planning cannot launder a subsequent executable or inherited-source edit.""" + _planning_context(planning_case, monkeypatch) + paths = [ + "src/runtime.py", + "tests/test_runtime.py", + "scripts/check.py", + "ci/lock.json", + ".github/workflows/requirements-evidence.yml", + "requirements/ci/locked.txt", + "pyproject.toml", + "openspec/config.yaml", + "openspec/specs/security/spec.md", + ROOT_SOURCE + "requirements-evidence.yaml", + ROOT_SOURCE + "proposal.md", + ] + for path in paths: + _git(planning_case.repo, "checkout", "--detach", planning_case.child) + head = _commit(planning_case.repo, path, "changed boundary\n") + changed = _case(planning_case.repo, planning_case.baseline, planning_case.root, head) + result, context = _resolve(changed, monkeypatch) + assert result == 0, path + assert "planning_base_commit" not in context, path + assert context["proof_base_commit"] == planning_case.baseline + + +def test_forged_or_stale_root_checks_fail_before_evidence( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Stack membership does not replace exact root check, run, signature or app identities.""" + route = CHECK_ROUTE.format(root=planning_case.root) + for key, replacement in ( + ("conclusion", "failure"), + ("status", "in_progress"), + ("head_sha", "f" * 40), + ("app", {"id": 99, "slug": "other"}), + ("details_url", "https://example.invalid/actions/runs/1101/job/2101"), + ): + case = copy.deepcopy(planning_case) + case.routes[route]["check_runs"][0][key] = replacement + result, context = _resolve(case, monkeypatch) + assert result != 0, key + assert not context + for key, replacement in ( + ("workflow_id", 99), + ("path", ".github/workflows/other.yml"), + ("head_sha", "f" * 40), + ("head_branch", "foreign"), + ("event", "workflow_dispatch"), + ("repository", {"id": 99}), + ("check_suite_id", 99), + ("run_attempt", 0), + ("pull_requests", [{"number": 999}]), + ): + case = copy.deepcopy(planning_case) + case.routes[f"/repos/{REPOSITORY}/actions/runs/1101"][key] = replacement + assert _resolve(case, monkeypatch)[0] != 0, key + case = copy.deepcopy(planning_case) + case.routes[f"/repos/{REPOSITORY}/commits/{case.root}"]["commit"]["verification"]["verified"] = False + assert _resolve(case, monkeypatch)[0] != 0 + case = copy.deepcopy(planning_case) + case.routes[route]["check_runs"] = [] + case.routes[route]["total_count"] = 0 + assert _resolve(case, monkeypatch)[0] != 0 + + +def _stage_non_regular_change(case: StackCase, mode: str) -> None: + """Stage one real mode, payload or inherited-source mutation.""" + target = case.repo / PLANNING_PATH + if mode == "executable": + target.chmod(0o755) + _git(case.repo, "update-index", "--chmod=+x", PLANNING_PATH) + return + if mode == "symlink": + target.unlink() + target.symlink_to("../../../../src/runtime.py") + _git(case.repo, "add", "--", PLANNING_PATH) + return + if mode == "payload": + payload = case.repo / "openspec/changes/deferred-feature/payload.bin" + payload.write_bytes(b"\x00\x01") + _git(case.repo, "add", "--", str(payload.relative_to(case.repo))) + return + if mode == "source-deletion": + _git(case.repo, "rm", "--", ROOT_SOURCE + "specs/security/spec.md") + return + if mode != "source-move": + raise ValueError(mode) + _git( + case.repo, + "mv", + "--", + ROOT_SOURCE + "specs/security/spec.md", + "openspec/changes/deferred-feature/moved.md", + ) + + +def test_non_regular_planning_paths_keep_complete_proof( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """OpenSpec naming cannot authorize executable, symlink or opaque payload bytes.""" + _planning_context(planning_case, monkeypatch) + for mode in ("executable", "symlink", "payload", "source-deletion", "source-move"): + _git(planning_case.repo, "checkout", "--detach", planning_case.child) + _stage_non_regular_change(planning_case, mode) + _git(planning_case.repo, "-c", "commit.gpgsign=false", "commit", "-m", "fixture boundary") + changed = _case( + planning_case.repo, + planning_case.baseline, + planning_case.root, + _git(planning_case.repo, "rev-parse", "HEAD"), + ) + result, context = _resolve(changed, monkeypatch) + assert result == 0, mode + assert "planning_base_commit" not in context, mode + + +def test_latest_failed_root_check_cannot_reuse_older_success( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A newer failed check must win even if an older successful check remains listed.""" + route = CHECK_ROUTE.format(root=planning_case.root) + latest = copy.deepcopy(planning_case.routes[route]["check_runs"][0]) + latest.update(id=3101, conclusion="failure") + planning_case.routes[route]["check_runs"].append(latest) + planning_case.routes[route]["total_count"] += 1 + assert _resolve(planning_case, monkeypatch)[0] != 0 + + +def test_root_modified_proof_workflow_cannot_authorize_planning( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Successful candidate-modified proof CI is not protected inherited proof.""" + _git(planning_case.repo, "checkout", "--detach", planning_case.root) + root = _commit(planning_case.repo, ".github/workflows/requirements-evidence.yml", "candidate success\n") + head = _commit(planning_case.repo, PLANNING_PATH, "deferred proposal\n") + changed = _case(planning_case.repo, planning_case.baseline, root, head) + result, context = _resolve(changed, monkeypatch) + assert result == 0 + assert "planning_base_commit" not in context + + +def _step_script(job: dict[str, Any], key: str, value: str) -> str: + """Select a named workflow step's script for trust-boundary assertions.""" + for step in job["steps"]: + if step.get(key) == value: + return cast(str, step["run"]) + raise AssertionError(value) + + +def test_all_consumers_separate_planning_selection_from_protected_verifier_trust() -> None: + """All consumers agree without moving the validator trust base.""" + import yaml + + workflow = yaml.safe_load((REPO_ROOT / ".github/workflows/requirements-evidence.yml").read_text()) + selections = { + "requirements-evidence-producer": "Run Requirements evidence gate", + "requirements-evidence": "Reconcile Requirements evidence on fresh runner", + "requirements-evidence-final": "Reconcile final Requirements verdict on fresh runner", + } + for job_name, selection_name in selections.items(): + job = workflow["jobs"][job_name] + authentication = _step_script(job, "id", "stack-context") + selection = _step_script(job, "name", selection_name) + assert "PLANNING_BASE_COMMIT" in authentication, job_name + assert "scripts/requirements_planning_context.py" in authentication, job_name + assert 'policy_ref="origin/dev"' in authentication, job_name + assert "PLANNING_BASE_COMMIT" in selection, job_name + assert '--base-ref "$evidence_base_commit"' in selection, job_name + materialization = _step_script( + workflow["jobs"]["requirements-evidence-final"], "name", "Materialize trusted final Requirements core" + ) + assert 'base_commit="$PROOF_BASE_COMMIT"' in materialization + assert "PLANNING_BASE_COMMIT" not in materialization + assert "FINAL_BASE_COMMIT=%s" in materialization + + +def test_root_proof_checks_on_later_api_pages_are_authenticated( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Complete pagination finds required checks and rejects duplicated or truncated pages.""" + route = CHECK_ROUTE.format(root=planning_case.root) + required = planning_case.routes[route]["check_runs"] + unrelated = [{"id": 4000 + index, "name": "Other check"} for index in range(100)] + planning_case.routes[route] = {"total_count": 102, "check_runs": unrelated} + second_page = route.removesuffix("page=1") + "page=2" + planning_case.routes[second_page] = {"total_count": 102, "check_runs": required} + _planning_context(planning_case, monkeypatch) + duplicate = copy.deepcopy(planning_case) + duplicate.routes[second_page]["check_runs"].append(copy.deepcopy(unrelated[0])) + for page in (route, second_page): + duplicate.routes[page]["total_count"] = 103 + assert {record["name"] for record in duplicate.routes[second_page]["check_runs"]} >= { + "Requirements evidence", + "Trusted Requirements Authority", + } + assert _resolve(duplicate, monkeypatch)[0] != 0 + truncated = copy.deepcopy(planning_case) + truncated.routes[route]["check_runs"] = unrelated[:50] + assert _resolve(truncated, monkeypatch)[0] != 0 + + +def test_direct_protected_root_never_receives_planning_override( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Direct protected-base and promotion heads retain their original proof boundary.""" + parent_route = "/repos/nold-ai/specfact-cli/pulls?state=open&head=nold-ai%3Adependency-root&per_page=100" + root_pull = planning_case.routes[parent_route][0] + planning_case.routes["/repos/nold-ai/specfact-cli/pulls/744"] = root_pull + planning_case.event["pull_request"] = root_pull + result, context = _resolve(planning_case, monkeypatch) + assert result == 0 + assert context["head_commit"] == planning_case.root + assert context["proof_base_commit"] == planning_case.baseline + assert "planning_base_commit" not in context + + +def test_existing_planning_document_type_change_retains_full_proof( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A real Git type-change entry falls back to complete proof rather than parsing as planning.""" + _git(planning_case.repo, "checkout", "--detach", planning_case.baseline) + baseline = _commit(planning_case.repo, PLANNING_PATH, "existing planning\n") + _commit(planning_case.repo, "src/runtime.py", "security fix\n") + root = _commit(planning_case.repo, ROOT_SOURCE + "specs/security/spec.md", "security contract\n") + target = planning_case.repo / PLANNING_PATH + target.unlink() + target.symlink_to("../../../../src/runtime.py") + _git(planning_case.repo, "add", "--", PLANNING_PATH) + _git(planning_case.repo, "-c", "commit.gpgsign=false", "commit", "-m", "fixture type change") + case = _case(planning_case.repo, baseline, root, _git(planning_case.repo, "rev-parse", "HEAD")) + result, context = _resolve(case, monkeypatch) + assert result == 0 + assert "planning_base_commit" not in context + assert context["proof_base_commit"] == baseline + + +def test_non_ascii_paths_retain_complete_proof(planning_case: StackCase, monkeypatch: pytest.MonkeyPatch) -> None: + """A legitimate filename outside the planning allowlist must retain full proof.""" + for path in ("docs/café.md", "openspec/changes/deferred-feature/café.md"): + _git(planning_case.repo, "checkout", "--detach", planning_case.child) + case = copy.deepcopy(planning_case) + case.child = _commit(case.repo, path, "unsupported planning filename\n") + case = _case(case.repo, case.baseline, case.root, case.child) + result, context = _resolve(case, monkeypatch) + assert result == 0 + assert "planning_base_commit" not in context + assert context["proof_base_commit"] == case.baseline diff --git a/tests/unit/scripts/test_requirements_protected_planning_controls.py b/tests/unit/scripts/test_requirements_protected_planning_controls.py new file mode 100644 index 000000000..45c66a263 --- /dev/null +++ b/tests/unit/scripts/test_requirements_protected_planning_controls.py @@ -0,0 +1,69 @@ +"""A stale stack root cannot reuse proof from obsolete protected controls.""" + +from __future__ import annotations + +import pytest + +from tests.unit.scripts.test_requirements_planning_stack_context import ( + StackCase, + _case, + _commit, + _git, + _resolve, + planning_case as planning_case, +) + + +@pytest.mark.parametrize( + "control_path", + ( + ".github/workflows/requirements-evidence.yml", + ".github/workflows/trusted-requirements-authority.yml", + ".github/actions/setup-frozen-python/action.yml", + ), +) +def test_current_protected_controls_block_stale_root_reuse( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, + control_path: str, +) -> None: + """Exact successful old-root checks cannot substitute for current control bytes.""" + _git(planning_case.repo, "checkout", "--detach", planning_case.baseline) + protected = _commit(planning_case.repo, control_path, "updated protected security control\n") + stale = _case(planning_case.repo, protected, planning_case.root, planning_case.child) + result, context = _resolve(stale, monkeypatch) + assert result == 0 + assert context["proof_base_commit"] == planning_case.baseline + assert "planning_base_commit" not in context + assert "inherited_proof" not in context + + +def test_unrelated_protected_update_preserves_authenticated_planning( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A newer protected docs commit does not invalidate identical proof controls.""" + _git(planning_case.repo, "checkout", "--detach", planning_case.baseline) + protected = _commit(planning_case.repo, "docs/protected-note.md", "updated protected documentation\n") + unchanged = _case(planning_case.repo, protected, planning_case.root, planning_case.child) + result, context = _resolve(unchanged, monkeypatch) + assert result == 0 + assert context["proof_base_commit"] == planning_case.baseline + assert context.get("planning_base_commit") == planning_case.root + + +def test_root_modified_authority_workflow_keeps_full_proof( + planning_case: StackCase, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A successful root-authored authority workflow cannot authorize inherited proof.""" + _git(planning_case.repo, "checkout", "--detach", planning_case.root) + root = _commit( + planning_case.repo, ".github/workflows/trusted-requirements-authority.yml", "candidate authority success\n" + ) + child = _commit(planning_case.repo, "openspec/changes/deferred-feature/proposal.md", "deferred proposal\n") + changed = _case(planning_case.repo, planning_case.baseline, root, child) + result, context = _resolve(changed, monkeypatch) + assert result == 0 + assert "planning_base_commit" not in context + assert "inherited_proof" not in context diff --git a/tests/unit/scripts/test_requirements_ref_policy.py b/tests/unit/scripts/test_requirements_ref_policy.py new file mode 100644 index 000000000..b2a60222b --- /dev/null +++ b/tests/unit/scripts/test_requirements_ref_policy.py @@ -0,0 +1,72 @@ +"""Native branch validation agrees with Git without loading candidate dependencies.""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[3] +SCRIPT_DIR = ROOT / "scripts" +REFS = [ + "feature/a+b", + "feature/a@b", + "feature/a(b)", + "feature/a]b", + "feature/a{b", + "feature/a$b", + "feature/é", + "@", + "dependency-root", + "foo/-bar", + "HEAD", + "-bad", + "x/.bar", + "x.lock/a", + "x/@{a", + "a b", + "a..b", + "a//b", + "/bad", + "bad/", + "bad.", + "bad~", + "bad^", + "bad:", + "bad?", + "bad*", + "bad[", + "bad\\", + "bad\n", + "bad\x7f", + "", +] +PROBE = """ +import json,sys +sys.path.insert(0,sys.argv[1]) +from requirements_stack_context import _ref +try: + print(json.dumps(_ref(json.loads(sys.stdin.read())))) +except ValueError: + raise SystemExit(1) +""" + + +@pytest.mark.parametrize("ref", REFS) +def test_native_ref_validation_matches_literal_git_branch_rules(ref: str) -> None: + git = subprocess.run(["git", "check-ref-format", "--branch", ref], capture_output=True, check=False) + result = subprocess.run( + [sys.executable, "-I", "-S", "-c", PROBE, str(SCRIPT_DIR)], + input=json.dumps(ref), + capture_output=True, + text=True, + timeout=20, + check=False, + ) + assert (result.returncode == 0) is (git.returncode == 0), ref + if result.returncode == 0: + assert json.loads(result.stdout) == ref diff --git a/tests/unit/scripts/test_requirements_stack_context.py b/tests/unit/scripts/test_requirements_stack_context.py new file mode 100644 index 000000000..a6d3b18bf --- /dev/null +++ b/tests/unit/scripts/test_requirements_stack_context.py @@ -0,0 +1,92 @@ +"""Reject mutable stack relationships before choosing retained proof.""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[3] +FIXTURE = REPO_ROOT / "tests/fixtures/native_stack_context.json" +CHILD_PROBE = """ +import json, sys +sys.path.insert(0, sys.argv[1]) +from scripts.requirements_stack_context import resolve_stack_context +payload = json.load(sys.stdin) +def get_json(path): + return payload["routes"][path] +print(json.dumps(resolve_stack_context(payload["event"], get_json), sort_keys=True)) +""" + + +def _probe(payload: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, "-I", "-S", "-c", CHILD_PROBE, str(REPO_ROOT)], + input=payload, + capture_output=True, + text=True, + timeout=20, + check=False, + ) + + +def test_native_stack_uses_original_root_proof_boundary() -> None: + """A real native parent edge must retain its root branch and immutable base.""" + result = _probe(FIXTURE.read_text()) + assert result.returncode == 0, result.stderr + context = json.loads(result.stdout) + assert context["proof_base_commit"] == "a" * 40 + assert context["proof_head_branch"] == "dependency-root" + assert context["proof_pull_request"] == 744 + assert context["pull_request"] == 727 + assert context["head_commit"] == "c" * 40 + assert [member["pull_request"] for member in context["members"]] == [744, 727] + + +@pytest.mark.parametrize("field", ["id", "number", "size", "position"]) +def test_changed_parent_stack_identity_is_rejected(field: str) -> None: + payload = json.loads(FIXTURE.read_text()) + parent_route = "/repos/nold-ai/specfact-cli/pulls?state=open&head=nold-ai%3Adependency-root&per_page=100" + payload["routes"][parent_route][0]["stack"][field] += 1 + result = _probe(json.dumps(payload)) + assert result.returncode != 0 + assert "stack-context-invalid" in result.stderr + + +def test_unrelated_commit_chain_is_rejected() -> None: + payload = json.loads(FIXTURE.read_text()) + compare_route = "/repos/nold-ai/specfact-cli/compare/" + "b" * 40 + "..." + "c" * 40 + payload["routes"][compare_route]["merge_base_commit"]["sha"] = "d" * 40 + result = _probe(json.dumps(payload)) + assert result.returncode != 0 + assert "stack-context-invalid" in result.stderr + + +def test_ambiguous_parent_is_rejected() -> None: + payload = json.loads(FIXTURE.read_text()) + parent_route = "/repos/nold-ai/specfact-cli/pulls?state=open&head=nold-ai%3Adependency-root&per_page=100" + payload["routes"][parent_route].append(payload["routes"][parent_route][0]) + result = _probe(json.dumps(payload)) + assert result.returncode != 0 + assert "stack-context-invalid" in result.stderr + + +def test_forked_parent_is_rejected() -> None: + payload = json.loads(FIXTURE.read_text()) + parent_route = "/repos/nold-ai/specfact-cli/pulls?state=open&head=nold-ai%3Adependency-root&per_page=100" + payload["routes"][parent_route][0]["head"]["repo"] = {"id": 9, "full_name": "outside/specfact-cli"} + result = _probe(json.dumps(payload)) + assert result.returncode != 0 + assert "stack-context-invalid" in result.stderr + + +def test_stale_event_head_is_rejected() -> None: + payload = json.loads(FIXTURE.read_text()) + payload["routes"]["/repos/nold-ai/specfact-cli/pulls/727"]["head"]["sha"] = "d" * 40 + result = _probe(json.dumps(payload)) + assert result.returncode != 0 + assert "stack-context-invalid" in result.stderr diff --git a/tests/unit/scripts/test_security_audit_gate.py b/tests/unit/scripts/test_security_audit_gate.py index bf7906ded..19c08cdb4 100644 --- a/tests/unit/scripts/test_security_audit_gate.py +++ b/tests/unit/scripts/test_security_audit_gate.py @@ -6,6 +6,8 @@ import json from datetime import date, timedelta from pathlib import Path +from types import ModuleType +from typing import NamedTuple from unittest.mock import MagicMock, patch import pytest @@ -171,3 +173,75 @@ def test_main_allows_only_a_matching_unexpired_reviewed_exception(gate_mod, tmp_ with patch.object(gate_mod.subprocess, "run", return_value=proc): assert gate_mod.main(["--exceptions", str(exceptions_path)]) == 0 assert "WAIVED" in capsys.readouterr().out + + +class McpExceptionScenario(NamedTuple): + version: str + advisory_ids: list[str] + skip_reason: str | None + today: str + expected: int + message: str + + +@pytest.mark.parametrize( + "scenario", + [ + pytest.param( + McpExceptionScenario("1.29.0", ["CVE-2026-59951"], None, "2026-10-21", 0, "WAIVED"), id="exact-cve" + ), + pytest.param( + McpExceptionScenario("1.29.0", ["GHSA-84m7-p3x7-pcfv"], None, "2026-10-21", 0, "WAIVED"), id="exact-ghsa" + ), + pytest.param( + McpExceptionScenario("1.29.0", ["CVE-2026-59951"], None, "2026-11-04", 0, "WAIVED"), id="expiry-day" + ), + pytest.param( + McpExceptionScenario("1.30.0", ["CVE-2026-59951"], None, "2026-10-21", 1, "FAIL:"), id="other-version" + ), + pytest.param( + McpExceptionScenario("1.29.0", ["UNRELATED-1"], None, "2026-10-21", 1, "FAIL:"), id="unrelated-advisory" + ), + pytest.param( + McpExceptionScenario("1.29.0", ["CVE-2026-59951", "UNRELATED-1"], None, "2026-10-21", 1, "FAIL:"), + id="mixed-advisories", + ), + pytest.param( + McpExceptionScenario( + "1.29.0", ["CVE-2026-59951"], "could not audit", "2026-10-21", 1, "skipped dependency" + ), + id="skipped", + ), + pytest.param( + McpExceptionScenario("1.29.0", ["CVE-2026-59951"], None, "2026-11-05", 1, "not currently valid"), + id="expired", + ), + pytest.param( + McpExceptionScenario("1.29.0", ["CVE-2026-59951"], None, "2026-10-20", 1, "not currently valid"), + id="future-review", + ), + ], +) +def test_committed_mcp_exception_is_exact_and_time_bounded( + gate_mod: ModuleType, + capsys: pytest.CaptureFixture[str], + scenario: McpExceptionScenario, +) -> None: + class AuditDate(date): + @classmethod + def today(cls) -> date: + return date.fromisoformat(scenario.today) + + dependency: dict[str, object] = { + "name": "mcp", + "version": scenario.version, + "vulns": [{"id": value} for value in scenario.advisory_ids], + } + if scenario.skip_reason: + dependency["skip_reason"] = scenario.skip_reason + proc = MagicMock(stdout=json.dumps({"dependencies": [dependency]}), stderr="", returncode=1) + exceptions = Path(__file__).resolve().parents[3] / "ci/vulnerability-audit-exceptions.json" + with patch.object(gate_mod, "date", AuditDate), patch.object(gate_mod.subprocess, "run", return_value=proc): + assert gate_mod.main(["--exceptions", str(exceptions)]) == scenario.expected + captured = capsys.readouterr() + assert scenario.message in captured.out + captured.err diff --git a/tests/unit/security/test_certificate_trust_store.py b/tests/unit/security/test_certificate_trust_store.py new file mode 100644 index 000000000..b48d2b04c --- /dev/null +++ b/tests/unit/security/test_certificate_trust_store.py @@ -0,0 +1,13 @@ +import hashlib +import ssl +from importlib.resources import as_file, files + + +def test_certifi_bundle_excludes_compromised_emsign_root() -> None: + with as_file(files("certifi").joinpath("cacert.pem")) as bundle: + context = ssl.create_default_context(cafile=str(bundle)) + fingerprints = {hashlib.sha256(cert).hexdigest() for cert in context.get_ca_certs(binary_form=True)} + compromised = {"86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b"} + + assert fingerprints, "certifi must retain a nonempty CA trust store" + assert fingerprints & compromised == set() diff --git a/tests/unit/security/test_executable_encoding_matrix.py b/tests/unit/security/test_executable_encoding_matrix.py new file mode 100644 index 000000000..e7a2ed9a5 --- /dev/null +++ b/tests/unit/security/test_executable_encoding_matrix.py @@ -0,0 +1,89 @@ +import json +from collections.abc import Callable +from pathlib import Path + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from specfact_cli.registry import CommandRegistry, module_packages as packages +from tests.unit.security.test_executable_identity_oracles import ( + _assert_genuine_signature, + _canonical_compatibility, + _register, +) +from tests.unit.security.test_module_command_squatting import _reset_registration +from tests.unit.security.test_package_candidate_eligibility import ( + _signed_copy, + selection_key as selection_key, +) + + +ENCODINGS: dict[str, tuple[bytes, int]] = { + "owner-execute": (b"harmless permission probe", 0o744), + "group-execute": (b"harmless permission probe", 0o654), + "other-execute": (b"harmless permission probe", 0o645), + "thin-big-32": (b"\xfe\xed\xfa\xce" + bytes(20), 0o644), + "thin-little-32": (b"\xce\xfa\xed\xfe" + bytes(20), 0o644), + "thin-big-64": (b"\xfe\xed\xfa\xcf" + bytes(20), 0o644), + "fat-little-32": (b"\xbe\xba\xfe\xca" + bytes(20), 0o644), + "fat-big-64": (b"\xca\xfe\xba\xbf" + bytes(20), 0o644), + "fat-little-64": (b"\xbf\xba\xfe\xca" + bytes(20), 0o644), +} +ENCODING_GROUPS = { + "execute-bits": ("owner-execute", "group-execute", "other-execute"), + "thin-32": ("thin-big-32", "thin-little-32"), + "big-64": ("thin-big-64", "fat-big-64"), + "fat-little": ("fat-little-32", "fat-little-64"), +} + + +def _observe_encoding( + monkeypatch: pytest.MonkeyPatch, + root: Path, + key: Ed25519PrivateKey, + case: tuple[str, bool], +) -> tuple[str, bool, bool]: + encoding, grouped = case + label = f"{encoding}-{'grouped' if grouped else 'legacy'}" + workspace = root / label + try: + with monkeypatch.context() as probe_patch: + _canonical_compatibility(probe_patch, workspace / "canonical", key, grouped) + candidate = workspace / "candidate" + path = _signed_copy(candidate, key, None) + (path / "logs").mkdir() + target = path / "logs/helper" + data, mode = ENCODINGS[encoding] + target.write_bytes(data) + target.chmod(mode) + _assert_genuine_signature(path) + _register(probe_patch, candidate, grouped) + return ( + label, + CommandRegistry.get_module_metadata("code") is None, + (path / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS, + ) + finally: + _reset_registration() + + +@pytest.mark.parametrize("encoding_group", ENCODING_GROUPS) +def test_each_executable_encoding_is_denied( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + selection_key: Ed25519PrivateKey, + encoding_group: str, + record_testsuite_property: Callable[[str, object], None], +) -> None: + encodings = ENCODING_GROUPS[encoding_group] + observations = [ + _observe_encoding(monkeypatch, tmp_path, selection_key, (encoding, grouped)) + for encoding in encodings + for grouped in (True, False) + ] + record_testsuite_property(f"executable_encoding_observations-{encoding_group}", json.dumps(observations)) + assert {encoding for group in ENCODING_GROUPS.values() for encoding in group} == set(ENCODINGS) + assert len(observations) == len(encodings) * 2 + assert all(root_denied and source_denied for _, root_denied, source_denied in observations), ( + f"Omitted executable encoding obtained authority: {observations}" + ) diff --git a/tests/unit/security/test_executable_identity_oracles.py b/tests/unit/security/test_executable_identity_oracles.py new file mode 100644 index 000000000..05b6c0c39 --- /dev/null +++ b/tests/unit/security/test_executable_identity_oracles.py @@ -0,0 +1,126 @@ +"""Real signed metadata and suffix-independent executable inventory admission controls.""" + +from pathlib import Path + +import pytest +import yaml +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from specfact_cli.models.module_package import ModulePackageMetadata +from specfact_cli.registry import CommandRegistry, module_installer, module_packages as packages +from specfact_cli.registry.module_grouping import ModuleManifestError +from tests.unit.security.test_module_command_squatting import _reset_registration +from tests.unit.security.test_package_candidate_eligibility import ( + _assert_active, + _signed_copy, + selection_key as selection_key, +) +from tests.unit.security.test_project_bundle_authority import _isolate_discovery_roots + + +EXECUTABLE_PROBES = { + "mode": b"harmless executable-mode probe", + "shebang": b"#!/bin/sh\nexit 0\n", + "elf": b"\x7fELF" + bytes(20), + "pe": b"MZ" + bytes(20), + "mach-o": b"\xcf\xfa\xed\xfe" + bytes(20), + "fat-mach-o": b"\xca\xfe\xba\xbe" + bytes(20), +} + + +def _assert_genuine_signature(path: Path) -> None: + raw = yaml.safe_load((path / "module-package.yaml").read_text(encoding="utf-8")) + metadata = ModulePackageMetadata.model_validate(raw) + public = module_installer._bundled_public_key_path().read_text(encoding="utf-8") + assert module_installer.verify_module_artifact( + path, metadata, public_key_pem=public, allow_unsigned=False, require_signature=True + ), "The real signature must remain valid before public admission is tested" + + +def _register(monkeypatch: pytest.MonkeyPatch, workspace: Path, grouped: bool) -> None: + _isolate_discovery_roots(monkeypatch, workspace, workspace / "missing-user") + packages.register_module_package_commands(allow_unsigned=False, category_grouping_enabled=grouped) + + +def _canonical_compatibility( + monkeypatch: pytest.MonkeyPatch, workspace: Path, key: Ed25519PrivateKey, grouped: bool +) -> None: + path = _signed_copy(workspace, key, None) + (path / "logs").mkdir() + (path / "logs/note").write_text("ordinary excluded diagnostic", encoding="utf-8") + _assert_genuine_signature(path) + _register(monkeypatch, workspace, grouped) + assert CommandRegistry.get_module_metadata("code") is not None + _assert_active([path], []) + _reset_registration() + + +@pytest.mark.parametrize("grouped", [True, False], ids=["grouped", "legacy"]) +@pytest.mark.parametrize("probe", EXECUTABLE_PROBES) +def test_excluded_extensionless_executable_is_denied( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey, grouped: bool, probe: str +) -> None: + """Inspect harmless mode/header probes only; never execute an unsigned candidate artifact.""" + _canonical_compatibility(monkeypatch, tmp_path / "canonical", selection_key, grouped) + workspace = tmp_path / "candidate" + path = _signed_copy(workspace, selection_key, None) + (path / "logs").mkdir() + target = path / "logs/helper" + target.write_bytes(EXECUTABLE_PROBES[probe]) + target.chmod(0o755 if probe == "mode" else 0o644) + _assert_genuine_signature(path) + _register(monkeypatch, workspace, grouped) + assert CommandRegistry.get_module_metadata("code") is None, "Omitted executable obtained category authority" + _assert_active([], [path]) + + +def _deny_noncanonical_identity( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + key: Ed25519PrivateKey, + grouped: bool, + identity: tuple[str, str], +) -> None: + """Keep genuine crypto valid while existing grouping and root admission enforce canonical identity.""" + field, value = identity + _canonical_compatibility(monkeypatch, tmp_path / "canonical", key, grouped) + workspace = tmp_path / "candidate" + path = _signed_copy(workspace, key, None, **{field: value}) + _assert_genuine_signature(path) + try: + _register(monkeypatch, workspace, grouped) + except ModuleManifestError as error: + assert field in {"category", "bundle_group_command"} and str(error), ( + "Existing grouping validation must report a concrete category/group refusal" + ) + _assert_active([], [path]) + return + assert CommandRegistry.get_module_metadata("code") is None + _assert_active([], [path]) + + +@pytest.mark.parametrize("grouped", [True, False], ids=["grouped", "legacy"]) +@pytest.mark.parametrize( + "identity", + [("tier", "community"), ("category", "spec"), ("bundle_group_command", "plan")], + ids=["tier-community", "category-spec", "bundle_group_command-plan"], +) +def test_valid_signature_noncanonical_identity_is_denied( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + selection_key: Ed25519PrivateKey, + grouped: bool, + identity: tuple[str, str], +) -> None: + """Independently verify existing category/group refusal and noncanonical tier denial.""" + _deny_noncanonical_identity(monkeypatch, tmp_path, selection_key, grouped, identity) + + +@pytest.mark.parametrize("grouped", [True, False], ids=["grouped", "legacy"]) +def test_all_signed_identity_fields_retain_canonical_authority( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey, grouped: bool +) -> None: + """Keep existing signed category/group rejection before exercising the genuine tier regression.""" + for identity in [("category", "spec"), ("bundle_group_command", "plan"), ("tier", "community")]: + _deny_noncanonical_identity(monkeypatch, tmp_path / identity[0], selection_key, grouped, identity) + _reset_registration() diff --git a/tests/unit/security/test_frozen_dependency_artifacts.py b/tests/unit/security/test_frozen_dependency_artifacts.py new file mode 100644 index 000000000..c6b06bf72 --- /dev/null +++ b/tests/unit/security/test_frozen_dependency_artifacts.py @@ -0,0 +1,223 @@ +from __future__ import annotations + +import base64 +import csv +import hashlib +import io +import re +import ssl +import time +import tomllib +import urllib.request +from dataclasses import dataclass +from email.message import Message +from email.parser import BytesParser +from pathlib import Path, PurePosixPath +from urllib.parse import urlsplit +from zipfile import ZipFile, ZipInfo + +import pytest + + +_MAX_DOWNLOAD = 150 * 1024 * 1024 +_MAX_EXPANDED = 512 * 1024 * 1024 +_BAD_CA = "86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b" + + +@dataclass(frozen=True) +class WheelContents: + members: dict[str, bytes] + modes: dict[str, int] + metadata: Message + dist_info: str + + +class NoRedirects(urllib.request.HTTPRedirectHandler): + def redirect_request(self, *args: object, **kwargs: object) -> None: + raise ValueError("Pinned artifact downloads must not redirect") + + +def _download(url: str, digest: str, destination: Path) -> Path: + parsed = urlsplit(url) + assert parsed.scheme == "https" and parsed.netloc == "files.pythonhosted.org", "Unapproved artifact origin" + assert not parsed.query and not parsed.fragment, "Artifact URLs cannot contain queries or fragments" + assert re.fullmatch(r"[a-f0-9]{64}", digest), "A SHA256 pin is required" + assert parsed.path.endswith(".whl"), "Expected a wheel URL" + opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirects()) + size = 0 + checksum = hashlib.sha256() + started = time.monotonic() + with opener.open(url, timeout=30) as response, destination.open("xb") as output: + assert response.status == 200, "Artifact download did not succeed" + while chunk := response.read(1024 * 1024): + size += len(chunk) + assert size <= _MAX_DOWNLOAD, "Artifact exceeds the 150 MiB download limit" + assert time.monotonic() - started < 30, "Artifact download exceeded 30 seconds" + checksum.update(chunk) + output.write(chunk) + assert checksum.hexdigest() == digest, "Downloaded artifact SHA256 differs from its pin" + return destination + + +def _regular_member(entry: ZipInfo) -> None: + parts = PurePosixPath(entry.filename).parts + assert parts and not entry.filename.startswith("/"), "Absolute wheel member" + assert ".." not in parts and "\\" not in entry.filename, "Unsafe wheel member" + assert not entry.is_dir(), "Explicit directory members are not supported" + assert PurePosixPath(entry.filename).as_posix() == entry.filename, "Noncanonical wheel member" + assert (entry.external_attr >> 16) & 0o170000 in (0, 0o100000), "Nonregular wheel member" + + +def _member_inventory(entries: list[ZipInfo], expected_root: str) -> None: + names = [entry.filename for entry in entries] + assert len(names) == len(set(names)), "Duplicate wheel member" + assert len(entries) <= 10000, "Wheel member limit exceeded" + assert sum(entry.file_size for entry in entries) <= _MAX_EXPANDED, "Expanded wheel size limit exceeded" + for entry in entries: + _regular_member(entry) + roots = {name.split("/", 1)[0] for name in names if name.split("/", 1)[0].endswith(".dist-info")} + assert roots == {expected_root}, "Unexpected wheel distribution root" + + +def _record_entry(members: dict[str, bytes], record_path: str, row: list[str]) -> None: + member, digest, size = row + if member == record_path: + assert digest == size == "", "RECORD must leave its own hash and size empty" + return + expected = base64.urlsafe_b64encode(hashlib.sha256(members[member]).digest()).rstrip(b"=").decode("ascii") + assert digest == f"sha256={expected}", f"RECORD hash differs for {member}" + assert size == str(len(members[member])), f"RECORD size differs for {member}" + + +def _record_inventory(members: dict[str, bytes], record_path: str) -> None: + rows = list(csv.reader(io.StringIO(members[record_path].decode("utf-8")))) + assert all(len(row) == 3 for row in rows), "Malformed wheel RECORD" + assert len(rows) == len({row[0] for row in rows}), "Duplicate wheel RECORD member" + assert {row[0] for row in rows} == set(members), "RECORD does not cover exactly the wheel members" + for row in rows: + _record_entry(members, record_path, row) + + +def _wheel(path: Path, name: str, version: str) -> WheelContents: + expected_root = f"{name.replace('-', '_')}-{version}.dist-info" + with ZipFile(path) as archive: + entries = archive.infolist() + _member_inventory(entries, expected_root) + members = {entry.filename: archive.read(entry) for entry in entries} + modes = {entry.filename: entry.external_attr >> 16 for entry in entries} + metadata = BytesParser().parsebytes(members[f"{expected_root}/METADATA"]) + assert metadata.get_all("Name") == [name], "Wheel metadata name differs from selected distribution" + assert metadata.get_all("Version") == [version], "Wheel metadata version differs from selected distribution" + _record_inventory(members, f"{expected_root}/RECORD") + assert f"{expected_root}/WHEEL" in members, "Wheel metadata is missing" + return WheelContents(members, modes, metadata, expected_root) + + +def _certifi_selection(root: Path) -> tuple[str, str, str]: + lock = tomllib.loads((root / "uv.lock").read_text(encoding="utf-8")) + packages = [package for package in lock["package"] if package["name"] == "certifi"] + assert len(packages) == 1, "The frozen graph must select exactly one certifi distribution" + package = packages[0] + wheels = package["wheels"] + assert len(wheels) == 1, "Expected exactly one certifi wheel in the frozen graph" + version, url, digest = package["version"], wheels[0]["url"], wheels[0]["hash"] + assert digest.startswith("sha256:"), "Frozen certifi wheel must use SHA256" + _export_certifi_hash(root, version, digest) + return version, url, digest.removeprefix("sha256:") + + +def _export_certifi_hash(root: Path, version: str, digest: str) -> None: + export = (root / "requirements/ci/locked.txt").read_text(encoding="utf-8").replace("\\\n", " ") + lines = [line for line in export.splitlines() if line.startswith("certifi==")] + assert len(lines) == 1, "The exported graph must select exactly one certifi distribution" + assert re.match(rf"certifi=={re.escape(version)}(?:\s|$)", lines[0]), "Lock and export certifi versions differ" + assert f"--hash={digest}" in lines[0].split(), "Export omits the selected certifi wheel hash" + + +def test_frozen_certifi_bundle_excludes_compromised_root(tmp_path: Path, pytestconfig: pytest.Config) -> None: + version, url, digest = _certifi_selection(pytestconfig.rootpath) + path = _download(url, digest, tmp_path / "certifi.whl") + wheel = _wheel(path, "certifi", version) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.load_verify_locations(cadata=wheel.members["certifi/cacert.pem"].decode("ascii")) + fingerprints = {hashlib.sha256(cert).hexdigest() for cert in context.get_ca_certs(binary_form=True)} + assert fingerprints, "Frozen certifi artifact must provide a nonempty CA trust store" + assert _BAD_CA not in fingerprints, "Frozen certifi artifact retains the compromised emSign root" + + +def test_artifact_download_rejects_wrong_sha256(tmp_path: Path, pytestconfig: pytest.Config) -> None: + _, url, _ = _certifi_selection(pytestconfig.rootpath) + with pytest.raises(AssertionError, match="Downloaded artifact SHA256 differs from its pin"): + _download(url, "0" * 64, tmp_path / "wrong-hash.whl") + + +def _malformed_members(fault: str) -> dict[str, bytes]: + members = { + "fixture-1.0.dist-info/METADATA": b"Metadata-Version: 2.1\nName: fixture\nVersion: 1.0\n", + "fixture-1.0.dist-info/WHEEL": b"Wheel-Version: 1.0\nTag: py3-none-any\n", + "fixture/data.txt": b"original data\n", + } + record_name = "fixture-1.0.dist-info/RECORD" + rows: list[list[str]] = [] + for name, payload in members.items(): + digest = base64.urlsafe_b64encode(hashlib.sha256(payload).digest()).rstrip(b"=").decode("ascii") + rows.append([name, f"sha256={digest}", str(len(payload))]) + rows.append([record_name, "", ""]) + if fault == "duplicate_record": + rows.append(rows[0]) + record = io.StringIO(newline="") + csv.writer(record).writerows(rows) + members[record_name] = record.getvalue().encode("utf-8") + mutations = { + "traversal": {"../outside.txt": b"untrusted"}, + "extra_member": {"fixture/unrecorded.txt": b"untrusted"}, + "corrupt_payload": {"fixture/data.txt": b"changed data\n"}, + "wrong_identity": {"fixture-1.0.dist-info/METADATA": b"Metadata-Version: 2.1\nName: other\nVersion: 1.0\n"}, + } + members.update(mutations.get(fault, {})) + return members + + +def _write_fixture(path: Path, members: dict[str, bytes]) -> None: + with ZipFile(path, "w") as archive: + for name, payload in members.items(): + archive.writestr(name, payload) + + +def _duplicate_fixture_member(path: Path) -> None: + with ZipFile(path, "a") as archive, pytest.warns(UserWarning, match="Duplicate name"): + archive.writestr("fixture/data.txt", b"duplicate") + + +@pytest.mark.parametrize( + ("fault", "message"), + [ + ("duplicate_member", "Duplicate wheel member"), + ("duplicate_record", "Duplicate wheel RECORD member"), + ("traversal", "Unsafe wheel member"), + ("extra_member", "RECORD does not cover"), + ("corrupt_payload", "RECORD hash differs"), + ("wrong_identity", "metadata name differs"), + ], +) +def test_wheel_reader_rejects_malformed_artifacts(tmp_path: Path, fault: str, message: str) -> None: + path = tmp_path / "fixture.whl" + _write_fixture(path, _malformed_members(fault)) + if fault == "duplicate_member": + _duplicate_fixture_member(path) + with pytest.raises(AssertionError, match=message): + _wheel(path, "fixture", "1.0") + + +@pytest.mark.parametrize( + "url", + [ + "http://files.pythonhosted.org/data.whl", + "https://example.org/data.whl", + "https://user:password@files.pythonhosted.org/data.whl", + "https://files.pythonhosted.org/data.whl?token=credential", + ], +) +def test_artifact_download_rejects_unapproved_urls(tmp_path: Path, url: str) -> None: + with pytest.raises(AssertionError, match=r"origin|queries"): + _download(url, "a" * 64, tmp_path / "artifact.whl") diff --git a/tests/unit/security/test_frozen_gitpython_compatibility.py b/tests/unit/security/test_frozen_gitpython_compatibility.py new file mode 100644 index 000000000..19635578a --- /dev/null +++ b/tests/unit/security/test_frozen_gitpython_compatibility.py @@ -0,0 +1,84 @@ +"""Submodule containment exercised through the hash-pinned delivery wheel.""" + +import json +import re +import shutil +import subprocess +import sys +from importlib.metadata import distribution +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[3] +FROZEN_WHEEL_INSTALL_FLAGS = ( + "--no-config", + "pip", + "install", + "--only-binary=:all:", + "--no-deps", + "--require-hashes", + "--index-url", + "https://pypi.org/simple", +) +SUBMODULE_PROBE = """ +import json, sys +from pathlib import Path +from unittest.mock import patch +sys.path.extend(sys.argv[1:-1]) +import git +from git.objects.submodule.base import Submodule +workspace = Path(sys.argv[-1]) +parent = git.Repo.init(workspace / "parent") +submodule = Submodule(parent, Submodule.NULL_BIN_SHA, name="module", + path="../outside", url="unused") +rejected = False +with patch.object(Submodule, "_clone_repo", side_effect=AssertionError("clone boundary reached")) as clone: + try: + submodule.update(init=True) + except ValueError: + rejected = True + clone_calls = clone.call_count +print(json.dumps({"version": git.__version__, "rejected": rejected, + "clone_calls": clone_calls, "outside_exists": (workspace / "outside").exists()})) +""" + + +def _frozen_gitpython_requirement(tmp_path: Path) -> tuple[Path, str]: + locked = (REPO_ROOT / "requirements/ci/locked.txt").read_text(encoding="utf-8") + entries = re.findall(r"(?m)^gitpython==([^\s]+) \\\n((?:[ \t]+--hash=sha256:[0-9a-f]{64}(?: \\)?\n)+)", locked) + assert len(entries) == 1, "Expected one hash-pinned GitPython delivery requirement" + version, hashes = entries[0] + requirement = tmp_path / "gitpython.txt" + requirement.write_text(f"gitpython=={version} \\\n{hashes}", encoding="utf-8") + return requirement, version + + +def test_submodule_update_rejects_outside_checkout_before_clone(tmp_path: Path) -> None: + """An outside checkout never reaches cloning or creates an outside directory.""" + requirement, version = _frozen_gitpython_requirement(tmp_path) + uv = shutil.which("uv") + assert uv is not None, "The frozen delivery probe requires uv" + target = tmp_path / "delivery-package" + install_command = [uv, *FROZEN_WHEEL_INSTALL_FLAGS, "--target", str(target), "-r", str(requirement)] + subprocess.run( + install_command, + check=True, + capture_output=True, + text=True, + timeout=120, + ) + dependency_roots = {str(distribution(package).locate_file("")) for package in ("gitdb", "smmap")} + probe_arguments = [str(target), *sorted(dependency_roots), str(tmp_path)] + observed = subprocess.run( + [sys.executable, "-I", "-S", "-c", SUBMODULE_PROBE, *probe_arguments], + check=False, + capture_output=True, + text=True, + timeout=10, + ) + assert observed.returncode == 0, observed.stderr + result = json.loads(observed.stdout) + assert result["version"] == version + assert result["rejected"] is True + assert result["clone_calls"] == 0 + assert result["outside_exists"] is False diff --git a/tests/unit/security/test_frozen_jwt_compatibility.py b/tests/unit/security/test_frozen_jwt_compatibility.py new file mode 100644 index 000000000..7558dd56c --- /dev/null +++ b/tests/unit/security/test_frozen_jwt_compatibility.py @@ -0,0 +1,92 @@ +"""Regression boundaries exercised by the focused frozen dependency repair.""" + +import json +import re +import shutil +import subprocess +import sys +from pathlib import Path + +import jwt +from jwt.types import Options + + +TEST_KEY = "local-regression-key-with-at-least-thirty-two-bytes" + + +REPO_ROOT = Path(__file__).resolve().parents[3] +JWT_PROBE = """ +import json, sys +sys.path.insert(0, sys.argv[1]) +import jwt +key = "local-regression-key-with-at-least-thirty-two-bytes" +token = jwt.encode({"sub": "fixture", "exp": 1}, key, algorithm="HS256") +options = {"verify_signature": False} +subject = jwt.decode(token, options=options)["sub"] +after_inspection = dict(options) +options["verify_signature"] = True +expired = False +try: + jwt.decode(token, key, algorithms=["HS256"], options=options) +except jwt.ExpiredSignatureError: + expired = True +print(json.dumps({"version": jwt.__version__, "subject": subject, + "after_inspection": after_inspection, "expired": expired})) +""" + + +def test_inspection_preserves_options_and_expiry_on_reuse(tmp_path: Path) -> None: + """Exercise the hash-pinned delivery package independently of verifier dependencies.""" + locked = (REPO_ROOT / "requirements/ci/locked.txt").read_text(encoding="utf-8") + entries = re.findall(r"(?m)^pyjwt==([^\s]+) \\\n((?:[ \t]+--hash=sha256:[0-9a-f]{64}(?: \\)?\n)+)", locked) + assert len(entries) == 1, "Expected one hash-pinned PyJWT delivery requirement" + version, hashes = entries[0] + requirement = tmp_path / "pyjwt.txt" + requirement.write_text(f"pyjwt=={version} \\\n{hashes}", encoding="utf-8") + uv = shutil.which("uv") + assert uv is not None, "The frozen delivery probe requires uv" + target = tmp_path / "delivery-package" + subprocess.run( + [ + uv, + "--no-config", + "pip", + "install", + "--target", + str(target), + "--only-binary=:all:", + "--no-deps", + "--require-hashes", + "--index-url", + "https://pypi.org/simple", + "-r", + str(requirement), + ], + check=True, + capture_output=True, + text=True, + timeout=120, + ) + observed = subprocess.run( + [sys.executable, "-I", "-S", "-c", JWT_PROBE, str(target)], + check=True, + capture_output=True, + text=True, + timeout=10, + ) + result = json.loads(observed.stdout) + assert result["version"] == version + assert result["subject"] == "fixture" + assert result["after_inspection"] == {"verify_signature": False} + assert result["expired"] is True + + +def test_signed_token_and_trailing_signature_padding_remain_supported() -> None: + """Preserve normal signed-token decoding and supported signature padding.""" + token = jwt.encode({"sub": "fixture"}, TEST_KEY, algorithm="HS256") + options: Options = {} + + assert jwt.decode(token, TEST_KEY, algorithms=["HS256"], options=options) == {"sub": "fixture"} + padded_token = token + "=" * (-len(token.rsplit(".", 1)[1]) % 4) + assert jwt.decode(padded_token, TEST_KEY, algorithms=["HS256"], options=options) == {"sub": "fixture"} + assert options == {} diff --git a/tests/unit/security/test_malformed_module_manifests.py b/tests/unit/security/test_malformed_module_manifests.py new file mode 100644 index 000000000..b88b8b0c1 --- /dev/null +++ b/tests/unit/security/test_malformed_module_manifests.py @@ -0,0 +1,35 @@ +"""Public fail-closed controls for alternate manifest parsing during project authority admission.""" + +from pathlib import Path + +import pytest +import yaml + +from specfact_cli.registry import CommandRegistry, module_packages as packages +from tests.unit.security.test_module_command_squatting import ( + _register_package, + clean_registry as clean_registry, +) +from tests.unit.security.test_project_bundle_authority import ( + _assert_codebase_authority_available, + _forged_project_package, + preserve_fixture_cache_authenticity as preserve_fixture_cache_authenticity, +) + + +@pytest.mark.parametrize("grouped", [True, False]) +def test_malformed_alternate_manifest_is_denied_without_startup_failure( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, grouped: bool +) -> None: + """Pair genuine project compatibility with public denial instead of an escaping YAML parser failure.""" + _assert_codebase_authority_available(monkeypatch, tmp_path / "authentic") + package = _forged_project_package(tmp_path / "hostile", "specfact-codebase", "unchanged", monkeypatch) + (package[0] / "metadata.yaml").write_text("broken: [\n", encoding="utf-8") + escaped_parser_errors: list[yaml.YAMLError] = [] + try: + _register_package(monkeypatch, package, grouped) + except yaml.YAMLError as error: + escaped_parser_errors.append(error) + assert not escaped_parser_errors, "Malformed alternate manifest escaped admission and would abort startup" + assert CommandRegistry.get_module_metadata("code") is None + assert not packages._ACTIVE_MODULE_SRC_DIRS diff --git a/tests/unit/security/test_module_command_squatting.py b/tests/unit/security/test_module_command_squatting.py new file mode 100644 index 000000000..35e46d96d --- /dev/null +++ b/tests/unit/security/test_module_command_squatting.py @@ -0,0 +1,433 @@ +"""Regression probes for workspace root-command squatting and core impersonation.""" + +import shutil +import sys +from collections.abc import Generator, Mapping, Sequence +from pathlib import Path + +import pytest +import typer +from typer.testing import CliRunner + +from specfact_cli.models.module_package import ModulePackageMetadata, SchemaExtension, ServiceBridgeMetadata +from specfact_cli.registry import CommandRegistry, module_packages as packages +from specfact_cli.registry.bridge_registry import BridgeRegistry +from specfact_cli.registry.extension_registry import ExtensionRegistry +from specfact_cli.registry.metadata import CommandMetadata + + +@pytest.fixture(autouse=True) +def clean_registry() -> Generator[None, None, None]: + """Isolate real registries, active import roots and synthetic probe modules for each control.""" + CommandRegistry._clear_for_testing() + yield + packages._clear_active_module_src_dirs() + CommandRegistry._clear_for_testing() + + +def _reset_registration() -> None: + """Clear both registries and activated package import roots between boundary probes.""" + packages._clear_active_module_src_dirs() + CommandRegistry._clear_for_testing() + + +def _workspace_package(tmp_path: Path, category: str | None, command: str) -> tuple[Path, ModulePackageMetadata]: + """Create a clearly synthetic workspace package whose execution leaves an observable marker.""" + package_dir = tmp_path / ".specfact" / "modules" / "hostile" + (package_dir / "src").mkdir(parents=True) + category_line = f"category: {category}\n" if category else "" + if category and category != "core": + group = "code" if category == "codebase" else category + category_line += f"bundle: specfact-{category}\nbundle_group_command: {group}\nbundle_sub_command: {command}\n" + (package_dir / "module-package.yaml").write_text( + f"name: hostile\nversion: '0.1.0'\ncommands: [{command}]\nsource: builtin\n{category_line}", + encoding="utf-8", + ) + marker = tmp_path / "executed" + (package_dir / "src" / "app.py").write_text( + "from pathlib import Path\nimport typer\n" + f"Path({str(marker)!r}).write_text('executed')\napp = typer.Typer()\n" + "@app.command()\ndef hostile_command(): pass\n", + encoding="utf-8", + ) + discovered = packages.discover_package_metadata(package_dir.parent, source="project") + assert len(discovered) == 1 + assert discovered[0][1].source == "project", "Manifest source must not override discovery provenance" + return discovered[0] + + +def _register_package( + monkeypatch: pytest.MonkeyPatch, + package: tuple[Path, ModulePackageMetadata], + grouped: bool = True, +) -> None: + """Exercise actual registration with explicit discovery and grouping inputs.""" + monkeypatch.setattr(packages, "discover_all_package_metadata", lambda **_kwargs: [package]) + monkeypatch.setattr(packages, "read_modules_state", dict) + packages.register_module_package_commands(allow_unsigned=True, category_grouping_enabled=grouped) + + +def test_workspace_missing_category_cannot_squat_root(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Reject grouped category-less workspace commands before registration or execution.""" + package = _workspace_package(tmp_path, None, "validate") + _register_package(monkeypatch, package) + assert "validate" not in CommandRegistry.list_commands() + assert not (tmp_path / "executed").exists() + + +def test_workspace_core_category_cannot_extend_permanent_roots(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Reserve permanent command authority for real built-ins across both registration modes.""" + safe_app = typer.Typer() + + def safe_loader() -> typer.Typer: + """Supply a harmless Typer app while observing admission independently from package execution.""" + return safe_app + + for category, grouped in (("core", True), ("core", False), (None, False)): + with monkeypatch.context() as context: + probe_root = tmp_path / f"{category}-{grouped}" + package = _workspace_package(probe_root, category, "init") + package[1].commands = list(packages.CORE_NAMES) + for command in packages.CORE_NAMES: + CommandRegistry.register(command, safe_loader, CommandMetadata(name=command, help="Safe core")) + _register_package(context, package, grouped) + for command in packages.CORE_NAMES: + assert CommandRegistry.get_typer(command) is safe_app + assert not CommandRegistry._module_entries + assert not (probe_root / "executed").exists() + _reset_registration() + + +def test_external_core_claim_rejected_even_with_forged_source_and_builtin_symlink( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Reject external resolved paths despite forged built-in labels or escaping symlinks.""" + for grouped in (True, False): + with monkeypatch.context() as context: + probe_root = tmp_path / str(grouped) + package_dir, metadata = _workspace_package(probe_root, "core", "upgrade") + metadata.source = "builtin" + builtin_root = probe_root / "builtin" + builtin_root.mkdir() + symlink = builtin_root / "upgrade" + symlink.symlink_to(package_dir, target_is_directory=True) + + def probe_builtin_root(root: Path = builtin_root) -> Path: + """Observe built-in root lookup while verifying configured discovery authority.""" + return root.resolve() + + context.setattr(packages, "get_modules_root", probe_builtin_root) + _register_package(context, (symlink, metadata), grouped) + assert "upgrade" not in CommandRegistry.list_commands() + assert not CommandRegistry._module_entries + assert not (probe_root / "executed").exists() + _reset_registration() + + +def test_explicit_legacy_workspace_registration_remains_available( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Keep explicit unreserved workspace legacy commands after grouped denial is proved.""" + package = _workspace_package(tmp_path, None, "legacy") + _register_package(monkeypatch, package) + assert "legacy" not in CommandRegistry.list_commands() + assert not (tmp_path / "executed").exists() + _reset_registration() + _register_package(monkeypatch, package, grouped=False) + assert "legacy" in CommandRegistry.list_commands() + CommandRegistry.get_typer("legacy") + assert (tmp_path / "executed").read_text() == "executed" + + +def test_non_workspace_legacy_registration_remains_available(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Preserve supported unreserved user, marketplace and custom legacy commands.""" + project = _workspace_package(tmp_path / "project", None, "legacy") + _register_package(monkeypatch, project) + assert "legacy" not in CommandRegistry.list_commands() + _reset_registration() + for source in ("user", "marketplace", "custom"): + with monkeypatch.context() as context: + package_dir, metadata = _workspace_package(tmp_path / source, None, "legacy") + metadata.source = source + _register_package(context, (package_dir, metadata)) + assert "legacy" in CommandRegistry.list_commands() + assert isinstance(CommandRegistry.get_typer("legacy"), typer.Typer) + assert (tmp_path / source / "executed").read_text() == "executed" + _reset_registration() + + +def test_real_builtin_core_registration_remains_available(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Deny the external core claim while preserving loadable real built-in initialization.""" + hostile = _workspace_package(tmp_path, "core", "init") + _register_package(monkeypatch, hostile) + assert "init" not in CommandRegistry.list_commands() + _reset_registration() + discovered = packages.discover_package_metadata(packages.get_modules_root(), source="builtin") + package = next(item for item in discovered if item[1].name == "init") + _register_package(monkeypatch, package) + assert "init" in CommandRegistry.list_commands() + assert isinstance(CommandRegistry.get_typer("init"), typer.Typer) + + +def _assert_installed_category_root_unchanged( + context: pytest.MonkeyPatch, package: tuple[Path, ModulePackageMetadata], grouped: bool +) -> None: + """Observe exact installed root/module loader identities before and after a hostile claim.""" + package_dir, metadata = package + command = metadata.commands[0] + safe_app = typer.Typer() + + def sentinel_loader(app: typer.Typer = safe_app) -> typer.Typer: + """Return the installed root app without running workspace code.""" + return app + + sentinel_metadata = CommandMetadata(name=command, help="Installed category sentinel") + CommandRegistry.register(command, sentinel_loader, sentinel_metadata) + CommandRegistry.register_module(command, sentinel_loader, sentinel_metadata) + _register_package(context, (package_dir, metadata), grouped) + for entries in (CommandRegistry._entries, CommandRegistry._module_entries): + entry = next(entry for entry in entries if entry.get("name") == command) + assert entry.get("loader") is sentinel_loader, "Workspace must not replace or wrap installed loaders" + assert entry.get("metadata") is sentinel_metadata + assert CommandRegistry.get_typer(command) is safe_app + assert CommandRegistry.get_module_typer(command) is safe_app + assert (package_dir / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + assert not (package_dir.parents[2] / "executed").exists() + + +def test_workspace_cannot_supply_official_category_roots(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Reject every official workspace category-root claim before any registration side effect.""" + roots = ( + ("specfact-project", "project"), + ("specfact-backlog", "backlog"), + ("specfact-codebase", "code"), + ("specfact-spec", "spec"), + ("specfact-govern", "govern"), + ("specfact-requirements", "requirements"), + ) + for bundle, command in roots: + category = bundle.removeprefix("specfact-") + for declared_category, grouped in ((category, True), (category, False), (None, False)): + with monkeypatch.context() as context: + probe_root = tmp_path / command / f"{declared_category}-{grouped}" + package_dir, metadata = _workspace_package(probe_root, declared_category, command) + _assert_installed_category_root_unchanged(context, (package_dir, metadata), grouped) + _reset_registration() + _register_package(context, (package_dir, metadata), grouped) + assert command not in CommandRegistry.list_commands() + assert not CommandRegistry._module_entries + _reset_registration() + + +def _configure_hostile_side_effects( + monkeypatch: pytest.MonkeyPatch, package: tuple[Path, ModulePackageMetadata], marker: Path +) -> tuple[ExtensionRegistry, BridgeRegistry]: + """Supply valid real schema/bridge declarations and isolated observable registries.""" + hostile, hostile_meta = package + bridge_name = "specfact_squatting_bridge_probe" + (hostile / "src" / f"{bridge_name}.py").write_text( + "from pathlib import Path\n" + f"Path({str(marker)!r}).write_text('hostile-bridge')\n" + "class Converter:\n" + " def to_bundle(self, external_data): return dict(external_data)\n" + " def from_bundle(self, bundle_data): return dict(bundle_data)\n" + ) + hostile_meta.schema_extensions = [SchemaExtension(target="Feature", field="workspace_probe", type_hint="str")] + hostile_meta.service_bridges = [ + ServiceBridgeMetadata(id="workspace-probe", converter_class=f"{bridge_name}.Converter") + ] + extensions = ExtensionRegistry() + bridges = BridgeRegistry() + monkeypatch.setattr(packages, "get_extension_registry", lambda: extensions) + monkeypatch.setattr(packages, "BRIDGE_REGISTRY", bridges) + return extensions, bridges + + +def test_rejected_workspace_cannot_shadow_eligible_package_imports( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Load the approved dependency without activating or executing rejected workspace sources.""" + hostile, hostile_meta = _workspace_package(tmp_path, None, "validate") + marker = tmp_path / "shadow-executed" + bridge_name = "specfact_squatting_bridge_probe" + extensions, bridges = _configure_hostile_side_effects(monkeypatch, (hostile, hostile_meta), marker) + before_extensions = extensions.list_all() + before_bridges = bridges.as_mapping() + dependency_name = "specfact_squatting_probe_dependency" + (hostile / "src" / f"{dependency_name}.py").write_text( + f"from pathlib import Path\nPath({str(marker)!r}).write_text('hostile')\nVALUE = 'hostile'\n" + ) + trusted = tmp_path / ".specfact" / "modules" / "trusted" + (trusted / "src").mkdir(parents=True) + dependency_root = tmp_path / "approved-dependency" + dependency_root.mkdir() + (dependency_root / f"{dependency_name}.py").write_text("VALUE = 'approved'\n") + monkeypatch.setattr(sys, "path", [str(dependency_root), *sys.path]) + (trusted / "src" / "app.py").write_text( + f"import {dependency_name} as dependency\nimport typer\n" + "assert dependency.VALUE == 'approved'\napp = typer.Typer()\n" + ) + metadata = ModulePackageMetadata(name="trusted", version="0.1.0", commands=["trusted"], source="user") + monkeypatch.setattr( + packages, "discover_all_package_metadata", lambda **_kwargs: [(trusted, metadata), (hostile, hostile_meta)] + ) + monkeypatch.setattr(packages, "read_modules_state", dict) + try: + packages.register_module_package_commands(allow_unsigned=True) + assert extensions.list_all() == before_extensions, "Rejected metadata must not register schema extensions" + assert bridges.as_mapping() == before_bridges, "Rejected metadata must not instantiate service bridges" + assert isinstance(CommandRegistry.get_typer("trusted"), typer.Typer) + assert not marker.exists() + assert (hostile / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + finally: + sys.modules.pop(dependency_name, None) + sys.modules.pop(bridge_name, None) + + +def test_explicit_repository_root_preserves_builtin_authority(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Preserve contained built-in authority from an operator-configured checkout.""" + effective_root = tmp_path / "checkout" / "src" / "specfact_cli" / "modules" + shutil.copytree(packages.get_modules_root() / "init", effective_root / "init") + monkeypatch.setenv("SPECFACT_REPO_ROOT", str(tmp_path / "checkout")) + assert packages.get_modules_root() == effective_root + hostile_dir, hostile_meta = _workspace_package(tmp_path / "external", "core", "upgrade") + hostile_meta.source = "builtin" + escaped = effective_root / "upgrade" + escaped.symlink_to(hostile_dir, target_is_directory=True) + _register_package(monkeypatch, (escaped, hostile_meta)) + assert "upgrade" not in CommandRegistry.list_commands() + _reset_registration() + discovered = packages.discover_package_metadata(effective_root, source="builtin") + _register_package(monkeypatch, next(item for item in discovered if item[1].name == "init")) + assert "init" in CommandRegistry.list_commands() + assert isinstance(CommandRegistry.get_typer("init"), typer.Typer) + + +def test_spec_api_routing_preserves_reserved_workspace_root(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Reserve the legacy flat root and execute an eligible spec bundle through its api route.""" + hostile = _workspace_package(tmp_path / "workspace", None, "spec") + _register_package(monkeypatch, hostile, grouped=False) + assert "spec" not in CommandRegistry.list_commands() + assert not (tmp_path / "workspace" / "executed").exists() + _reset_registration() + package_dir, metadata = _workspace_package(tmp_path / "eligible", "spec", "spec") + metadata.source = "user" + metadata.bundle_sub_command = "api" + (package_dir / "src" / "app.py").write_text( + "import typer\napp = typer.Typer()\napi = typer.Typer()\napp.add_typer(api, name='api')\n" + "@api.command()\ndef validate(): typer.echo('safe-api-route')\n" + ) + _register_package(monkeypatch, (package_dir, metadata), grouped=False) + spec_app = CommandRegistry.get_typer("spec") + result = CliRunner().invoke(spec_app, ["api", "validate"]) + assert result.exit_code == 0, result.output + assert "safe-api-route" in result.output + + +@pytest.mark.parametrize( + "command", ["init", "module", "upgrade", "project", "backlog", "code", "spec", "govern", "requirements"] +) +def test_reserved_flat_claim_is_denied(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, command: str) -> None: + """Each reserved flat name independently proves rejection in explicit legacy mode.""" + package_dir, metadata = _workspace_package(tmp_path, None, command) + _register_package(monkeypatch, (package_dir, metadata), grouped=False) + assert command not in CommandRegistry.list_commands() + assert not CommandRegistry._module_entries + assert (package_dir / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + assert not (tmp_path / "executed").exists() + + +def test_workspace_eligibility_does_not_import_category_builders( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + """Admission checks must not initialize category apps for a rejected package.""" + import builtins + + original_import = builtins.__import__ + imported_groups: list[str] = [] + + def observed_import( + name: str, + globals: Mapping[str, object] | None = None, + locals: Mapping[str, object] | None = None, + fromlist: Sequence[str] | None = (), + level: int = 0, + ) -> object: + """Observe category-builder imports during the actual eligibility probe.""" + if name.startswith("specfact_cli.groups."): + imported_groups.append(name) + return original_import(name, globals, locals, fromlist, level) + + package_dir, metadata = _workspace_package(tmp_path, None, "project") + monkeypatch.setattr(builtins, "__import__", observed_import) + _register_package(monkeypatch, (package_dir, metadata)) + assert not imported_groups, "Rejected admission must not import category builders" + assert "project" not in CommandRegistry.list_commands() + assert not CommandRegistry._module_entries + assert (package_dir / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + assert not (tmp_path / "executed").exists() + + +@pytest.mark.parametrize("grouped", [True, False]) +@pytest.mark.parametrize("existing_root", [False, True]) +def test_workspace_bundle_member_cannot_induce_category_mount( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, grouped: bool, existing_root: bool +) -> None: + """Deny actual bundle-driven mounts and harmless member execution before root replacement.""" + package = _workspace_package(tmp_path, "project", "plan") + safe_app = typer.Typer() + + def safe_loader() -> typer.Typer: + """Preserve the existing unextended root app as a separate compatibility sentinel.""" + return safe_app + + if existing_root: + CommandRegistry.register("project", safe_loader, CommandMetadata(name="project", help="Safe project")) + _register_package(monkeypatch, package, grouped) + if "project" in CommandRegistry.list_commands(): + CommandRegistry.get_typer("project") + assert not (tmp_path / "executed").exists(), "Unsigned member code executed through the induced project root" + assert CommandRegistry.get_module_metadata("plan") is None + assert not packages._ACTIVE_MODULE_SRC_DIRS + if existing_root: + assert CommandRegistry.get_typer("project") is safe_app + else: + assert "project" not in CommandRegistry.list_commands() + + +@pytest.mark.parametrize("grouped", [True, False]) +def test_workspace_core_member_cannot_extend_existing_category( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, grouped: bool +) -> None: + """Protect an actual category builder from cross-category injection into an existing member loader.""" + from specfact_cli.groups.project_group import build_app + + package = _workspace_package(tmp_path, "core", "plan") + safe_app = typer.Typer() + + def safe_loader() -> typer.Typer: + """Supply a legitimate member whose identity must survive hostile registration.""" + return safe_app + + CommandRegistry.register_module("plan", safe_loader, CommandMetadata(name="plan", help="Safe plan")) + CommandRegistry.register("project", build_app, CommandMetadata(name="project", help="Safe project")) + _register_package(monkeypatch, package, grouped) + CommandRegistry.get_typer("project") + assert not (tmp_path / "executed").exists(), "Cross-category member executed through the existing project root" + assert CommandRegistry.get_module_typer("plan") is safe_app + assert (package[0] / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + + +@pytest.mark.parametrize("claim", ["bundle", "name"]) +def test_workspace_legacy_alias_cannot_mount_category( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, claim: str +) -> None: + """Reject explicit and namespaced bundle aliases even when legacy flat mounting is enabled.""" + package_dir, metadata = _workspace_package(tmp_path, None, "unreserved") + if claim == "bundle": + metadata.bundle = "specfact-project" + else: + metadata.name = "hostile/specfact-project" + _register_package(monkeypatch, (package_dir, metadata), grouped=False) + assert "project" not in CommandRegistry.list_commands(), "Unverified legacy alias induced a reserved root" + assert not packages._ACTIVE_MODULE_SRC_DIRS diff --git a/tests/unit/security/test_native_planning_lineage.py b/tests/unit/security/test_native_planning_lineage.py new file mode 100644 index 000000000..fee46fb40 --- /dev/null +++ b/tests/unit/security/test_native_planning_lineage.py @@ -0,0 +1,86 @@ +"""Exercise the real native-stack consumer preflight in isolated Git histories.""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import yaml + + +_REPO_ROOT = Path(__file__).resolve().parents[3] + + +def _git(repo: Path, *arguments: str) -> str: + return subprocess.run( + ["git", "-C", str(repo), *arguments], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +def _history(tmp_path: Path) -> tuple[Path, str, str, str, str]: + repo = tmp_path / "lineage" + repo.mkdir() + _git(repo, "init", "--quiet", "--initial-branch=dev") + _git(repo, "config", "user.name", "Lineage control") + _git(repo, "config", "user.email", "lineage@example.invalid") + _git(repo, "config", "commit.gpgsign", "false") + _git(repo, "commit", "--quiet", "--allow-empty", "-m", "protected baseline") + protected = _git(repo, "rev-parse", "HEAD") + _git(repo, "update-ref", "refs/remotes/origin/dev", protected) + _git(repo, "commit", "--quiet", "--allow-empty", "-m", "verified runtime parent") + parent = _git(repo, "rev-parse", "HEAD") + _git(repo, "commit", "--quiet", "--allow-empty", "-m", "eligible planning child") + candidate = _git(repo, "rev-parse", "HEAD") + tree = _git(repo, "rev-parse", "HEAD^{tree}") + unrelated = _git(repo, "commit-tree", tree, "-m", "unrelated proof") + future = _git(repo, "commit-tree", tree, "-p", candidate, "-m", "future evidence") + return repo, protected, parent, unrelated, future + + +def _preflight(repo: Path, proof_base: str, planning_base: str | None) -> subprocess.CompletedProcess[str]: + workflow = yaml.safe_load((_REPO_ROOT / ".github/workflows/requirements-evidence.yml").read_text()) + steps = workflow["jobs"]["requirements-evidence"]["steps"] + scripts = [step["run"] for step in steps if step.get("name") == "Reconcile Requirements evidence on fresh runner"] + assert len(scripts) == 1 + preflight, _reconciliation = scripts[0].split('validator_site="', maxsplit=1) + env = os.environ.copy() + env["PROOF_BASE_COMMIT"] = proof_base + env["REVIEW_BASE_BRANCH"] = "dev" + env.pop("PLANNING_BASE_COMMIT", None) + if planning_base is not None: + env["PLANNING_BASE_COMMIT"] = planning_base + return subprocess.run( + ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", preflight], + cwd=repo, + env=env, + capture_output=True, + text=True, + check=False, + ) + + +def test_native_planning_parent_preserves_review_trust(tmp_path: Path) -> None: + repo, protected, parent, _unrelated, _future = _history(tmp_path) + ordinary = _preflight(repo, protected, None) + assert ordinary.returncode == 0, ordinary.stderr + native = _preflight(repo, protected, parent) + assert native.returncode == 0, ( + "Authenticated planning parent newer than dev must reach reconciliation; " + f"actual workflow preflight exited {native.returncode}: {native.stderr}" + ) + + +def test_independent_lineage_rejects_forged_trust_or_parent(tmp_path: Path) -> None: + repo, protected, parent, unrelated, future = _history(tmp_path) + forged_trust = _preflight(repo, unrelated, protected) + assert forged_trust.returncode != 0, "Planning selection must not conceal unrelated protected trust" + assert _preflight(repo, parent, protected).returncode != 0, "Trust cannot be newer than the review base" + assert _preflight(repo, "invalid", protected).returncode != 0, "Malformed trust identity must be rejected" + assert _preflight(repo, protected, unrelated).returncode != 0, "Unrelated planning ancestry must be rejected" + assert _preflight(repo, protected, future).returncode != 0, "Evidence newer than the candidate must be rejected" + valid = _preflight(repo, protected, parent) + assert valid.returncode == 0, valid.stderr diff --git a/tests/unit/security/test_package_candidate_eligibility.py b/tests/unit/security/test_package_candidate_eligibility.py new file mode 100644 index 000000000..15ae14a48 --- /dev/null +++ b/tests/unit/security/test_package_candidate_eligibility.py @@ -0,0 +1,266 @@ +"""Public registration selects an eligible signed same-name installation before effects.""" + +import base64 +import hashlib +import os +import sys +from collections.abc import Generator +from pathlib import Path +from typing import Any + +import pytest +import yaml +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from typer.testing import CliRunner + +from specfact_cli.registry import CommandRegistry, module_installer, module_packages as packages +from tests.unit.security.test_module_command_squatting import _reset_registration +from tests.unit.security.test_project_bundle_authority import _isolate_discovery_roots + + +def _signed_copy(parent: Path, key: Ed25519PrivateKey, failure: str | None, **changes: Any) -> Path: + """Sign a harmless bundle with the test trust anchor after declaring actual compatibility inputs.""" + if parent.name != "modules" or parent.parent.name != ".specfact": + parent = parent / ".specfact/modules" + package = parent / "specfact-codebase" + (package / "src").mkdir(parents=True) + (package / "src/app.py").write_text( + "import typer\napp = typer.Typer()\n@app.callback()\ndef root(): pass\n" + "@app.command()\ndef probe(): typer.echo('eligible-installed-copy')\n", + encoding="utf-8", + ) + raw: dict[str, Any] = { + "name": "nold-ai/specfact-codebase", + "version": "1.0.0", + "commands": ["code"], + "category": "codebase", + "bundle": "specfact-codebase", + "bundle_group_command": "code", + "tier": "official", + "publisher": {"name": "nold-ai", "email": "fixture@example.invalid"}, + } + if failure == "core": + raw["core_compatibility"] = "<0.1" + elif failure == "schema": + raw["schema_version"] = "incompatible-fixture-schema" + elif failure == "dependency": + raw["module_dependencies"] = ["fixture/missing-provider"] + raw.update(changes) + manifest = package / "module-package.yaml" + manifest.write_text(yaml.safe_dump(raw), encoding="utf-8") + payload = module_installer._module_artifact_payload_signed(package) + raw["integrity"] = { + "checksum": "sha256:" + hashlib.sha256(payload).hexdigest(), + "signature": base64.b64encode(key.sign(payload)).decode("ascii"), + } + manifest.write_text(yaml.safe_dump(raw), encoding="utf-8") + return package + + +@pytest.mark.parametrize("grouped", [True, False], ids=["grouped", "legacy"]) +@pytest.mark.parametrize("failure", ["core", "schema", "dependency"]) +def test_signed_ineligible_project_preserves_eligible_installed_copy( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, grouped: bool, failure: str +) -> None: + """Use actual discovery and crypto, then observe installed activation and its public command.""" + monkeypatch.setattr(sys, "dont_write_bytecode", True) + key = Ed25519PrivateKey.generate() + public_key = key.public_key().public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo + ) + key_file = tmp_path / "fixture-public.pem" + key_file.write_bytes(public_key) + monkeypatch.setattr(module_installer, "_bundled_public_key_path", lambda: key_file) + project = _signed_copy(tmp_path / "workspace/.specfact/modules", key, failure) + installed = _signed_copy(tmp_path / "user", key, None) + _isolate_discovery_roots(monkeypatch, tmp_path / "workspace", installed.parent) + for path, source in ((project, "project"), (installed, "user")): + metadata = packages.discover_package_metadata(path.parent, source=source)[0][1] + assert module_installer.verify_module_artifact( + path, metadata, public_key_pem=public_key.decode(), allow_unsigned=False, require_signature=True + ) + _reset_registration() + try: + packages.register_module_package_commands(allow_unsigned=False, category_grouping_enabled=grouped) + assert (installed / "src").resolve() in packages._ACTIVE_MODULE_SRC_DIRS, ( + "An ineligible signed project copy suppressed its eligible same-name installation" + ) + assert (project / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + app = CommandRegistry.get_module_typer("code") + assert app is not None + result = CliRunner().invoke(app, ["probe"]) + assert result.exit_code == 0, result.output + assert "eligible-installed-copy" in result.output + finally: + _reset_registration() + + +@pytest.fixture +def selection_key(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Generator[Ed25519PrivateKey, None, None]: + """Use a real test trust anchor while retaining production verification decisions.""" + monkeypatch.setattr(sys, "dont_write_bytecode", True) + key = Ed25519PrivateKey.generate() + public = tmp_path / "selection-public.pem" + public.write_bytes( + key.public_key().public_bytes(serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo) + ) + monkeypatch.setattr(module_installer, "_bundled_public_key_path", lambda: public) + monkeypatch.setattr(packages, "get_modules_root", lambda: tmp_path / "missing-builtin") + monkeypatch.setattr(packages, "read_modules_state", dict) + _reset_registration() + yield key + _reset_registration() + + +def _register_candidates(monkeypatch: pytest.MonkeyPatch, paths: list[Path]) -> None: + """Discover actual configured installations in priority order through the public entry point.""" + workspace = paths[0].parents[3] / "empty-workspace" + workspace.mkdir(exist_ok=True) + _isolate_discovery_roots(monkeypatch, workspace, workspace / "missing-user") + roots = dict.fromkeys(str(path.parent) for path in paths) + monkeypatch.setenv("SPECFACT_MODULES_ROOTS", os.pathsep.join(roots)) + packages.register_module_package_commands(allow_unsigned=False) + + +def _assert_active(paths: list[Path], rejected: list[Path]) -> None: + """Inspect exact activated sources instead of trusting a provisional metadata selection.""" + active = set(packages._ACTIVE_MODULE_SRC_DIRS) + assert all((path / "src").resolve() in active for path in paths), "Eligible copies were suppressed" + assert all((path / "src").resolve() not in active for path in rejected), "Rejected copies gained import authority" + + +def _provider(parent: Path, key: Ed25519PrivateKey, version: str, failure: str | None = None, **changes: Any) -> Path: + """Author a distinct signed spec provider with actual declared version constraints.""" + return _signed_copy( + parent, + key, + failure, + name="nold-ai/specfact-spec", + version=version, + category="spec", + bundle="specfact-spec", + bundle_group_command="spec", + commands=["spec"], + **changes, + ) + + +def test_integrity_rejected_user_copy_keeps_signed_fallback( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey +) -> None: + """Ordinary integrity failure must not consume a name before the next signed copy is tested.""" + invalid = _signed_copy(tmp_path / "invalid", selection_key, None) + valid = _signed_copy(tmp_path / "valid", selection_key, None) + (invalid / "src/app.py").write_text("TAMPERED = True\n", encoding="utf-8") + _register_candidates(monkeypatch, [invalid, valid]) + _assert_active([valid], [invalid]) + + +def test_dependency_selection_reconsiders_higher_priority_consumer( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey +) -> None: + """A provider fallback can restore the first consumer that needs its actual version.""" + dependency = [{"name": "nold-ai/specfact-spec", "version_specifier": ">=2"}] + consumer = _signed_copy(tmp_path / "a1", selection_key, None, module_dependencies_versioned=dependency) + fallback = _signed_copy(tmp_path / "a2", selection_key, None, version="2.0.0") + old = _provider(tmp_path / "b1", selection_key, "1.0.0", "dependency") + current = _provider(tmp_path / "b2", selection_key, "2.0.0") + _register_candidates(monkeypatch, [consumer, fallback, old, current]) + _assert_active([consumer, current], [fallback, old]) + + +def test_rejected_provider_cannot_satisfy_consumer_dependencies( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey +) -> None: + """A provider that cannot load contributes neither existence nor versions or mounts.""" + provider = _provider(tmp_path / "provider", selection_key, "1.0.0", "core") + consumer = _signed_copy(tmp_path / "consumer", selection_key, None, module_dependencies=["nold-ai/specfact-spec"]) + _register_candidates(monkeypatch, [consumer, provider]) + _assert_active([], [consumer, provider]) + assert CommandRegistry.get_metadata("spec") is None, "Rejected provider created a category mount" + + +def test_consistent_dependency_cycle_keeps_commands( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey +) -> None: + """Existing consistent edge-local dependencies remain compatible rather than being erased by ordering.""" + consumer = _signed_copy(tmp_path / "a", selection_key, None, module_dependencies=["nold-ai/specfact-spec"]) + provider = _provider(tmp_path / "b", selection_key, "1.0.0", module_dependencies=["nold-ai/specfact-codebase"]) + _register_candidates(monkeypatch, [consumer, provider]) + _assert_active([consumer, provider], []) + + +def test_conflicting_dependency_cycle_refuses_only_unstable_component( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey +) -> None: + """Actual incompatible priority selections terminate before effects while an unrelated module survives.""" + consumers = [ + _signed_copy( + tmp_path / folder, + selection_key, + None, + version=version, + module_dependencies_versioned=[{"name": "nold-ai/specfact-spec", "version_specifier": constraint}], + ) + for folder, version, constraint in (("a1", "1.0.0", ">=2"), ("a2", "2.0.0", "<2")) + ] + providers = [ + _provider( + tmp_path / folder, + selection_key, + version, + module_dependencies_versioned=[{"name": "nold-ai/specfact-codebase", "version_specifier": constraint}], + ) + for folder, version, constraint in (("b1", "1.0.0", "<2"), ("b2", "2.0.0", ">=2")) + ] + unrelated = _signed_copy( + tmp_path / "unrelated", + selection_key, + None, + name="fixture/independent", + category=None, + bundle=None, + bundle_group_command=None, + commands=["independent"], + ) + _register_candidates(monkeypatch, [*consumers, *providers, unrelated]) + _assert_active([unrelated], consumers + providers) + assert CommandRegistry.get_metadata("code") is None + assert CommandRegistry.get_metadata("spec") is None + + +def test_final_revalidation_rejects_payload_changed_after_initial_check( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey +) -> None: + """Observe an actual successful signature check, then deny the changed copy before activation.""" + changed = _signed_copy(tmp_path / "changed", selection_key, None) + fallback = _signed_copy(tmp_path / "fallback", selection_key, None) + actual_verify = packages.verify_module_artifact + mutated = False + + def observe_verified_payload(path: Path, metadata: packages.ModulePackageMetadata, **kwargs: Any) -> bool: + nonlocal mutated + valid = actual_verify(path, metadata, **kwargs) + if path == changed and valid and not mutated: + mutated = True + (path / "src/app.py").write_text("UNSIGNED_AFTER_VERIFICATION = True\n", encoding="utf-8") + return valid + + monkeypatch.setattr(packages, "verify_module_artifact", observe_verified_payload) + _register_candidates(monkeypatch, [changed, fallback]) + assert mutated, "The observer must follow a genuine successful verifier decision" + _assert_active([fallback], [changed]) + + +def test_versioned_self_dependency_selects_eligible_fallback( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey +) -> None: + """Use each real signed candidate's own version for a declared self-dependency.""" + dependency = [{"name": "nold-ai/specfact-codebase", "version_specifier": ">=2"}] + invalid = _signed_copy(tmp_path / "v1", selection_key, None, module_dependencies_versioned=dependency) + valid = _signed_copy( + tmp_path / "v2", selection_key, None, version="2.0.0", module_dependencies_versioned=dependency + ) + _register_candidates(monkeypatch, [invalid, valid]) + _assert_active([valid], [invalid]) diff --git a/tests/unit/security/test_package_path_link_authority.py b/tests/unit/security/test_package_path_link_authority.py new file mode 100644 index 000000000..f8ce054aa --- /dev/null +++ b/tests/unit/security/test_package_path_link_authority.py @@ -0,0 +1,209 @@ +import json +from collections.abc import Callable +from pathlib import Path + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from specfact_cli.models.module_package import ModulePackageMetadata +from specfact_cli.registry import CommandRegistry, module_installer as installer, module_packages as packages +from tests.unit.security.test_executable_identity_oracles import ( + _assert_genuine_signature, + _canonical_compatibility, + _register, +) +from tests.unit.security.test_module_command_squatting import _reset_registration +from tests.unit.security.test_package_candidate_eligibility import _signed_copy, selection_key as selection_key +from tests.unit.security.test_project_bundle_authority import _isolate_discovery_roots + + +def _linked_project_copy(root: Path, key: Ed25519PrivateKey, kind: str) -> tuple[Path, Path]: + workspace = root / kind / "workspace" + store = workspace / ".specfact/modules" + target = _signed_copy(root / kind / "authentic", key, None) + link, destination = { + "package": (store / target.name, target), + "modules": (store, target.parent), + "specfact": (store.parent, target.parent.parent), + }[kind] + link.parent.mkdir(parents=True) + link.symlink_to(destination, target_is_directory=True) + return workspace, store / target.name + + +def _observe_path_link( + monkeypatch: pytest.MonkeyPatch, + root: Path, + key: Ed25519PrivateKey, + case: tuple[str, bool], +) -> tuple[str, bool, bool, bool]: + kind, grouped = case + try: + with monkeypatch.context() as probe: + _canonical_compatibility(probe, root / kind / "canonical", key, grouped) + workspace, path = _linked_project_copy(root, key, kind) + _assert_genuine_signature(path) + _register(probe, workspace, grouped) + root_denied = CommandRegistry.get_module_metadata("code") is None + source_denied = (path / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + _reset_registration() + installed = _signed_copy(root / kind / "user", key, None) + _isolate_discovery_roots(probe, workspace, installed.parent) + packages.register_module_package_commands(allow_unsigned=False, category_grouping_enabled=grouped) + fallback_kept = (installed / "src").resolve() in packages._ACTIVE_MODULE_SRC_DIRS and ( + path / "src" + ).resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + return kind, root_denied, source_denied, fallback_kept + finally: + _reset_registration() + + +@pytest.mark.parametrize("grouped", [True, False], ids=["grouped", "legacy"]) +def test_project_path_links_have_no_authority( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + selection_key: Ed25519PrivateKey, + grouped: bool, + record_testsuite_property: Callable[[str, object], None], +) -> None: + observations = [ + _observe_path_link(monkeypatch, tmp_path, selection_key, (kind, grouped)) + for kind in ("package", "modules", "specfact") + ] + record_testsuite_property(f"project_path_link_observations-{grouped}", json.dumps(observations)) + assert len(observations) == 3 + assert all( + root_denied and source_denied and fallback_kept for _, root_denied, source_denied, fallback_kept in observations + ), observations + + +def test_legacy_native_suffixes_have_no_authority( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + selection_key: Ed25519PrivateKey, + record_testsuite_property: Callable[[str, object], None], +) -> None: + observations: list[tuple[str, bool, bool]] = [] + for suffix in ("dll", "dylib", "bundle", "so", "pyd"): + try: + with monkeypatch.context() as probe: + root = tmp_path / suffix + _canonical_compatibility(probe, root / "canonical", selection_key, False) + workspace = root / "workspace" + path = _signed_copy(workspace / ".specfact/modules", selection_key, None) + (path / "logs").mkdir() + (path / "logs" / f"probe.{suffix}").write_bytes(b"harmless omitted native library probe") + _assert_genuine_signature(path) + _register(probe, workspace, False) + observations.append( + ( + suffix, + CommandRegistry.get_module_metadata("code") is None, + (path / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS, + ) + ) + finally: + _reset_registration() + record_testsuite_property("legacy_native_suffix_observations", json.dumps(observations)) + assert len(observations) == 5 + assert all(root_denied and source_denied for _, root_denied, source_denied in observations), observations + + +@pytest.mark.parametrize("grouped", [True, False], ids=["grouped", "legacy"]) +def test_workspace_alias_keeps_canonical_signed_authority( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + selection_key: Ed25519PrivateKey, + grouped: bool, +) -> None: + workspace = tmp_path / "workspace" + path = _signed_copy(workspace / ".specfact/modules", selection_key, None) + alias = tmp_path / "workspace-alias" + alias.symlink_to(workspace, target_is_directory=True) + _assert_genuine_signature(path) + _register(monkeypatch, alias, grouped) + assert CommandRegistry.get_module_metadata("code") is not None + assert (path / "src").resolve() in packages._ACTIVE_MODULE_SRC_DIRS + + +@pytest.mark.parametrize("grouped", [True, False], ids=["grouped", "legacy"]) +def test_final_selection_revalidates_new_package_link( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + selection_key: Ed25519PrivateKey, + grouped: bool, +) -> None: + workspace = tmp_path / "workspace" + project = _signed_copy(workspace, selection_key, None) + installed = _signed_copy(tmp_path / "user", selection_key, None) + target = tmp_path / "authenticated-relocation" / project.name + target.parent.mkdir() + original = packages._verified_project_bundle_root + + def link_after_verification(package_dir: Path, metadata: ModulePackageMetadata) -> bool: + verified = original(package_dir, metadata) + if verified and package_dir == project and not project.is_symlink(): + project.rename(target) + project.symlink_to(target, target_is_directory=True) + return verified + + _assert_genuine_signature(project) + _assert_genuine_signature(installed) + _isolate_discovery_roots(monkeypatch, workspace, installed.parent) + monkeypatch.setattr(packages, "_verified_project_bundle_root", link_after_verification) + packages.register_module_package_commands(allow_unsigned=False, category_grouping_enabled=grouped) + assert project.is_symlink() + assert CommandRegistry.get_module_metadata("code") is not None + assert (installed / "src").resolve() in packages._ACTIVE_MODULE_SRC_DIRS + assert (target / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + + +def _link_before_snapshot_resolution(monkeypatch: pytest.MonkeyPatch, project: Path, target: Path, kind: str) -> Path: + source = {"package": project, "modules": project.parent, "specfact": project.parent.parent}[kind] + original_resolve = Path.resolve + original_verify = installer.verify_module_artifact_from_filesystem + active = False + project_checks = 0 + + def resolve(path: Path, strict: bool = False) -> Path: + if active and path == project and not source.is_symlink(): + source.rename(target) + source.symlink_to(target, target_is_directory=True) + return original_resolve(path, strict=strict) + + def verify(package_dir: Path, metadata: ModulePackageMetadata, public_key: str) -> bool: + nonlocal active, project_checks + project_checks += int(package_dir == project) + active = package_dir == project and project_checks == 2 + result = original_verify(package_dir, metadata, public_key) + active = False + return result + + monkeypatch.setattr(Path, "resolve", resolve) + monkeypatch.setattr(installer, "verify_module_artifact_from_filesystem", verify) + return source + + +@pytest.mark.parametrize("grouped", [True, False], ids=["grouped", "legacy"]) +@pytest.mark.parametrize("kind", ["package", "modules", "specfact"]) +def test_final_admission_reobserves_owned_path_links( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + selection_key: Ed25519PrivateKey, + grouped: bool, + kind: str, +) -> None: + _canonical_compatibility(monkeypatch, tmp_path / "canonical", selection_key, grouped) + workspace = tmp_path / "workspace" + project = _signed_copy(workspace, selection_key, None) + installed = _signed_copy(tmp_path / "user", selection_key, None) + target = tmp_path / "authenticated-relocation" + _assert_genuine_signature(project) + _assert_genuine_signature(installed) + _isolate_discovery_roots(monkeypatch, workspace, installed.parent) + source = _link_before_snapshot_resolution(monkeypatch, project, target, kind) + packages.register_module_package_commands(allow_unsigned=False, category_grouping_enabled=grouped) + assert source.is_symlink() + assert CommandRegistry.get_module_metadata("code") is not None + assert (installed / "src").resolve() in packages._ACTIVE_MODULE_SRC_DIRS + assert (project / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS diff --git a/tests/unit/security/test_planning_review_context.py b/tests/unit/security/test_planning_review_context.py new file mode 100644 index 000000000..5867c5e71 --- /dev/null +++ b/tests/unit/security/test_planning_review_context.py @@ -0,0 +1,345 @@ +"""Exercise actual review shells and the immutable provider with synthetic root artifacts.""" + +from __future__ import annotations + +import copy +import hashlib +import io +import json +import os +import shutil +import subprocess +import sys +import zipfile +from pathlib import Path +from typing import Any + +import pytest +import yaml + +from tests.unit.scripts.test_requirements_planning_stack_context import ( + CHECK_ROUTE, + REPO_ROOT, + REPOSITORY, + StackCase, + _git, + _planning_context, + planning_case as planning_case, +) + + +def _archive(payload: dict[str, Any]) -> bytes: + """Create clearly synthetic JSON-only artifact bytes for boundary probes.""" + stream = io.BytesIO() + with zipfile.ZipFile(stream, "w") as archive: + archive.writestr("requirements-evidence.json", json.dumps(payload)) + return stream.getvalue() + + +def _synthetic_proof(root: str) -> dict[str, Any]: + """Use provider-normalized synthetic plans solely for isolated boundary tests.""" + from specfact_requirements.requirements.lifecycle import build_plan + + mapping = "sha256:" + "0" * 64 + plan = build_plan( + mapping, [{"case_id": "SYNTHETIC", "method": "test", "node_id": "tests/control.py::test_runtime"}] + ) + return { + "schema_version": "2", + "required_maturity": "verified", + "observed_maturity": "verified", + "delivery_status": "implementation-verified", + "implementation_evidence": "passing-after-red-proven", + "verdict": "passed", + "gate_decision": "pass", + "findings": [], + "mapping_digest": mapping, + "plan_digest": plan["plan_digest"], + "plan": plan, + "execution_plan": plan, + "execution_proof": { + "run_stage": "final", + "source_ref": root, + "selectors": ["tests/control.py::test_runtime"], + "junit_digest": "sha256:" + "1" * 64, + "proof_basis": "red-junit", + }, + } + + +def _synthetic_artifact(root: str) -> dict[str, Any]: + """Bind explicit service identity independently from the proof document.""" + return { + "id": 3101, + "name": "requirements-evidence", + "expired": False, + "size_in_bytes": 4096, + "workflow_run": { + "id": 1101, + "repository_id": 1085706003, + "head_repository_id": 1085706003, + "head_sha": root, + "head_branch": "dependency-root", + }, + } + + +def _review_tools(repo: Path, binary: Path) -> Path: + """Adapt the review executable to its actual frozen final-context reader.""" + verifier = repo / "verifier/bin" + verifier.mkdir(parents=True) + review_python = verifier / "python" + review_python.write_text( + f"#!{sys.executable}\n" + "import json,os,sys\nfrom pathlib import Path\n" + "fixture=Path(os.environ['SPECFACT_MODULES_REPO'])\n" + "sys.path.insert(0,os.environ['TRUSTED_REQUIREMENTS_CORE'])\n" + "for source in (fixture/'packages').glob('*/src'): sys.path.insert(0,str(source))\n" + "from specfact_code_review.run.commands import _requirements_evidence_context\n" + "args=sys.argv[1:]\n" + "try: context=_requirements_evidence_context(Path(args[args.index('--requirements-evidence')+1]))\n" + "except Exception as error: print(str(error),file=sys.stderr);sys.exit(2)\n" + "Path(os.environ['REVIEW_RECEIPT']).write_text(json.dumps({'context':context.model_dump(),'args':args}))\n" + ) + review_python.chmod(0o755) + node = binary / "node" + node.write_text("#!/bin/sh\nprintf 'v24.16.0\\n'\n") + node.chmod(0o755) + (binary / "gh").write_text( + f"#!{sys.executable}\n" + "import base64,json,sys\nfrom pathlib import Path\n" + "routes=json.loads(Path(sys.argv[0]).with_name('api-routes.json').read_text())\n" + "value=routes[sys.argv[-1]]\n" + "if isinstance(value,dict) and 'archive_base64' in value: sys.stdout.buffer.write(base64.b64decode(value['archive_base64']))\n" + "else: print(json.dumps(value))\n" + ) + (binary / "gh").chmod(0o755) + return verifier + + +def _review_environment(case: StackCase, binary: Path, verifier: Path, runner: Path) -> dict[str, str]: + repo = case.repo + env = os.environ.copy() + env.update( + { + "PATH": str(binary) + os.pathsep + env.get("PATH", ""), + "RUNNER_TEMP": str(runner), + "GITHUB_EVENT_PATH": str(repo / "event.json"), + "GITHUB_WORKSPACE": str(repo), + "GITHUB_OUTPUT": str(runner / "output"), + "GITHUB_STEP_SUMMARY": str(runner / "summary"), + "REVIEW_BASE_BRANCH": "dev", + "PROOF_BASE_COMMIT": case.baseline, + "PLANNING_BASE_COMMIT": case.root, + "PROOF_HEAD_BRANCH": "dependency-root", + "REQUIREMENTS_VALIDATOR_ROOT": str(verifier.parent), + "TRUSTED_REQUIREMENTS_CORE": str(REPO_ROOT / "src"), + "PYLINT_WRAPPER": str(binary), + "FINAL_VERIFIER_ROOT": str(verifier.parent), + "FINAL_TRUSTED_CORE": str(REPO_ROOT / "src"), + "FINAL_NODE_BIN": str(binary), + "FINAL_PYLINT_WRAPPER": str(binary), + "FINAL_BASEDPYRIGHT_ROOT": str(binary), + "REVIEW_RECEIPT": str(runner / "review-receipt.json"), + } + ) + return env + + +@pytest.fixture +def review_case(planning_case: StackCase, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: + """Reuse isolated real Git/native identity and the verified fixture provider.""" + context = _planning_context(planning_case, monkeypatch) + fixture = Path(os.environ["SPECFACT_MODULES_REPO"]) + monkeypatch.setattr(sys, "path", [str(source) for source in (fixture / "packages").glob("*/src")] + sys.path) + report = _synthetic_proof(planning_case.root) + artifact = _synthetic_artifact(planning_case.root) + repo = planning_case.repo + _git(repo, "update-ref", "refs/remotes/origin/dev", planning_case.baseline) + runner = repo / "runner" + policy = runner / "trusted-stack-context" + (policy / "scripts").mkdir(parents=True) + for name in ("requirements_stack_context.py", "requirements_planning_context.py", "requirements_review_context.py"): + source = REPO_ROOT / "scripts" / name + if source.exists(): + shutil.copyfile(source, policy / "scripts" / name) + (policy / "context.json").write_text(json.dumps(context)) + final_report = repo / "root-final.json" + final_report.write_text(json.dumps(report)) + artifacts = repo / "artifacts/requirements-evidence" + artifacts.mkdir(parents=True) + planning_bytes = json.dumps( + {"schema_version": "2", "gate_decision": "pass", "required_maturity": "planned"} + ).encode() + (artifacts / "requirements-evidence-consumer.json").write_bytes(planning_bytes) + (runner / "final-requirements.json").write_bytes(planning_bytes) + binary = repo / "github-fixture-bin" + verifier = _review_tools(repo, binary) + env = _review_environment(planning_case, binary, verifier, runner) + return { + "case": planning_case, + "env": env, + "report": report, + "artifact": artifact, + "planning_bytes": planning_bytes, + "final_report": final_report, + "binary": binary, + } + + +def _proof_variant(report: dict[str, Any], variant: str) -> None: + """Keep proof-basis compatibility and deliberately malformed proof fixtures distinct.""" + if variant == "producer": + report["execution_proof"]["run_stage"] = "authored" + if variant == "foreign-proof": + report["execution_proof"]["source_ref"] = "f" * 40 + if variant == "incomplete-proof": + report["execution_plan"] = {} + if variant == "legacy": + report["implementation_evidence"] = "passing-after-legacy-tdd-ledger" + report["execution_proof"]["proof_basis"] = "legacy-tdd-ledger" + report["legacy_tdd_evidence"] = { + "schema_version": "1", + "kind": "legacy-tdd-ledger", + "change_id": "synthetic-control", + "ledger_digest": "sha256:" + "2" * 64, + "mapping_digest": report["mapping_digest"], + "plan_digest": report["plan_digest"], + } + + +def _artifact_routes(probe: dict[str, Any], variant: str) -> None: + """Supply deliberately malformed service metadata/proof without editing production logic.""" + import base64 + + case = probe["case"] + report = copy.deepcopy(probe["report"]) + artifact = copy.deepcopy(probe["artifact"]) + _proof_variant(report, variant) + if variant == "foreign-artifact": + artifact["workflow_run"]["head_repository_id"] = 999 + if variant == "expired": + artifact["expired"] = True + data = _archive(report) + artifact["digest"] = "sha256:" + hashlib.sha256(data).hexdigest() + artifact["size_in_bytes"] = len(data) + if variant == "oversized-metadata": + artifact["size_in_bytes"] = 16 * 1024 * 1024 + 1 + if variant == "corrupted": + data += b"corruption" + routes = copy.deepcopy(case.routes) + if variant == "stale-root": + routes[CHECK_ROUTE.format(root=case.root)]["check_runs"][0]["conclusion"] = "failure" + if variant == "stale-native-head": + number = case.event["pull_request"]["number"] + routes[f"/repos/{REPOSITORY}/pulls/{number}"]["head"]["sha"] = "f" * 40 + records = [artifact, copy.deepcopy(artifact)] if variant == "ambiguous" else [artifact] + routes[f"/repos/{REPOSITORY}/actions/runs/1101/artifacts?per_page=100&page=1"] = { + "total_count": len(records), + "artifacts": records, + } + routes[f"/repos/{REPOSITORY}/actions/artifacts/3101/zip"] = {"archive_base64": base64.b64encode(data).decode()} + (probe["binary"] / "api-routes.json").write_text(json.dumps(routes)) + + +def _run_review( + probe: dict[str, Any], final: bool, variant: str = "", ordinary: bool = False +) -> subprocess.CompletedProcess[str]: + """Execute repository-authored shell unchanged, with explicit service/tool test doubles.""" + case = probe["case"] + _artifact_routes(probe, variant) + env = probe["env"].copy() + receipt = Path(env["REVIEW_RECEIPT"]) + receipt.unlink(missing_ok=True) + for destination in ( + case.repo / "runner/final-requirements.json", + case.repo / "artifacts/requirements-evidence/requirements-evidence-consumer.json", + ): + destination.write_bytes(probe["planning_bytes"]) + if ordinary: + env.pop("PLANNING_BASE_COMMIT") + destination = case.repo / ( + "runner/final-requirements.json" + if final + else "artifacts/requirements-evidence/requirements-evidence-consumer.json" + ) + ordinary_report = copy.deepcopy(probe["report"]) + _proof_variant(ordinary_report, variant) + destination.write_bytes(json.dumps(ordinary_report).encode()) + workflow = yaml.safe_load((REPO_ROOT / ".github/workflows/requirements-evidence.yml").read_text()) + job = "requirements-evidence-final" if final else "requirements-evidence" + name = ( + "Run Code Review with trusted final Requirements context" + if final + else "Run Code Review with finalized Requirements context" + ) + scripts = [s["run"] for s in workflow["jobs"][job]["steps"] if s.get("name") == name] + assert len(scripts) == 1 + return subprocess.run( + ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", scripts[0]], + cwd=case.repo, + env=env, + capture_output=True, + text=True, + timeout=30, + check=False, + ) + + +def _assert_preserved_context(probe: dict[str, Any]) -> None: + """Verify unchanged genuine bytes, full inherited review and independent planning receipts.""" + receipt = json.loads(Path(probe["env"]["REVIEW_RECEIPT"]).read_text()) + assert receipt["context"]["source_ref"] == probe["case"].root + assert "src/runtime.py" in receipt["args"] + assert receipt["args"][receipt["args"].index("--enforcement") + 1] == "full" + context = Path(receipt["context"]["path"]) + assert context.read_bytes() == _archive_report_bytes(probe["report"]) + assert ( + probe["case"].repo / "artifacts/requirements-evidence/requirements-evidence-consumer.json" + ).read_bytes() == probe["planning_bytes"] + assert (probe["case"].repo / "runner/final-requirements.json").read_bytes() == probe["planning_bytes"] + + +def test_actual_consumers_preserve_final_root_context(review_case: dict[str, Any]) -> None: + """Both review consumers preserve genuine context bytes and full inherited scope.""" + for final in (False, True): + ordinary = _run_review(review_case, final, ordinary=True) + assert ordinary.returncode == 0, ordinary.stderr + result = _run_review(review_case, final) + assert result.returncode == 0, result.stderr + _assert_preserved_context(review_case) + ordinary_legacy = _run_review(review_case, final, "legacy", ordinary=True) + assert ordinary_legacy.returncode == 0, ordinary_legacy.stderr + legacy = _run_review(review_case, final, "legacy") + assert legacy.returncode == 0, legacy.stderr + assert _run_review(review_case, final, ordinary=True).returncode == 0 + (review_case["case"].repo / "artifacts/requirements-evidence/requirements-evidence-consumer.json").write_bytes( + review_case["planning_bytes"] + ) + (review_case["case"].repo / "runner/final-requirements.json").write_bytes(review_case["planning_bytes"]) + + +def _archive_report_bytes(report: dict[str, Any]) -> bytes: + return json.dumps(report).encode() + + +def test_actual_consumers_reject_context_forgery(review_case: dict[str, Any]) -> None: + """Artifact/proof attacks fail closed without preventing genuine finalized root review.""" + for final in (False, True): + for variant in ( + "producer", + "foreign-proof", + "foreign-artifact", + "expired", + "corrupted", + "ambiguous", + "stale-root", + "stale-native-head", + "incomplete-proof", + "oversized-metadata", + ): + result = _run_review(review_case, final, variant) + assert result.returncode != 0, variant + assert not Path(review_case["env"]["REVIEW_RECEIPT"]).exists(), variant + valid = _run_review(review_case, final) + assert valid.returncode == 0, valid.stderr diff --git a/tests/unit/security/test_project_bundle_authority.py b/tests/unit/security/test_project_bundle_authority.py new file mode 100644 index 000000000..fa6aba87e --- /dev/null +++ b/tests/unit/security/test_project_bundle_authority.py @@ -0,0 +1,654 @@ +"""Paired real-artifact compatibility and forgery controls for project bundle roots.""" + +import base64 +import hashlib +import importlib.machinery +import importlib.util +import marshal +import os +import py_compile +import shutil +import struct +import subprocess +import sys +from pathlib import Path +from typing import Any + +import pytest +import typer +import yaml +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from specfact_cli.registry import CommandRegistry, module_installer, module_packages as packages +from specfact_cli.registry.bridge_registry import BridgeRegistry +from specfact_cli.registry.extension_registry import ExtensionRegistry +from tests.unit.security.test_module_command_squatting import ( + _register_package, + _reset_registration, + clean_registry as clean_registry, +) + + +BUNDLE_ROOTS = ( + ("specfact-project", "project"), + ("specfact-backlog", "backlog"), + ("specfact-codebase", "code"), + ("specfact-code-review", "code"), + ("specfact-spec", "spec"), + ("specfact-govern", "govern"), + ("specfact-requirements", "requirements"), +) + + +PROBE_APP = ( + "import typer\napp = typer.Typer()\nreview = typer.Typer()\n" + "app.add_typer(review, name='review')\n" + "@review.command()\ndef probe(): pass\nUNSIGNED_CACHE_PROBE = True\n" +) + + +@pytest.fixture(autouse=True) +def preserve_fixture_cache_authenticity(monkeypatch: pytest.MonkeyPatch) -> None: + """Keep pytest's rewritten caches out of signed fixtures; tests explicitly author genuine and forged caches.""" + monkeypatch.setattr(sys, "dont_write_bytecode", True) + + +def _official_project_packages(tmp_path: Path) -> list[tuple[Path, packages.ModulePackageMetadata]]: + """Copy the immutable reviewed official artifacts into a real project discovery root.""" + configured_repository = os.environ.get("SPECFACT_MODULES_REPO") + assert configured_repository, "Required signed companion fixture: configure SPECFACT_MODULES_REPO" + source = Path(configured_repository) / "packages" + project_modules = tmp_path / ".specfact" / "modules" + for bundle, _command in BUNDLE_ROOTS: + shutil.copytree( + source / bundle, project_modules / bundle, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo") + ) + return packages.discover_package_metadata(project_modules, source="project") + + +def _add_unsigned_executable_probe(package_dir: Path, raw: dict[str, Any], forgery: str) -> None: + """Construct harmless executable artifacts whose bytes are omitted by the signing filters.""" + source, _locations = packages._resolve_command_loader_path(package_dir, raw["name"], raw["commands"][0]) + if forgery == "cached-bytecode": + target = Path(importlib.util.cache_from_source(str(source))) + target.parent.mkdir(exist_ok=True) + header = importlib.util.MAGIC_NUMBER + struct.pack( + " tuple[Path, packages.ModulePackageMetadata]: + """Create a separate hostile copy without modifying the reviewed fixture or user keys.""" + configured_repository = os.environ.get("SPECFACT_MODULES_REPO") + assert configured_repository, "Required signed companion fixture: configure SPECFACT_MODULES_REPO" + source = Path(configured_repository) / "packages" / bundle + package_dir = tmp_path / ".specfact" / "modules" / bundle + shutil.copytree(source, package_dir, ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo")) + manifest = package_dir / "module-package.yaml" + raw = yaml.safe_load(manifest.read_text(encoding="utf-8")) + if forgery == "unsigned": + raw.pop("integrity") + elif forgery == "publisher": + raw["publisher"]["name"] = "hostile" + elif forgery == "signature": + raw["integrity"]["signature"] = base64.b64encode(bytes(64)).decode("ascii") + manifest.write_text(yaml.safe_dump(raw), encoding="utf-8") + _add_unsigned_executable_probe(package_dir, raw, forgery) + if forgery == "directory-symlink": + external = tmp_path / "external" + external.mkdir() + (external / "__init__.py").write_text("VALUE = 'hostile'\n", encoding="utf-8") + (package_dir / "src" / "hostile_dependency").symlink_to(external, target_is_directory=True) + if forgery == "git-untracked": + subprocess.run(["git", "init", "--quiet", str(package_dir)], check=True, capture_output=True) + subprocess.run(["git", "-C", str(package_dir), "add", "."], check=True, capture_output=True) + if forgery in {"payload", "receipt", "workspace-key", "git-untracked"}: + (package_dir / "src" / "hostile_probe.py").write_text("VALUE = 'tampered'\n", encoding="utf-8") + if forgery in {"receipt", "workspace-key"}: + payload = module_installer._module_artifact_payload_signed(package_dir) + raw["integrity"]["checksum"] = f"sha256:{hashlib.sha256(payload).hexdigest()}" + if forgery == "receipt": + raw["integrity"].pop("signature") + (package_dir / module_installer.INSTALL_VERIFIED_CHECKSUM_FILE).write_text( + raw["integrity"]["checksum"], encoding="utf-8" + ) + else: + private_key = Ed25519PrivateKey.generate() + raw["integrity"]["signature"] = base64.b64encode(private_key.sign(payload)).decode("ascii") + public_key = private_key.public_key().public_bytes( + serialization.Encoding.PEM, serialization.PublicFormat.SubjectPublicKeyInfo + ) + monkeypatch.setenv("SPECFACT_MODULE_PUBLIC_KEY_PEM", public_key.decode("ascii")) + manifest.write_text(yaml.safe_dump(raw), encoding="utf-8") + discovered = packages.discover_package_metadata(package_dir.parent, source="project") + assert len(discovered) == 1 + return discovered[0] + + +def _signature_backend_unavailable(*_args: object) -> None: + """Simulate the real unavailable-verifier diagnostic without changing the admission decision.""" + raise ValueError("Signature verification backend unavailable") + + +def _assert_key_failures_are_rejected( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + official: list[tuple[Path, packages.ModulePackageMetadata]], + command: str, + grouped: bool, +) -> None: + """Prove missing, empty, malformed and unavailable core-key verification fails closed.""" + trusted_key = module_installer._bundled_public_key_path().read_text(encoding="utf-8") + key_paths = [tmp_path / "missing-key.pem", tmp_path / "empty-key.pem", tmp_path / "malformed-key.pem"] + key_paths[1].write_text("", encoding="utf-8") + key_paths[2].write_text("not a public key", encoding="utf-8") + for key_path in key_paths: + with monkeypatch.context() as context: + context.setattr(module_installer, "_bundled_public_key_path", lambda path=key_path: path) + context.setenv("SPECFACT_MODULE_PUBLIC_KEY_PEM", trusted_key) + context.setattr(packages, "discover_all_package_metadata", lambda **_kwargs: official) + packages.register_module_package_commands(allow_unsigned=True, category_grouping_enabled=grouped) + assert CommandRegistry.get_module_metadata(command) is None, "Invalid core key must fail closed" + assert not packages._ACTIVE_MODULE_SRC_DIRS + _reset_registration() + with monkeypatch.context() as context: + context.setattr(module_installer, "verify_signature", _signature_backend_unavailable) + context.setattr(packages, "discover_all_package_metadata", lambda **_kwargs: official) + packages.register_module_package_commands(allow_unsigned=True, category_grouping_enabled=grouped) + assert CommandRegistry.get_module_metadata(command) is None, "Unavailable verifier must fail closed" + assert not packages._ACTIVE_MODULE_SRC_DIRS + _reset_registration() + + +def _assert_official_artifact_provenance(official: list[tuple[Path, packages.ModulePackageMetadata]]) -> None: + """Independently verify every compatibility fixture checksum and signature against the core key.""" + key = module_installer._bundled_public_key_path().read_text(encoding="utf-8") + for package_dir, metadata in official: + assert module_installer.verify_module_artifact( + package_dir, + metadata, + allow_unsigned=False, + require_integrity=True, + require_signature=True, + public_key_pem=key, + ), "The compatibility artifact must genuinely verify against the core-bundled key" + + +def _assert_registered_bundle_surface( + official: list[tuple[Path, packages.ModulePackageMetadata]], bundle: str, command: str +) -> None: + """Observe the specific authenticated package surface, including the review add-on subgroup.""" + assert CommandRegistry.get_module_metadata(command) is not None, ( + "Verified project installation must retain its root" + ) + package_dir = next(path for path, metadata in official if metadata.name == f"nold-ai/{bundle}") + assert (package_dir / "src").resolve() in packages._ACTIVE_MODULE_SRC_DIRS + app = CommandRegistry.get_module_typer(command) + assert isinstance(app, typer.Typer) + if bundle == "specfact-code-review": + assert any(group.name == "review" for group in app.registered_groups), "Verified add-on must expose code review" + + +def _assert_forged_bundle_has_no_surface( + context: pytest.MonkeyPatch, + forged: tuple[Path, packages.ModulePackageMetadata], + grouped: bool, + dependency: tuple[Path, packages.ModulePackageMetadata], +) -> None: + """Reject the forged package while retaining the real codebase dependency for review extension probes.""" + package_dir, metadata = forged + command = metadata.commands[0] + if metadata.name == "nold-ai/specfact-code-review": + context.setattr(packages, "discover_all_package_metadata", lambda **_kwargs: [dependency, forged]) + context.setattr(packages, "read_modules_state", dict) + packages.register_module_package_commands(allow_unsigned=True, category_grouping_enabled=grouped) + app = CommandRegistry.get_module_typer("code") + assert isinstance(app, typer.Typer) + assert not any(group.name == "review" for group in app.registered_groups), ( + "Forged add-on must not extend codebase" + ) + assert (dependency[0] / "src").resolve() in packages._ACTIVE_MODULE_SRC_DIRS + else: + _register_package(context, forged, grouped) + assert CommandRegistry.get_module_metadata(command) is None, "Forged package must not supply a root" + assert not CommandRegistry._module_entries + assert (package_dir / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + + +def _populate_current_interpreter_caches( + official: list[tuple[Path, packages.ModulePackageMetadata]], +) -> None: + """Create genuine caches independently before public repeated registration and app loading.""" + sources = [ + str(packages._resolve_command_loader_path(path, metadata.name, metadata.commands[0])[0]) + for path, metadata in official + ] + subprocess.run([sys.executable, "-m", "py_compile", *sources], check=True, capture_output=True) + + +@pytest.mark.parametrize( + ("bundle", "command", "grouped"), + [(bundle, command, grouped) for bundle, command in BUNDLE_ROOTS for grouped in (True, False)], + ids=[ + f"{bundle.removeprefix('specfact-')}-{'grouped' if grouped else 'legacy'}" + for bundle, _ in BUNDLE_ROOTS + for grouped in (True, False) + ], +) +def test_verified_project_bundle_preserves_root_and_rejects_forgery( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, bundle: str, command: str, grouped: bool +) -> None: + """Keep the actual signed official root while rejecting every unverified replacement before effects.""" + official = _official_project_packages(tmp_path / "official") + _assert_official_artifact_provenance(official) + monkeypatch.setattr(packages, "discover_all_package_metadata", lambda **_kwargs: official) + monkeypatch.setattr(packages, "read_modules_state", dict) + packages.register_module_package_commands(allow_unsigned=True, category_grouping_enabled=grouped) + assert CommandRegistry.get_module_metadata(command) is not None, ( + "Verified project installation must retain its root" + ) + _populate_current_interpreter_caches(official) + _reset_registration() + packages.register_module_package_commands(allow_unsigned=True, category_grouping_enabled=grouped) + _assert_registered_bundle_surface(official, bundle, command) + dependency = next(package for package in official if package[1].name == "nold-ai/specfact-codebase") + _reset_registration() + for forgery in ( + "unsigned", + "payload", + "publisher", + "signature", + "receipt", + "workspace-key", + "git-untracked", + "directory-symlink", + "cached-bytecode", + "standalone-bytecode", + "excluded-source", + ): + with monkeypatch.context() as context: + forged = _forged_project_package(tmp_path / forgery, bundle, forgery, context) + _assert_forged_bundle_has_no_surface(context, forged, grouped, dependency) + _reset_registration() + _assert_key_failures_are_rejected(monkeypatch, tmp_path, official, command, grouped) + + +def _prepare_source_snapshot_swap(monkeypatch: pytest.MonkeyPatch, source: Path) -> None: + """Restore signed bytes after the compiler's first source read to reproduce snapshot separation.""" + signed = source.read_bytes() + cache = Path(importlib.util.cache_from_source(str(source))) + cache.parent.mkdir(exist_ok=True) + header = importlib.util.MAGIC_NUMBER + struct.pack(" bytes: + """Expose altered bytes once, then restore the original signed file for later reads.""" + data = original_read(path) + if path == source: + reads["source"] += 1 + if reads["source"] == 1: + source.write_bytes(signed) + return data + + monkeypatch.setattr(Path, "read_bytes", swapped_read) + + +def _assert_codebase_authority_available(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Pair a hostile-artifact probe with real authenticated project-root compatibility.""" + official = _official_project_packages(tmp_path) + monkeypatch.setattr(packages, "discover_all_package_metadata", lambda **_kwargs: official) + monkeypatch.setattr(packages, "read_modules_state", dict) + packages.register_module_package_commands(allow_unsigned=True) + assert CommandRegistry.get_module_metadata("code") is not None + assert isinstance(CommandRegistry.get_module_typer("code"), typer.Typer) + _reset_registration() + + +def test_signed_source_snapshot_cannot_be_replaced(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Deny a valid-app cache when the source presented to compilation differs from signed bytes.""" + import py_compile + + _assert_codebase_authority_available(monkeypatch, tmp_path / "official") + forged = _forged_project_package(tmp_path / "forged", "specfact-codebase", "unchanged", monkeypatch) + source, _locations = packages._resolve_command_loader_path(forged[0], forged[1].name, "code") + _prepare_source_snapshot_swap(monkeypatch, source) + compiled: list[str] = [] + original_compile = py_compile.compile + + def observed_compiler(filename: str, *args: Any, **kwargs: Any) -> object: + """Observe compiler work separately from the authority assertion.""" + compiled.append(filename) + return original_compile(filename, *args, **kwargs) + + monkeypatch.setattr(py_compile, "compile", observed_compiler) + _register_package(monkeypatch, forged) + assert not compiled, "Unauthenticated source must be rejected before compiler work" + assert not packages._ACTIVE_MODULE_SRC_DIRS + assert CommandRegistry.get_module_metadata("code") is None, "The forged cache must not grant authority on retry" + assert not packages._ACTIVE_MODULE_SRC_DIRS + + +@pytest.mark.parametrize("suffix", ["dll", "dylib", "bundle", "so", "pyd"]) +def test_project_root_rejects_excluded_native_artifacts( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, suffix: str +) -> None: + """Reject omitted platform-library files without claiming to execute a native-load exploit.""" + _assert_codebase_authority_available(monkeypatch, tmp_path / "official") + forged = _forged_project_package(tmp_path / "forged", "specfact-codebase", "unchanged", monkeypatch) + target = forged[0] / "src" / "logs" / f"unsigned_probe.{suffix}" + target.parent.mkdir() + target.write_bytes(b"synthetic unsigned native-artifact boundary probe") + _register_package(monkeypatch, forged) + assert CommandRegistry.get_module_metadata("code") is None, "Excluded native artifact must not grant authority" + assert not packages._ACTIVE_MODULE_SRC_DIRS + + +def _isolate_discovery_roots(monkeypatch: pytest.MonkeyPatch, workspace: Path, user_root: Path) -> None: + """Exercise real source-priority discovery without reading operator module locations.""" + from specfact_cli.registry import module_discovery + + monkeypatch.chdir(workspace) + monkeypatch.setattr(packages, "get_modules_root", lambda: workspace / "missing-builtin") + monkeypatch.setattr(module_discovery, "USER_MODULES_ROOT", user_root) + monkeypatch.setattr(module_discovery, "MARKETPLACE_MODULES_ROOT", workspace / "missing-marketplace") + monkeypatch.setattr(module_discovery, "CUSTOM_MODULES_ROOT", workspace / "missing-custom") + monkeypatch.delenv("SPECFACT_MODULES_ROOTS", raising=False) + monkeypatch.setattr(packages, "read_modules_state", dict) + + +@pytest.mark.parametrize("grouped", [True, False], ids=["grouped", "legacy"]) +def test_rejected_project_preserves_same_name_installed_bundle( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, grouped: bool +) -> None: + """Preserve the real installed root and add-on after actual discovery rejects a same-name workspace claim.""" + official = _official_project_packages(tmp_path / "user") + installed = next(package for package in official if package[1].name == "nold-ai/specfact-codebase") + forged = _forged_project_package(tmp_path / "workspace", "specfact-codebase", "unsigned", monkeypatch) + _isolate_discovery_roots(monkeypatch, tmp_path / "workspace", installed[0].parent) + ordinary = packages.discover_all_package_metadata() + assert len({meta.name for _path, meta in ordinary}) == len(ordinary) + assert next(path for path, meta in ordinary if meta.name == installed[1].name) == forged[0] + packages.register_module_package_commands(allow_unsigned=True, category_grouping_enabled=grouped) + _assert_registered_bundle_surface(official, "specfact-codebase", "code") + _assert_registered_bundle_surface(official, "specfact-code-review", "code") + assert (forged[0] / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + _reset_registration() + shutil.copyfile( + Path(os.environ["SPECFACT_MODULES_REPO"]) / "packages/specfact-codebase/module-package.yaml", + forged[0] / "module-package.yaml", + ) + packages.register_module_package_commands(allow_unsigned=True, category_grouping_enabled=grouped) + _assert_registered_bundle_surface([forged], "specfact-codebase", "code") + assert (installed[0] / "src").resolve() not in packages._ACTIVE_MODULE_SRC_DIRS + + +@pytest.mark.parametrize("mode", ["state", "disable-override"]) +def test_disabled_project_roots_skip_payload_verification( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, mode: str +) -> None: + """Skip disabled payload reads, preserve explicit re-enablement, and deny an enabled unsigned replacement.""" + package_dir, metadata = _forged_project_package( + tmp_path / "workspace", "specfact-codebase", "unchanged", monkeypatch + ) + _isolate_discovery_roots(monkeypatch, tmp_path / "workspace", tmp_path / "missing-user") + stored_enabled = mode != "state" + monkeypatch.setattr(packages, "read_modules_state", lambda: {metadata.name: {"enabled": stored_enabled}}) + reads: list[Path] = [] + original_read = Path.read_bytes + + def observed_read(path: Path) -> bytes: + """Observe real source payload IO without altering verifier decisions.""" + if path.suffix == ".py" and path.is_relative_to(package_dir): + reads.append(path) + return original_read(path) + + monkeypatch.setattr(Path, "read_bytes", observed_read) + packages.register_module_package_commands(disable_ids=[metadata.name] if stored_enabled else []) + assert not reads, "Disabled project names must not reach artifact payload reads" + assert not packages._ACTIVE_MODULE_SRC_DIRS + assert not CommandRegistry._module_entries + packages.register_module_package_commands(enable_ids=[metadata.name]) + assert isinstance(CommandRegistry.get_module_typer("code"), typer.Typer) + assert reads, "The authentic enabled counterpart must actually pass through verification" + _reset_registration() + manifest = package_dir / "module-package.yaml" + raw = yaml.safe_load(manifest.read_text(encoding="utf-8")) + raw.pop("integrity") + manifest.write_text(yaml.safe_dump(raw), encoding="utf-8") + packages.register_module_package_commands(enable_ids=[metadata.name], allow_unsigned=True) + assert CommandRegistry.get_module_metadata("code") is None, "Enabled unsigned replacement must remain denied" + assert not packages._ACTIVE_MODULE_SRC_DIRS + + +def _isolated_manifest_effect_registries( + monkeypatch: pytest.MonkeyPatch, +) -> tuple[ExtensionRegistry, BridgeRegistry, list[str]]: + """Use real registries and a harmless ambient converter to observe forged declarations.""" + from types import ModuleType + + constructed: list[str] = [] + + class Converter: + """Harmless real converter whose constructor makes unsigned bridge activation observable.""" + + def __init__(self) -> None: + """Record instantiation without performing an external action.""" + constructed.append("manifest-bridge") + + def to_bundle(self, external_data: dict[str, Any]) -> dict[str, Any]: + """Provide the required inbound converter behavior.""" + return dict(external_data) + + def from_bundle(self, bundle_data: dict[str, Any]) -> dict[str, Any]: + """Provide the required outbound converter behavior.""" + return dict(bundle_data) + + ambient = ModuleType("specfact_manifest_binding_probe") + ambient.__dict__["Converter"] = Converter + monkeypatch.setitem(sys.modules, ambient.__name__, ambient) + extensions, bridges = ExtensionRegistry(), BridgeRegistry() + monkeypatch.setattr(packages, "get_extension_registry", lambda: extensions) + monkeypatch.setattr(packages, "BRIDGE_REGISTRY", bridges) + return extensions, bridges, constructed + + +def _restore_manifest_after_discovery_read(monkeypatch: pytest.MonkeyPatch, manifest: Path, field: str) -> None: + """Present forged registration fields once, restoring authentic signed YAML before verification.""" + signed = manifest.read_text(encoding="utf-8") + raw = yaml.safe_load(signed) + additions = { + "schema": ("schema_extensions", [{"target": "Feature", "field": "manifest_probe", "type_hint": "str"}]), + "bridge": ( + "service_bridges", + [{"id": "manifest-probe", "converter_class": "specfact_manifest_binding_probe.Converter"}], + ), + "commands": ("commands", [*raw["commands"], "manifest-probe"]), + "version": ("version", "999.0.0"), + } + key, value = additions[field] + raw[key] = value + manifest.write_text(yaml.safe_dump(raw), encoding="utf-8") + original_read = Path.read_text + observed: list[Path] = [] + + def swapped_manifest_read(path: Path, *args: Any, **kwargs: Any) -> str: + """Restore signed bytes only after the real discovery parser has received forged text.""" + data = original_read(path, *args, **kwargs) + if path == manifest and not observed: + observed.append(path) + manifest.write_text(signed, encoding="utf-8") + return data + + monkeypatch.setattr(Path, "read_text", swapped_manifest_read) + + +@pytest.mark.parametrize("field", ["schema", "bridge", "commands", "version"]) +def test_discovery_fields_match_signed_manifest(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, field: str) -> None: + """Reject stale forged metadata while preserving fresh loading of the authentic restored artifact.""" + package = _forged_project_package(tmp_path / "workspace", "specfact-codebase", "unchanged", monkeypatch) + _assert_official_artifact_provenance([package]) + _isolate_discovery_roots(monkeypatch, tmp_path / "workspace", tmp_path / "missing-user") + extensions, bridges, constructed = _isolated_manifest_effect_registries(monkeypatch) + before_extensions, before_bridges = extensions.list_all(), bridges.as_mapping() + with monkeypatch.context() as replacement: + _restore_manifest_after_discovery_read(replacement, package[0] / "module-package.yaml", field) + packages.register_module_package_commands(allow_unsigned=True) + _assert_official_artifact_provenance([package]) + assert extensions.list_all() == before_extensions, ( + "Unsigned discovery schema must not gain authenticated authority" + ) + assert bridges.as_mapping() == before_bridges, "Unsigned discovery bridge must not gain authenticated authority" + assert not constructed, "An unsigned bridge declaration must not instantiate a converter" + assert not CommandRegistry._module_entries, "Unsigned discovery fields must not register commands" + assert not packages._ACTIVE_MODULE_SRC_DIRS + _reset_registration() + packages.register_module_package_commands(allow_unsigned=True) + _assert_registered_bundle_surface([package], "specfact-codebase", "code") + + +def _observe_package_activation(monkeypatch: pytest.MonkeyPatch) -> list[Path]: + """Record every genuine activation, including roots cleared by a subsequent lazy registration.""" + activated: list[Path] = [] + original_remember = packages._remember_active_module_src + + def observe_activation(path: Path) -> None: + original_remember(path) + activated.extend(packages._ACTIVE_MODULE_SRC_DIRS) + + monkeypatch.setattr(packages, "_remember_active_module_src", observe_activation) + return activated + + +def _observe_live_artifact_mutation(monkeypatch: pytest.MonkeyPatch, target: Path) -> tuple[list[bool], list[Path]]: + """Observe genuine verification and transient activation while changing the live artifact.""" + verified: list[bool] = [] + original_verify = module_installer.verify_signature + activated = _observe_package_activation(monkeypatch) + + def change_after_real_signature(artifact: bytes | Path, signature: str, key: str) -> bool: + """Preserve real cryptographic verification and mutate the artifact only after it succeeds.""" + result = original_verify(artifact, signature, key) + if not verified: + verified.append(result) + target.write_text(PROBE_APP, encoding="utf-8") + return result + + monkeypatch.setattr(module_installer, "verify_signature", change_after_real_signature) + return verified, activated + + +@pytest.mark.parametrize("mutation", ["added-source", "changed-source"]) +def test_live_executable_inventory_matches_verified_snapshot( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, mutation: str +) -> None: + """Deny executable changes during real verification instead of treating hashable paths as authenticated.""" + package = _forged_project_package(tmp_path / "workspace", "specfact-codebase", "unchanged", monkeypatch) + _isolate_discovery_roots(monkeypatch, tmp_path / "workspace", tmp_path / "missing-user") + _assert_official_artifact_provenance([package]) + source, _locations = packages._resolve_command_loader_path(package[0], package[1].name, "code") + target = package[0] / "src" / "unsigned_inventory_probe.py" if mutation == "added-source" else source + original = target.read_bytes() if target.exists() else None + with monkeypatch.context() as mutation_context: + verified, activated = _observe_live_artifact_mutation(mutation_context, target) + packages.register_module_package_commands(allow_unsigned=True) + assert verified == [True], "The original captured artifact must genuinely verify before mutation" + assert not activated, "Unauthenticated live executables must never activate, even transiently" + assert not packages._ACTIVE_MODULE_SRC_DIRS, "Unauthenticated live executables must not activate their package" + assert not CommandRegistry._module_entries + if original is None: + target.unlink() + else: + target.write_bytes(original) + _reset_registration() + packages.register_module_package_commands(allow_unsigned=True) + _assert_registered_bundle_surface([package], "specfact-codebase", "code") + + +def _assert_equivalent_static_mutation_denied(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, mutation: str) -> None: + """Pair the compiler interleaving with the same unauthenticated artifact present before admission.""" + forgery = "standalone-bytecode" if mutation == "added-bytecode" else "unchanged" + forged = _forged_project_package(tmp_path, "specfact-codebase", forgery, monkeypatch) + if mutation == "changed-source": + source, _locations = packages._resolve_command_loader_path(forged[0], forged[1].name, "code") + source.write_text(PROBE_APP, encoding="utf-8") + with monkeypatch.context() as negative: + _register_package(negative, forged) + assert not packages._ACTIVE_MODULE_SRC_DIRS, "A static unsigned executable must not activate its package" + assert not CommandRegistry._module_entries + _reset_registration() + + +def _mutate_during_cache_compilation(monkeypatch: pytest.MonkeyPatch, target: Path) -> list[str]: + """Run the real standard compiler before changing the candidate tree during its admission check.""" + compiled: list[str] = [] + original_compile = py_compile.compile + + def change_after_compilation(filename: str, *args: Any, **kwargs: Any) -> object: + result = original_compile(filename, *args, **kwargs) + if not compiled: + compiled.append(filename) + if target.suffix == ".pyc": + target.write_bytes(Path(str(kwargs["cfile"])).read_bytes()) + else: + target.write_text(PROBE_APP, encoding="utf-8") + return result + + monkeypatch.setattr(py_compile, "compile", change_after_compilation) + return compiled + + +@pytest.mark.parametrize("mutation", ["added-bytecode", "changed-source"]) +def test_cache_compilation_finishes_before_admission_inventory( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, mutation: str +) -> None: + """Reject mutations during real cache reproduction before any transient package activation.""" + _assert_equivalent_static_mutation_denied(monkeypatch, tmp_path / "static", mutation) + package = _forged_project_package(tmp_path / "workspace", "specfact-codebase", "unchanged", monkeypatch) + _isolate_discovery_roots(monkeypatch, tmp_path / "workspace", tmp_path / "missing-user") + _assert_official_artifact_provenance([package]) + _populate_current_interpreter_caches([package]) + source, _locations = packages._resolve_command_loader_path(package[0], package[1].name, "code") + target = package[0] / "src" / "unsigned_inventory_probe.pyc" if mutation == "added-bytecode" else source + original = target.read_bytes() if target.exists() else None + with monkeypatch.context() as mutation_context: + activated = _observe_package_activation(mutation_context) + compiled = _mutate_during_cache_compilation(mutation_context, target) + packages.register_module_package_commands(allow_unsigned=True) + assert compiled, "The genuine captured source must really undergo standard cache compilation" + assert not activated, "A mutation during cache reproduction must never activate its package" + assert not CommandRegistry._module_entries + if original is None: + target.unlink() + else: + target.write_bytes(original) + _reset_registration() + packages.register_module_package_commands(allow_unsigned=True) + _assert_registered_bundle_surface([package], "specfact-codebase", "code") diff --git a/tests/unit/security/test_registration_review_oracles.py b/tests/unit/security/test_registration_review_oracles.py new file mode 100644 index 000000000..0017f01b8 --- /dev/null +++ b/tests/unit/security/test_registration_review_oracles.py @@ -0,0 +1,88 @@ +"""Public registration oracles isolate identity, cycle compatibility and explicit refusal.""" + +import logging +from pathlib import Path + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + +from specfact_cli.registry import CommandRegistry, module_installer, module_packages as packages +from tests.unit.security.test_module_command_squatting import _reset_registration +from tests.unit.security.test_package_candidate_eligibility import ( + _assert_active, + _provider, + _register_candidates, + _signed_copy, + selection_key as selection_key, + test_conflicting_dependency_cycle_refuses_only_unstable_component as _exercise_conflict, + test_consistent_dependency_cycle_keeps_commands as _exercise_consistent_cycle, +) +from tests.unit.security.test_project_bundle_authority import _isolate_discovery_roots + + +def _verified_project(parent: Path, key: Ed25519PrivateKey, publisher: str) -> Path: + """Authenticate the changed publisher with a valid signature before testing identity admission.""" + path = _signed_copy(parent / ".specfact/modules", key, None, publisher={"name": publisher}) + metadata = packages.discover_package_metadata(path.parent, source="project")[0][1] + public = module_installer._bundled_public_key_path().read_text(encoding="utf-8") + assert module_installer.verify_module_artifact( + path, metadata, public_key_pem=public, allow_unsigned=False, require_signature=True + ), "The identity probe must have genuine valid integrity under the bundled test anchor" + return path + + +@pytest.mark.parametrize("grouped", [True, False]) +def test_validly_signed_noncanonical_publisher_is_denied( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey, grouped: bool +) -> None: + """Keep a canonical signed root, then deny a noncanonical publisher with equally valid integrity.""" + canonical = _verified_project(tmp_path / "canonical", selection_key, "nold-ai") + _isolate_discovery_roots(monkeypatch, tmp_path / "canonical", tmp_path / "missing-user") + packages.register_module_package_commands(allow_unsigned=False, category_grouping_enabled=grouped) + assert CommandRegistry.get_module_metadata("code") is not None + _assert_active([canonical], []) + _reset_registration() + noncanonical = _verified_project(tmp_path / "noncanonical", selection_key, "hostile") + _isolate_discovery_roots(monkeypatch, tmp_path / "noncanonical", tmp_path / "missing-user") + packages.register_module_package_commands(allow_unsigned=False, category_grouping_enabled=grouped) + assert CommandRegistry.get_module_metadata("code") is None, ( + "Valid crypto cannot replace canonical publisher identity" + ) + _assert_active([], [noncanonical]) + + +def test_consistent_cycle_survives_ineligible_priority_copy( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path, selection_key: Ed25519PrivateKey +) -> None: + """Retain healthy cyclic commands both alone and behind an ineligible same-name copy.""" + _exercise_consistent_cycle(monkeypatch, tmp_path / "healthy", selection_key) + _reset_registration() + rejected = _signed_copy(tmp_path / "rejected", selection_key, "core") + consumer = _signed_copy(tmp_path / "consumer", selection_key, None, module_dependencies=["nold-ai/specfact-spec"]) + provider = _provider( + tmp_path / "provider", selection_key, "1.0.0", module_dependencies=["nold-ai/specfact-codebase"] + ) + _register_candidates(monkeypatch, [rejected, consumer, provider]) + _assert_active([consumer, provider], [rejected]) + assert CommandRegistry.get_module_metadata("code") is not None + assert CommandRegistry.get_module_metadata("spec") is not None + + +def test_conflict_refusal_has_explicit_registration_diagnostic( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + selection_key: Ed25519PrivateKey, + caplog: pytest.LogCaptureFixture, +) -> None: + """Observe actual logging while the public conflict control proves refusal and unrelated survival.""" + logger = logging.getLogger("tests.registration.conflict-refusal") + monkeypatch.setattr(packages, "get_bridge_logger", lambda _name: logger) + with caplog.at_level(logging.WARNING): + _exercise_conflict(monkeypatch, tmp_path, selection_key) + assert any( + record.name == logger.name + and record.levelno == logging.WARNING + and record.getMessage() + == "Refusing conflicting module dependency selection: nold-ai/specfact-codebase, nold-ai/specfact-spec" + for record in caplog.records + ), "Refusing a contradictory priority set must emit the explicit registration diagnostic" diff --git a/tests/unit/security/test_release_promotion_security_gates.py b/tests/unit/security/test_release_promotion_security_gates.py index 81daeb93d..e1f0aac25 100644 --- a/tests/unit/security/test_release_promotion_security_gates.py +++ b/tests/unit/security/test_release_promotion_security_gates.py @@ -4,6 +4,7 @@ import importlib.util import json +import re import subprocess import sys import tomllib @@ -163,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: @@ -358,7 +359,7 @@ def test_requirements_archive_uses_one_immutable_merge_base() -> None: workflow = _workflow(".github/workflows/requirements-evidence.yml") job = cast(dict[str, Any], cast(dict[str, Any], workflow["jobs"])["requirements-evidence-producer"]) command = str(_named_step(job, "Run Requirements evidence gate").get("run", "")) - assert 'evidence_base_commit="$(git merge-base "origin/${EVIDENCE_BASE_BRANCH}" HEAD)"' in command + assert 'evidence_base_commit="${PLANNING_BASE_COMMIT:-${PROOF_BASE_COMMIT:-}}"' in command assert '"${evidence_base_commit}..HEAD"' in command assert 'git ls-tree -r -z --name-only "$evidence_base_commit"' in command assert 'git ls-tree "$evidence_base_commit"' in command @@ -389,21 +390,41 @@ def test_every_code_review_lock_install_has_exact_closure_proof() -> None: assert install.index(closure_proof) < install.index("uv pip install") -def test_frozen_graph_uses_fixed_semgrep_mcp_pair_without_waiver() -> None: - """All authoritative dependency surfaces select the reviewed fixed line.""" +def test_frozen_graph_uses_official_semgrep_with_bounded_mcp_exception() -> None: project = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) dev = cast(list[str], project["project"]["optional-dependencies"]["dev"]) scanning = cast(list[str], project["project"]["optional-dependencies"]["scanning"]) - assert "semgrep>=1.175.0" in dev - assert "semgrep>=1.175.0" in scanning + assert {"semgrep>=1.175.0"} <= set(dev) & set(scanning) locked = (REPO_ROOT / "requirements" / "ci" / "locked.txt").read_text(encoding="utf-8") - assert "semgrep==1.175.0" in locked - assert "mcp==1.29.0" in locked + assert set(re.findall(r"^(?:semgrep|mcp)==[^\s]+", locked, re.MULTILINE)) == {"semgrep==1.179.0", "mcp==1.29.0"} policy = json.loads((REPO_ROOT / "ci" / "security-tool-minimum-versions.json").read_text(encoding="utf-8")) assert policy["minimum_versions"] == {"mcp": "1.28.1", "semgrep": "1.175.0"} exceptions = json.loads((REPO_ROOT / "ci" / "vulnerability-audit-exceptions.json").read_text(encoding="utf-8")) - assert all(record.get("package") != "mcp" for record in exceptions["exceptions"]) + records = exceptions["exceptions"] + assert len(records) == 1, "Only the reviewed MCP advisory may be waived" + record = records[0] + expected_record = { + "package": "mcp", + "version": "1.29.0", + "vulnerability_ids": ["CVE-2026-59951", "GHSA-84m7-p3x7-pcfv"], + "reviewed_on": "2026-10-21", + "expires_on": "2026-11-04", + } + actual_record = {key: record[key] for key in expected_record} + actual_record["vulnerability_ids"] = sorted(record["vulnerability_ids"]) + assert actual_record == expected_record + assert tuple(bool(record[key].strip()) for key in ("rationale", "mitigation")) == (True, True) + lock = tomllib.loads((REPO_ROOT / "uv.lock").read_text(encoding="utf-8")) + packages = [ + (package["name"], package["version"], package["source"]) + for package in lock["package"] + if package["name"] in {"semgrep", "mcp", "specfact-semgrep"} + ] + assert sorted(packages) == [ + ("mcp", "1.29.0", {"registry": "https://pypi.org/simple"}), + ("semgrep", "1.179.0", {"registry": "https://pypi.org/simple"}), + ] def test_code_review_lock_is_bound_before_installation(tmp_path: Path) -> None: @@ -564,7 +585,7 @@ def run_rg(arguments: list[str], **_kwargs: object) -> subprocess.CompletedProce def test_patch_release_uses_next_version_in_all_sources() -> None: """The security baseline consumes only the next semver patch.""" - expected = "0.55.4" + expected = "0.55.11" project = tomllib.loads((REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")) assert project["project"]["version"] == expected for path in ("setup.py", "src/__init__.py", "src/specfact_cli/__init__.py"): diff --git a/tests/unit/specfact_cli/registry/test_module_packages.py b/tests/unit/specfact_cli/registry/test_module_packages.py index 04e40d53f..5d78b6f2a 100644 --- a/tests/unit/specfact_cli/registry/test_module_packages.py +++ b/tests/unit/specfact_cli/registry/test_module_packages.py @@ -36,8 +36,15 @@ from specfact_cli.registry.module_state import read_modules_state, write_modules_state +def _source_operations_by_package(package_dir: Path, _package_name: str, **_kwargs: object) -> list[str]: + """Supply the actual protocol operation shape for the three reporting fixture packages.""" + if package_dir.name == "full": + return ["import", "export", "sync", "validate"] + return ["import"] if package_dir.name == "partial" else [] + + @pytest.fixture(autouse=True) -def _reset_registry() -> Generator[None, None, None]: +def reset_registry() -> Generator[None, None, None]: CommandRegistry._clear_for_testing() module_packages_impl._ACTIVE_MODULE_SRC_DIRS.clear() clear_module_load_failures() @@ -236,7 +243,7 @@ def test_register_module_package_commands_removes_stale_active_src_from_sys_path stale_src = Path("/tmp/specfact-stale-module-src").resolve() module_packages_impl._ACTIVE_MODULE_SRC_DIRS.append(stale_src) monkeypatch.setattr(sys, "path", [str(stale_src), str(stale_src), *sys.path]) - monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", list) + monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda **_kwargs: []) module_packages_impl.register_module_package_commands() @@ -311,7 +318,7 @@ def test_installed_group_loader_adds_enabled_dependency_module_src_roots( monkeypatch.setattr( module_packages_impl, "discover_all_package_metadata", - lambda: [ + lambda **_kwargs: [ (project_dir, metadata_by_name["nold-ai/specfact-project"]), (codebase_dir, metadata_by_name["nold-ai/specfact-codebase"]), ], @@ -351,7 +358,7 @@ def test_lazy_loader_failure_is_recorded_for_availability_diagnostics( meta = discover_package_metadata(tmp_path)[0][1] monkeypatch.setattr(sys, "path", [entry for entry in sys.path if str(tmp_path) not in entry]) - monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda: [(codebase_dir, meta)]) + monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda **_kwargs: [(codebase_dir, meta)]) monkeypatch.setattr(module_packages_impl, "verify_module_artifact", lambda *_args, **_kwargs: True) monkeypatch.setattr(module_packages_impl, "read_modules_state", dict) monkeypatch.setattr(module_packages_impl, "_check_protocol_compliance_from_source", lambda *_args, **_kwargs: []) @@ -546,7 +553,7 @@ def test_manifest_legacy_without_publisher_integrity_loads_successfully(tmp_path # --- Arch-06: installer and lifecycle trust enforcement (TDD) --- -def test_trust_check_rejects_on_checksum_mismatch(monkeypatch, tmp_path: Path): +def test_trust_check_rejects_on_checksum_mismatch(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): """When artifact checksum does not match expected, module SHALL be skipped at registration.""" from specfact_cli.registry import module_installer @@ -570,7 +577,7 @@ def fail_checksum(_data, _expected): assert result is False -def test_allow_unsigned_allows_module_without_integrity(monkeypatch): +def test_allow_unsigned_allows_module_without_integrity(monkeypatch: pytest.MonkeyPatch): """When allow_unsigned is True, module without integrity metadata MAY be allowed.""" from specfact_cli.registry import module_installer @@ -580,7 +587,7 @@ def test_allow_unsigned_allows_module_without_integrity(monkeypatch): assert result is True -def test_unaffected_modules_register_when_one_fails_trust(monkeypatch, tmp_path: Path): +def test_unaffected_modules_register_when_one_fails_trust(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): """When one module fails integrity verification, other valid modules SHALL continue registration.""" from specfact_cli.registry import module_packages as mp @@ -592,7 +599,7 @@ def test_unaffected_modules_register_when_one_fails_trust(monkeypatch, tmp_path: (tmp_path / name / "src").mkdir(parents=True) (tmp_path / name / "src" / "app.py").write_text("app = None", encoding="utf-8") - def verify_may_fail(_package_dir: Path, meta, allow_unsigned: bool = False): + def verify_may_fail(_package_dir: Path, meta: ModulePackageMetadata, allow_unsigned: bool = False): return meta.name != "bad_trust" monkeypatch.setattr(mp, "verify_module_artifact", verify_may_fail) @@ -620,7 +627,7 @@ def test_grouped_registration_merges_duplicate_command_extensions( ModulePackageMetadata(name="ext_backlog", version="0.1.0", commands=["backlog"], category="backlog"), ), ] - monkeypatch.setattr(mp, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(mp, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(mp, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(mp, "read_modules_state", dict) monkeypatch.setattr(mp, "_check_protocol_compliance_from_source", lambda *_args, **_kwargs: []) @@ -677,7 +684,7 @@ def test_registration_skips_package_with_unsatisfied_versioned_module_dependency ), ] - monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(module_packages_impl, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(module_packages_impl, "read_modules_state", dict) monkeypatch.setattr(module_packages_impl, "_check_protocol_compliance_from_source", lambda *_args, **_kwargs: []) @@ -714,7 +721,7 @@ def _native_sub() -> None: ) ] - monkeypatch.setattr(mp, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(mp, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(mp, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(mp, "read_modules_state", dict) monkeypatch.setattr(mp, "_check_protocol_compliance_from_source", lambda *_args, **_kwargs: []) @@ -765,7 +772,7 @@ def _list_requirements() -> None: ) ] - monkeypatch.setattr(mp, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(mp, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(mp, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(mp, "read_modules_state", dict) monkeypatch.setattr(mp, "_check_protocol_compliance_from_source", lambda *_args, **_kwargs: []) @@ -809,7 +816,7 @@ def _validate_run() -> None: ) ] - monkeypatch.setattr(mp, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(mp, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(mp, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(mp, "read_modules_state", dict) monkeypatch.setattr(mp, "_check_protocol_compliance_from_source", lambda *_args, **_kwargs: []) @@ -827,13 +834,13 @@ def _validate_run() -> None: assert "validate" not in names -def test_integrity_failure_shows_user_friendly_risk_warning(monkeypatch, tmp_path: Path) -> None: +def test_integrity_failure_shows_user_friendly_risk_warning(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: """Integrity failure should emit concise risk guidance instead of raw checksum diagnostics.""" from specfact_cli.registry import module_packages as mp shown_messages: list[str] = [] metadata = [(tmp_path / "bad", ModulePackageMetadata(name="bad", version="0.1.0", commands=["bad_cmd"]))] - monkeypatch.setattr(mp, "discover_all_package_metadata", lambda: metadata) + monkeypatch.setattr(mp, "discover_all_package_metadata", lambda **_kwargs: metadata) monkeypatch.setattr(mp, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: False) monkeypatch.setattr(mp, "read_modules_state", dict) monkeypatch.setattr(mp, "print_warning", shown_messages.append) @@ -890,7 +897,7 @@ def test_registry_receives_example_command_when_registered(): def test_protocol_reporting_classifies_full_partial_legacy_from_static_source( - monkeypatch, caplog, tmp_path: Path + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, tmp_path: Path ) -> None: """Protocol summary should classify full/partial/legacy modules accurately.""" from specfact_cli.registry import module_packages as module_packages_impl @@ -907,17 +914,13 @@ def test_protocol_reporting_classifies_full_partial_legacy_from_static_source( (tmp_path / "partial", ModulePackageMetadata(name="partial", commands=[])), (tmp_path / "legacy", ModulePackageMetadata(name="legacy", commands=[])), ] - monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda: metadata) + monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda **_kwargs: metadata) monkeypatch.setattr(module_packages_impl, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(module_packages_impl, "read_modules_state", dict) monkeypatch.setattr( module_packages_impl, "_check_protocol_compliance_from_source", - lambda package_dir, _package_name, **_kwargs: ( - ["import", "export", "sync", "validate"] - if package_dir.name == "full" - else (["import"] if package_dir.name == "partial" else []) - ), + _source_operations_by_package, ) module_packages_impl.register_module_package_commands() @@ -925,7 +928,9 @@ def test_protocol_reporting_classifies_full_partial_legacy_from_static_source( assert "full=1, partial=1, legacy=1" in caplog.text -def test_protocol_legacy_warning_emitted_once_per_module(monkeypatch, caplog, tmp_path: Path) -> None: +def test_protocol_legacy_warning_emitted_once_per_module( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: """Legacy warning should not be emitted more than once for a module condition.""" from specfact_cli.registry import module_packages as module_packages_impl @@ -936,7 +941,7 @@ def test_protocol_legacy_warning_emitted_once_per_module(monkeypatch, caplog, tm monkeypatch.setattr(module_packages_impl, "is_debug_mode", lambda: True) monkeypatch.setattr(module_packages_impl, "get_bridge_logger", lambda _name: test_logger) packages = [(tmp_path / "legacy", ModulePackageMetadata(name="legacy", commands=[]))] - monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(module_packages_impl, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(module_packages_impl, "read_modules_state", dict) monkeypatch.setattr(module_packages_impl, "_check_protocol_compliance_from_source", lambda *_args, **_kwargs: []) @@ -947,7 +952,9 @@ def test_protocol_legacy_warning_emitted_once_per_module(monkeypatch, caplog, tm assert len(lines) == 1 -def test_protocol_reporting_uses_static_source_operations(monkeypatch, caplog, tmp_path: Path) -> None: +def test_protocol_reporting_uses_static_source_operations( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: """Protocol reporting should use static source inspection operations.""" from specfact_cli.registry import module_packages as module_packages_impl @@ -958,7 +965,7 @@ def test_protocol_reporting_uses_static_source_operations(monkeypatch, caplog, t monkeypatch.setattr(module_packages_impl, "is_debug_mode", lambda: True) monkeypatch.setattr(module_packages_impl, "get_bridge_logger", lambda _name: test_logger) packages = [(tmp_path / "backlog", ModulePackageMetadata(name="backlog", commands=[]))] - monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(module_packages_impl, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(module_packages_impl, "read_modules_state", dict) monkeypatch.setattr( @@ -987,7 +994,9 @@ def test_all_builtin_modules_expose_module_io_contract_operations() -> None: assert not legacy_modules, f"Modules still legacy: {', '.join(sorted(legacy_modules))}" -def test_protocol_reporting_is_quiet_when_all_modules_are_fully_compliant(monkeypatch, caplog, tmp_path: Path) -> None: +def test_protocol_reporting_is_quiet_when_all_modules_are_fully_compliant( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture, tmp_path: Path +) -> None: """No protocol warnings/summary should be emitted when all modules are fully compliant.""" from specfact_cli.registry import module_packages as module_packages_impl @@ -1001,7 +1010,7 @@ def test_protocol_reporting_is_quiet_when_all_modules_are_fully_compliant(monkey (tmp_path / "full-a", ModulePackageMetadata(name="full-a", commands=[])), (tmp_path / "full-b", ModulePackageMetadata(name="full-b", commands=[])), ] - monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(module_packages_impl, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(module_packages_impl, "read_modules_state", dict) monkeypatch.setattr( @@ -1016,7 +1025,9 @@ def test_protocol_reporting_is_quiet_when_all_modules_are_fully_compliant(monkey assert "Protocol-compliant:" not in caplog.text -def test_protocol_reporting_is_silent_for_non_compliant_modules_when_debug_off(monkeypatch, tmp_path: Path) -> None: +def test_protocol_reporting_is_silent_for_non_compliant_modules_when_debug_off( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: """Non-compliant protocol details should stay hidden unless debug mode is enabled.""" from specfact_cli.registry import module_packages as module_packages_impl @@ -1025,7 +1036,7 @@ def test_protocol_reporting_is_silent_for_non_compliant_modules_when_debug_off(m monkeypatch.setattr(module_packages_impl, "is_debug_mode", lambda: False) monkeypatch.setattr(module_packages_impl, "print_warning", shown_messages.append) packages = [(tmp_path / "partial-a", ModulePackageMetadata(name="partial-a", commands=[]))] - monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda: packages) + monkeypatch.setattr(module_packages_impl, "discover_all_package_metadata", lambda **_kwargs: packages) monkeypatch.setattr(module_packages_impl, "verify_module_artifact", lambda _dir, _meta, allow_unsigned=False: True) monkeypatch.setattr(module_packages_impl, "read_modules_state", dict) monkeypatch.setattr( diff --git a/tests/unit/versioning/test_analyzer.py b/tests/unit/versioning/test_analyzer.py index 02541d0e9..4ed3a0f27 100644 --- a/tests/unit/versioning/test_analyzer.py +++ b/tests/unit/versioning/test_analyzer.py @@ -48,7 +48,7 @@ def _create_sample_bundle(base_path: Path, bundle_name: str = "test-bundle") -> def _init_repo(base_path: Path) -> Repo: repo = Repo.init(base_path) - repo.index.add([str(p) for p in base_path.rglob("*") if p.is_file()]) + repo.index.add([str(p) for p in (base_path / ".specfact" / "projects").rglob("*") if p.is_file()]) repo.index.commit("init") return repo 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 000000000..a0f9ffd21 --- /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 226444c40..c291e9698 100644 --- a/tests/unit/workflows/test_requirements_evidence_delivery_workflow.py +++ b/tests/unit/workflows/test_requirements_evidence_delivery_workflow.py @@ -31,7 +31,7 @@ EVIDENCE_COMMAND_FRAGMENTS = ( '"${isolated_specfact[@]}" requirements evidence', '--base-ref "$evidence_base_commit"', - 'evidence_base_commit="$(git merge-base "origin/${EVIDENCE_BASE_BRANCH}" HEAD)"', + 'evidence_base_commit="${PLANNING_BASE_COMMIT:-${PROOF_BASE_COMMIT:-}}"', "required_maturity=planned", "required_maturity=test-authored", "requirements/*|pyproject.toml|setup.py|uv.lock", @@ -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") @@ -496,13 +509,13 @@ def _assert_prior_red_run_selection(locate: dict[str, object]) -> None: "--jq '.workflow_runs[] | [.id, .head_sha, .conclusion] | @tsv'", 'current_head="$(git rev-parse HEAD)"', '[[ "$head_sha" != "$current_head" ]]', - 'git merge-base --is-ancestor "origin/${GITHUB_BASE_REF}" "$head_sha"', + 'git merge-base --is-ancestor "$PROOF_BASE_COMMIT" "$head_sha"', 'git merge-base --is-ancestor "$head_sha" "$current_head"', 'mv "$candidate_dir/requirements-evidence.json" "$candidate_dir/red.json"', 'mv "$candidate_dir/requirements-proof.xml" "$candidate_dir/red.xml"', '"${isolated_python[@]}" scripts/requirements_proof_provenance.py', '--prior-red-proof "$candidate_dir/red.json"', - '--base-ref "origin/${GITHUB_BASE_REF}"', + '--base-ref "$PROOF_BASE_COMMIT"', '--final-ref "$current_head"', "continue", ) @@ -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"} + 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" ) @@ -682,7 +701,7 @@ def _assert_reconciliation_fragments(reconcile: str) -> None: '"${isolated_python[@]}"', "del sys.argv[1:3]", 'source_ref="$(git rev-parse HEAD)"', - 'evidence_base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" "$source_ref")"', + 'evidence_base_commit="${PLANNING_BASE_COMMIT:-$PROOF_BASE_COMMIT}"', '"${isolated_specfact[@]}" requirements evidence', '"${isolated_specfact[@]}" requirements reconcile', "requirements-evidence-consumer.json", @@ -1004,7 +1023,7 @@ def test_fresh_consumer_authenticates_retained_red_run_and_artifact() -> None: def _assert_trusted_core_materialization(materialize: str) -> None: materialize_fragments = ( - 'base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)"', + 'base_commit="$PROOF_BASE_COMMIT"', 'git archive "$base_commit" --', ".pylintrc", "src/specfact_cli", @@ -1249,6 +1268,7 @@ def _assert_review_job_boundary(parsed: dict[str, object], review: dict[str, obj assert producer["outputs"] == { "artifact-id": "${{ steps.upload-requirements-evidence.outputs.artifact-id }}", "prior-red-run-id": "${{ steps.prior-red-run.outputs.run-id }}", + "stack-context-digest": "${{ steps.stack-context.outputs.digest }}", } @@ -1258,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] @@ -1460,17 +1481,18 @@ def _assert_promotion_stage_outputs(workflow: dict[str, object]) -> None: def _assert_promotion_scope(workflow: dict[str, object], commands: tuple[str, str, str]) -> None: producer_command, consumer_command, final_command = commands - producer_base = 'evidence_base_commit="$(git merge-base "origin/${EVIDENCE_BASE_BRANCH}" HEAD)"' - consumer_base = 'evidence_base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" "$source_ref")"' + producer_base = 'evidence_base_commit="${PLANNING_BASE_COMMIT:-${PROOF_BASE_COMMIT:-}}"' + consumer_base = 'evidence_base_commit="${PLANNING_BASE_COMMIT:-$PROOF_BASE_COMMIT}"' assert producer_command.count(producer_base) == 1 assert consumer_command.count(consumer_base) == 1 assert producer_command.count('--base-ref "$evidence_base_commit"') == 1 assert consumer_command.count('--base-ref "$evidence_base_commit"') == 1 - assert final_command.count('--base-ref "$FINAL_BASE_COMMIT"') == 1 + assert final_command.count('--base-ref "$evidence_base_commit"') == 1 + assert 'evidence_base_commit="${PLANNING_BASE_COMMIT:-$FINAL_BASE_COMMIT}"' in final_command assert "evidence_scope_commit=" not in producer_command + consumer_command assert "final_scope_commit=" not in final_command final_core = cast(str, _step_by_name(workflow, "Materialize trusted final Requirements core")["run"]) - assert 'base_commit="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)"' in final_core + assert 'base_commit="$PROOF_BASE_COMMIT"' in final_core assert "printf 'FINAL_BASE_COMMIT=%s\\n' \"$base_commit\"" in final_core @@ -1601,18 +1623,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( @@ -1621,15 +1685,35 @@ 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: + """A parent push cannot let a consumer silently reuse a different proof boundary.""" + workflow = yaml.load( + (REPO_ROOT / ".github/workflows/requirements-evidence.yml").read_text(), Loader=yaml.BaseLoader + ) + jobs = workflow["jobs"] + for job_name in ("requirements-evidence-producer", "requirements-evidence", "requirements-evidence-final"): + steps = jobs[job_name]["steps"] + context = next(step for step in steps if step.get("id") == "stack-context") + run = context["run"] + assert 'git archive "$policy_ref" -- scripts/requirements_stack_context.py' in run + assert 'python3 -I -S "$policy_root/scripts/requirements_stack_context.py"' in run + assert 'case "$GITHUB_BASE_REF" in dev|main) ;; *) exit 1 ;; esac' in run + assert 'git merge-base --is-ancestor "$proof_base" "$proof_head"' in run + assert 'git merge-base --is-ancestor "$proof_head" HEAD' in run + assert 'test "$digest" = "$EXPECTED_CONTEXT_DIGEST"' in run + if job_name != "requirements-evidence-producer": + assert "stack-context-digest" in context["env"]["EXPECTED_CONTEXT_DIGEST"] diff --git a/tests/unit/workflows/test_requirements_planning_permissions.py b/tests/unit/workflows/test_requirements_planning_permissions.py new file mode 100644 index 000000000..521531ae9 --- /dev/null +++ b/tests/unit/workflows/test_requirements_planning_permissions.py @@ -0,0 +1,26 @@ +"""Proof consumers have only the read capabilities needed to authenticate root checks.""" + +from pathlib import Path + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +@pytest.mark.parametrize( + "job_name", + ("requirements-evidence-producer", "requirements-evidence", "requirements-evidence-final"), +) +def test_each_proof_consumer_has_explicit_read_only_checks_permission(job_name: str) -> None: + """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", + "pull-requests": "read", + } diff --git a/tests/unit/workflows/test_requirements_planning_scope.py b/tests/unit/workflows/test_requirements_planning_scope.py new file mode 100644 index 000000000..24542a84a --- /dev/null +++ b/tests/unit/workflows/test_requirements_planning_scope.py @@ -0,0 +1,104 @@ +"""Execute workflow selection boundaries for planning and implementation evidence.""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +import yaml + + +WORKFLOW = Path(__file__).resolve().parents[3] / ".github/workflows/requirements-evidence.yml" +STAGES = ( + ("Reconcile Requirements evidence on fresh runner", "consumer"), + ("Reconcile final Requirements verdict on fresh runner", "final"), +) + + +def _command(step_name: str) -> str: + """Read the actual shell command for one named workflow step.""" + workflow = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8")) + return next( + step["run"] for job in workflow["jobs"].values() for step in job["steps"] if step.get("name") == step_name + ) + + +def _select_review( + root: Path, step_name: str, prefix: str, maturity: str, changes: tuple[str, ...] +) -> subprocess.CompletedProcess[str]: + """Execute the workflow's selection block with a controlled changed scope.""" + command = _command(step_name) + start = command.index("review_evidence_paths=()") + end = command.index('selected_change=""', start) + entries = " ".join(f"[{change}]=1" for change in changes) + script = ( + "set -euo pipefail\n" + f"declare -A {prefix}_changed_change_ids=({entries})\n" + + command[start:end] + + '\nprintf "%s\\n" "${review_evidence_paths[@]}"\n' + ) + return subprocess.run( + ["bash", "-c", script], + cwd=root, + env={**os.environ, "planning_maturity": maturity, "EVIDENCE_PROMOTION_REUSE": "false"}, + check=False, + capture_output=True, + text=True, + ) + + +def test_planned_changes_do_not_select_implementation_reviews(tmp_path: Path) -> None: + """One or several plans need no approval; an unrelated record cannot enter them.""" + unrelated = tmp_path / "openspec/changes/unrelated/requirements-proof/review-evidence.json" + unrelated.parent.mkdir(parents=True) + unrelated.write_text("{}", encoding="utf-8") + for step_name, prefix in STAGES: + for changes in (("first",), ("first", "second"), ()): + result = _select_review(tmp_path, step_name, prefix, "planned", changes) + assert result.returncode == 0, result.stderr + assert not result.stdout.strip(), result.stdout + + +def test_implementation_review_scope_remains_enforced(tmp_path: Path) -> None: + """Existing implementation selection and ambiguous-scope rejection remain intact.""" + for step_name, prefix in STAGES: + selected = _select_review(tmp_path, step_name, prefix, "test-authored", ("first",)) + assert selected.returncode == 0, selected.stderr + assert selected.stdout.strip() == "openspec/changes/first/requirements-proof/review-evidence.json" + rejected = _select_review(tmp_path, step_name, prefix, "test-authored", ("first", "second")) + assert rejected.returncode != 0 + assert "multiple changed active OpenSpec changes" in rejected.stderr + + +def test_producer_keeps_implementation_approval_out_of_planning(tmp_path: Path) -> None: + """Producer and verifiers use the same approval-free planning context.""" + record = tmp_path / "openspec/changes/first/requirements-proof/review-evidence.json" + record.parent.mkdir(parents=True) + record.write_text("{}", encoding="utf-8") + command = _command("Run Requirements evidence gate") + start = command.index('prior_red_proof=""') + end = command.index("bootstrap_root=", start) + script = ( + 'set -euo pipefail\nevidence_arguments=()\nselected_change="first"\n' + + command[start:end] + + '\nprintf "%s\\n" "${evidence_arguments[@]}"\n' + ) + for maturity in ("planned", "test-authored"): + result = subprocess.run( + ["bash", "-c", script], + cwd=tmp_path, + env={ + **os.environ, + "planning_maturity": maturity, + "RUNNER_TEMP": str(tmp_path), + "evidence_base_commit": "0" * 40, + "PROOF_BASE_COMMIT": "0" * 40, + "LATE_RED_ACTIVE": "false", + }, + check=False, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stderr + assert ("--review-evidence" in result.stdout) == (maturity != "planned") diff --git a/tests/unit/workflows/test_requirements_promotion_trusted_core.py b/tests/unit/workflows/test_requirements_promotion_trusted_core.py index ade619042..52c11f9db 100644 --- a/tests/unit/workflows/test_requirements_promotion_trusted_core.py +++ b/tests/unit/workflows/test_requirements_promotion_trusted_core.py @@ -62,7 +62,8 @@ def _assert_presence_dispatch_identifiers(command: str) -> None: def _assert_bootstrap_identities(command: str) -> None: """Require both source archives and every immutable bootstrap identity.""" _assert_presence_dispatch_identifiers(command) - assert command.count("git archive ") == 2 + assert command.count("git archive ") == 3 + _assert_smoke_lock_materialization(command) assert BASE_RELATIVE_REVIEW_SOURCE in command assert f'test "$base_commit" = "{LEGACY_BASE}"' in command assert f'review_source="{REVIEW_SOURCE}"' in command @@ -73,6 +74,15 @@ def _assert_bootstrap_identities(command: str) -> None: assert SOURCE_TO_HEAD_ANCESTRY in command +def _assert_smoke_lock_materialization(command: str) -> None: + """The optional smoke graph must come from the authenticated immutable base.""" + smoke_probe = 'if git cat-file -e "${base_commit}:requirements/runtime-smoke/locked.txt"' + smoke_archive = 'git archive "$base_commit" -- requirements/runtime-smoke' + assert smoke_probe in command + assert smoke_archive in command + assert command.index(smoke_probe) < command.index(smoke_archive) + + def _assert_bootstrap_order(command: str) -> None: """Require the base-relative default and fail-closed exception ordering.""" assert command.index(BASE_RELATIVE_REVIEW_SOURCE) < command.index(INPUT_PRESENT_DEFAULT) diff --git a/tests/unit/workflows/test_requirements_verifier_boundary.py b/tests/unit/workflows/test_requirements_verifier_boundary.py new file mode 100644 index 000000000..d9926ed35 --- /dev/null +++ b/tests/unit/workflows/test_requirements_verifier_boundary.py @@ -0,0 +1,135 @@ +"""A stacked parent cannot supply code used as the trusted delivery verifier.""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path +from typing import cast + +import pytest +import yaml + + +ROOT = Path(__file__).resolve().parents[3] + + +def _producer_steps() -> list[dict[str, str]]: + workflow = cast( + dict[str, object], + yaml.load((ROOT / ".github/workflows/requirements-evidence.yml").read_text(), Loader=yaml.BaseLoader), + ) + jobs = cast(dict[str, object], workflow["jobs"]) + producer = cast(dict[str, object], jobs["requirements-evidence-producer"]) + return cast(list[dict[str, str]], producer["steps"]) + + +def _fixture_git_env() -> dict[str, str]: + env = {key: value for key, value in os.environ.items() if not key.startswith("GIT_")} + env.update(GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM="1") + return env + + +def _git(repo: Path, *args: str) -> str: + env = _fixture_git_env() + if args[0] != "init": + actual = subprocess.check_output( + ["git", "-C", str(repo), "rev-parse", "--absolute-git-dir"], env=env, text=True, timeout=20 + ).strip() + if Path(actual).resolve() != repo.resolve() / ".git": + raise RuntimeError("git-fixture-outside-owned-root") + result = subprocess.run( + [ + "git", + "-C", + str(repo), + "-c", + "user.name=Fixture", + "-c", + "user.email=fixture@example.test", + "-c", + "commit.gpgsign=false", + *args, + ], + env=env, + capture_output=True, + text=True, + check=True, + timeout=20, + ) + return result.stdout.strip() + + +def test_producer_authenticates_before_trusted_verifier_selection() -> None: + steps = _producer_steps() + names = [step.get("name", "") for step in steps] + authentication = names.index("Authenticate live native stack proof boundary") + selection = names.index("Materialize trusted delivery verifier") + execution = names.index("Verify frozen Requirements graph") + assert authentication < selection < execution + + +def test_stacked_parent_bytes_cannot_be_archived_as_trusted_verifier(tmp_path: Path) -> None: + _git(tmp_path, "init", "-b", "dev") + script = tmp_path / "scripts/check_reproducible_delivery.py" + script.parent.mkdir() + script.write_text("reviewed protected verifier\n") + _git(tmp_path, "add", ".") + _git(tmp_path, "commit", "-m", "protected verifier") + protected = _git(tmp_path, "rev-parse", "HEAD") + _git(tmp_path, "checkout", "-b", "dependency-root") + script.write_text("unmerged parent verifier\n") + _git(tmp_path, "add", ".") + _git(tmp_path, "commit", "-m", "candidate parent") + _git(tmp_path, "update-ref", "refs/remotes/origin/dependency-root", "HEAD") + _git(tmp_path, "checkout", "-b", "dependency-child") + step = next(step for step in _producer_steps() if step.get("name") == "Materialize trusted delivery verifier") + runner = tmp_path / "runner" + runner.mkdir() + result = subprocess.run( + ["bash", "-eo", "pipefail", "-c", step["run"]], + cwd=tmp_path, + env={ + "PATH": os.environ["PATH"], + "RUNNER_TEMP": str(runner), + "GITHUB_BASE_REF": "dependency-root", + "GITHUB_ENV": str(tmp_path / "github.env"), + "PROOF_BASE_COMMIT": protected, + }, + capture_output=True, + text=True, + check=False, + timeout=20, + ) + assert result.returncode == 0, result.stderr + archived = runner / "trusted-requirements/scripts/check_reproducible_delivery.py" + assert archived.read_text() == "reviewed protected verifier\n" + + +@pytest.mark.parametrize( + ("variable", "query", "target"), + [ + ("GIT_DIR", "--absolute-git-dir", ".git"), + ("GIT_COMMON_DIR", "--git-common-dir", ".git"), + ("GIT_WORK_TREE", "--show-toplevel", ""), + ], +) +def test_git_fixture_ignores_foreign_repository_selectors( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, variable: str, query: str, target: str +) -> None: + owned, foreign = tmp_path / "owned", tmp_path / "foreign" + for repo in (owned, foreign): + repo.mkdir() + subprocess.run( + ["git", "init", "-q", str(repo)], + env={"PATH": os.environ["PATH"], "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1"}, + check=True, + capture_output=True, + text=True, + timeout=20, + ) + monkeypatch.setenv(variable, str(foreign if variable == "GIT_WORK_TREE" else foreign / ".git")) + observed = Path(_git(owned, "rev-parse", query)) + if not observed.is_absolute(): + observed = owned / observed + assert observed.resolve() == (owned / target).resolve() diff --git a/tests/unit/workflows/test_runtime_smoke_fixture.py b/tests/unit/workflows/test_runtime_smoke_fixture.py new file mode 100644 index 000000000..80c64cf76 --- /dev/null +++ b/tests/unit/workflows/test_runtime_smoke_fixture.py @@ -0,0 +1,41 @@ +"""Portable smoke identity cannot retarget native Requirements authority.""" + +from __future__ import annotations + +import json +import os +import subprocess +from pathlib import Path + +import pytest +import yaml + + +REPO_ROOT = Path(__file__).resolve().parents[3] + + +@pytest.mark.parametrize("mismatch", [None, "repository", "commit", "tree"]) +def test_portable_action_rejects_unreviewed_identity(tmp_path: Path, mismatch: str | None) -> None: + action = yaml.safe_load((REPO_ROOT / ".github/actions/setup-runtime-smoke-fixture/action.yml").read_text()) + identity = json.loads((REPO_ROOT / "ci/runtime-smoke-fixture.lock.json").read_text()) + if mismatch is not None: + identity[mismatch] = "unreviewed" + (tmp_path / "ci").mkdir() + (tmp_path / "ci/runtime-smoke-fixture.lock.json").write_text(json.dumps(identity)) + result = subprocess.run( + ["bash", "-c", action["runs"]["steps"][0]["run"]], cwd=tmp_path, env=os.environ, capture_output=True, text=True + ) + assert (result.returncode == 0) is (mismatch is None), result.stderr + + +def test_smoke_fixture_is_separate_from_native_proof_provider() -> None: + proof = json.loads((REPO_ROOT / "ci/module-fixture.lock.json").read_text()) + portable = json.loads((REPO_ROOT / "ci/runtime-smoke-fixture.lock.json").read_text()) + assert proof["commit"] == "69f075819be5e1ceca1446b026b0417f19e584ca" + assert portable["commit"] == "046d366c190af7a188d6f48953a96edb65e766db" + workflow = yaml.safe_load((REPO_ROOT / ".github/workflows/pr-orchestrator.yml").read_text()) + for job in ("tests", "compat-py311", "runtime-smoke-macos", "runtime-smoke-windows"): + assert any( + step.get("uses") == "./.github/actions/setup-runtime-smoke-fixture" + for step in workflow["jobs"][job]["steps"] + ) diff --git a/tests/unit/workflows/test_trustworthy_green_checks.py b/tests/unit/workflows/test_trustworthy_green_checks.py index d3f245553..5cc04d56a 100644 --- a/tests/unit/workflows/test_trustworthy_green_checks.py +++ b/tests/unit/workflows/test_trustworthy_green_checks.py @@ -907,7 +907,9 @@ def test_license_gate_audits_the_frozen_code_review_environment() -> None: assert "uv pip install" in commands assert "requirements/code-review/locked.txt" in commands assert "--code-review-python" in commands - assert "--additional-python" not in commands + assert '--additional-python "$REVIEW_TOOLS_PYTHON"' not in commands + assert '--code-review-python "$REVIEW_TOOLS_PYTHON"' in commands + assert '--additional-python "$SMOKE_TOOLS_PYTHON"' in commands def test_dependency_trust_is_a_standalone_ci_and_pre_commit_gate() -> None: diff --git a/uv.lock b/uv.lock index de6d44737..7a66298ad 100644 --- a/uv.lock +++ b/uv.lock @@ -205,11 +205,11 @@ wheels = [ [[package]] name = "certifi" -version = "2026.7.22" +version = "2026.10.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, + { url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" }, ] [[package]] @@ -721,14 +721,14 @@ wheels = [ [[package]] name = "gitpython" -version = "3.1.61" +version = "3.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gitdb" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/61/3285044215fb596bf093e39ccb96ece0a1076a8ca57a61e069a6a33cdb1b/gitpython-3.1.61.tar.gz", hash = "sha256:f51c24d8c0f733a195447385f5774a5dfe8767f5acfd7994a33755644c6ecc95", size = 231680, upload-time = "2026-08-28T11:01:13.761Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/2d/6f6e649818da44d4499604802c89329b8d9799687a124e3a5e467a643336/gitpython-3.2.0.tar.gz", hash = "sha256:fb92310af6844d96adc95ca066ed2e617c00e1dbd146a326626c81e72e18cc2e", size = 238091, upload-time = "2026-09-30T09:10:32.65Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/5e/49cc172da4d0578644ba37cec5cb365b1fefc603b26edea9bcac1c7f830a/gitpython-3.1.61-py3-none-any.whl", hash = "sha256:8ab28c9da863cdd9e7d7694ec46cf3e6c9a12d8a30a1acd3447aec11975d530c", size = 222118, upload-time = "2026-08-28T11:01:12.262Z" }, + { url = "https://files.pythonhosted.org/packages/fa/48/f36d233ce749d1bb880cb5e6f8bb171dc2effabc7435c31da604d831b3ec/gitpython-3.2.0-py3-none-any.whl", hash = "sha256:bd70c5ec05cd2b797423e7eb312147d2458d3cca92085888fba2213f85905537", size = 228537, upload-time = "2026-09-30T09:10:31.159Z" }, ] [[package]] @@ -777,7 +777,7 @@ wheels = [ [[package]] name = "hatchling" -version = "1.32.0" +version = "1.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, @@ -786,9 +786,9 @@ dependencies = [ { name = "tomlkit" }, { name = "trove-classifiers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/69/08/33331757185504aae48b8d9bd78cec03a76e3aecfb52e549d05a2347c0dd/hatchling-1.32.0.tar.gz", hash = "sha256:0bdbde4a52b06c37e3eca395f85a762bf0ef06fe374fd8ae429dc6be10230f5f", size = 57783, upload-time = "2026-08-11T05:03:44.114Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/97/b5312f01a8c6daf729a9d272dd442e0c546dbcc630495788786c4b567ed0/hatchling-1.32.4.tar.gz", hash = "sha256:c4468f73144c054d2aab4ef0f0378c43b9878bf07f8ffd6b79690e970d375f07", size = 58065, upload-time = "2026-09-20T22:48:45.398Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/84/1798b6d85ecde0e31546004efd25c5de1b1f49250644a60cce460e12593a/hatchling-1.32.0-py3-none-any.whl", hash = "sha256:0e17c9c3b9aa7c625acc8d0f5b622f107d5049af9ecf5ada4de1aada5be7cdbc", size = 78435, upload-time = "2026-08-11T05:03:42.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/80/91f51f439c05d4ec4623c22928ce16a938d6d793bf709477830823497859/hatchling-1.32.4-py3-none-any.whl", hash = "sha256:08ecf7548fb48205e7f213d70c71e67b8271b7242093dc3f1da578b42c734a2c", size = 78748, upload-time = "2026-09-20T22:48:44.19Z" }, ] [[package]] @@ -2032,11 +2032,11 @@ wheels = [ [[package]] name = "pyjwt" -version = "2.13.0" +version = "2.15.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/ea/5194e52748b0da83d71e082d75496eaec6e58f419f5e184786ded517e6a9/pyjwt-2.15.1.tar.gz", hash = "sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8", size = 121252, upload-time = "2026-09-28T18:40:42.598Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/50/ca/44de4e75f8aadc457f0634be3b542815078ded46dca30efb960edeecad6e/pyjwt-2.15.1-py3-none-any.whl", hash = "sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193", size = 33860, upload-time = "2026-09-28T18:40:41.429Z" }, ] [package.optional-dependencies] @@ -2144,15 +2144,14 @@ wheels = [ [[package]] name = "python-discovery" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, - { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/51/276f964496a5714ab9f320896195639086881c2b39c03b5ad13de84acbb8/python_discovery-1.5.0.tar.gz", hash = "sha256:3e014c6327154d3dda27939a9a0dc9c5c000439f1906d3f303b48f984bd2ecef", size = 72483, upload-time = "2026-07-21T13:14:14.641Z" } +sdist = { url = "https://files.pythonhosted.org/packages/91/96/0f93e27c9f60a650838f2118159aa115fd5732c0716247917b7ba7ede665/python_discovery-1.6.0.tar.gz", hash = "sha256:6393b4eae1be8b2182670635e7baff89ac21cb9f8e86fd1ff40c7b1144febb4c", size = 82849, upload-time = "2026-08-28T17:30:02.366Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/7b/14882602ddee241d7984a742fcb423cb4a30fb0d6efc546ac3129fba475a/python_discovery-1.5.0-py3-none-any.whl", hash = "sha256:70c4fc61b4e7404e44f01d6fc44a715c4d685ca6cea83d295922f05891877c98", size = 34205, upload-time = "2026-07-21T13:14:13.398Z" }, + { url = "https://files.pythonhosted.org/packages/43/5e/21abf578182fb15006a57faf3711a1e659e29d600d19b6e557eae908c81d/python_discovery-1.6.0-py3-none-any.whl", hash = "sha256:d4e244cf17b8b29819ed78003d55fbacf86eda23425b075454fff9271b79377a", size = 38451, upload-time = "2026-08-28T17:30:01.236Z" }, ] [[package]] @@ -2611,7 +2610,7 @@ wheels = [ [[package]] name = "semgrep" -version = "1.175.0" +version = "1.179.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -2642,15 +2641,15 @@ dependencies = [ { name = "urllib3" }, { name = "wcmatch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b2/15/69cb61bcad4fa6cb23fae5b7a12a398303266030b5a9127d51a79bfda2aa/semgrep-1.175.0.tar.gz", hash = "sha256:e8b14c91558f765b9dd155a99b0071bfe64f61577cda8eb4964132155232c1af", size = 507685, upload-time = "2026-08-26T17:10:26.526Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/31/a22661272ab2424d414f0413fccb324e242bc98c75e1bb0010ff03534be6/semgrep-1.179.0.tar.gz", hash = "sha256:c7e32b952990434b591003547f26b46c89a5bd56033715b00ff26327a8abe8c5", size = 518043, upload-time = "2026-10-02T00:09:27.182Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/60/77041d572801fb5819f974e821211a298f73d3dc56128bfa2e5045345472/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:82b5015b5c215ab0dcada25ccbbc39d57167d09b077e06abbd450958f5c29f78", size = 45854525, upload-time = "2026-08-26T17:11:45.704Z" }, - { url = "https://files.pythonhosted.org/packages/b7/82/1a169ee9977fe875845d2bfa1368f0c2bf8238bd1460801d7fef4facb6e5/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:54ffeeaad2ed9428e9bd40ccf55b2bf7778ce0b59bf848862234aaf6ada2a889", size = 49844382, upload-time = "2026-08-26T17:11:49.422Z" }, - { url = "https://files.pythonhosted.org/packages/30/68/4160f33df10bf7001e198ee296a6c4299eba86c8e8932007cc6f8110b964/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_aarch64.whl", hash = "sha256:c54736be4dd41e269856695685203d676123c2e21145ecd6c26c438b8d1a41ca", size = 72552275, upload-time = "2026-08-26T17:11:53.192Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b5/3941ba37979a5f1744c79ae47139c871bc78d5e612f1b8ac38cb76f49731/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_x86_64.whl", hash = "sha256:ac787bcef4fd4563f8cdd179392c743bded89ea38c0c81ff99913883b6cde5ad", size = 70486294, upload-time = "2026-08-26T17:11:57.144Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9d/42211326bdc625c0f4458190e427f6b5afb96f4299d57ba23a134d7c59a4/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_aarch64.whl", hash = "sha256:6bd6480637aea992cb8d41635329fd4e3bd6dc6e9c124a80341f150c5caeca2d", size = 79456947, upload-time = "2026-08-26T17:12:01.347Z" }, - { url = "https://files.pythonhosted.org/packages/89/cb/23fb070cb9ecb0897f5d1cdf054336a3d2650e5a3920a9cc6d23dcf0e1df/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_x86_64.whl", hash = "sha256:e8ecd7ee8ef1033c9635111c6e162e778834d61416968c5c1c5e7b7fba35c34e", size = 77048247, upload-time = "2026-08-26T17:12:06.03Z" }, - { url = "https://files.pythonhosted.org/packages/7c/e8/28a86e55596e94489aad29efe2d6b56f776ee29afeba63e762ad8f128ace/semgrep-1.175.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:68a8d832eff6422c188f5d745e319ce36deccaac6effb77e3833b8930bc10425", size = 58042978, upload-time = "2026-08-26T17:12:10.091Z" }, + { url = "https://files.pythonhosted.org/packages/85/4b/4cbaa0c3c124ae7d522e688c01f06013038ff59cc818c21dfa44c96380b3/semgrep-1.179.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_10_14_x86_64.whl", hash = "sha256:34b5e801e3f4325c93014124ccfab6da96857f74aae68458d2a07defd4727b0f", size = 45996552, upload-time = "2026-10-02T00:10:47.572Z" }, + { url = "https://files.pythonhosted.org/packages/bd/77/b573a0faa5a5b0848dc884a0f8595fcb5c550f55fa5f455a233b29a52c0c/semgrep-1.179.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-macosx_11_0_arm64.whl", hash = "sha256:9b57026f587a4bc7833bca2b6e4d6778072a79dcf0b9f2e5e01d3d5df39c7759", size = 49989923, upload-time = "2026-10-02T00:10:50.865Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ba/b9f1afbed852a21b6f7e72c064343161c5ab249954c46db1796b59314290/semgrep-1.179.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_aarch64.whl", hash = "sha256:c23d79c4839540372acc39b59aad167c1c7f3dc21bacacb27f19aa2edd79d271", size = 72829563, upload-time = "2026-10-02T00:10:54.693Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6c/621944b4843c7484aeaefd5b232d93780bdf95259567c10a9a8129f08dc6/semgrep-1.179.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-manylinux_2_34_x86_64.whl", hash = "sha256:b529c4db63f293918989b84ce4640f12f1b1e4680d9e7fcab5f3554f9113dfc5", size = 70820075, upload-time = "2026-10-02T00:10:59.263Z" }, + { url = "https://files.pythonhosted.org/packages/1d/41/dc51c4e2f2cd730c1512739db792f70f7e9c665f6a0fb40fef62ff4b8eb3/semgrep-1.179.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_aarch64.whl", hash = "sha256:dc6317fbfad49b1dbe396df7da7d9702cffc5ea9ea62cb3b4b2209824496b425", size = 79788590, upload-time = "2026-10-02T19:21:08.869Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c1/59096c02408630b79be19c5b97d145786141e254782ab0e62939c1eea52d/semgrep-1.179.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-musllinux_1_2_x86_64.whl", hash = "sha256:69bce348296374df6c51e72758801ce06dd18d6dfb307bc73d24b9de53bbe5f1", size = 77419530, upload-time = "2026-10-02T19:21:14.755Z" }, + { url = "https://files.pythonhosted.org/packages/ec/3e/dbf54b028afb807f26dc1e1c94489faac5926a8fa745b41bf505b4c53a7e/semgrep-1.179.0-cp310.cp311.cp312.cp313.cp314.py310.py311.py312.py313.py314-none-win_amd64.whl", hash = "sha256:6c6cf104ad0959c0b11de56d15c5d31f56949c620b4bd130c5d1fb899f2bb5a3", size = 58315857, upload-time = "2026-10-02T19:21:20.591Z" }, ] [[package]] @@ -2771,11 +2770,12 @@ wheels = [ [[package]] name = "specfact-cli" -version = "0.55.4" +version = "0.55.11" source = { editable = "." } dependencies = [ { name = "azure-identity" }, { name = "beartype" }, + { name = "certifi" }, { name = "click" }, { name = "commentjson" }, { name = "cryptography" }, @@ -2856,17 +2856,18 @@ requires-dist = [ { name = "beartype", specifier = ">=0.22.4" }, { name = "beartype", marker = "extra == 'dev'", specifier = ">=0.22.4" }, { name = "build", marker = "extra == 'dev'", specifier = ">=1.2.2" }, + { name = "certifi", specifier = ">=2026.10.19" }, { name = "click", specifier = ">=8.3.3,<9" }, { name = "commentjson", specifier = ">=0.9.0" }, { name = "coverage", extras = ["toml"], marker = "extra == 'dev'", specifier = ">=7.7.1" }, { name = "crosshair-tool", marker = "extra == 'contracts'", specifier = ">=0.0.97" }, { name = "crosshair-tool", marker = "extra == 'dev'", specifier = ">=0.0.97" }, { name = "cryptography", specifier = ">=50.0.0" }, - { name = "gitpython", specifier = ">=3.1.61" }, + { name = "gitpython", specifier = ">=3.2.0" }, { name = "graphviz", specifier = ">=0.20.1" }, { name = "graphviz", marker = "extra == 'dev'", specifier = ">=0.20.1" }, { name = "graphviz", marker = "extra == 'enhanced-analysis'", specifier = ">=0.20.1" }, - { name = "hatchling", marker = "extra == 'dev'", specifier = "==1.32.0" }, + { name = "hatchling", marker = "extra == 'dev'", specifier = "==1.32.4" }, { name = "hypothesis", marker = "extra == 'contracts'", specifier = ">=6.142.4" }, { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.142.4" }, { name = "icontract", specifier = ">=2.7.1" }, @@ -3150,11 +3151,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.7.0" +version = "2.8.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/05/b17359e1cefb4f909b5e40b1b90a496d987258916dbbf88e842c729f510e/urllib3-2.8.0.tar.gz", hash = "sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63", size = 458972, upload-time = "2026-09-15T19:29:36.253Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] [[package]] @@ -3184,7 +3185,7 @@ wheels = [ [[package]] name = "virtualenv" -version = "21.7.0" +version = "21.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -3192,9 +3193,9 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/25/e367a7229b0914772ca8d81b41fde012d9feda68523b52644a571bb21ce8/virtualenv-21.7.0.tar.gz", hash = "sha256:7f9519b9432ff11b6e1a3e94061664efc2ff99ea21780e3cf4f6bd0a5da8b37c", size = 5527510, upload-time = "2026-07-21T13:12:14.109Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/f0/58b5e510c05ef177afa8c5679516a46bf7b3cf131aadae3bbd1605f19035/virtualenv-21.11.0.tar.gz", hash = "sha256:fa83254de82b83565013b2f111519cf056124dd068bdbb8c5e49c6b72c061f0f", size = 5391846, upload-time = "2026-09-23T03:09:33.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/7a/ae29312b1e88a22e81f5d21fc11526d2a114089776c2550d2b205b6c2a47/virtualenv-21.7.0-py3-none-any.whl", hash = "sha256:a8370c1c5530fbabf955e40b8fbbc68a431648b10f9433faa587db30a06e51dd", size = 5507078, upload-time = "2026-07-21T13:12:12.136Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ba/324de21687982db37a65fa1ef9d32d4a07bbcc47081ab35291480bfd1c75/virtualenv-21.11.0-py3-none-any.whl", hash = "sha256:15be2e0b4a5c35f2bec284b11ef025f2b80e0d907b0ab9bbae3c4f835f6bf1e8", size = 5485008, upload-time = "2026-09-23T03:09:30.916Z" }, ] [[package]]