diff --git a/.Jules/palette.md b/.Jules/palette.md index 159662d06c..9864e9e356 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -1,3 +1,5 @@ -## 2024-05-20 - Repository without UI Codebase -**Learning:** This repository is a GitHub organization profile consisting entirely of Markdown documentation and static assets, and does not contain an active UI or frontend application codebase. -**Action:** Since there is no UI, no UX enhancements can be applied. Aborting UX enhancements and PR creation as per instructions. +## 2026-09-29 - Product UI boundary confirmed + +**Learning:** This repository owns the ContextualWisdomLab GitHub Actions control plane, including production CI scripts, tests, workflows, and documentation. It does not own an interactive product UI or frontend application. + +**Action:** Do not invent product UI work in this repository. Improve the control-plane artifacts here and route reusable product UI work to its canonical product owner. diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000000..dbd43187c2 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,5 @@ +# Source validation only; labels do not attest runtime isolation. +self-hosted-runner: + labels: + - cwlab-ci-isolated + - cwlab-control diff --git a/.github/actions/noema-review/two_phase.py b/.github/actions/noema-review/two_phase.py index c850da81b9..d5da7b8270 100755 --- a/.github/actions/noema-review/two_phase.py +++ b/.github/actions/noema-review/two_phase.py @@ -156,6 +156,7 @@ def prepare_verdict(repo: str, number: int, expected_head: str, path: Path) -> i return 0 expected_base = _canonical_base(pull_request) actor = _reviewer_actor() + author = gate.require_distinct_pr_author(pull_request, actor) if pull_request.get("isDraft"): print("PR is draft; Noema verdict preparation skipped.") return 0 @@ -190,6 +191,8 @@ def prepare_verdict(repo: str, number: int, expected_head: str, path: Path) -> i "pull_request_number": number, "expected_head": expected, "expected_base": expected_base, + "reviewer_actor": actor, + "pull_request_author": author, "verdict": verdict, }, ) @@ -250,6 +253,8 @@ def publish_verdict(repo: str, number: int, expected_head: str, path: Path) -> i "pull_request_number", "expected_head", "expected_base", + "reviewer_actor", + "pull_request_author", "verdict", } if set(payload) != required_keys: @@ -277,6 +282,14 @@ def publish_verdict(repo: str, number: int, expected_head: str, path: Path) -> i print("Pull request base advanced after model review; stale prepared verdict was not published.") return 0 actor = _reviewer_actor(allow_refreshed_app=True) + author = gate.require_distinct_pr_author(current_pull_request, actor) + if payload["pull_request_author"] != author: + raise RuntimeError("Noema pull request author changed after verdict preparation") + reviewer_actor = payload["reviewer_actor"] + if not isinstance(reviewer_actor, str) or not reviewer_actor.strip(): + raise RuntimeError("Noema prepared reviewer actor is invalid") + if reviewer_actor.strip().casefold() != actor.strip().casefold(): + raise RuntimeError("Noema refreshed reviewer actor differs from verdict preparation") if current_pull_request.get("isDraft"): print("PR became draft after model review; prepared verdict was not published.") return 0 diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 71201af75d..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - target-branch: "main" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 - - - package-ecosystem: "pip" - directory: "/" - target-branch: "main" - schedule: - interval: "weekly" - open-pull-requests-limit: 5 diff --git a/.github/runner-images/law-ai-agent/Dockerfile b/.github/runner-images/law-ai-agent/Dockerfile new file mode 100644 index 0000000000..5c73db15e5 --- /dev/null +++ b/.github/runner-images/law-ai-agent/Dockerfile @@ -0,0 +1,8 @@ +FROM ghcr.io/actions/actions-runner@sha256:e5496277be5d09bc968b3d64911b74e219ac4a3f2edce956a3ecf9271bea1ef4 +USER root +RUN apt-get update \ + && apt-get install -y --no-install-recommends postgresql-16 postgresql-client-16 libpq-dev python3-venv ca-certificates git curl \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* +USER runner +WORKDIR /home/runner diff --git a/.github/workflows/actions-queue-health.yml b/.github/workflows/actions-queue-health.yml index b6286ee922..febf0679b2 100644 --- a/.github/workflows/actions-queue-health.yml +++ b/.github/workflows/actions-queue-health.yml @@ -15,7 +15,9 @@ permissions: jobs: collect: name: Collect exact-head queue evidence - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 30 permissions: contents: read diff --git a/.github/workflows/agent-mention-noema-dispatch.yml b/.github/workflows/agent-mention-noema-dispatch.yml index c9514a47a1..c2729b9819 100644 --- a/.github/workflows/agent-mention-noema-dispatch.yml +++ b/.github/workflows/agent-mention-noema-dispatch.yml @@ -28,7 +28,9 @@ permissions: jobs: validate-and-forward: if: github.repository == 'ContextualWisdomLab/.github' - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: actions: read diff --git a/.github/workflows/agent-mention-opencode-dispatch.yml b/.github/workflows/agent-mention-opencode-dispatch.yml index 3b21667832..221a5bdb74 100644 --- a/.github/workflows/agent-mention-opencode-dispatch.yml +++ b/.github/workflows/agent-mention-opencode-dispatch.yml @@ -28,7 +28,9 @@ permissions: jobs: validate-and-forward: if: github.repository == 'ContextualWisdomLab/.github' - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: actions: read diff --git a/.github/workflows/agent-mention-router-quality-ci.yml b/.github/workflows/agent-mention-router-quality-ci.yml index 9c36a89119..57030437ef 100644 --- a/.github/workflows/agent-mention-router-quality-ci.yml +++ b/.github/workflows/agent-mention-router-quality-ci.yml @@ -13,7 +13,10 @@ on: - "scripts/ci/agent_mention_sweep.py" - "tests/test_agent_mention_*.py" - "tests/test_pr_review_fix_scheduler_coverage.py" + - "requirements-opencode-review-ci.txt" + - "requirements-noema-document-ci.txt" - "requirements-opencode-review-ci-hashes.txt" + - "scripts/ci/compile_opencode_review_lock.sh" push: branches: [main] paths: @@ -26,7 +29,10 @@ on: - "scripts/ci/agent_mention_sweep.py" - "tests/test_agent_mention_*.py" - "tests/test_pr_review_fix_scheduler_coverage.py" + - "requirements-opencode-review-ci.txt" + - "requirements-noema-document-ci.txt" - "requirements-opencode-review-ci-hashes.txt" + - "scripts/ci/compile_opencode_review_lock.sh" concurrency: group: agent-mention-router-quality-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} @@ -40,7 +46,9 @@ env: jobs: quality: - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 20 steps: - name: Harden runner diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index dc3baea170..9b2de6745c 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -5,6 +5,10 @@ on: branches: [main] paths: - ".github/workflows/agent-review-runtime-quality-ci.yml" + - "scripts/ci/codeql_ghas_configuration_identity.py" + - "tests/test_codeql_ghas_configuration_identity.py" + - "tests/test_codeql_ghas_analyses_pagination.py" + - "tests/test_github_api_url_boundary.py" - ".github/workflows/noema-review.yml" - ".github/actions/noema-review/two_phase.py" - "tests/test_noema_reviewer_token_lifetime.py" @@ -60,9 +64,17 @@ on: - "scripts/ci/zdr_policy.py" - "scripts/ci/contextual_orchestrator_review_policy.py" - "scripts/ci/contextual_orchestrator_review_launcher.py" + - "scripts/ci/personal_review_route.py" - "scripts/ci/contextual_orchestrator_review_sidecar.sh" - "tests/test_zdr_policy.py" - "tests/test_contextual_orchestrator_review_policy.py" + - "tests/test_contextual_orchestrator_review_launcher.py" + - "tests/test_review_auto_route.py" + - "tests/test_personal_review_route.py" + - "tests/test_tokyo_review_consumer.py" + - "tests/test_review_auto_cost_admission.py" + - "tests/test_review_ai_app_independence.py" + - "tests/test_review_preflight_concurrency.py" - "tests/test_contextual_orchestrator_review_sidecar_contract.py" - "tests/test_sidecar_disk_free_guard.py" - "tests/test_hourly_review_repair_callers.py" @@ -116,9 +128,11 @@ on: - "docs/doctoring/exact-artifact-sbom-attestation.md" - "docs/doctoring/exact-artifact-sbom-quality-runner-consolidation-20260903.md" - "CHANGELOG.d/20260903-exact-artifact-quality-runner-consolidation.md" + - "requirements-opencode-review-ci.txt" - "requirements-opencode-review-ci-hashes.txt" - "requirements-noema-document-ci.txt" - "requirements-noema-document-ci-hashes.txt" + - "scripts/ci/compile_opencode_review_lock.sh" # PR validation only: a new head cancels only an older run of this workflow # for the same repository and pull request. @@ -132,7 +146,9 @@ permissions: jobs: agent_review_runtime_quality: name: agent-review-runtime-quality - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 25 env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true @@ -173,6 +189,7 @@ jobs: review_repair_suite=false commercial_readiness_suite=false exact_artifact_suite=false + ghas_suite=false while IFS= read -r changed_path; do case "$changed_path" in @@ -184,6 +201,14 @@ jobs: review_repair_suite=true commercial_readiness_suite=true exact_artifact_suite=true + ghas_suite=true + ;; + scripts/ci/codeql_ghas_configuration_identity.py|\ + tests/test_codeql_ghas_configuration_identity.py|\ + tests/test_codeql_ghas_analyses_pagination.py|\ + tests/test_github_api_url_boundary.py|\ + tests/test_agent_review_runtime_quality_consolidation.py) + ghas_suite=true ;; tests/test_pr_review_autofix_nvidia_nim_contract.py) opencode_suite=true @@ -237,11 +262,13 @@ jobs: tests/test_strix_quality_timeout_fixture_budget.py) strix_suite=true ;; - requirements-opencode-review-ci-hashes.txt) + requirements-opencode-review-ci.txt|\ + requirements-opencode-review-ci-hashes.txt|\ + requirements-noema-document-ci.txt|\ + scripts/ci/compile_opencode_review_lock.sh) noema_suite=true opencode_suite=true ;; - requirements-noema-document-ci.txt|\ requirements-noema-document-ci-hashes.txt) noema_suite=true ;; @@ -264,9 +291,17 @@ jobs: scripts/ci/zdr_policy.py|\ scripts/ci/contextual_orchestrator_review_policy.py|\ scripts/ci/contextual_orchestrator_review_launcher.py|\ + scripts/ci/personal_review_route.py|\ scripts/ci/contextual_orchestrator_review_sidecar.sh|\ tests/test_zdr_policy.py|\ tests/test_contextual_orchestrator_review_policy.py|\ + tests/test_contextual_orchestrator_review_launcher.py|\ + tests/test_review_auto_route.py|\ + tests/test_personal_review_route.py|\ + tests/test_tokyo_review_consumer.py|\ + tests/test_review_auto_cost_admission.py|\ + tests/test_review_ai_app_independence.py|\ + tests/test_review_preflight_concurrency.py|\ tests/test_contextual_orchestrator_review_sidecar_contract.py|\ tests/test_sidecar_disk_free_guard.py|\ tests/test_hourly_review_repair_callers.py|\ @@ -336,6 +371,7 @@ jobs: echo "review_repair=$review_repair_suite" echo "commercial_readiness=$commercial_readiness_suite" echo "exact_artifact=$exact_artifact_suite" + echo "ghas=$ghas_suite" } >>"$GITHUB_OUTPUT" - name: Install exact hash-verified base dependencies @@ -444,11 +480,19 @@ jobs: --cov=scripts.ci.pr_review_autofix_context \ --cov=scripts.ci.zdr_policy \ --cov=scripts.ci.contextual_orchestrator_review_policy \ + --cov=scripts.ci.contextual_orchestrator_review_launcher \ --cov-branch \ --cov-fail-under=100 \ tests/test_pr_review_conflict_scope.py \ tests/test_zdr_policy.py \ tests/test_contextual_orchestrator_review_policy.py \ + tests/test_contextual_orchestrator_review_launcher.py \ + tests/test_review_auto_route.py \ + tests/test_personal_review_route.py \ + tests/test_tokyo_review_consumer.py \ + tests/test_review_auto_cost_admission.py \ + tests/test_review_ai_app_independence.py \ + tests/test_review_preflight_concurrency.py \ tests/test_contextual_orchestrator_review_sidecar_contract.py \ tests/test_sidecar_disk_free_guard.py \ tests/test_hourly_review_repair_callers.py \ @@ -488,6 +532,13 @@ jobs: scripts/ci/contextual_orchestrator_review_launcher.py \ tests/test_zdr_policy.py \ tests/test_contextual_orchestrator_review_policy.py \ + tests/test_contextual_orchestrator_review_launcher.py \ + tests/test_review_auto_route.py \ + tests/test_personal_review_route.py \ + tests/test_tokyo_review_consumer.py \ + tests/test_review_auto_cost_admission.py \ + tests/test_review_ai_app_independence.py \ + tests/test_review_preflight_concurrency.py \ tests/test_contextual_orchestrator_review_sidecar_contract.py \ tests/test_sidecar_disk_free_guard.py \ tests/test_hourly_review_repair_callers.py \ @@ -568,6 +619,21 @@ jobs: tests/test_verify_exact_artifact_sbom_handoff.py \ tests/test_exact_artifact_quality_single_runner.py + - name: Verify GHAS identity, pagination, and API boundary contracts + if: steps.affected_suites.outputs.ghas == 'true' + shell: bash --noprofile --norc -e -o pipefail {0} + run: | + set -euo pipefail + python -m pytest -q \ + tests/test_codeql_ghas_configuration_identity.py \ + tests/test_codeql_ghas_analyses_pagination.py \ + tests/test_github_api_url_boundary.py + python -m compileall -q \ + scripts/ci/codeql_ghas_configuration_identity.py \ + tests/test_codeql_ghas_configuration_identity.py \ + tests/test_codeql_ghas_analyses_pagination.py \ + tests/test_github_api_url_boundary.py + - name: Verify consolidated workflow contract run: | set -euo pipefail diff --git a/.github/workflows/audit-central-ruleset.yml b/.github/workflows/audit-central-ruleset.yml index 655a319835..e735ae1923 100644 --- a/.github/workflows/audit-central-ruleset.yml +++ b/.github/workflows/audit-central-ruleset.yml @@ -25,7 +25,9 @@ permissions: jobs: audit: name: Audit organization ruleset 18156473 - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} env: GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} steps: diff --git a/.github/workflows/cloudflare-dns.yml b/.github/workflows/cloudflare-dns.yml index 991202f4e9..6b23e3b053 100644 --- a/.github/workflows/cloudflare-dns.yml +++ b/.github/workflows/cloudflare-dns.yml @@ -44,7 +44,9 @@ permissions: jobs: reconcile: name: Reconcile zones (${{ github.event.client_payload.mode || 'dry-run' }}) - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/codeql-pr.yml b/.github/workflows/codeql-pr.yml index 356244f7fc..1be7ae0449 100644 --- a/.github/workflows/codeql-pr.yml +++ b/.github/workflows/codeql-pr.yml @@ -60,7 +60,9 @@ jobs: name: Detect CodeQL languages # Draft PRs get no runner; ready_for_review re-runs this on the same head. if: (github.event.action != 'closed') && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} + runs-on: + group: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -164,7 +166,9 @@ jobs: # dependency exactly; the only case where it's genuinely skipped is a # closed PR, where this job being implicitly skipped too is fine because # closed PRs need no required check. - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} + runs-on: + group: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # Verdict reads have exceeded five minutes; retain the ten-minute control budget. timeout-minutes: 10 permissions: @@ -367,7 +371,9 @@ jobs: && github.event.pull_request.state != 'closed' && needs.detect-languages.result == 'success' && needs.detect-languages.outputs.code == 'true' - runs-on: ${{ fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '"ubuntu-24.04"') }} + runs-on: + group: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (fromJSON(github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/codeql-pr.yml@refs/heads/main' && '{"group":"CWL central control","labels":["self-hosted","linux","x64"]}' || '{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read diff --git a/.github/workflows/company-default-head-trust-gate.yml b/.github/workflows/company-default-head-trust-gate.yml new file mode 100644 index 0000000000..08745e92d0 --- /dev/null +++ b/.github/workflows/company-default-head-trust-gate.yml @@ -0,0 +1,64 @@ +name: Reusable company default-head trust gate + +on: + workflow_call: + inputs: + target_runner_label: + required: true + type: string + trusted_ref_sha: + required: false + type: string + default: "" + trusted_ref_ack: + required: false + type: string + default: "" + outputs: + runner_matrix: + value: ${{ jobs.validate.outputs.runner_matrix }} + +permissions: + contents: read + +jobs: + validate: + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] + timeout-minutes: 5 + outputs: + runner_matrix: ${{ steps.runner.outputs.runner_matrix }} + steps: + - id: runner + shell: bash + env: + TARGET: ${{ inputs.target_runner_label }} + run: | + case "$TARGET" in + all) matrix='["itxllmpgw01","itxllmpgw02","xtrmsales-llmgw1","xtrmsales-llmgw2"]' ;; + itxllmpgw01|itxllmpgw02|itxllmpgw03|itxllmpgw04|xtrmsales-llmgw1|xtrmsales-llmgw2|llm-gateway-dev|itx-llm-operation-1|itx-llm-operation-2) + matrix="[\"$TARGET\"]" + ;; + *) echo "unsupported target_runner_label=$TARGET" >&2; exit 2 ;; + esac + echo "runner_matrix=$matrix" >> "$GITHUB_OUTPUT" + - shell: bash + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch || 'main' }} + REF_NAME: ${{ github.ref_name }} + SHA: ${{ github.sha }} + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} + run: | + set -Eeuo pipefail + if [ "$REF_NAME" != "$DEFAULT_BRANCH" ]; then + echo "self-hosted failure diagnostics require the protected default branch" >&2 + exit 2 + fi + current_sha="$(gh api "repos/$REPOSITORY/commits/$DEFAULT_BRANCH" --jq .sha)" + if [ "$SHA" != "$current_sha" ]; then + echo "refusing stale default-branch run sha=$SHA current_sha=$current_sha" >&2 + exit 2 + fi + echo "trusted_ref_status=current_default_head sha=$SHA" diff --git a/.github/workflows/company-settlement-validate.yml b/.github/workflows/company-settlement-validate.yml new file mode 100644 index 0000000000..2cd0498ba3 --- /dev/null +++ b/.github/workflows/company-settlement-validate.yml @@ -0,0 +1,56 @@ +# Source-only fixed-pool candidate. Caller-context group admission remains separate. +name: Company settlement validate +on: + workflow_call: {} +permissions: + contents: read +jobs: + validate: + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + persist-credentials: false + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 + - name: Install locked test dependencies + run: uv pip install --system -r requirements.txt -r requirements-dev.txt + - name: Start isolated PostgreSQL without a TCP listener + shell: bash + run: | + set -Eeuo pipefail + pg_bin="$(pg_config --bindir)" + test -x "$pg_bin/initdb" + test -x "$pg_bin/pg_ctl" + "$pg_bin/postgres" --version + data_dir="$RUNNER_TEMP/settlement-verify-postgres" + socket_dir="$RUNNER_TEMP/settlement-verify-socket" + mkdir -m 700 "$data_dir" "$socket_dir" + printf 'SETTLEMENT_VERIFY_PG_BIN=%s\n' "$pg_bin" >> "$GITHUB_ENV" + printf 'SETTLEMENT_VERIFY_PG_DATA=%s\n' "$data_dir" >> "$GITHUB_ENV" + "$pg_bin/initdb" -D "$data_dir" --no-locale --encoding=UTF8 \ + --auth-local=trust --auth-host=reject + "$pg_bin/pg_ctl" -D "$data_dir" \ + -o "-F -h '' -k $socket_dir" -l "$RUNNER_TEMP/settlement-verify-postgres.log" -w start + printf 'SETTLEMENT_VERIFY_TEST_DSN=host=%s dbname=postgres\n' "$socket_dir" >> "$GITHUB_ENV" + - name: Verify exact-head PgCat and lock regressions + env: + SETTLEMENT_VERIFY_REQUIRE_POSTGRES: "1" + run: >- + python -m pytest -q tests/test_issue119_pgcat_endpoint_resolver.py + tests/test_issue119_settlement_live_verify.py + tests/test_issue119_settlement_replica_routing.py + tests/test_issue119_settlement_review_regressions.py + tests/test_multi_instance_runtime_contract.py + - name: Stop isolated test database + if: always() + shell: bash + run: | + if test -f "${SETTLEMENT_VERIFY_PG_DATA:-}/postmaster.pid"; then + "$SETTLEMENT_VERIFY_PG_BIN/pg_ctl" -D "$SETTLEMENT_VERIFY_PG_DATA" -m fast -w stop + fi diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1bb83c2baf..fc63256794 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,6 +1,6 @@ # Reusable Dependency Review (workflow_call), consolidating the near- -# identical dependency-review.yml files argos, mightyETL, newsdom-api, -# scopeweave, and naruon each carried independently. See +# identical dependency-review.yml files argos, mightyETL, naruon, newsdom-api, +# and scopeweave each carried independently. See # docs/adr/0024-dependency-review-reusable-workflow-consolidation.md and # docs/doctoring/dependency-review-reusable-workflow-consolidation.md for the # per-repo field audit behind these inputs. @@ -9,34 +9,41 @@ # calling repo's own thin workflow file -- a workflow_call target cannot also # be the thing GitHub triggers directly on pull_request. # -# Dependency Review requires GitHub Dependency Graph (and, on private repos -# without GitHub Advanced Security, it is unavailable regardless of a repo's -# own settings). scopeweave's original workflow already detected this -# dynamically via the dependency-graph compare API instead of assuming from -# public/private repository status (mightyETL's original approach, which is -# wrong for a private repo that does have GHAS). This reusable workflow -# adopts the dynamic detection as the common, more-correct behavior for -# every caller, so no per-repo public/private input is needed. +# Dependency Review requires a successful Dependency Graph comparison for the +# exact pull-request base/head. Repository visibility is not a sufficient +# capability signal, and HTTP 403/404 are not safe availability signals: +# GitHub can use those statuses for authorization/policy denials as well as +# unavailable resources. For a pull_request, only exact immutable base/head +# object IDs plus a valid owner/name repository identity may reach transport, +# and only HTTP 200 authorizes running the pinned action. Every non-200 +# comparison fails closed. Non-pull_request triggers may skip because they do +# not carry the exact PR base/head identity. +# +# Reusable-workflow permissions can only be maintained or reduced through the +# call chain. Every thin caller therefore must declare at least `contents: +# read` and `pull-requests: read`; this workflow cannot elevate a caller token +# that omitted those scopes. # # Example caller (.github/workflows/dependency-review.yml in a product repo). -# Pin `uses:` to this file's exact commit SHA, not @main: an unpinned mutable -# ref would run an unreviewed central change against every PR check in the -# calling repo (Devin flagged this on the first four callers; fixed in all of -# them). If the calling repo's branch protection requires a status check -# literally named after the old standalone job, converting to `uses:` here -# will rename the published check to " / dependency-review" and -# silently break that required check -- update the branch protection's -# required-check name to match before or immediately after merging a caller. +# Pin `uses:` to the exact protected-main commit SHA carrying this workflow, +# never `@main`: a mutable central ref could run an unreviewed workflow change +# against the caller's PR checks. If branch protection requires the old +# standalone job name, note that reusable workflow adoption publishes the +# combined check name ` / dependency-review`; update the required +# check name to the exact published context without weakening the gate. # # name: Dependency Review # on: # pull_request: +# permissions: +# contents: read +# pull-requests: read # concurrency: # group: dependency-review-${{ github.event.pull_request.number || github.ref }} # cancel-in-progress: true # jobs: # dependency-review: -# uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@ +# uses: ContextualWisdomLab/.github/.github/workflows/dependency-review.yml@ # with: # fail_on_severity: high # allow_ghsas: "GHSA-69w3-r845-3855" @@ -86,7 +93,9 @@ permissions: jobs: dependency-review: - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] env: # Opts every JS action this job runs (checkout, dependency-review-action) # into the Node 24 actions runtime ahead of GitHub's default cutover, @@ -121,30 +130,43 @@ jobs: exit 0 fi + git_object_id='^[0-9a-f]{40}([0-9a-f]{24})?$' + repository_identity='^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$' + if ! [[ "${BASE_SHA}" =~ $git_object_id ]] || ! [[ "${HEAD_SHA}" =~ $git_object_id ]]; then + echo "::error::Dependency review evidence unavailable: exact 40- or 64-character hexadecimal base and head revisions are required before any compare request. Named refs are not evidence. Verify the pull-request event SHAs, then rerun. Failing closed." + exit 1 + fi + if ! [[ "${REPOSITORY}" =~ $repository_identity ]]; then + echo "::error::Dependency review evidence unavailable: owner/name repository identity is required before any compare request. Verify the pull-request repository, then rerun. Failing closed." + exit 1 + fi + repository_owner="${REPOSITORY%%/*}" + repository_name="${REPOSITORY#*/}" + if [ "${repository_owner}" = "." ] || [ "${repository_owner}" = ".." ] || [ "${repository_name}" = "." ] || [ "${repository_name}" = ".." ]; then + echo "::error::Dependency review evidence unavailable: owner/name repository identity is required before any compare request. Dot or parent-directory path components are not evidence. Verify the pull-request repository, then rerun. Failing closed." + exit 1 + fi + api_url="${GITHUB_API_URL:-https://api.github.com}" response_file="$(mktemp)" + curl_exit=0 status="$( curl -fsS -o "$response_file" -w '%{http_code}' \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer ${GH_TOKEN}" \ -H "X-GitHub-Api-Version: 2022-11-28" \ "${api_url}/repos/${REPOSITORY}/dependency-graph/compare/${BASE_SHA}...${HEAD_SHA}" \ - || true - )" + )" || curl_exit=$? - if [ "$status" = "200" ]; then + if [ "$curl_exit" -eq 0 ] && [ "$status" = "200" ]; then echo "available=true" >>"$GITHUB_OUTPUT" exit 0 fi - if [ "$status" = "403" ] || [ "$status" = "404" ]; then - echo "::warning::Dependency graph compare returned HTTP ${status} for ${REPOSITORY}; skipping the dependency-review hard gate (GitHub Dependency Graph, or GitHub Advanced Security on a private repository, is unavailable)." - echo "available=false" >>"$GITHUB_OUTPUT" - exit 0 + echo "::error::Dependency graph comparison failed with curl exit ${curl_exit} and HTTP ${status:-unavailable}. For a pull_request, transport failures and non-200 responses are ambiguous between feature availability and authorization/policy/transport failure, so Dependency Review fails closed instead of being skipped." + if [ -s "$response_file" ]; then + cat "$response_file" fi - - echo "::error::Dependency graph availability check failed with HTTP ${status}. This is not a 'graph unavailable' response (403/404) -- treating it as a genuine failure instead of silently skipping the security gate." - cat "$response_file" exit 1 - name: Dependency review @@ -155,9 +177,3 @@ jobs: fail-on-severity: ${{ inputs.fail_on_severity }} allow-ghsas: ${{ inputs.allow_ghsas }} comment-summary-in-pr: ${{ inputs.comment_summary_in_pr }} - - - name: Dependency graph unavailable note - if: steps.dependency_graph.outputs.available != 'true' && github.event_name == 'pull_request' - run: | - echo "Dependency Review requires GitHub Dependency Graph to be enabled for this repository (and, on private repositories, GitHub Advanced Security)." - echo "Other required dependency-vulnerability gates (OSV-Scanner, Scorecard) remain the blocking coverage until Dependency Graph is available here." diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index f86b614022..8097a8c1aa 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -45,7 +45,9 @@ permissions: jobs: deploy_pages: name: Deploy ${{ inputs.project_name }} - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] steps: - name: Checkout caller repo uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/exact-artifact-sbom-attestation.yml b/.github/workflows/exact-artifact-sbom-attestation.yml index 198392aa0c..1e280ecb4f 100644 --- a/.github/workflows/exact-artifact-sbom-attestation.yml +++ b/.github/workflows/exact-artifact-sbom-attestation.yml @@ -64,7 +64,9 @@ env: jobs: verify-evidence-artifact: name: Verify inert sealed evidence - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 15 permissions: actions: read @@ -180,7 +182,9 @@ jobs: attest-exact-artifacts: name: Attest exact wheel and sdist SBOMs needs: verify-evidence-artifact - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 20 permissions: actions: read @@ -372,13 +376,13 @@ jobs: EOF { printf '\n## Exact signed identity\n\n' - printf -- '- Source repository: `%s`\n' "$SOURCE_REPOSITORY" - printf -- '- Source SHA: `%s`\n' "$SOURCE_SHA" - printf -- '- Signer repository: `%s`\n' "$SIGNER_REPOSITORY" - printf -- '- Signer workflow: `%s`\n' "$signer_workflow" - printf -- '- Predicate type: `%s`\n' "$PREDICATE_TYPE" - printf -- '- Wheel: `%s`\n' "$WHEEL_FILENAME" - printf -- '- Source distribution: `%s`\n' "$SDIST_FILENAME" + printf -- "- Source repository: \`%s\`\n" "$SOURCE_REPOSITORY" + printf -- "- Source SHA: \`%s\`\n" "$SOURCE_SHA" + printf -- "- Signer repository: \`%s\`\n" "$SIGNER_REPOSITORY" + printf -- "- Signer workflow: \`%s\`\n" "$signer_workflow" + printf -- "- Predicate type: \`%s\`\n" "$PREDICATE_TYPE" + printf -- "- Wheel: \`%s\`\n" "$WHEEL_FILENAME" + printf -- "- Source distribution: \`%s\`\n" "$SDIST_FILENAME" cat <> offline-attestation-evidence/README.md ( cd offline-attestation-evidence - LC_ALL=C find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\n' \ - | LC_ALL=C sort \ + evidence_names="$(LC_ALL=C find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%f\n' | LC_ALL=C sort)" + printf '%s\n' "$evidence_names" \ | while IFS= read -r evidence_file; do + [ -n "$evidence_file" ] || continue sha256sum "$evidence_file" done > SHA256SUMS ) diff --git a/.github/workflows/exact-head-coverage-quality-gate.yml b/.github/workflows/exact-head-coverage-quality-gate.yml index 6b957571d2..f0670a4b50 100644 --- a/.github/workflows/exact-head-coverage-quality-gate.yml +++ b/.github/workflows/exact-head-coverage-quality-gate.yml @@ -37,7 +37,9 @@ permissions: jobs: quality-gate: - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: ${{ inputs.timeout_minutes }} steps: - name: Checkout exact source revision diff --git a/.github/workflows/law-ai-agent-ci.yml b/.github/workflows/law-ai-agent-ci.yml new file mode 100644 index 0000000000..d0c19baf53 --- /dev/null +++ b/.github/workflows/law-ai-agent-ci.yml @@ -0,0 +1,117 @@ +name: Law AI Agent central CI + +on: + workflow_call: + +permissions: + contents: read + +concurrency: + group: law-ci-${{ github.workflow }}-${{ github.repository }}-${{ github.event.pull_request.number || format('{0}-{1}', github.event_name, github.ref) }} + cancel-in-progress: true + +jobs: + quality: + runs-on: + group: CWL law CI + labels: [self-hosted, Linux, X64, law-ai-agent-ci] + timeout-minutes: 20 + strategy: + fail-fast: false + max-parallel: 1 + matrix: + python-version: ["3.12", "3.14"] + steps: + - name: Admit exact owned caller before checkout + id: admission + shell: bash + env: + CALLER_REPOSITORY: ${{ github.repository }} + CALLER_EVENT: ${{ github.event_name }} + CALLER_REF: ${{ github.ref }} + SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + CENTRAL_REPOSITORY: ${{ job.workflow_repository }} + CENTRAL_REF: ${{ job.workflow_ref }} + CENTRAL_SHA: ${{ job.workflow_sha }} + run: | + set -euo pipefail + python3 - <<'PY' + """Fail closed before checking out consumer or central source.""" + import json + import os + import re + import sys + from pathlib import Path + e = os.environ + owned = "ContextualWisdomLab/law-ai-agent" + valid = e["CALLER_REPOSITORY"] == owned + valid &= e["CENTRAL_REPOSITORY"] == "ContextualWisdomLab/.github" + valid &= bool(re.fullmatch(r"[0-9a-f]{40}", e["SOURCE_SHA"])) + valid &= bool(re.fullmatch(r"[0-9a-f]{40}", e["CENTRAL_SHA"])) + valid &= e["CENTRAL_REF"] == ( + "ContextualWisdomLab/.github/.github/workflows/law-ai-agent-ci.yml@" + + e["CENTRAL_SHA"] + ) + payload = json.loads(Path(e["GITHUB_EVENT_PATH"]).read_text()) + event = e["CALLER_EVENT"] + if event == "pull_request": + pr = payload.get("pull_request", {}) + head, base = pr.get("head", {}), pr.get("base", {}) + valid &= head.get("repo", {}).get("full_name") == owned + valid &= base.get("repo", {}).get("full_name") == owned + valid &= head.get("sha") == e["SOURCE_SHA"] + valid &= base.get("ref") == "main" + elif event == "push": + valid &= e["CALLER_REF"] == "refs/heads/main" + valid &= payload.get("ref") == "refs/heads/main" + valid &= payload.get("after") == e["SOURCE_SHA"] + valid &= payload.get("deleted") is False + elif event == "workflow_dispatch": + valid &= e["CALLER_REF"] == "refs/heads/main" + else: + valid = False + if not valid: + sys.exit("law-ci: admission refused") + with open(e["GITHUB_OUTPUT"], "a", encoding="utf-8") as output: + output.write("source_sha=" + e["SOURCE_SHA"] + "\n") + PY + - name: Checkout admitted consumer head + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 verified commit + with: + repository: ContextualWisdomLab/law-ai-agent + ref: ${{ steps.admission.outputs.source_sha }} + path: source + persist-credentials: false + - name: Checkout this reusable workflow source + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 verified commit + with: + repository: ${{ job.workflow_repository }} + ref: ${{ job.workflow_sha }} + path: central + persist-credentials: false + - name: Verify both checkout identities + shell: bash + env: + SOURCE_SHA: ${{ steps.admission.outputs.source_sha }} + CENTRAL_SHA: ${{ job.workflow_sha }} + run: | + set -euo pipefail + test "$(git -C "$GITHUB_WORKSPACE/source" rev-parse HEAD)" = "$SOURCE_SHA" + test "$(git -C "$GITHUB_WORKSPACE/central" rev-parse HEAD)" = "$CENTRAL_SHA" + - name: Provision pinned uv and matrix Python + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 dereferenced commit + with: + version: "0.12.5" + python-version: ${{ matrix.python-version }} + enable-cache: false + - name: Run isolated native database and artifact gates + shell: bash + env: + LAW_CI_SOURCE: ${{ github.workspace }}/source + LAW_CI_SOURCE_SHA: ${{ steps.admission.outputs.source_sha }} + LAW_CI_PYTHON_VERSION: ${{ matrix.python-version }} + run: bash "$GITHUB_WORKSPACE/central/scripts/ci/law_ai_agent_ci.sh" run + - name: Cleanup owned native cluster on cancellation + if: always() + shell: bash + run: bash "$GITHUB_WORKSPACE/central/scripts/ci/law_ai_agent_ci.sh" cleanup diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 08ea600538..6c81d859e2 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -41,7 +41,9 @@ jobs: && github.event.action != 'converted_to_draft' && github.event.pull_request.head.repo.full_name == github.repository ) - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis' || github.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis' || github.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 outputs: admitted: ${{ steps.live_head.outputs.admitted }} @@ -70,7 +72,13 @@ jobs: exit 1 fi echo "repository=${TARGET_REPOSITORY#*/}" >>"$GITHUB_OUTPUT" - if [ -n "${METADATA_TOKEN:-}" ]; then + if [ "$TARGET_REPOSITORY" = "ContextualWisdomLab/k-csap-skills" ]; then + if [ -z "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] || [ -z "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then + echo "::error::k-csap-skills Noema metadata requires the native GitHub App client ID and private key; credential fallback is forbidden." + exit 1 + fi + echo "source=github-app" >>"$GITHUB_OUTPUT" + elif [ -n "${METADATA_TOKEN:-}" ]; then echo "source=pat" >>"$GITHUB_OUTPUT" elif [ -n "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] && [ -n "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then echo "source=github-app" >>"$GITHUB_OUTPUT" @@ -94,7 +102,7 @@ jobs: - name: Admit only the exact live Noema head id: live_head env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.noema_metadata_app_token.outputs.token || github.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_metadata_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.noema_metadata_app_token.outputs.token || github.token)) || '' }} run: | set -euo pipefail echo "admitted=false" >>"$GITHUB_OUTPUT" @@ -137,7 +145,9 @@ jobs: # PR/REPO lookup naturally falls through to "scan everything" for that # path, matching strix.yml's identical repository_dispatch behavior. if: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft') - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis' || github.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis' || github.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -193,7 +203,9 @@ jobs: if: >- github.event_name == 'pull_request_target' && (github.event.action == 'closed' || github.event.action == 'converted_to_draft') - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis' || github.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis' || github.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # Bound this job well short of GitHub's 360-minute platform default. Its # only step is a single-repository, status-filtered gh api --paginate # list-and-cancel sweep (up to 3 passes x 5 statuses), no branch update @@ -371,7 +383,9 @@ jobs: noema-review: name: noema-review needs: [admit-current-head, changed-scope] - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis' || github.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis' || github.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # No job-level timeout-minutes here, deliberately. This job's "Prepare # Noema model verdict" step calls two_phase.py's call_llm synchronously # via the contextual-orchestrator gateway and blocks on the model's own @@ -425,6 +439,8 @@ jobs: # diff. PR_NUMBER: ${{ (needs.changed-scope.outputs.code != 'false' && (github.event.pull_request.number || github.event.client_payload.pr_number)) || '' }} EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha || '' }} + NOEMA_TOOLING_MANIFEST_SHA256: ${{ vars.NOEMA_TOOLING_MANIFEST_SHA256 || '' }} + NOEMA_EXECUTION_ADMISSION_SHA256: ${{ vars.NOEMA_EXECUTION_ADMISSION_SHA256 || '' }} steps: - name: Skip events without pull request context if: env.PR_NUMBER == '' @@ -493,6 +509,10 @@ jobs: trusted_archive="${RUNNER_TEMP}/trusted-noema-source.tar.gz" api_url="${GITHUB_API_URL:-https://api.github.com}" curl -fsSL \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 1 \ + --retry-max-time 30 \ -H "Authorization: Bearer ${GH_TOKEN}" \ -H "Accept: application/vnd.github+json" \ -o "$trusted_archive" \ @@ -516,7 +536,13 @@ jobs: exit 1 fi echo "repository=${TARGET_REPOSITORY#*/}" >>"$GITHUB_OUTPUT" - if [ -n "${METADATA_TOKEN:-}" ]; then + if [ "$TARGET_REPOSITORY" = "ContextualWisdomLab/k-csap-skills" ]; then + if [ -z "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] || [ -z "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then + echo "::error::k-csap-skills Noema metadata requires the native GitHub App client ID and private key; credential fallback is forbidden." + exit 1 + fi + echo "source=github-app" >>"$GITHUB_OUTPUT" + elif [ -n "${METADATA_TOKEN:-}" ]; then echo "source=pat" >>"$GITHUB_OUTPUT" elif [ -n "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] && [ -n "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then echo "source=github-app" >>"$GITHUB_OUTPUT" @@ -540,7 +566,7 @@ jobs: - name: Reject a stale trigger before credential or model setup if: env.PR_NUMBER != '' env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.noema_metadata_app_token.outputs.token || github.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_metadata_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.noema_metadata_app_token.outputs.token || github.token)) || '' }} run: | set -euo pipefail if [[ ! "$EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then @@ -631,6 +657,34 @@ jobs: done echo "Cancelled ${cancelled} superseded Noema run(s) after live-head validation." + - name: Admit independently pinned premerge tooling and execution + if: env.PR_NUMBER != '' + env: + GH_TOKEN: ${{ steps.noema_metadata_app_token.outputs.token || github.token }} + NOEMA_TOOLING_MANIFEST_SHA256: ${{ vars.NOEMA_TOOLING_MANIFEST_SHA256 || '' }} + NOEMA_EXECUTION_ADMISSION_SHA256: ${{ vars.NOEMA_EXECUTION_ADMISSION_SHA256 || '' }} + run: | + set -euo pipefail + # The existing custodian supplies original manifests, accepted namespace, + # source/dependency pins and phase JWT files. Missing evidence stays STOP. + python3 -I -B "$GITHUB_WORKSPACE/scripts/ci/noema_bootstrap_producer.py" \ + --materialize --private-parent "$RUNNER_TEMP/noema-private-bootstrap" \ + --source-root "$RUNNER_TEMP/noema-tooling-supplier" \ + --manifest "$RUNNER_TEMP/noema-accepted-tooling.json" \ + --accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256" \ + --admission "$RUNNER_TEMP/noema-execution-admission.json" \ + --admission-digest "$NOEMA_EXECUTION_ADMISSION_SHA256" \ + --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" \ + --expected-head "$EXPECTED_HEAD_SHA" + python3 -I -B "$GITHUB_WORKSPACE/scripts/ci/premerge_review_tooling.py" \ + --admit-production --tooling-root "$RUNNER_TEMP/noema-private-bootstrap/tooling" \ + --manifest "$RUNNER_TEMP/noema-accepted-tooling.json" \ + --accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256" \ + --admission "$RUNNER_TEMP/noema-execution-admission.json" \ + --admission-digest "$NOEMA_EXECUTION_ADMISSION_SHA256" \ + --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" \ + --expected-head "$EXPECTED_HEAD_SHA" + - name: Select fail-closed Noema reviewer credential if: env.PR_NUMBER != '' id: noema_credential @@ -638,7 +692,7 @@ jobs: NOEMA_GITHUB_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID || '' }} NOEMA_GITHUB_APP_PRIVATE_KEY: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY || '' }} TOKEN_EXCHANGE_URL: ${{ vars.NOEMA_TOKEN_EXCHANGE_URL || vars.NOEMA_EXCHANGE_URL || '' }} - NOEMA_REVIEW_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN }} + NOEMA_REVIEW_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || '' }} run: | set -euo pipefail @@ -649,6 +703,15 @@ jobs: repository_name="${TARGET_REPOSITORY#*/}" echo "repository=$repository_name" >>"$GITHUB_OUTPUT" + if [ "$TARGET_REPOSITORY" = "ContextualWisdomLab/k-csap-skills" ]; then + if [ -z "${NOEMA_GITHUB_APP_CLIENT_ID:-}" ] || [ -z "${NOEMA_GITHUB_APP_PRIVATE_KEY:-}" ]; then + echo "::error::k-csap-skills Noema reviewer requires the native GitHub App client ID and private key; PAT/OIDC fallback is forbidden." + exit 1 + fi + echo "source=github-app" >>"$GITHUB_OUTPUT" + exit 0 + fi + if [ -n "${NOEMA_REVIEW_TOKEN:-}" ]; then echo "source=pat" >>"$GITHUB_OUTPUT" echo "::notice::Noema reviewer using the NOEMA_REVIEW_TOKEN secret fallback identity." @@ -679,14 +742,9 @@ jobs: private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} owner: ContextualWisdomLab repositories: ${{ steps.noema_credential.outputs.repository }} - permission-actions: read - permission-checks: read permission-contents: read permission-metadata: read - permission-pull-requests: write - permission-security-events: read - permission-statuses: read - permission-vulnerability-alerts: read + permission-pull-requests: read - name: Exchange Noema app token through OIDC if: env.PR_NUMBER != '' && steps.noema_credential.outputs.source == 'oidc' @@ -749,7 +807,7 @@ jobs: - name: Validate current pull request head if: env.PR_NUMBER != '' env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token)) || '' }} run: | set -euo pipefail if ! [[ "$EXPECTED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then @@ -769,7 +827,7 @@ jobs: if: env.PR_NUMBER != '' id: target_visibility env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token)) || '' }} run: | set -euo pipefail if [ -z "${GH_TOKEN:-}" ]; then @@ -822,9 +880,13 @@ jobs: if: env.PR_NUMBER != '' id: live_draft env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token)) || '' }} run: | - set -uo pipefail + set -euo pipefail + if [ -z "${GH_TOKEN:-}" ]; then + echo "::error::Noema reviewer credential is missing for live draft check." + exit 1 + fi live_draft=false if pull_request_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}" 2>/tmp/noema-live-draft-error)"; then if jq -e -s 'length == 1 and (.[0] | type == "object" and .draft == true)' <<<"$pull_request_json" >/dev/null 2>&1; then @@ -853,9 +915,71 @@ jobs: python-version: "3.12" update-environment: false + - name: Admit existing personal LiteLLM auto route + id: personal_route + if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' + env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + PERSONAL_REVIEW_ZERO_COST_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZERO_COST_ATTESTED || 'false' }} + PERSONAL_REVIEW_ZDR_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZDR_ATTESTED || 'false' }} + PRIVATE_TARGET: ${{ steps.target_visibility.outputs.require_zdr }} + run: | + set -euo pipefail + if [ -z "${LLM_GATEWAY_API_KEY:-}" ]; then + echo '::error::STOP: existing personal review Secret is unavailable.' + exit 1 + fi + python3 -m scripts.ci.personal_review_route \ + --zero-cost "$PERSONAL_REVIEW_ZERO_COST_ATTESTED" \ + --zero-retention "$PERSONAL_REVIEW_ZDR_ATTESTED" \ + --private-target "$PRIVATE_TARGET" + echo 'selected=true' >>"$GITHUB_OUTPUT" + + - name: Initialize current Noema sidecar evidence + id: noema_evidence + if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' && steps.personal_route.outputs.selected != 'true' + run: | + set -euo pipefail + # Separate setup from the producer: unsafe paths fail before its outcome + # can admit an upload. Clear only these two files, before early startup. + python3 -I <<'PY' + import os + import stat + from pathlib import Path + + workspace_text = os.environ["GITHUB_WORKSPACE"] + workspace = Path(workspace_text) + if ( + not workspace.is_absolute() + or ".." in workspace.parts + or workspace.resolve(strict=True) != workspace + or not workspace.is_dir() + ): + raise SystemExit("Noema evidence workspace must be an unaliased directory") + evidence = workspace / "strix_runs" + if evidence.is_symlink() or (evidence.exists() and not evidence.is_dir()): + raise SystemExit("Noema evidence parent must be a non-symlink directory") + owned = ( + evidence / "contextual-orchestrator-preflight.json", + evidence / "contextual-orchestrator-sidecar.stderr.log", + ) + # Validate both before deleting either; reject dangling links, special + # files and hardlink aliases as well as nonregular evidence paths. + for path in owned: + try: + metadata = path.lstat() + except FileNotFoundError: + continue + if not stat.S_ISREG(metadata.st_mode) or metadata.st_nlink != 1: + raise SystemExit("Noema evidence must be regular, single-link files") + evidence.mkdir(mode=0o700, exist_ok=True) + for path in owned: + path.unlink(missing_ok=True) + PY + - name: Provision contextual-orchestrator review sidecar id: noema_sidecar - if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' + if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' && steps.personal_route.outputs.selected != 'true' env: SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} @@ -864,6 +988,7 @@ jobs: OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ steps.target_visibility.outputs.require_zdr }} + CONTEXTUAL_ORCHESTRATOR_POOL: auto run: | set -euo pipefail test ! -L "$GITHUB_WORKSPACE/strix_runs" @@ -912,11 +1037,15 @@ jobs: if: env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' id: noema_prepare env: - GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token)) || '' }} NOEMA_REVIEW_TOKEN_SOURCE: ${{ steps.noema_credential.outputs.source == 'pat' && 'noema-review-pat' || steps.noema_credential.outputs.source == 'github-app' && 'noema-review-github-app' || 'noema-review-app-oidc' }} NOEMA_REVIEW_ACTOR: ${{ steps.noema_github_app_token.outputs['app-slug'] && format('{0}[bot]', steps.noema_github_app_token.outputs['app-slug']) || '' }} NOEMA_REVIEW_INSTALLATION_ID: ${{ steps.noema_github_app_token.outputs['installation-id'] }} NOEMA_TRANSPORT_RETRY_ATTEMPT: ${{ toJSON(github.event.client_payload.transport_retry_attempt) }} + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + PERSONAL_ROUTE_SELECTED: ${{ steps.personal_route.outputs.selected }} + NOEMA_GITHUB_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }} + NOEMA_GITHUB_APP_PRIVATE_KEY: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} run: | set -euo pipefail if [ -z "${PR_NUMBER:-}" ]; then @@ -928,18 +1057,34 @@ jobs: echo "::error::Noema reviewer credential selection succeeded but no token was minted; review cannot prepare a verdict." exit 1 fi + if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then + test -n "${LLM_GATEWAY_API_KEY:-}" + export NOEMA_LLM_API_URL="https://litellm.poinnetworks.net/v1/chat/completions" + export NOEMA_LLM_MODEL="auto" + export NOEMA_LLM_API_KEY="$LLM_GATEWAY_API_KEY" + unset NOEMA_LLM_VIA_ORCHESTRATOR + else if [ -z "${CONTEXTUAL_ORCHESTRATOR_BASE_URL:-}" ] || [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN_FILE:-}" ]; then echo "::error::contextual-orchestrator review sidecar must be provisioned before Noema LLM review." exit 1 fi source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" export NOEMA_LLM_API_URL="${CONTEXTUAL_ORCHESTRATOR_BASE_URL%/}/v1/chat/completions" - export NOEMA_LLM_MODEL="orchestrator/free" + export NOEMA_LLM_MODEL="orchestrator/auto" export NOEMA_LLM_API_KEY="${CONTEXTUAL_ORCHESTRATOR_TOKEN}" export NOEMA_LLM_VIA_ORCHESTRATOR=1 - verdict_file="${RUNNER_TEMP}/noema-verdict-envelope.json" - rm -f "$verdict_file" - python3 "$GITHUB_WORKSPACE/.github/actions/noema-review/two_phase.py" --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" --expected-head "$EXPECTED_HEAD_SHA" --prepare-verdict-file "$verdict_file" + fi + verdict_file="${RUNNER_TEMP}/noema-private-bootstrap/verdict.json" + test ! -e "$verdict_file" + node "$GITHUB_WORKSPACE/scripts/ci/noema_existing_app_issuer.cjs" \ + --phase prepare --private-parent "$RUNNER_TEMP/noema-private-bootstrap" \ + --source-root "$RUNNER_TEMP/noema-tooling-supplier" \ + --manifest "$RUNNER_TEMP/noema-accepted-tooling.json" \ + --accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256" \ + --admission "$RUNNER_TEMP/noema-execution-admission.json" \ + --admission-digest "$NOEMA_EXECUTION_ADMISSION_SHA256" \ + --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" \ + --expected-head "$EXPECTED_HEAD_SHA" --verdict-file "$verdict_file" if [ -f "$verdict_file" ]; then echo "prepared=true" >>"$GITHUB_OUTPUT" else @@ -948,7 +1093,10 @@ jobs: fi - name: Upload contextual-orchestrator sidecar evidence - if: always() && env.PR_NUMBER != '' + # A live Draft never provisions the sidecar. Retain only current producer + # terminal diagnostics; absent/skipped outcomes cannot publish stale files. + # Non-Draft upload failures still fail the required review job. + if: always() && env.PR_NUMBER != '' && steps.live_draft.outputs.live_draft != 'true' && (steps.noema_sidecar.outcome == 'success' || steps.noema_sidecar.outcome == 'failure' || steps.noema_sidecar.outcome == 'cancelled') uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: noema-sidecar-evidence @@ -958,6 +1106,36 @@ jobs: if-no-files-found: ignore retention-days: 5 + - name: Refresh read-only Noema GitHub App metadata token after model + if: env.PR_NUMBER != '' && steps.noema_prepare.outputs.prepared == 'true' && steps.noema_credential.outputs.source == 'github-app' + id: noema_publication_metadata_token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} + owner: ContextualWisdomLab + repositories: ${{ steps.noema_credential.outputs.repository }} + permission-contents: read + permission-metadata: read + permission-pull-requests: read + + - name: Revalidate prepared scope before publisher credential refresh + if: env.PR_NUMBER != '' && steps.noema_prepare.outputs.prepared == 'true' + env: + GH_TOKEN: ${{ steps.noema_publication_metadata_token.outputs.token || '' }} + run: | + set -euo pipefail + python3 -I -B "$GITHUB_WORKSPACE/scripts/ci/noema_bootstrap_producer.py" \ + --revalidate-publish --private-parent "$RUNNER_TEMP/noema-private-bootstrap" \ + --source-root "$RUNNER_TEMP/noema-tooling-supplier" \ + --manifest "$RUNNER_TEMP/noema-accepted-tooling.json" \ + --accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256" \ + --admission "$RUNNER_TEMP/noema-execution-admission.json" \ + --admission-digest "$NOEMA_EXECUTION_ADMISSION_SHA256" \ + --verdict-file "$RUNNER_TEMP/noema-private-bootstrap/verdict.json" \ + --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" \ + --expected-head "$EXPECTED_HEAD_SHA" + - name: Refresh repository-scoped Noema GitHub App token for publication if: env.PR_NUMBER != '' && steps.noema_prepare.outputs.prepared == 'true' && steps.noema_credential.outputs.source == 'github-app' id: noema_github_app_publication_token @@ -967,34 +1145,43 @@ jobs: private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} owner: ContextualWisdomLab repositories: ${{ steps.noema_credential.outputs.repository }} - permission-actions: read - permission-checks: read permission-contents: read permission-metadata: read - permission-pull-requests: write - permission-security-events: read - permission-statuses: read - permission-vulnerability-alerts: read + permission-pull-requests: read - name: Publish prepared Noema verdict on the exact live head if: env.PR_NUMBER != '' && steps.noema_prepare.outputs.prepared == 'true' env: - GH_TOKEN: ${{ steps.noema_credential.outputs.source == 'pat' && secrets.NOEMA_REVIEW_TOKEN || steps.noema_credential.outputs.source == 'github-app' && steps.noema_github_app_publication_token.outputs.token || steps.noema_credential.outputs.source == 'oidc' && steps.noema_oidc_token.outputs.token || '' }} + GH_TOKEN: ${{ env.TARGET_REPOSITORY == 'ContextualWisdomLab/k-csap-skills' && steps.noema_github_app_publication_token.outputs.token || (env.TARGET_REPOSITORY != 'ContextualWisdomLab/k-csap-skills' && (steps.noema_credential.outputs.source == 'pat' && secrets.NOEMA_REVIEW_TOKEN || steps.noema_credential.outputs.source == 'github-app' && steps.noema_github_app_publication_token.outputs.token || steps.noema_credential.outputs.source == 'oidc' && steps.noema_oidc_token.outputs.token || '')) || '' }} NOEMA_REVIEW_TOKEN_SOURCE: ${{ steps.noema_credential.outputs.source == 'pat' && 'noema-review-pat' || steps.noema_credential.outputs.source == 'github-app' && 'noema-review-github-app-refresh' || steps.noema_credential.outputs.source == 'oidc' && 'noema-review-app-oidc' || '' }} NOEMA_REVIEW_ACTOR: ${{ steps.noema_github_app_publication_token.outputs['app-slug'] && format('{0}[bot]', steps.noema_github_app_publication_token.outputs['app-slug']) || '' }} NOEMA_REVIEW_INSTALLATION_ID: ${{ steps.noema_github_app_publication_token.outputs['installation-id'] }} + NOEMA_GITHUB_APP_CLIENT_ID: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }} + NOEMA_GITHUB_APP_PRIVATE_KEY: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }} run: | set -euo pipefail if [ -z "${GH_TOKEN:-}" ]; then echo "::error::Noema publication has no credential for the explicitly selected reviewer source; refusing any GITHUB_TOKEN or author fallback." exit 1 fi - verdict_file="${RUNNER_TEMP}/noema-verdict-envelope.json" + verdict_file="${RUNNER_TEMP}/noema-private-bootstrap/verdict.json" if [ ! -f "$verdict_file" ]; then echo "::error::Noema prepared-verdict output claimed success but its private envelope is missing." exit 1 fi - python3 "$GITHUB_WORKSPACE/.github/actions/noema-review/two_phase.py" --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" --expected-head "$EXPECTED_HEAD_SHA" --publish-verdict-file "$verdict_file" + if [ "${{ steps.noema_credential.outputs.source }}" != 'github-app' ]; then + echo "::error::Two-phase Noema issuer requires the existing authenticated GitHub App route; no PAT/OIDC authority relabeling." + exit 1 + fi + node "$GITHUB_WORKSPACE/scripts/ci/noema_existing_app_issuer.cjs" \ + --phase publish --private-parent "$RUNNER_TEMP/noema-private-bootstrap" \ + --source-root "$RUNNER_TEMP/noema-tooling-supplier" \ + --manifest "$RUNNER_TEMP/noema-accepted-tooling.json" \ + --accepted-digest "$NOEMA_TOOLING_MANIFEST_SHA256" \ + --admission "$RUNNER_TEMP/noema-execution-admission.json" \ + --admission-digest "$NOEMA_EXECUTION_ADMISSION_SHA256" \ + --repo "$TARGET_REPOSITORY" --pr-number "$PR_NUMBER" \ + --expected-head "$EXPECTED_HEAD_SHA" --verdict-file "$verdict_file" continue-noema-transport: needs: [admit-current-head, noema-review] @@ -1004,7 +1191,9 @@ jobs: && needs.noema-review.result == 'failure' && needs.noema-review.outputs.transport_capacity_unavailable == 'true' && needs.noema-review.outputs.transport_retry_eligible == 'true' - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis' || github.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/contextual-orchestrator' || github.repository == 'ContextualWisdomLab/fast-mlsirm' || github.repository == 'ContextualWisdomLab/cwl-telemetry' || github.repository == 'ContextualWisdomLab/naruon' || github.repository == 'ContextualWisdomLab/late-life-anxiety-reanalysis' || github.repository == 'ContextualWisdomLab/tokyo-travel-2026') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 10 permissions: contents: write diff --git a/.github/workflows/opencode-review-coalesce-tick.yml b/.github/workflows/opencode-review-coalesce-tick.yml index 35f1da22c7..a9b657ba31 100644 --- a/.github/workflows/opencode-review-coalesce-tick.yml +++ b/.github/workflows/opencode-review-coalesce-tick.yml @@ -54,7 +54,9 @@ jobs: # gate back to step scope: that reintroduces multi-hour queue wait for # an inert echo under the org concurrent-job ceiling. if: vars.OPENCODE_REVIEW_COALESCE_ENABLED == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 4 permissions: actions: write @@ -85,18 +87,18 @@ jobs: repos_file="${RUNNER_TEMP}/coalesce-repos.txt" : >"$repos_file" cursor="" - for page in 1 2 3 4 5 6 7 8 9 10; do + for _page in 1 2 3 4 5 6 7 8 9 10; do cursor_arg=() if [ -n "$cursor" ]; then cursor_arg=(-f "cursor=$cursor") fi - response="$(gh api graphql -f query=' - query($cursor: String) { - search(query: "org:ContextualWisdomLab is:pr is:open draft:false", type: ISSUE, first: 100, after: $cursor) { + response="$(gh api graphql -f query=" + query(\$cursor: String) { + search(query: \"org:ContextualWisdomLab is:pr is:open draft:false\", type: ISSUE, first: 100, after: \$cursor) { nodes { ... on PullRequest { repository { nameWithOwner } } } pageInfo { hasNextPage endCursor } } - }' "${cursor_arg[@]}" 2>/dev/null || echo '{}')" + }" "${cursor_arg[@]}" 2>/dev/null || echo '{}')" printf '%s' "$response" | jq -r '.data.search.nodes[]?.repository.nameWithOwner // empty' >>"$repos_file" has_next="$(printf '%s' "$response" | jq -r '.data.search.pageInfo.hasNextPage // false')" [ "$has_next" = "true" ] || break diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 09bbf8181a..443888bf1f 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -396,9 +396,11 @@ jobs: if: >- needs.validate-pr-metadata.result == 'success' && github.event_name == 'repository_dispatch' + # Pull-request merge-tree tests run only on the clean-per-job isolated pool. + # Keep metadata and review publication on their separate trusted jobs. runs-on: - group: CWL central OpenCode - labels: [self-hosted, linux, x64] + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 300 permissions: # The PR tree arrives through a same-run artifact. No repository-content, @@ -747,7 +749,7 @@ jobs: changed_basename="${changed_path##*/}" case "$changed_basename" in *.py|pyproject.toml|uv.lock|poetry.lock|Pipfile|Pipfile.lock|\ - setup.py|setup.cfg|tox.ini|pytest.ini|.pytest.ini|\ + setup.cfg|tox.ini|pytest.ini|.pytest.ini|\ requirements.lock|requirements*.txt|requirements*.in|\ environment.yml|environment.yaml) python_coverage_required=1 @@ -976,6 +978,9 @@ jobs: && rm -rf /tmp/base-python-requirements \ && rm -f /usr/local/libexec/install-base-python-locks.py COPY base-rust-dependencies /opt/base-rust-dependencies + # Crate archives keep modes such as 0640 through COPY, so make the base + # Rust dependency tree readable by the unprivileged coverage sandbox UID. + RUN chmod -R a+rX /opt/base-rust-dependencies DOCKERFILE # Debian's rustc lags the MSRV of pinned repositories (fast-mlsirm pins # 1.97.1; Debian ships 1.85), so a base-pinned repository gets that exact @@ -1116,9 +1121,11 @@ jobs: } append_command() { - printf '$ ' >>"$summary_file" - printf '%q ' "$@" >>"$summary_file" - printf '\n' >>"$summary_file" + { + printf '$ ' + printf '%q ' "$@" + printf '\n' + } >>"$summary_file" } emit_captured_log() { @@ -1406,13 +1413,13 @@ jobs: project_is_maturin_project "$project_dir" || return 0 run_and_capture "Offline PyO3/maturin extension build (${project_dir})" \ env CARGO_NET_OFFLINE=true CARGO_BUILD_JOBS=1 \ - bash -c 'set -eu - cd "$1" - dist_dir="$(mktemp -d)" - python3 -m maturin build --offline --release -o "$dist_dir" - python3 -m pip install --user --no-index --no-deps --force-reinstall "$dist_dir"/*.whl - python3 "$2" "$dist_dir"/*.whl . - rm -rf "$dist_dir"' bash "$project_dir" "${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py" + bash -c "set -eu + cd \"\$1\" + dist_dir=\"\$(mktemp -d)\" + python3 -m maturin build --offline --release -o \"\$dist_dir\" + python3 -m pip install --user --no-index --no-deps --force-reinstall \"\$dist_dir\"/*.whl + python3 \"\$2\" \"\$dist_dir\"/*.whl . + rm -rf \"\$dist_dir\"" bash "$project_dir" "${GITHUB_WORKSPACE}/scripts/ci/place_maturin_extension.py" } run_python_test_coverage() { @@ -1431,14 +1438,14 @@ jobs: done <<<"$configured_commands_json" else run_and_capture "Python coverage with missing-line report (${project_dir})" \ - bash -c 'cd "$1" && PYTHONPATH="$([ -d src ] && printf src:. || printf .)" python3 -m coverage run -m pytest tests && python3 -m coverage report --show-missing' bash "$project_dir" + bash -c "cd \"\$1\" && PYTHONPATH=\"\$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest tests && python3 -m coverage report --show-missing" bash "$project_dir" fi done < <(tracked_python_projects_with_tests) if [ "$measured_projects" -eq 0 ]; then if has_tracked_files '*.py'; then run_and_capture "Python coverage with missing-line report" \ - bash -c 'PYTHONPATH="$([ -d src ] && printf src:. || printf .)" python3 -m coverage run -m pytest && python3 -m coverage report --show-missing' + bash -c "PYTHONPATH=\"\$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest && python3 -m coverage report --show-missing" elif python3 -I -c 'import pytest_cov' >/dev/null 2>&1; then run_and_capture "Python pytest-cov coverage" python3 -m pytest --cov=. --cov-report=term-missing else @@ -1589,7 +1596,7 @@ jobs: if [ -f "${project_dir}/tests/test_docstrings.py" ]; then measured_projects=1 run_and_capture "Python docstring coverage (${project_dir})" \ - bash -c 'cd "$1" && PYTHONPATH="$([ -d src ] && printf src:. || printf .)" python3 -m pytest tests/test_docstrings.py' bash "$project_dir" + bash -c "cd \"\$1\" && PYTHONPATH=\"\$([ -d src ] && printf src:. || printf .)\" python3 -m pytest tests/test_docstrings.py" bash "$project_dir" fi done < <(tracked_python_projects_with_tests) [ "$measured_projects" -eq 1 ] @@ -1957,21 +1964,21 @@ jobs: if [ -n "$package_name" ] && jq -e '.workspaces // empty' package.json >/dev/null 2>&1; then run_and_capture "Tauri frontendDist build (${package_dir})" corepack npm run build --workspace "$package_name" else - run_and_capture "Tauri frontendDist build (${package_dir})" bash -c 'cd "$1" && corepack npm run build' bash "$package_dir" + run_and_capture "Tauri frontendDist build (${package_dir})" bash -c "cd \"\$1\" && corepack npm run build" bash "$package_dir" fi ;; pnpm) if [ -n "$package_name" ] && [ -f pnpm-workspace.yaml ]; then run_and_capture "Tauri frontendDist build (${package_dir})" corepack pnpm --filter "$package_name" run build else - run_and_capture "Tauri frontendDist build (${package_dir})" bash -c 'cd "$1" && corepack pnpm run build' bash "$package_dir" + run_and_capture "Tauri frontendDist build (${package_dir})" bash -c "cd \"\$1\" && corepack pnpm run build" bash "$package_dir" fi ;; yarn) if [ -n "$package_name" ] && jq -e '.workspaces // empty' package.json >/dev/null 2>&1; then run_and_capture "Tauri frontendDist build (${package_dir})" yarn workspace "$package_name" build else - run_and_capture "Tauri frontendDist build (${package_dir})" bash -c 'cd "$1" && yarn build' bash "$package_dir" + run_and_capture "Tauri frontendDist build (${package_dir})" bash -c "cd \"\$1\" && yarn build" bash "$package_dir" fi ;; esac @@ -2087,8 +2094,13 @@ jobs: # coverage command still runs and reports any uncovered GPU lines # exactly as before, so Rust repositories without GPU code are # unaffected and no gate is weakened. - if ls /usr/share/vulkan/icd.d/lvp_icd*.json >/dev/null 2>&1; then - lvp_icd="$(ls /usr/share/vulkan/icd.d/lvp_icd*.json | head -n1)" + local lvp_icd="" candidate_icd + for candidate_icd in /usr/share/vulkan/icd.d/lvp_icd*.json; do + [ -f "$candidate_icd" ] || continue + lvp_icd="$candidate_icd" + break + done + if [ -n "$lvp_icd" ]; then export VK_ICD_FILENAMES="$lvp_icd" export VK_DRIVER_FILES="$lvp_icd" export WGPU_BACKEND=vulkan @@ -2655,7 +2667,27 @@ jobs: python-version: "3.12" update-environment: false + - name: Admit existing personal LiteLLM auto route + id: personal_route + env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + PERSONAL_REVIEW_ZERO_COST_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZERO_COST_ATTESTED || 'false' }} + PERSONAL_REVIEW_ZDR_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZDR_ATTESTED || 'false' }} + PRIVATE_TARGET: ${{ needs.validate-pr-metadata.outputs.is_private }} + run: | + set -euo pipefail + if [ -z "${LLM_GATEWAY_API_KEY:-}" ]; then + echo '::error::STOP: existing personal review Secret is unavailable.' + exit 1 + fi + python3 -m scripts.ci.personal_review_route \ + --zero-cost "$PERSONAL_REVIEW_ZERO_COST_ATTESTED" \ + --zero-retention "$PERSONAL_REVIEW_ZDR_ATTESTED" \ + --private-target "$PRIVATE_TARGET" + echo 'selected=true' >>"$GITHUB_OUTPUT" + - name: Provision contextual-orchestrator review sidecar + if: steps.personal_route.outputs.selected != 'true' env: SIDECAR_PYTHON: ${{ steps.sidecar_python.outputs.python-path }} BYTEZ_API_KEY: ${{ secrets.BYTEZ_API_KEY }} @@ -2664,6 +2696,7 @@ jobs: OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ needs.validate-pr-metadata.outputs.is_private }} + CONTEXTUAL_ORCHESTRATOR_POOL: auto run: | set -euo pipefail bash "$GITHUB_WORKSPACE/scripts/ci/contextual_orchestrator_review_sidecar.sh" @@ -3295,13 +3328,13 @@ jobs: language_signal="Match changed prose" fi - printf -- '- Preferred review language: `%s`\n' "$language_signal" + printf -- "- Preferred review language: \`%s\`\\n" "$language_signal" printf -- '- Rule: write human-readable review prose in the preferred language; keep file paths, identifiers, logs, quoted source, error text, and protocol literals unchanged.\n' - printf -- '- PR title: `%s`\n' "$(printf '%s' "$title" | tr '\r\n`' ' ' | cut -c 1-240)" + printf -- "- PR title: \`%s\`\\n" "$(printf '%s' "$title" | tr '\r\n`' ' ' | cut -c 1-240)" if [ -n "$body" ]; then - printf -- '- PR body excerpt: `%s`\n' "$(printf '%s' "$body" | tr '\r\n`' ' ' | cut -c 1-360)" + printf -- "- PR body excerpt: \`%s\`\\n" "$(printf '%s' "$body" | tr '\r\n`' ' ' | cut -c 1-360)" else - printf -- '- PR body excerpt: `[empty]`\n' + printf -- "- PR body excerpt: \`[empty]\`\\n" fi } @@ -3580,7 +3613,7 @@ jobs: shift if ! git -C "$OPENCODE_SOURCE_WORKDIR" diff "$@"; then - printf 'Unable to collect %s from `%s` to `%s`; continue review from available changed-file evidence and direct file inspection.\n' "$description" "$PR_MERGE_BASE" "$PR_HEAD_SHA" + printf "Unable to collect %s from \`%s\` to \`%s\`; continue review from available changed-file evidence and direct file inspection.\\n" "$description" "$PR_MERGE_BASE" "$PR_HEAD_SHA" fi } @@ -3590,13 +3623,13 @@ jobs: printf -- "- Base SHA: \`%s\`\n" "$PR_BASE_SHA" printf -- "- Head SHA: \`%s\`\n\n" "$PR_HEAD_SHA" if ! PR_MERGE_BASE="$(git -C "$OPENCODE_SOURCE_WORKDIR" merge-base "$PR_BASE_SHA" "$PR_HEAD_SHA")"; then - printf 'Merge-base discovery failed for `%s` and `%s`; falling back to base SHA for bounded diff evidence.\n\n' "$PR_BASE_SHA" "$PR_HEAD_SHA" + printf "Merge-base discovery failed for \`%s\` and \`%s\`; falling back to base SHA for bounded diff evidence.\\n\\n" "$PR_BASE_SHA" "$PR_HEAD_SHA" PR_MERGE_BASE="$PR_BASE_SHA" fi printf -- "- Merge base SHA: \`%s\`\n\n" "$PR_MERGE_BASE" printf '## Current-head authority order\n\n' printf 'Treat current-head sections in this file as authoritative for this run: Other unresolved review thread evidence, Failed GitHub Check evidence, Coverage execution evidence, Changed files, and Focused changed hunks.\n' - printf 'All PR reviews and comments evidence is historical context only and may contain stale bot conclusions. Do not infer active failed checks, unresolved threads, or missing changed files from those comments unless current-head evidence corroborates the same claim for Head SHA `%s`.\n\n' "$PR_HEAD_SHA" + printf "All PR reviews and comments evidence is historical context only and may contain stale bot conclusions. Do not infer active failed checks, unresolved threads, or missing changed files from those comments unless current-head evidence corroborates the same claim for Head SHA \`%s\`.\\n\\n" "$PR_HEAD_SHA" if ! git -C "$OPENCODE_SOURCE_WORKDIR" diff --name-only --find-renames "$PR_MERGE_BASE" "$PR_HEAD_SHA" | awk 'NF > 0 && $0 !~ /^\// && $0 !~ /(^|\/)\.\.($|\/)/ { print }' >"$OPENCODE_CHANGED_FILES_FILE"; then printf 'Changed-file discovery failed; downstream review must inspect the PR head directly.\n\n' @@ -3752,6 +3785,10 @@ jobs: - name: Prepare isolated OpenCode review workspace env: + PERSONAL_ROUTE_SELECTED: ${{ steps.personal_route.outputs.selected }} + PERSONAL_REVIEW_ZERO_COST_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZERO_COST_ATTESTED || 'false' }} + PERSONAL_REVIEW_ZDR_ATTESTED: ${{ vars.PERSONAL_REVIEW_ZDR_ATTESTED || 'false' }} + PRIVATE_TARGET: ${{ needs.validate-pr-metadata.outputs.is_private }} OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_EVIDENCE_FILE: ${{ runner.temp }}/opencode-review-evidence.md OPENCODE_FAILED_CHECK_EVIDENCE_FILE: ${{ runner.temp }}/opencode-failed-check-evidence.md @@ -4101,8 +4138,8 @@ jobs: jq -n '{ "$schema": "https://opencode.ai/config.json", - "model": "contextual-orchestrator/orchestrator/free", - "small_model": "contextual-orchestrator/orchestrator/free", + "model": "contextual-orchestrator/orchestrator/auto", + "small_model": "contextual-orchestrator/orchestrator/auto", "enabled_providers": ["contextual-orchestrator"], "lsp": false, "mcp": {}, @@ -4184,8 +4221,8 @@ jobs: gateway_config="${OPENCODE_REVIEW_WORKDIR}/opencode.gateway.jsonc" jq ' - .model = "contextual-orchestrator/orchestrator/free" | - .small_model = "contextual-orchestrator/orchestrator/free" | + .model = "contextual-orchestrator/orchestrator/auto" | + .small_model = "contextual-orchestrator/orchestrator/auto" | .enabled_providers = ["contextual-orchestrator"] | .provider = { "contextual-orchestrator": { @@ -4196,8 +4233,8 @@ jobs: "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}" }, "models": { - "orchestrator/free": { - "name": "Orchestrator Free (ZDR-first zero-cost pool)", + "orchestrator/auto": { + "name": "Orchestrator Auto (cost-fenced dynamic selection)", "tool_call": true, "reasoning": true, "options": { @@ -4219,11 +4256,18 @@ jobs: ' "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" >"$gateway_config" mv -- "$gateway_config" "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" - if ! grep -Fq 'contextual-orchestrator/orchestrator/free' "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" \ + if ! grep -Fq 'contextual-orchestrator/orchestrator/auto' "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" \ || ! grep -Fq 'CONTEXTUAL_ORCHESTRATOR_BASE_URL' "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc"; then echo '::error::Generated isolated opencode.jsonc is missing the contextual-orchestrator gateway route.' exit 1 fi + if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then + python3 -m scripts.ci.personal_review_route \ + --zero-cost "$PERSONAL_REVIEW_ZERO_COST_ATTESTED" \ + --zero-retention "$PERSONAL_REVIEW_ZDR_ATTESTED" \ + --private-target "$PRIVATE_TARGET" \ + --opencode-config "${OPENCODE_REVIEW_WORKDIR}/opencode.jsonc" + fi printf 'Prepared isolated OpenCode review workspace: %s\n' "$OPENCODE_REVIEW_WORKDIR" - name: Run OpenCode PR Review model pool @@ -4231,10 +4275,12 @@ jobs: if: needs.coverage-evidence.result == 'success' continue-on-error: true env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + PERSONAL_ROUTE_SELECTED: ${{ steps.personal_route.outputs.selected }} SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/free" + OPENCODE_MODEL_CANDIDATES: "contextual-orchestrator/orchestrator/auto" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. @@ -4272,7 +4318,12 @@ jobs: RUN_ATTEMPT: ${{ github.run_attempt }} run: | set -euo pipefail - source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then + test -n "${LLM_GATEWAY_API_KEY:-}" + export OPENCODE_MODEL_CANDIDATES="personal-litellm/auto" + else + source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + fi set +e bash "$GITHUB_WORKSPACE/scripts/ci/run_opencode_review_model_pool.sh" pool_status=$? @@ -4607,27 +4658,27 @@ jobs: "$@" } - self_check_filter=' + self_check_filter=" def self_check: - (.name // "") as $n - | ["opencode-review", "coverage-evidence", "coverage-source-tree", "required-workflow-bootstrap", "metadata-only gate evaluation", "scan-pr-queue"] | index($n); + (.name // \"\") as \$n + | [\"opencode-review\", \"coverage-evidence\", \"coverage-source-tree\", \"required-workflow-bootstrap\", \"metadata-only gate evaluation\", \"scan-pr-queue\"] | index(\$n); def latest_peer_checks: [ (.check_runs // [])[] | select(self_check | not) | . + { checkedAt: ( - if ((.started_at // "") != "") then .started_at - else (.completed_at // "") + if ((.started_at // \"\") != \"\") then .started_at + else (.completed_at // \"\") end ) } ] - | sort_by(.app.slug // "", .name // "", .checkedAt // "", .id // 0) - | group_by([.app.slug // "", .name // ""]) + | sort_by(.app.slug // \"\", .name // \"\", .checkedAt // \"\", .id // 0) + | group_by([.app.slug // \"\", .name // \"\"]) | map(last) | .[]; - ' + " check_runs_file="$(mktemp)" pending_checks_file="$(mktemp)" @@ -4830,6 +4881,8 @@ jobs: # extensions for image validation or package/GPU builds plus API and # publication overhead. env: + LLM_GATEWAY_API_KEY: ${{ secrets.LLM_GATEWAY_API_KEY }} + PERSONAL_ROUTE_SELECTED: ${{ steps.personal_route.outputs.selected }} GH_TOKEN: ${{ steps.opencode_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} CHECK_LOOKUP_GH_TOKEN: ${{ github.token }} # The OpenCode app installation token is exchanged from api.opencode.ai @@ -4852,7 +4905,7 @@ jobs: COVERAGE_EVIDENCE_SUMMARY: ${{ needs.coverage-evidence.outputs.coverage_summary || 'Coverage evidence job did not run or did not publish coverage evidence.' }} OPENCODE_REVIEW_WORKDIR: ${{ runner.temp }}/opencode-review-project OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head - MODEL: contextual-orchestrator/orchestrator/free + MODEL: contextual-orchestrator/orchestrator/auto USE_GITHUB_TOKEN: "true" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" @@ -4886,6 +4939,10 @@ jobs: OPENCODE_EXPORT_TIMEOUT_SECONDS: "60" run: | set -euo pipefail + if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then + test -n "${LLM_GATEWAY_API_KEY:-}" + export MODEL="personal-litellm/auto" + fi quoted_coverage="${COVERAGE_EVIDENCE_RESULT:-}" COVERAGE_EVIDENCE_RESULT="$(python3 scripts/ci/opencode_coverage_identity.py \ --repo "$GH_REPOSITORY" \ @@ -5274,7 +5331,7 @@ jobs: if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then { printf '## OpenCode %s review body\n\n' "$event" - printf -- '- Head SHA: `%s`\n' "$HEAD_SHA" + printf -- "- Head SHA: \`%s\`\\n" "$HEAD_SHA" printf -- '- Workflow run: %s\n' "$RUN_ID" printf -- '- Workflow attempt: %s\n\n' "$RUN_ATTEMPT" printf '%s\n' "$body" @@ -5628,7 +5685,7 @@ jobs: printf '## Summary\n\n' printf '%s\n\n' "$summary" printf '## Adversarial validation\n\n' - printf '```json\n%s\n```\n\n' "$adversarial_evidence" + printf "\`\`\`json\\n%s\\n\`\`\`\\n\\n" "$adversarial_evidence" printf -- '- Result: REQUEST_CHANGES\n' printf -- '- Reason: %s\n\n' "$reason" printf -- "- Head SHA: \`%s\`\n" "$HEAD_SHA" @@ -6214,9 +6271,15 @@ jobs: printf 'Skipping publish-step failed-check OpenCode diagnosis for central review-process self-repair; using collected current-head failed-check logs/SARIF fallback so the publish step stays bounded.\n' >&2 return 1 fi - source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" - if [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then - return 1 + if [ "${PERSONAL_ROUTE_SELECTED:-}" = 'true' ]; then + if [ -z "${LLM_GATEWAY_API_KEY:-}" ]; then + return 1 + fi + else + source "$GITHUB_WORKSPACE/scripts/ci/load_contextual_orchestrator_token.sh" + if [ -z "${CONTEXTUAL_ORCHESTRATOR_TOKEN:-}" ]; then + return 1 + fi fi if ! python3 "$GITHUB_WORKSPACE/scripts/ci/assert_opencode_reasoning_effort.py" \ --config "$OPENCODE_REVIEW_WORKDIR/opencode.jsonc" \ @@ -6443,33 +6506,33 @@ jobs: case "$mode" in failed) - jq_filter=' + jq_filter=" [.[].check_runs[]?] - | sort_by((.started_at // .completed_at // .created_at // ""), (.id // 0)) - | group_by(.name // "") + | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0)) + | group_by(.name // \"\") | map(last) | .[]? - | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence","metadata-only gate evaluation"] | index($n)) | not) - | select((.status // "") == "completed") - | select((.conclusion // "" | ascii_upcase) as $c | ["FAILURE","TIMED_OUT","ACTION_REQUIRED","CANCELLED","STARTUP_FAILURE"] | index($c)) - | select((.name // "") != "scan-pr-queue") - | select(((.conclusion // "" | ascii_downcase) == "cancelled" and ((.name // "") | contains("$" + "{{"))) | not) - | select(((.conclusion // "" | ascii_downcase) == "cancelled" and (.name // "") == "noema-review") | not) - | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.conclusion // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) - ' + | select(((.name // \"\" | ascii_downcase) as \$n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index(\$n)) | not) + | select((.status // \"\") == \"completed\") + | select((.conclusion // \"\" | ascii_upcase) as \$c | [\"FAILURE\",\"TIMED_OUT\",\"ACTION_REQUIRED\",\"CANCELLED\",\"STARTUP_FAILURE\"] | index(\$c)) + | select((.name // \"\") != \"scan-pr-queue\") + | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and ((.name // \"\") | contains(\"\$\" + \"{{\"))) | not) + | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and (.name // \"\") == \"noema-review\") | not) + | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.conclusion // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end) + " ;; pending) - jq_filter=' + jq_filter=" [.[].check_runs[]?] - | sort_by((.started_at // .completed_at // .created_at // ""), (.id // 0)) - | group_by(.name // "") + | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0)) + | group_by(.name // \"\") | map(last) | .[]? - | select(((.name // "" | ascii_downcase) as $n | ["opencode-review","coverage-evidence","metadata-only gate evaluation"] | index($n)) | not) - | select((.name // "") != "scan-pr-queue") - | select((.status // "") != "completed") - | "- " + (if (.name // "") == "strix" then "Strix Security Scan/strix" else ((.name // "check") + " check run") end) + ": " + (.status // "unknown") + (if (.details_url // .html_url // "") != "" then " (" + (.details_url // .html_url) + ")" else "" end) - ' + | select(((.name // \"\" | ascii_downcase) as \$n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index(\$n)) | not) + | select((.name // \"\") != \"scan-pr-queue\") + | select((.status // \"\") != \"completed\") + | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.status // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end) + " ;; *) return 1 @@ -6527,10 +6590,10 @@ jobs: -f owner="$owner" \ -f name="$name" \ -F number="$PR_NUMBER" \ - -f query=' - query($owner:String!,$name:String!,$number:Int!) { - repository(owner:$owner,name:$name) { - pullRequest(number:$number) { + -f query=" + query(\$owner:String!,\$name:String!,\$number:Int!) { + repository(owner:\$owner,name:\$name) { + pullRequest(number:\$number) { statusCheckRollup { contexts(first: 100) { nodes { @@ -6555,7 +6618,7 @@ jobs: } } } - ' \ + " \ --jq ' (.data.repository.pullRequest.statusCheckRollup.contexts.nodes // []) | map( @@ -6680,7 +6743,7 @@ jobs: -f owner="$owner" \ -f name="$name" \ -F number="$PR_NUMBER" \ - -f query='query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id}}}' \ + -f query="query(\$owner:String!,\$name:String!,\$number:Int!){repository(owner:\$owner,name:\$name){pullRequest(number:\$number){id}}}" \ --jq '.data.repository.pullRequest.id // empty')"; then echo "GitHub Checks statusCheckRollup PR id lookup failed; falling back to current-head REST check-runs." >&2 pr_node_id="" @@ -7124,21 +7187,21 @@ jobs: -f "ref=refs/heads/${head_ref}" \ -f state=open \ -F per_page=100 \ - --jq ' + --jq " [ .[] | { number: (.number // 0), - rule: (.rule.id // .rule.name // "unknown"), - tool: (.tool.name // "code-scanning"), - severity: (.rule.security_severity_level // .rule.severity // "unknown"), - url: (.html_url // "") + rule: (.rule.id // .rule.name // \"unknown\"), + tool: (.tool.name // \"code-scanning\"), + severity: (.rule.security_severity_level // .rule.severity // \"unknown\"), + url: (.html_url // \"\") } - | select((.severity | ascii_downcase) as $s | ["medium","high","critical","warning","error"] | index($s)) + | select((.severity | ascii_downcase) as \$s | [\"medium\",\"high\",\"critical\",\"warning\",\"error\"] | index(\$s)) ] | .[] - | "- " + .tool + "/" + .rule + ": " + .severity + " alert #" + (.number | tostring) + (if .url != "" then " (" + .url + ")" else "" end) - ' >"$output_file" 2>"$lookup_error_file"; then + | \"- \" + .tool + \"/\" + .rule + \": \" + .severity + \" alert #\" + (.number | tostring) + (if .url != \"\" then \" (\" + .url + \")\" else \"\" end) + " >"$output_file" 2>"$lookup_error_file"; then sed 's/^/gh: /' "$lookup_error_file" >&2 || true rm -f "$lookup_error_file" return 1 @@ -7186,7 +7249,7 @@ jobs: printf 'OpenCode could not approve from deterministic current-head evidence because GitHub Checks have failed.\n\n' printf '## Findings\n\n' printf '### 1. HIGH Current-head GitHub Checks - Fix failed required checks before approval\n' - printf -- '- Problem: Failed same-head checks remain for `%s`.\n' "$HEAD_SHA" + printf -- "- Problem: Failed same-head checks remain for \`%s\`.\\n" "$HEAD_SHA" printf -- '- Root cause: The model-unavailable evidence fallback is allowed only when peer GitHub Checks are complete and clean.\n' printf -- '- Fix: Read and fix the failed check logs below, then rerun the current-head checks.\n' printf -- '- Regression test: Keep the model-unavailable fallback gated on an empty failed-check rollup.\n\n' @@ -7242,11 +7305,11 @@ jobs: if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then { printf '## OpenCode required check satisfied by existing same-head approval\n\n' - printf -- '- Result: `EXISTING_CURRENT_HEAD_APPROVAL`\n' - printf -- '- Head SHA: `%s`\n' "$HEAD_SHA" + printf -- "- Result: \`EXISTING_CURRENT_HEAD_APPROVAL\`\\n" + printf -- "- Head SHA: \`%s\`\\n" "$HEAD_SHA" printf -- '- Workflow run: %s\n' "$RUN_ID" printf -- '- Workflow attempt: %s\n' "$RUN_ATTEMPT" - printf -- '- Model-pool outcome: `%s`\n' "${OPENCODE_MODEL_POOL_OUTCOME:-unknown}" + printf -- "- Model-pool outcome: \`%s\`\\n" "${OPENCODE_MODEL_POOL_OUTCOME:-unknown}" printf -- '- Reason: a prior real-model OpenCode APPROVED review with passed structured adversarial probes already targets this exact head, and the fallback rechecked coverage, peer checks, code-scanning alerts, and unresolved review threads before accepting it.\n' printf -- '- Review state: unchanged; no duplicate APPROVE review was posted from model-output-unavailable evidence.\n\n' } >>"$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 1194d7eea6..1cfbea07b7 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -43,7 +43,9 @@ jobs: # queue wait between two single-digit-second jobs. See # docs/doctoring/actions-capacity-root-cause-20260917.md for the # underlying measurement methodology. - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} permissions: contents: read pull-requests: read @@ -152,6 +154,10 @@ jobs: api_url="${GITHUB_API_URL:-https://api.github.com}" mkdir -p "$trusted_source_dir" curl -fsSL \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 1 \ + --retry-max-time 30 \ -H "Authorization: Bearer ${GH_TOKEN}" \ -H "Accept: application/vnd.github+json" \ -o "$trusted_archive" \ @@ -304,7 +310,9 @@ jobs: # gap using the identical classifier. Fails OPEN: an unreadable, empty, # or truncated file list reviews everything. if: (github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft')) && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -358,7 +366,9 @@ jobs: name: coverage-source-tree needs: [required-workflow-bootstrap] if: needs.required-workflow-bootstrap.outputs.admitted == 'true' - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} steps: - run: >- echo "PR-head source and coverage execution are delegated to the @@ -378,7 +388,9 @@ jobs: # through coverage-source-tree, so an unadmitted head still skips it. needs: [required-workflow-bootstrap] if: needs.required-workflow-bootstrap.outputs.admitted == 'true' - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} steps: - run: >- echo "This required-workflow job preserves the stable branch-protection @@ -397,7 +409,9 @@ jobs: # directly by this job's own `if:` below, not inherited through that edge. needs: [required-workflow-bootstrap, changed-scope] if: needs.required-workflow-bootstrap.outputs.admitted == 'true' - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} permissions: contents: read pull-requests: read @@ -517,6 +531,7 @@ jobs: PR_ACTION: ${{ github.event.action }} PR_DRAFT: ${{ github.event.pull_request.draft }} CHANGED_SCOPE_CODE: ${{ needs.changed-scope.outputs.code }} + WORKFLOW_SHA: ${{ github.workflow_sha }} run: | set -euo pipefail if [ "$PR_ACTION" = "closed" ]; then @@ -558,35 +573,40 @@ jobs: if [ "$PR_DRAFT" = "true" ]; then echo "Event draft snapshot is stale; checking the verdict for the live ready PR." fi - reviews="$(gh api --paginate "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews?per_page=100")" - verdict="$(printf '%s\n' "$reviews" | jq -r -s --arg sha "$HEAD_SHA" ' - (add // []) - | [ - .[] - | select( - (.user.login // "" | ascii_downcase) as $user - | $user == "opencode-agent" or $user == "opencode-agent[bot]" - ) - | select((.commit_id // "" | ascii_downcase) == ($sha | ascii_downcase)) - | select(.state == "APPROVED" or .state == "CHANGES_REQUESTED") - ] - | (last // {}) as $review - | ($review.body // "" | ascii_downcase) as $body - | if $review.state == "CHANGES_REQUESTED" then - "CHANGES_REQUESTED" - elif $review.state == "APPROVED" - and ($body | contains("deterministic current-head evidence") | not) - and ($body | contains("deterministic fallback approval") | not) - and ($body | contains("model-unavailable evidence fallback") | not) - and ($body | contains("did not emit a usable current-head control block") | not) - and ($body | contains("scope: `unsupported`") | not) - and ($body | contains("model-pool outcome: `unknown`") | not) - then - "APPROVED" - else - empty - end - ')" + reviews="$(gh api --paginate --slurp "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews?per_page=100")" + helper="$(mktemp)" + trap 'rm -f "$helper"' EXIT + gh api "repos/ContextualWisdomLab/.github/contents/scripts/ci/opencode_review_receipt_gate.py?ref=${WORKFLOW_SHA}" \ + --jq .content | base64 --decode >"$helper" + verdict="$(python3 - "$helper" "$reviews" "$HEAD_SHA" <<'PY' + import importlib.machinery + import importlib.util + import json + import sys + + helper_path, reviews_json, head_sha = sys.argv[1:] + loader = importlib.machinery.SourceFileLoader("trusted_opencode_receipt_gate", helper_path) + spec = importlib.util.spec_from_loader(loader.name, loader) + if spec is None or spec.loader is None: + raise RuntimeError("trusted OpenCode receipt helper could not be loaded") + gate = importlib.util.module_from_spec(spec) + spec.loader.exec_module(gate) + pages = json.loads(reviews_json) + if not isinstance(pages, list) or not all( + isinstance(page, list) and all(isinstance(review, dict) for review in page) + for page in pages + ): + raise ValueError("formal review lookup returned malformed paginated JSON") + reviews = [review for page in pages for review in page + if str((review.get("user") or {}).get("login") or "").lower() + in {"opencode-agent", "opencode-agent[bot]"}] + receipt, reason = gate.evaluate_receipts(reviews, head_sha, is_draft=False) + if receipt is None: + print(f"No current-head formal OpenCode verdict: {reason}", file=sys.stderr) + raise SystemExit(0) + print(receipt["state"]) + PY + )" if [ -z "$verdict" ]; then echo "::error::No APPROVED or CHANGES_REQUESTED from opencode-agent on the current head. The dispatch workflow will rerun this failed job after publishing an authenticated exact-head verdict." exit 1 @@ -607,7 +627,9 @@ jobs: # head no longer matches the live one. The target job also revalidates the # live PR before dispatch and verdict admission. if: github.event_name == 'pull_request_target' && github.event.action == 'synchronize' - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} permissions: actions: write contents: read diff --git a/.github/workflows/organization-commercial-readiness-loop.yml b/.github/workflows/organization-commercial-readiness-loop.yml index ad70331b4c..fa1ef018ff 100644 --- a/.github/workflows/organization-commercial-readiness-loop.yml +++ b/.github/workflows/organization-commercial-readiness-loop.yml @@ -16,7 +16,9 @@ jobs: if: >- github.repository == 'ContextualWisdomLab/.github' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 25 permissions: contents: read diff --git a/.github/workflows/pr-auto-rebase.yml b/.github/workflows/pr-auto-rebase.yml index 1f9ec482e8..4992224926 100644 --- a/.github/workflows/pr-auto-rebase.yml +++ b/.github/workflows/pr-auto-rebase.yml @@ -68,7 +68,9 @@ permissions: jobs: auto-rebase: - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} permissions: contents: read # checkout only; force-push uses the app or PR-write token below pull-requests: write # read PR queue and manage labels diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index a0cf3642d4..4528a3f014 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -22,7 +22,9 @@ permissions: jobs: autofix: - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] # No job-level timeout-minutes here, deliberately. This job's dominant # cost is `opencode run` (up to two invocations: the main autofix pass, # and a base-merge conflict-resolution pass) -- a job-level wall-clock diff --git a/.github/workflows/pr-review-fix-scheduler.yml b/.github/workflows/pr-review-fix-scheduler.yml index b6d3fff560..5543bbc2b9 100644 --- a/.github/workflows/pr-review-fix-scheduler.yml +++ b/.github/workflows/pr-review-fix-scheduler.yml @@ -92,7 +92,9 @@ jobs: # API reads and autofix dispatch only; no pull-request content is checked # out. The central main caller uses the idle control pool instead of the # saturated hosted queue; any other caller keeps hosted Ubuntu 24.04. - runs-on: ${{ github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/hourly-review-repair.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/hourly-review-repair.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/hourly-review-repair.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 35 env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index a26fd4857e..7777f02bed 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -354,6 +354,10 @@ jobs: trusted_archive="${RUNNER_TEMP}/trusted-scheduler-source.tar.gz" api_url="${GITHUB_API_URL:-https://api.github.com}" curl -fsSL \ + --retry 3 \ + --retry-all-errors \ + --retry-delay 1 \ + --retry-max-time 30 \ -H "Authorization: Bearer ${GH_TOKEN}" \ -H "Accept: application/vnd.github+json" \ -o "$trusted_archive" \ diff --git a/.github/workflows/python-security.yml b/.github/workflows/python-security.yml index 1788cfd40b..6e07d817c2 100644 --- a/.github/workflows/python-security.yml +++ b/.github/workflows/python-security.yml @@ -46,7 +46,9 @@ jobs: detect-python: name: Detect Python if: github.event.action != 'closed' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] outputs: has_python: ${{ steps.detect.outputs.has_python }} has_manifest: ${{ steps.detect.outputs.has_manifest }} @@ -77,7 +79,9 @@ jobs: name: Bandit (Python SAST) needs: detect-python if: github.event.action != 'closed' && needs.detect-python.outputs.has_python == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read security-events: write @@ -204,7 +208,9 @@ jobs: name: pip-audit (Python dependency audit) needs: detect-python if: github.event.action != 'closed' && needs.detect-python.outputs.has_manifest == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read steps: diff --git a/.github/workflows/r-package-check.yml b/.github/workflows/r-package-check.yml index 221d66a838..0d23daac2a 100644 --- a/.github/workflows/r-package-check.yml +++ b/.github/workflows/r-package-check.yml @@ -45,8 +45,8 @@ on: description: >- JSON array of {os, r, http-user-agent?} objects for strategy.matrix.config. Default is a single ubuntu-latest/release - leg; override with a JSON array for a multi-OS/multi-R-version - matrix. + leg. Only ubuntu-latest and ubuntu-24.04 Linux/x64 image aliases + execute; other OS/architecture legs are explicitly STOP, not PASS. required: false type: string default: '[{"os": "ubuntu-latest", "r": "release"}]' @@ -91,14 +91,63 @@ permissions: contents: read jobs: + admit-platforms: + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] + outputs: + matrix: ${{ steps.admit.outputs.matrix }} + steps: + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.10' + - id: admit + env: + R_MATRIX: ${{ inputs.r_matrix }} + shell: bash + run: | + matrix=$(python - "$R_MATRIX" <<'PY' + """Bound the R matrix to supported Linux/x64 image aliases, never runner labels.""" + import json + import sys + + + def admit(raw): + """Reject unsupported matrix legs before any R job can be scheduled.""" + rows = json.loads(raw) + if not isinstance(rows, list) or not 1 <= len(rows) <= 64: + raise ValueError('Expected 1..64 matrix objects') + for row in rows: + if not isinstance(row, dict) or not isinstance(row.get('os'), str): + raise ValueError('Expected an OS string in each matrix object') + if not isinstance(row.get('r'), str) or not row['r'].strip(): + raise ValueError('Expected a nonempty R version') + if row['os'] in ('ubuntu-latest', 'ubuntu-24.04'): + continue + raise ValueError('STOP unsupported platform leg: ' + repr(row['os'])) + return rows + + + if __name__ == '__main__': + try: + print(json.dumps(admit(sys.argv[1]), separators=(',', ':'))) + except (ValueError, TypeError, IndexError) as error: + print(str(error), file=sys.stderr) + sys.exit(1) + PY + ) + printf 'matrix=%s\n' "$matrix" >> "$GITHUB_OUTPUT" R-CMD-check: - runs-on: ${{ matrix.config.os }} + needs: admit-platforms + runs-on: + group: CWL CI isolated + labels: [self-hosted, Linux, X64, cwlab-ci-isolated] name: ${{ matrix.config.os }} (${{ matrix.config.r }}) strategy: fail-fast: false matrix: - config: ${{ fromJSON(inputs.r_matrix) }} + config: ${{ fromJSON(needs.admit-platforms.outputs.matrix) }} env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-dependency-license-strix-gate.yml b/.github/workflows/release-dependency-license-strix-gate.yml index 68d48d15ef..8fbec4df37 100644 --- a/.github/workflows/release-dependency-license-strix-gate.yml +++ b/.github/workflows/release-dependency-license-strix-gate.yml @@ -162,7 +162,9 @@ env: jobs: prepare: name: Verify exact distributions and approve the licence closure - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 180 permissions: contents: read @@ -455,7 +457,9 @@ jobs: strix: name: Strix ${{ matrix.key }} needs: prepare - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 360 permissions: contents: read @@ -709,7 +713,9 @@ jobs: name: Strix overflow ${{ matrix.key }} needs: prepare if: ${{ needs.prepare.outputs.has_overflow == 'true' }} - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 360 permissions: contents: read @@ -726,7 +732,9 @@ jobs: ((needs.prepare.outputs.has_overflow == 'true' && needs.strix_overflow.result == 'success') || (needs.prepare.outputs.has_overflow == 'false' && needs.strix_overflow.result == 'skipped')) }} name: Collect every Strix binding and seal the complete verdict - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 180 permissions: contents: read diff --git a/.github/workflows/repository-metadata-reconcile.yml b/.github/workflows/repository-metadata-reconcile.yml index 16316a9298..fd14ba112f 100644 --- a/.github/workflows/repository-metadata-reconcile.yml +++ b/.github/workflows/repository-metadata-reconcile.yml @@ -18,6 +18,10 @@ on: - "tests/test_repository_label_convergence.py" - "tests/test_repository_label_identity.py" - "tests/test_repository_label_live_verification.py" + - "requirements-opencode-review-ci.txt" + - "requirements-opencode-review-ci-hashes.txt" + - "requirements-noema-document-ci.txt" + - "scripts/ci/compile_opencode_review_lock.sh" - ".github/workflows/repository-metadata-reconcile.yml" schedule: - cron: "23 * * * *" @@ -31,7 +35,9 @@ concurrency: jobs: validate: - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 20 steps: - name: Harden runner @@ -42,6 +48,7 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 persist-credentials: false - name: Verify exact revision shell: bash @@ -103,7 +110,9 @@ jobs: apply: if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' needs: validate - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 45 environment: repository-metadata-maintenance steps: diff --git a/.github/workflows/sast-semgrep.yml b/.github/workflows/sast-semgrep.yml index f8ab04b865..2f128c1aea 100644 --- a/.github/workflows/sast-semgrep.yml +++ b/.github/workflows/sast-semgrep.yml @@ -50,7 +50,9 @@ jobs: # Fails OPEN: an unreadable, empty, or truncated file list scans everything. # The gate lives inside this job as a step-level guard (one runner, not two). if: github.event.action != 'closed' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read pull-requests: read diff --git a/.github/workflows/sbom-generation.yml b/.github/workflows/sbom-generation.yml index 588baefe1e..7d630dd78a 100644 --- a/.github/workflows/sbom-generation.yml +++ b/.github/workflows/sbom-generation.yml @@ -43,7 +43,9 @@ permissions: jobs: generate-sbom: - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: # write is needed for release-asset upload and dependency submission. contents: write diff --git a/.github/workflows/sbom-inventory-scheduler.yml b/.github/workflows/sbom-inventory-scheduler.yml index 5d3210728d..e60d66b7d0 100644 --- a/.github/workflows/sbom-inventory-scheduler.yml +++ b/.github/workflows/sbom-inventory-scheduler.yml @@ -30,7 +30,9 @@ permissions: jobs: aggregate-sbom-inventory: - runs-on: ${{ github.repository == 'ContextualWisdomLab/.github' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ (github.repository == 'ContextualWisdomLab/.github' && github.ref == 'refs/heads/main' && endsWith(github.workflow_ref, '@refs/heads/main') && fromJSON('{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} permissions: contents: write id-token: write diff --git a/.github/workflows/scheduled-security-scan.yml b/.github/workflows/scheduled-security-scan.yml index 6b6a90aa36..0c2f118d1c 100644 --- a/.github/workflows/scheduled-security-scan.yml +++ b/.github/workflows/scheduled-security-scan.yml @@ -38,7 +38,9 @@ permissions: jobs: detect-languages: name: Detect CodeQL languages - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] outputs: matrix: ${{ steps.detect.outputs.matrix }} steps: @@ -72,7 +74,9 @@ jobs: codeql: name: CodeQL periodic (${{ matrix.language }}) needs: detect-languages - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: actions: read contents: read @@ -102,7 +106,9 @@ jobs: trivy-fs: name: Trivy filesystem (periodic) - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read security-events: write diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml index 6222b28b28..2064ab9b54 100644 --- a/.github/workflows/scorecard-analysis.yml +++ b/.github/workflows/scorecard-analysis.yml @@ -29,7 +29,9 @@ permissions: read-all jobs: analysis: name: Scorecard analysis - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: ["self-hosted", "linux", "x64", "cwlab-ci-isolated"] permissions: security-events: write id-token: write diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 948ae583d4..3698e30056 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -35,7 +35,9 @@ permissions: jobs: gitleaks: name: gitleaks (secret scan) - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read security-events: write diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index e04d7bf8f3..46cddd322e 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -71,7 +71,9 @@ jobs: # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. if: github.event.action != 'closed' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 5 permissions: contents: read @@ -126,7 +128,9 @@ jobs: osv-scan: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.deps == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 25 permissions: actions: read @@ -339,6 +343,10 @@ jobs: echo "::warning::OSV SARIF upload to code scanning failed after the base/head comparison. The PR-introduced vulnerability reporter above remains the hard gate, so upload rate limits cannot hide OSV findings." - name: Upload OSV debug artifacts if: always() + # The reporter and SARIF upload above preserve the security verdict. + # Debug retention quota is observability only and must not turn a clean + # comparison into a failed security gate. + continue-on-error: true uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v6.0.0 with: name: osv-scan-debug @@ -352,7 +360,9 @@ jobs: dependency-review: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.deps == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read pull-requests: read @@ -387,6 +397,23 @@ jobs: set -euo pipefail api_url="${GITHUB_API_URL:-https://api.github.com}" + git_object_id='^[0-9a-f]{40}([0-9a-f]{24})?$' + repository_identity='^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$' + if ! [[ "${BASE_SHA}" =~ $git_object_id ]] || ! [[ "${HEAD_SHA}" =~ $git_object_id ]]; then + echo "::error::Dependency review evidence unavailable: exact 40- or 64-character hexadecimal base and head revisions are required before any compare request. Named refs are not evidence. Verify the pull-request event SHAs, then rerun. Failing closed." + exit 1 + fi + if ! [[ "${REPOSITORY}" =~ $repository_identity ]]; then + echo "::error::Dependency review evidence unavailable: owner/name repository identity is required before any compare request. Verify the pull-request repository, then rerun. Failing closed." + exit 1 + fi + repository_owner="${REPOSITORY%%/*}" + repository_name="${REPOSITORY#*/}" + if [ "${repository_owner}" = "." ] || [ "${repository_owner}" = ".." ] || [ "${repository_name}" = "." ] || [ "${repository_name}" = ".." ]; then + echo "::error::Dependency review evidence unavailable: owner/name repository identity is required before any compare request. Dot or parent-directory path components are not evidence. Verify the pull-request repository, then rerun. Failing closed." + exit 1 + fi + set +e status="$( curl -sS --connect-timeout 10 --max-time 30 \ @@ -436,7 +463,9 @@ jobs: gitleaks: name: gitleaks (secret scan) if: github.event.action != 'closed' && github.repository == 'ContextualWisdomLab/.github' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read security-events: write @@ -526,7 +555,9 @@ jobs: trivy-fs: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.code == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] permissions: contents: read security-events: write @@ -553,6 +584,10 @@ jobs: - name: Trivy filesystem scan uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0 with: + # Runner groups can schedule onto different hosts. Reinstall the + # pinned binary instead of trusting a cache entry whose path or + # executable bit belongs to a previous runner. + cache: false scan-type: fs scan-ref: . scanners: vuln,secret,misconfig @@ -633,7 +668,9 @@ jobs: scorecard: needs: changed-scope if: github.event.action != 'closed' && needs.changed-scope.outputs.code == 'true' - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] # SOFT: posture findings are unrelated to the PR diff, so never block merge. continue-on-error: true permissions: diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index abdc8af49e..2b65a5cc64 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -130,7 +130,9 @@ jobs: # docs/doctoring/required-workflow-path-filter-boundary.md. # Fails OPEN: an unreadable, empty, or truncated file list scans everything. if: (github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft')) && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -190,7 +192,9 @@ jobs: github.event_name != 'pull_request_target' || (github.event.action != 'closed' && github.event.action != 'converted_to_draft') ) && (github.event.pull_request.draft != true || github.event.action == 'converted_to_draft' || github.event.action == 'closed') - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 5 permissions: contents: read @@ -340,7 +344,9 @@ jobs: github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # Bound this gh-api-only cleanup job so a stuck call (rate limit, hung # `gh api --paginate`) cannot silently occupy a runner for GitHub's # 360-minute platform default -- exactly the window when a busy PR is @@ -462,7 +468,9 @@ jobs: # scans may take more than two hours per model (docs/product-goal-directive.md). # Inference has no wall-clock deadline; cancellation is reserved for an # explicit operator action or a superseded head. - runs-on: ubuntu-24.04 + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] # Least-privilege token scoped to this job (Scorecard alert #43): the scan # exchanges an OIDC token (id-token) and publishes same-repo status evidence # from the scan job only. @@ -839,18 +847,22 @@ jobs: - name: Gate Strix secrets id: gate env: - STRIX_MODEL: contextual-orchestrator/orchestrator/free + STRIX_MODEL: auto STRIX_MODEL_REQUESTED: ${{ github.event.client_payload.strix_llm || '' }} run: | requested_model="$(printf '%s' "$STRIX_MODEL_REQUESTED" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" case "$requested_model" in - ""|orchestrator/free|contextual-orchestrator/orchestrator/free) ;; + ""|auto|orchestrator/auto|contextual-orchestrator/orchestrator/auto) ;; *) - echo '::error::Strix model overrides are limited to contextual-orchestrator/orchestrator/free.' + echo '::error::Strix model overrides must select literal auto; free and direct-provider routes are not admitted.' exit 1 ;; esac strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + if [ "$strix_model" != "auto" ]; then + echo '::error::Strix primary model must remain literal auto.' + exit 1 + fi { echo "strix_model=$strix_model" echo 'enabled=true' @@ -876,7 +888,7 @@ jobs: OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR: ${{ steps.target_visibility.outputs.is_private }} - CONTEXTUAL_ORCHESTRATOR_POOL: free + CONTEXTUAL_ORCHESTRATOR_POOL: auto run: | set -euo pipefail report_parent="$GITHUB_WORKSPACE/strix_runs" @@ -1033,11 +1045,11 @@ jobs: strix_llm_file="$RUNNER_TEMP/strix_llm.txt" strix_model="$(printf '%s' "$STRIX_MODEL" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" case "$strix_model" in - orchestrator/free | contextual-orchestrator/orchestrator/free) - printf '%s' 'orchestrator/free' > "$strix_llm_file" + auto) + printf '%s' 'auto' > "$strix_llm_file" ;; *) - echo '::error::STRIX_LLM must select contextual-orchestrator/orchestrator/free.' + echo '::error::STRIX_LLM must remain literal auto; free fallback is not admitted.' exit 1 ;; esac @@ -1297,7 +1309,9 @@ jobs: name: publish-manual-pr-evidence-status needs: strix if: ${{ always() && !cancelled() && github.event_name == 'repository_dispatch' && github.event.client_payload.pr_head_sha != '' }} - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} # Single-shot OIDC exchange plus a handful of curl/gh api calls, no loop # or pagination -- same shape as the agent-mention-*-dispatch.yml # validate-and-forward jobs, which bound at timeout-minutes: 5. Without @@ -1520,7 +1534,9 @@ jobs: && needs.strix.result == 'failure' && needs.strix.outputs.transport_capacity_unavailable == 'true' && needs.strix.outputs.transport_retry_eligible == 'true' - runs-on: ${{ (github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('["ubuntu-24.04"]') }} + runs-on: + group: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).group }} + labels: ${{ ((github.repository == 'ContextualWisdomLab/.github' || github.repository == 'ContextualWisdomLab/fast-mlsirm') && github.workflow_ref == 'ContextualWisdomLab/.github/.github/workflows/strix.yml@refs/heads/main' && fromJSON('{"group":"CWL central control","labels":["self-hosted","linux","x64","cwlab-control"]}') || fromJSON('{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}')).labels }} timeout-minutes: 10 permissions: contents: write diff --git a/.github/workflows/trusted-uv-materializer-quality-ci.yml b/.github/workflows/trusted-uv-materializer-quality-ci.yml index db70ec324c..8a5e9db20c 100644 --- a/.github/workflows/trusted-uv-materializer-quality-ci.yml +++ b/.github/workflows/trusted-uv-materializer-quality-ci.yml @@ -2,28 +2,43 @@ name: Trusted uv Materializer Quality CI on: pull_request: - branches: [main] paths: - ".github/workflows/trusted-uv-materializer-quality-ci.yml" + - ".github/workflows/company-default-head-trust-gate.yml" + - ".github/workflows/company-settlement-validate.yml" + - "tests/test_company_default_head_trust_gate.py" - "scripts/ci/materialize_base_python_requirements.py" + - "scripts/ci/verify_release_maturin_tool_assets.py" - "tests/conftest.py" - "tests/test_materialize*.py" - "tests/test_trusted_uv*.py" - "tests/test_uv*.py" - "tests/test_repository_branch_coverage_*.py" + - "tests/test_verify_release_maturin_tool_assets.py" + - "requirements-opencode-review-ci.txt" + - "requirements-noema-document-ci.txt" - "requirements-opencode-review-ci-hashes.txt" + - "scripts/ci/compile_opencode_review_lock.sh" - "pyproject.toml" push: branches: [main] paths: - ".github/workflows/trusted-uv-materializer-quality-ci.yml" + - ".github/workflows/company-default-head-trust-gate.yml" + - ".github/workflows/company-settlement-validate.yml" + - "tests/test_company_default_head_trust_gate.py" - "scripts/ci/materialize_base_python_requirements.py" + - "scripts/ci/verify_release_maturin_tool_assets.py" - "tests/conftest.py" - "tests/test_materialize*.py" - "tests/test_trusted_uv*.py" - "tests/test_uv*.py" - "tests/test_repository_branch_coverage_*.py" + - "tests/test_verify_release_maturin_tool_assets.py" + - "requirements-opencode-review-ci.txt" + - "requirements-noema-document-ci.txt" - "requirements-opencode-review-ci-hashes.txt" + - "scripts/ci/compile_opencode_review_lock.sh" - "pyproject.toml" concurrency: @@ -39,7 +54,9 @@ env: jobs: minimum-python-contract: name: Python 3.10 compatibility contract - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 10 steps: - name: Harden runner @@ -85,7 +102,9 @@ jobs: full-quality-gate: name: Python 3.14 full quality gate - runs-on: ubuntu-latest + runs-on: + group: CWL CI isolated + labels: [self-hosted, linux, x64, cwlab-ci-isolated] timeout-minutes: 20 steps: - name: Harden runner @@ -98,6 +117,7 @@ jobs: with: persist-credentials: false ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 - name: Set up current stable Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 diff --git a/CHANGELOG.d/20260930-github-api-http-error-close.md b/CHANGELOG.d/20260930-github-api-http-error-close.md new file mode 100644 index 0000000000..df2feb4a78 --- /dev/null +++ b/CHANGELOG.d/20260930-github-api-http-error-close.md @@ -0,0 +1,7 @@ +## Fixed + +- Close file-like `HTTPError` responses in the central CodeQL, Strix, Noema, + Pingora, review-preflight, Pages, and sandbox-readiness clients after bounded + status/telemetry extraction, preventing Python 3.14 resource leaks without + permitting redirects, suppressing warnings, or weakening bearer-token + authority checks; cap CodeQL diagnostic error-body reads at 400 bytes. diff --git a/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md b/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md index 5b64363842..9a950691b9 100644 --- a/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md +++ b/CHANGELOG.d/20260930-semgrep-maturin-asset-urlopen.md @@ -2,6 +2,8 @@ - `scripts/ci/verify_release_maturin_tool_assets.py` fetches from a fixed `https://github.com/PyO3/maturin/releases/download/v1.15.0/` origin, and `verify_assets` admits only - five literal asset names, but `p/default`'s `dynamic-urllib-use-detected` flagged the call on - main and failed Semgrep on every PR. The call now carries the repository's standard reasoned - `nosemgrep`/`nosec B310` suppression. + five literal asset names. The downloader now uses a standard-library opener + that admits one credential-free HTTPS redirect only from the exact GitHub + release path to `release-assets.githubusercontent.com`, bounds the response, + and closes it on every path. It uses neither `urlopen` nor + `HTTPSConnection`, and carries no `nosemgrep` or `nosec` suppression. diff --git a/CHANGELOG.d/20261001-gap-baseline-source-restoration.md b/CHANGELOG.d/20261001-gap-baseline-source-restoration.md new file mode 100644 index 0000000000..a2d7d53a8f --- /dev/null +++ b/CHANGELOG.d/20261001-gap-baseline-source-restoration.md @@ -0,0 +1,5 @@ +## Fixed + +- Restore the complete product and technical gap baseline after a connector + display truncation replaced the source artifact, and add a regression guard + that rejects future truncation banners. diff --git a/CHANGELOG.d/20261001-noema-document-reader-dependencies.md b/CHANGELOG.d/20261001-noema-document-reader-dependencies.md new file mode 100644 index 0000000000..486643700c --- /dev/null +++ b/CHANGELOG.d/20261001-noema-document-reader-dependencies.md @@ -0,0 +1,5 @@ +### Noema document reader closes new transitive advisories + +- The generated npm lock now selects `fast-uri` 3.1.8 and `ip-address` 10.7.1, + removing the three medium-severity findings published against the previous + transitive versions while leaving the direct runtime manifest unchanged. diff --git a/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md b/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md new file mode 100644 index 0000000000..4a19c4bb5e --- /dev/null +++ b/CHANGELOG.d/20261001-noema-document-reader-transitive-cves.md @@ -0,0 +1,8 @@ +### Noema document reader transitive security updates + +- Updated the locked `fast-uri` dependency from 3.1.7 to 3.1.8 and + `ip-address` from 10.7.0 to 10.7.1. These are the first releases outside + the affected ranges for GHSA-hrr3-gc8f-f4qj, GHSA-j6r3-76f7-8jcv, and + GHSA-h3mg-xc3c-68pw. The direct dependency ranges are unchanged. +- Added a deterministic regression contract that rejects reintroduction of + vulnerable hoisted or nested copies of either transitive package. diff --git a/CHANGELOG.d/20261001-strix-litellm-credential-exfiltration.md b/CHANGELOG.d/20261001-strix-litellm-credential-exfiltration.md new file mode 100644 index 0000000000..e4d748764c --- /dev/null +++ b/CHANGELOG.d/20261001-strix-litellm-credential-exfiltration.md @@ -0,0 +1,6 @@ +### Shared Strix LiteLLM security floor + +- Pinned LiteLLM 1.94.3 in the Strix source input and regenerated the complete + hash lock after exact-head `pip-audit` found CVE-2026-84377 in 1.94.1. +- Added a source/lock parity regression contract and preserved the stacked Noema + document-reader transitive security repair without copying its implementation. diff --git a/CHANGELOG.d/20261001-trusted-review-archive-retry.md b/CHANGELOG.d/20261001-trusted-review-archive-retry.md new file mode 100644 index 0000000000..783b243562 --- /dev/null +++ b/CHANGELOG.d/20261001-trusted-review-archive-retry.md @@ -0,0 +1,6 @@ +### Central review source survives transient GitHub archive failures + +- Required Noema, Required OpenCode, and the PR review merge scheduler now use + bounded native `curl` retries when materializing the immutable trusted + `.github` source archive. Authentication, exact-SHA binding, extraction, and + fail-closed behavior are unchanged. diff --git a/CHANGELOG.md b/CHANGELOG.md index d90fa0c899..ff4e5a0699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,148 @@ +### Law CI cleanup rejects symlinked PostgreSQL descendants + +- The central law-agent cleanup now refuses symbolic links at every component + from the owned scratch root through the PostgreSQL cluster and + `postmaster.pid`. Cancellation and normal-exit cleanup therefore retain + evidence instead of invoking `pg_ctl` through an untrusted descendant path. + Contracts cover all five descendant boundaries in normal and Actions-mode + execution. Hosted exact-head Checks and independent review remain required. +### Security scanner setup no longer inherits runner-local failures + +- Disable `trivy-action`'s binary cache on the heterogeneous `CWL CI isolated` + runner group. Exact run `37242578328`, job `112314054734`, restored a + `trivy-binary-v0.70.0-Linux-X64` cache entry but then failed before scanning + with `trivy: command not found`; installing the pinned binary per job removes + that runner-local path and executable-bit dependency. +- Treat the always-run OSV debug artifact upload as observability rather than a + security verdict. Exact job `112314054619` completed the base/head comparison, + reported no PR-introduced finding, and uploaded SARIF before GitHub artifact + quota exhaustion failed the debug upload. The scanner and SARIF gates remain + unchanged and fail closed; only that optional retention step is non-blocking. +- RED `cc42969ba8ed4abeb1819412c146af2a9dadae8f` adds both workflow contracts; + GREEN `26f993c0bbe88d50bf81473e31d179ad7b317657` passes 5,488 tests, 10 skips, + and 40 subtests. PR #2565 remains Draft/HOLD for fresh hosted evidence, + isolated-capacity canary, and qualifying independent approval. + +### Isolated CI routing gains a runner-group authority boundary + +- Reject the complete reusable R matrix when any requested leg is not one of + the admitted Linux/x64 image aliases. A required macOS, Windows, ARM, or + unknown leg can no longer disappear while the remaining Ubuntu leg reports + success; callers must request only executable legs or receive a failed + admission job. +- Serialize the caller-controlled R matrix OS label with GitHub `toJSON()` + before inserting it into the group-scoped runner selector. Quotes, + backslashes, newlines, and duplicate `group`/`labels` text remain one + data label and cannot replace the fixed `CWL CI isolated` authority + boundary. The executable workflow contract covers benign and hostile values. + The integrated Maturin verifier now also documents its archive extraction and + command entry point, restoring the repository's 100% public-doc gate. +- Scope every ordinary and non-main self-hosted fallback through the dedicated + `CWL CI isolated` runner group as well as the existing platform/isolation + labels. A durable regression rejects label-only isolated selectors, while + workflow-specific contracts preserve dynamic matrix and trusted-main routing. + The reusable R matrix selector now escapes its literal JSON braces for + GitHub's `format()` expression, so Linux, Windows, macOS and unsupported OS + labels render a valid group-scoped `runs-on` object instead of failing before + runner admission. + The change remains Draft/HOLD until operators provision the group, restrict + its repository access, demonstrate disposable cleanup, and return exact-head + canary and required-Check evidence. + +### Maturin download failures close every transport response + +- Refactor the bounded Maturin asset downloader so successful and rejected + responses share one unconditional close path while `HTTPError` keeps its own + explicit close path. A new regression exercises a non-200 response and an + opener-raised HTTP error. This removes an impossible optional-response branch + without changing hosts, redirects, byte limits, hashes, or fail-closed error + mapping; the focused suite is 17 passed with 100% statement and branch + coverage. The trusted full-suite workflow now also tracks the verifier source + and its focused test, so a future lifecycle change cannot omit the repository + coverage gate that detected this regression. The pull-request trigger admits + stacked canonical-owner bases as well as `main`; the protected-branch push + trigger remains restricted to `main`. + +### Shared Strix lock advances beyond the PyJWT recursion DoS + +- Advance the explicit Strix source pin and generated hash lock from PyJWT + `2.14.0` to `2.15.0`, closing GHSA-42vr-xj54-vc7v / CVE-2026-101918. Exact Security + Scan run `36741151937` found the advisory in dependent PR #2540; the + canonical owner repair stays in #2531. A source/lock contract, deterministic + lock regeneration, and pip-audit evidence keep the dependent branch free of a + leaf workaround and prevent a return to `2.14.0`. Exact-head hosted security + Checks, independent approval, ordinary protected integration, and immutable + consumer-pin advancement remain required before release admission. + +### Shared urllib3 locks close proxy and streaming CVEs + +- Pin urllib3 2.8.0 as an explicit source input in both the pip-audit and + Strix security-tooling closures, regenerate their hash locks without unrelated + version movement, and add a four-file parity contract. This closes + CVE-2026-97687 and CVE-2026-97689 found by exact-head Python Security while + preserving hash checking and the existing Strix cryptography override. + +### Full-suite parser locks and honest branch coverage converge + +- Consolidated the complete valid ancestry of `.github#2521` into `.github#2530` + with ordinary two-parent merges so the parser-lock and repository-coverage + gates no longer wait on one another. Behavior-level tests exercise the final + branches in the OpenCode queue, Strix dependency classifier, release runtime + prescreener, and release dependency gate without exclusions, pragmas, + threshold reductions, or sample shrinking. Direct consumers of the common + generated lock now also track both source requirements and the canonical + compiler. `.github#2532`'s warning-fatal HTTP response-lifecycle repair is + carried in the same successor so the complete suite can regenerate one + exact-head receipt. Protected hosted Checks and qualifying independent review + remain mandatory before ordinary merge; predecessors remain open until + merged-tree equivalence is proven. The combined Python 3.14 warnings-fatal + suite passes 5,291 tests with 5 optional skips and 40 subtests; all 18,729 + production statements and 7,642 branches are covered, and production + Docstring coverage is 100%. The durable review-repair owner also triggers, + executes, compiles, and measures both launcher runtime suites at 100%. + Exact Git blobs, Cargo development locks, runtime receipts, final fanout caps, + and the Python 3.10 TOML fallback are covered as explicit trust boundaries; + one unreachable postcondition was removed only after prior fail-closed + validation made that state mechanically impossible. + +### Full-suite quality environments install their collection parsers + +- The common OpenCode quality input now owns the existing hash-pinned + `defusedxml` document parser and `PyYAML` workflow parser used during complete + repository test collection. Its Python 3.14/Linux lock was regenerated by + the repository compiler without manual hash edits. Local verification + reproduced the lock byte-for-byte, installed the common and Noema locks + together, imported both parsers, and passed 73 focused contracts with two + optional skips. Hosted exact-head Checks and qualifying independent review + remain required before protected merge. + +### Shared security fixtures use patched PyJWT and PyO3 releases + +- The Strix hash lock now takes PyJWT `2.14.0` as an explicit source input, + closing CVE-2026-102274 without hiding the dependency in the cryptography-only + override file. The offline Rust coverage fixture advances from PyO3 `0.22.6` + to `0.29.2`, beyond the `0.29.0` fixes for GHSA-36hh-v3qg-5jq4 and + GHSA-chgr-c6px-7xpp. Source/lock parity tests prevent either generated lock + from silently returning to the vulnerable versions. Protected integration, + immutable consumer-pin advancement, and fresh exact-head hosted security + Checks remain required before release admission. +### Dependency Review requires a completed authenticated comparison + +- The reusable Dependency Review preflight now requires both curl exit zero + and HTTP 200 before publishing `available=true`. A regression reproduces a + partial transfer that prints `200` but exits 18, preserving the fail-closed + gate. The bundled Security Scan already enforced both signals and remains + unchanged. `.github#2565` now preserves the complete `.github#1725` owner + delta through published ordinary two-parent merge + `650bc6e35409d52d24fec890c2ab74063794e9b8` (tree + `9522f0b9c96247b3412e147bc6f89fd2595c0776`) while retaining its isolated + runner-group selectors. The merged-tree focused contract is 53/53 and the + warning-fatal full suite is 5,437 passed, 10 skipped, 40 subtests. Both PRs remain + Proposed until exact-head hosted Checks and qualifying independent review are + complete. The carried security decision is ADR-0034, the concurrent owned + CodeQL decision is ADR-0033, and a repository-wide executable contract now + rejects duplicate numeric ADR identities. + ### Intel macOS native archives are bound to x86_64 bytes - The release prescreener now requires every native member in an Intel macOS diff --git a/docs/adr/0034-dependency-review-fail-closed-permission-envelope.md b/docs/adr/0034-dependency-review-fail-closed-permission-envelope.md new file mode 100644 index 0000000000..75ec5f9cd2 --- /dev/null +++ b/docs/adr/0034-dependency-review-fail-closed-permission-envelope.md @@ -0,0 +1,72 @@ +# ADR-0034: Fail closed on ambiguous Dependency Review authority and preserve caller permissions + +- **Status:** Proposed +- **Date:** 2026-09-02 +- **Scope:** `.github/workflows/dependency-review.yml`, its thin product callers, and the reusable-workflow security contract +- **Supersedes:** ADR-0024 only where ADR-0024 treated HTTP 403/404 as confirmed Dependency Graph unavailability or showed callers without an explicit permission envelope + +## Problem + +The protected-main consolidation from #1724 exposed two independent security-contract defects. + +First, the reusable workflow classified Dependency Graph compare HTTP 403/404 as `available=false` and therefore skipped the blocking Dependency Review action. Those responses are not authoritative proof that the feature is unavailable: they can also be authorization or policy failures. Turning an ambiguous authorization-shaped response into success silently removes a security gate. + +Second, the migration examples and thin callers omitted the permission envelope that the original repository-local workflows carried. GitHub reusable workflows cannot elevate `GITHUB_TOKEN` permissions through the call chain. A called workflow may maintain or reduce permissions granted by the caller, but it cannot manufacture `pull-requests: read` when the caller did not grant it. The result is a workflow-level `startup_failure` before any job is created. + +## Constraints + +1. Dependency Review remains a distinct hard gate; OSV-Scanner, Scorecard, or another scanner cannot substitute for an ambiguous Dependency Review failure. +2. The called workflow needs only `contents: read` and `pull-requests: read`; no write permission is introduced. +3. Product callers remain thin and repository-owned. They keep repository-specific trigger, severity, allowlist, and `continue_on_error` policy. +4. Consumers pin the reusable workflow to an immutable protected-main commit after this proposal is merged. `@main`, branch URLs, and unmerged PR heads are not production authority. +5. Non-`pull_request` invocations may skip because they lack an exact PR base/head pair; pull requests fail closed unless the comparison endpoint returns HTTP 200. + +## Considered alternatives + +### Treat 403/404 as feature unavailable + +Rejected. The status code alone cannot distinguish a genuinely unavailable Dependency Graph from denied authorization/policy. A false negative here converts a required security control into a warning. + +### Infer support from repository visibility or GHAS assumptions + +Rejected. Visibility is not a capability proof and was already the weaker design ADR-0024 replaced. + +### Rely on the called workflow's `permissions:` block + +Rejected as insufficient. GitHub does not let a reusable workflow elevate permissions beyond the caller's grant. The called workflow still declares its least-privilege ceiling, but each thin caller must explicitly grant the same read scopes. + +### Grant broader token permissions globally + +Rejected. It increases blast radius and hides a caller-contract defect instead of repairing it. + +## Decision + +1. For `pull_request`, the Dependency Graph compare preflight sets `available=true` only when curl exits zero and the endpoint returns HTTP 200. Every transport failure or other HTTP status is emitted with an error and terminates the job nonzero. +2. Remove the pull-request "Dependency graph unavailable" success path. No alternate scanner is described as replacement authority. +3. Keep the called workflow at `contents: read` + `pull-requests: read` and require every thin caller to declare at least those same scopes explicitly. +4. Make the executable central contract fail when the canonical caller example omits either required scope. +5. Replace the mutable `@main` caller example with an immutable `` placeholder. After merge, consumers pin the resulting protected-main SHA. + +## Exact evidence + +- #1725 first RED: `cb07b8bb28ef9d3a147cc966a0c70654d132da1d`; first production repair: `31f60e532e135008cabd09fcddd46a53062b0ea0`. +- Permission-envelope RED: `ee0f1ce544965772775b590050e40476df4ea8f6`; it changes only the contract and requires the missing caller permission example. +- Permission-envelope production repair: `ca3bdbd210de988ccd31f7fb96d3a97adfdb9bff`. +- `newsdom-api#784@1623977e6c37c78cb1a94a7a48c48f6d02cac86c`: run `33622976911`, `startup_failure`, zero jobs, reusable workflow immutably resolved to `.github@0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03`. +- `mightyETL#330@65efdf7b4064df5b9811c0403defb707e6efbc02`: run `33623035969`, `startup_failure`, zero jobs. +- Consumer permission repairs then produced materialized current-head runs: newsdom-api `9a798d5ac7b9b295a1accb2327fc76611352290f` run `33623818000`; mightyETL `4576f863ede9fca0673d6cce5ae8a4093246f5ab` run `33623854807`; scopeweave `db8b8ed6d36a6dc6cc1d07255a7a9a86bc88bf4f` run `33623761776`; Argos #557 `ee4c5dd326977407435b0f2425fdecebc34a810f` run `33623867278`. + +Hosted exact-current-head Checks and independent review remain required before this ADR may become Accepted. + +## Consequences and follow-up + +- A missing or denied Dependency Graph comparison is visible as a blocking failure instead of silent coverage loss. +- Caller permission omissions become an executable contract defect rather than an undocumented deployment prerequisite. +- The central workflow still cannot repair a consumer's omitted permissions by itself; each consumer must carry the explicit read-only envelope and later bump its immutable reusable-workflow pin to the protected-main SHA that contains this decision. +- #1643's canary run remains independent diagnostic evidence for the required Security Scan path; its run/check result is not transferred to this gate. #1725 nevertheless integrates the durable immutable-identity validation source delta into the canonical reusable Dependency Review owner and verifies that integrated behavior with its own tests and exact-head Checks. + +## References + +GitHub. (n.d.). *Reusing workflow configurations*. GitHub Docs. https://docs.github.com/actions/using-workflows/reusing-workflows + +GitHub. (n.d.). *Use GITHUB_TOKEN for authentication in workflows*. GitHub Docs. https://docs.github.com/actions/security-guides/automatic-token-authentication diff --git a/docs/adr/adr-0032-owned-codeql-status-and-settlement-authority.md b/docs/adr/adr-0033-owned-codeql-status-and-settlement-authority.md similarity index 96% rename from docs/adr/adr-0032-owned-codeql-status-and-settlement-authority.md rename to docs/adr/adr-0033-owned-codeql-status-and-settlement-authority.md index 50c6392edc..70e554756e 100644 --- a/docs/adr/adr-0032-owned-codeql-status-and-settlement-authority.md +++ b/docs/adr/adr-0033-owned-codeql-status-and-settlement-authority.md @@ -1,5 +1,5 @@ --- -title: "ADR-0032: Owned CodeQL status and settlement authority" +title: "ADR-0033: Owned CodeQL status and settlement authority" status: Proposed date: "2026-09-27" authors: "Codex" @@ -8,7 +8,7 @@ supersedes: "" superseded_by: "" --- -# ADR-0032: Owned CodeQL status and settlement authority +# ADR-0033: Owned CodeQL status and settlement authority ## Status diff --git a/docs/doctoring/all-self-hosted-runner-routing-20261003.md b/docs/doctoring/all-self-hosted-runner-routing-20261003.md new file mode 100644 index 0000000000..5eebc87357 --- /dev/null +++ b/docs/doctoring/all-self-hosted-runner-routing-20261003.md @@ -0,0 +1,48 @@ +# All-self-hosted workflow routing — staged rollout + +## Authority and scope + +On October 3, 2026, the user directed this SDP workstream: “self hosted runner로 모두 전환”. This change covers every runner declaration in the central `.github` repository (73 jobs) and SDP (4 jobs). It does not claim migration of unrelated ecosystem repositories or deployment of runner infrastructure. + +## Routing and isolation + +Existing restricted control, CodeQL and OpenCode groups stay intact. Existing exact-main workflow predicates and repository predicates stay intact; only their GitHub-hosted fallback changes. Ordinary jobs and non-main fallbacks require the dedicated `CWL CI isolated` runner group and all labels `self-hosted`, `linux`, `x64`, `cwlab-ci-isolated`. Both constraints are mandatory: GitHub routes a group selector through the group's repository access boundary and then matches every label inside that group. The label remains a provisioning contract, not a claim that a suitable runner already exists. Never add a privileged control/review runner to this group or apply the isolation label merely to release queued PR jobs. + +PR code must execute on disposable, job-isolated machines without production credentials, privileged Docker sockets, internal-network access or state shared with trusted control jobs. Rebuild the job environment after each job. Secret-bearing trusted jobs need separate disposable instances from untrusted PR jobs. Labels are mutable metadata and GitHub does not validate that the `linux` and `x64` labels match the runner machine; label-only routing therefore cannot prove the authority boundary. The dedicated group is the repository-access boundary, while the labels express the required platform and isolation capability inside it. Keep current token permissions, environment approvals, immutable artifact checks, exact-head review and required merge gates. This patch declares the required group but does not widen runner-group allowlists, register runners, alter secrets or bypass protection. + +R reusable jobs use Linux/x64-only admission: every requested leg must name `ubuntu-latest` or `ubuntu-24.04`. R versions and other fields remain unchanged for an admitted matrix. Any unsupported or malformed leg stops the complete matrix with STOP / nonzero, including mixed and unsupported-only matrices; no requested check is dropped to manufacture success. Admission and R execution both use the fixed `CWL CI isolated` group with `self-hosted`, `Linux`, `X64`, and `cwlab-ci-isolated` labels. Windows, macOS, ARM and unknown images are not platform coverage, and stopped legs have no queued R check. Linux instances still need compatible userspace and CI dependencies; labels do not install packages or attest the image. + +The first group-scoped R selector encoded its runner object with unescaped +literal JSON braces inside GitHub's `format()` expression. GitHub requires +literal braces in a format template to be doubled; the unescaped opening brace +was parsed as a replacement field and stopped the matrix before runner +admission. RED `f9211898d83f039cdf7ff82c8f93c72c9c10dceb` +renders the exact workflow template for Linux, Windows, macOS and an unsupported +OS and reproduces the failure. GREEN +`7fc1e2e8ad5091adff6c56b501e93d1d4ac2f211` doubles only the outer braces, +retains `{0}` and `{1}`, and passes all eight focused contracts both normally +and with `GITHUB_ACTIONS=true`. This proves selector serialization only; it does +not prove that the runner group, capacity, cleanup or operating-system images +exist. + +## Actual admission observation and rollout prerequisite + +The complete organization runner inventory read during preparation had 9 runners: 7 online Linux x64 and 2 offline. None carried `cwlab-ci-isolated`; no registered Windows or macOS runners were observed, and no verified `CWL CI isolated` group/capacity record was available. Existing groups contain privileged or workflow-restricted runners. Therefore this configuration is not runtime-ready and must not be enabled or merged before the authorized runner operator creates the dedicated group, limits its repository access, and provides disposable isolated capacity. The OpenCode group's sole runner `cwlab-s1-04` was offline; changing fallback routing does not repair it. + +Before promotion, the operator must return real runner identity/group/access records, prove that `CWL CI isolated` excludes privileged pools and admits only the intended repositories, demonstrate isolated cleanup and tool availability, and produce a successful canary job plus every exact-head required gate. Provisioning and runtime canaries are not completed by local YAML parsing or pytest. Preserve existing current-head executions; do not cancel or duplicate model reviews. Ship through normal reviewed protected PRs only, after prerequisites are satisfied. + +## Verification and rollback + +Permanent regression tests enumerate declarations, reject every GitHub-hosted fallback, require the dedicated group whenever `cwlab-ci-isolated` is selected, and keep PR-executing jobs outside privileged pools. Existing workflow-contract assertions are migrated rather than deleted. A structured YAML inventory verifies all 77 central/product job declarations; normal test suites and immutable source comparisons are separate evidence. Rollback is an ordinary reviewed revert of the migration PRs, not a protection change, group-access expansion or runner relabeling. + +## Primary sources (APA 7) + +GitHub. (n.d.). Choosing the runner for a job. GitHub Docs. Retrieved October 3, 2026, from https://docs.github.com/en/actions/how-tos/write-workflows/choose-where-workflows-run/choose-the-runner-for-a-job + +GitHub. (n.d.). *Expressions: format*. GitHub Docs. Retrieved October 3, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#format + +GitHub. (n.d.). Self-hosted runners reference. GitHub Docs. Retrieved October 3, 2026, from https://docs.github.com/en/actions/reference/runners/self-hosted-runners + +GitHub. (n.d.). Managing access to self-hosted runners using groups. GitHub Docs. Retrieved October 3, 2026, from https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/manage-access + +GitHub. (n.d.). Using self-hosted runners in a workflow. GitHub Docs. Retrieved October 3, 2026, from https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners/use-in-a-workflow diff --git a/docs/doctoring/central-coverage-owner-stack-2521.md b/docs/doctoring/central-coverage-owner-stack-2521.md new file mode 100644 index 0000000000..412899d94e --- /dev/null +++ b/docs/doctoring/central-coverage-owner-stack-2521.md @@ -0,0 +1,46 @@ +# Central coverage owner stack RCA + +## Incident and ownership + +`ContextualWisdomLab/.github#2521` owns the repository-wide Python statement +and branch coverage repair. Concurrent PRs carried valid prerequisites rather +than competing implementations: #2530 owns the hash-pinned parser inputs used +by complete test collection, and #2532 owns explicit closure of file-like +GitHub `HTTPError` responses. Both were preserved as ordinary merge parents; +neither failure was treated as a reason to discard a PR. + +## Root causes + +The first warning-fatal integrated run separated three causes: + +1. the predecessor tree lacked parser dependencies required during collection; +2. synthetic GitHub redirect/error paths leaked response objects under Python + 3.14 and therefore failed when `ResourceWarning` was promoted to an error; +3. four central scripts retained executable paths that their tests did not + reach, while two tests invoked a live `gh` boundary or asserted nothing. + +The remaining coverage work was test-first and behavior-bound. It exercises +queue admission/cancellation, bounded repository scans, exact Git blobs, +Cargo workspace and development-lock identity, runtime receipt architecture, +release fanout limits, and Python 3.10 TOML parser fallback. The runtime archive +prescreener's final repeated count check was proven unreachable because its +earlier exact-cardinality, uniqueness, and per-archive validation already +reject every false case; only that redundant branch was removed. + +## Evidence and acceptance + +- RED integrated evidence: 5,196 passed, 6 skipped; 90 statements missed and + 29 partial branches across the four remaining owner modules. +- Focused release dependency evidence: 950 passed with warnings treated as + errors. +- GREEN combined-successor evidence after re-fetching and ordinarily merging + current #2530 and #2521 heads: 5,291 passed, 5 skipped, 40 subtests passed; + 18,729/18,729 production statements and 7,642/7,642 branches covered, with + production Docstring coverage at 100%. +- No warning, security result, or fail-closed input validation was suppressed. + +This local result is not merge authority. Acceptance requires publication to +the re-fetched #2530 successor branch without force, fresh hosted Checks bound +to that exact commit, qualifying independent review, and ordinary protected +merge. The #2521 predecessor remains open until that protected merge and +complete tree carryover are verified. diff --git a/docs/doctoring/codeql-ghas-analyses-pagination.md b/docs/doctoring/codeql-ghas-analyses-pagination.md new file mode 100644 index 0000000000..ad647f7d62 --- /dev/null +++ b/docs/doctoring/codeql-ghas-analyses-pagination.md @@ -0,0 +1,78 @@ +# Complete GHAS analyses pagination + +Status: local source repair on base `37b10243cec3d160ecc9c1be75c71428b160a703`; +protected integration, hosted security checks and independent review remain required. + +## Root cause and ownership + +`list_codeql_analyses()` returned only the first `per_page=100` response and +ignored `Link`. An identity first encountered on a later page could therefore +be absent from the evidence supplied to the existing base/head pairing rule. +The central standalone helper owns this retrieval defect, not a consumer +repository's CodeQL workflow or lifecycle policy. + +The read-only overlap survey collected 383 open central PRs and examined the +exact committed helper source in all 11 file-overlap candidates. Ten retained +the single-request implementation; #2205's head has no helper source. +In particular, #2532's HTTP-error-response closure is separate +work: it is not complete-history pagination and is not implemented here. +No duplicate pagination implementation was found in those candidate heads. + +## Contract + +- Preserve the public `list_codeql_analyses()` arguments and list return shape, + the optional ref filter, analysis order, and existing identity pairing policy. +- Follow GitHub's `next` Link only after validating canonical HTTPS + `api.github.com`, the exact repository analyses path, unchanged decoded + `ref`/`tool_name`/`per_page`, and the next sequential page number. GitHub's + numeric `/repositories//code-scanning/analyses` alias is accepted only + after an authenticated, redirect-free read of the original owner/name + repository metadata verifies that exact positive integer ID. Cache this + binding within one collection; never infer it from an analysis row. +- Reject ambiguous/malformed Link relations, query changes, cycles or skips + before creating another authenticated request. Continue refusing redirects. +- Collect at most **1000 pages**, each with integer `per_page` in **1..100**. + Exceeding that resource bound raises an incomplete-evidence error; no prefix + is returned as complete history. +- HTTP/transport errors, non-list JSON, invalid JSON/UTF-8 and empty HTTP bodies + abort the entire collection. A valid empty JSON array remains supported. +- Do not retire, filter or rewrite configuration identities; do not upload SARIF, + synthesize security status, weaken a gate or change lifecycle approval policy. + +The transport's `_request_json()` still returns JSON to its original callers; +its optional output-header mapping exposes Link to traversal. Repeated physical +Link fields are combined in response order using `get_all("Link")` before the +existing relation grammar is validated; later next relations cannot be hidden, +and duplicates/malformed fields fail closed before another authenticated request. +Dict-shaped test responses retain their single-value lookup compatibility. Its +pre-existing empty-body behavior remains compatible when that mapping is not +requested. +The module remains stdlib-only and executable as a standalone fetched script. + +## Regression and validation + +`tests/test_codeql_ghas_analyses_pagination.py` drives the actual opener seam +with synthetic HTTP responses, never live credentials or invented GHAS state. +A three-page, 201-row regression keeps an active identity on page three and +proves it still blocks a head missing that identity. Negative cases exercise +poisoned next targets (with no second bearer request), cycles, malformed Link, +later-page errors and the independent literal 1000-page boundary. Positive +cases cover no ref, PR refs, escaped ref characters, query ordering, single +pages, valid empty arrays and the pre-existing non-dict-row behavior. + +Validate the helper and existing authority/credential contracts in normal and +`GITHUB_ACTIONS=true` modes, with 100% statements, branches and docstrings. +Run the dispatch workflow contracts too. On macOS, nine unchanged workflow +fixtures fail because they explicitly invoke `/bin/bash` 3.2 with Bash 4+ +`${value,,}` syntax; the same failures reproduce in a clean archive of the +exact base. Do not reinterpret those baseline failures as passing CI. + +## Primary references + +- [GitHub REST pagination](https://docs.github.com/en/rest/using-the-rest-api/using-pagination-in-the-rest-api) + specifies following response Link relations rather than assuming one page. +- [List code scanning analyses](https://docs.github.com/en/rest/code-scanning/code-scanning?apiVersion=2022-11-28#list-code-scanning-analyses-for-a-repository) + documents the paginated analyses endpoint, ref filter and page-size limit. + +This API-contract repair adds no new scientific inference or model invocation; +there is no paper PDF to redistribute. diff --git a/docs/doctoring/dependency-review-fail-closed-permission-envelope.md b/docs/doctoring/dependency-review-fail-closed-permission-envelope.md new file mode 100644 index 0000000000..2f4dca0cc2 --- /dev/null +++ b/docs/doctoring/dependency-review-fail-closed-permission-envelope.md @@ -0,0 +1,114 @@ +# Dependency Review fail-closed authority and caller permission envelope + +## Incident boundary + +Protected `ContextualWisdomLab/.github` main introduced the central reusable Dependency Review workflow through #1724. Subsequent exact-head evidence exposed three owner-contract defects that are repaired together in #1725 because all belong to the canonical dependency-review admission boundary. + +### Defect A — ambiguous HTTP status normalized to success + +The initial reusable preflight treated Dependency Graph compare HTTP 403/404 as proof that the feature was unavailable, wrote `available=false`, and let the pull-request gate finish successfully. That inference is unsafe: an authorization/policy denial can present the same status shape. The repair keeps the exact base/head compare request but authorizes the action only on HTTP 200; every other pull-request response is blocking and reports its status. + +Test-first evidence: + +- RED `cb07b8bb28ef9d3a147cc966a0c70654d132da1d` makes 403/404-as-unavailable, the fallback note, and any non-explicit fail-closed response illegal. +- Production `31f60e532e135008cabd09fcddd46a53062b0ea0` removes the 403/404 success branch and fallback note, preserving the pinned action, inputs, and non-`pull_request` skip boundary. + +### Defect B — thin callers lost required `GITHUB_TOKEN` permissions + +The original repository-local workflows carried read permission envelopes, but the migration examples/thin callers did not preserve them uniformly. GitHub reusable workflows cannot elevate the caller token. Consequently the called workflow's own `permissions: contents: read, pull-requests: read` declaration is only a ceiling; it cannot grant `pull-requests: read` when the caller omitted it. + +Live immutable-pin evidence isolates this from mutable-ref resolution: + +| Consumer | Exact head | Run | Result before caller repair | +| --- | --- | --- | --- | +| `ContextualWisdomLab/newsdom-api#784` | `1623977e6c37c78cb1a94a7a48c48f6d02cac86c` | `33622976911` | `startup_failure`, zero jobs; referenced workflow resolved to `.github@0bcd22d8bb07650aafb0a8f116e4c2bbb8744f03` | +| `ContextualWisdomLab/mightyETL#330` | `65efdf7b4064df5b9811c0403defb707e6efbc02` | `33623035969` | `startup_failure`, zero jobs | + +The consumer-side repair explicitly restores: + +```yaml +permissions: + contents: read + pull-requests: read +``` + +Fresh heads then materialized Dependency Review runs instead of failing before job creation: + +- newsdom-api `9a798d5ac7b9b295a1accb2327fc76611352290f`, run `33623818000`; +- mightyETL `4576f863ede9fca0673d6cce5ae8a4093246f5ab`, run `33623854807`; +- scopeweave `db8b8ed6d36a6dc6cc1d07255a7a9a86bc88bf4f`, run `33623761776`; +- Argos #557 `ee4c5dd326977407435b0f2425fdecebc34a810f`, run `33623867278`. + +Central test-first repair for this second defect: + +- RED `ee0f1ce544965772775b590050e40476df4ea8f6` adds `test_example_caller_preserves_required_permission_envelope()` without changing production/example workflow text. Against its parent it fails because the canonical caller example has no `permissions:` block. +- GREEN production `ca3bdbd210de988ccd31f7fb96d3a97adfdb9bff` adds the least-privilege caller envelope, explains the reusable-workflow permission ceiling, and replaces the mutable `@main` example with ``. + +### Defect C — compare identity was trusted before transport + +The earlier bundled Security Scan accepted the event-provided repository/base/head strings without first proving they were exact immutable Git object identities and one legal `owner/name` repository identity. That left the preflight contract weaker than the evidence it claimed to authorize. + +The predecessor owner lane #1643 added fail-before-transport validation and a temporary A/B canary. Its exact-head canary run `33589436750`, job `100120235906`, executed on `2026-09-02` and produced decisive evidence for the same immutable pair: + +- repository: `ContextualWisdomLab/.github`; +- base: `bb14b014eee31e6abdb5d2fffbb805aa29420eac`; +- head: `a6a2759640e6aa1d1e1219e1cd7aacdeffef32c0`; +- anonymous compare: HTTP `404`, curl exit `0`; +- job-token compare with `contents: read` and `pull-requests: read`: HTTP `200`, curl exit `0`. + +This proves that anonymous status is not an availability authority and that the least-privilege job token can establish the exact comparison. The temporary canary itself is diagnostic-only and is not part of the publishable successor contract. + +#1725 carries the durable part of that predecessor delta into the canonical writer: + +- both the reusable workflow and bundled Security Scan reject non-exact base/head revisions before curl; +- repository identity must be exactly one non-dot `owner/name` pair; `ContextualWisdomLab/.github` remains legal; +- the compare request uses the job token and only HTTP 200 authorizes the pinned Dependency Review action; +- named refs, malformed identities, 403/404 and all other non-200 outcomes fail closed. + +### Defect D — HTTP text could hide a failed curl transfer + +The reusable preflight appended `|| true` to curl and then authorized solely on +the printed HTTP status. A partial body transfer can print `200` and still exit +nonzero, so the old branch could publish `available=true` for incomplete +evidence. The executable regression makes the fake transport print `200` and +exit `18`; the preflight must remain nonzero and must not publish availability. +The minimal repair captures curl's exit status and requires both exit zero and +HTTP 200. The bundled Security Scan already enforced both conditions and was +left unchanged. + +Test-first evidence: RED `fe9d193343d12dbd229815a18ce85b494b3acff9` adds only the executable transport +contract and fails on the inherited preflight; its ordinary child carries the +minimal production and documentation repair. + +## Security invariants + +1. Pull-request Dependency Review executes only after curl completes successfully and an exact base/head compare returns HTTP 200. +2. Base/head revisions must be exact 40- or 64-character lowercase hexadecimal Git object IDs before transport. +3. Repository identity must be exactly one non-dot `owner/name` pair before transport. +4. No anonymous response, HTTP 403/404, or other non-200 response is translated into a successful "unavailable" state. +5. OSV-Scanner, Scorecard, and the separate Security Scan path remain independent controls; they do not satisfy a failed Dependency Review gate. +6. The called workflow and each caller use only `contents: read` and `pull-requests: read` for this path. No write permission is introduced. +7. Product callers pin the central workflow to an immutable protected-main commit after merge. `@main`, PR heads, and branch URLs are not production authority. +8. A non-`pull_request` event may skip because it lacks the PR base/head identity required for the comparison. + +## Verification and merge boundary + +The focused owner contracts are: + +```bash +PYTHONPATH=. pytest -q \ + tests/test_dependency_review_reusable_workflow_contract.py \ + tests/test_dependency_review_bundled_scan_identity_contract.py +``` + +The repository's normal exact-current-head required Checks, full coverage evidence, security scans, and independent reviews remain authoritative. #1725 stays Proposed/Draft while those gates are non-terminal or any substantive finding is unresolved. Queue saturation does not authorize bypass of a startup, permission, provenance, review, or security defect. + +After #1725 reaches protected main through ordinary protection, each consumer must bump its immutable reusable-workflow pin to that protected-main SHA and regenerate exact-head Dependency Review evidence. No consumer should return to `@main`. + +## References + +GitHub. (n.d.-a). *Reusing workflow configurations*. GitHub Docs. https://docs.github.com/actions/using-workflows/reusing-workflows + +GitHub. (n.d.-b). *Use GITHUB_TOKEN for authentication in workflows*. GitHub Docs. https://docs.github.com/actions/security-guides/automatic-token-authentication + +GitHub. (n.d.-c). *REST API endpoints for the dependency graph*. GitHub Docs. https://docs.github.com/rest/dependency-graph diff --git a/docs/doctoring/full-suite-parser-locks-20260930.md b/docs/doctoring/full-suite-parser-locks-20260930.md new file mode 100644 index 0000000000..8454481059 --- /dev/null +++ b/docs/doctoring/full-suite-parser-locks-20260930.md @@ -0,0 +1,81 @@ +# Full-suite parser dependency admission + +## Incident + +At protected `main@37b10243cec3d160ecc9c1be75c71428b160a703`, the common +OpenCode quality lock did not install the parsers imported by repository test +collection. Agent Mention Router Quality run +[36443475158](https://github.com/ContextualWisdomLab/.github/actions/runs/36443475158) +and source-repair run +[36443475290](https://github.com/ContextualWisdomLab/.github/actions/runs/36443475290) +reported `ModuleNotFoundError: No module named 'defusedxml'` and 13 collection +errors. The newer main additionally has four workflow tests importing `yaml`. +These are environment failures before test execution, not product findings or +passing coverage measurements. + +## Repair boundary + +The shared tooling input reuses `requirements-noema-document-ci.txt` for the +existing `defusedxml==0.7.1` pin and adds the existing security-tooling +`PyYAML==6.0.3` pin. The generated lock is rebuilt only with +`./scripts/ci/compile_opencode_review_lock.sh` (Python 3.14, Linux target, +`--upgrade`, hashes). The required upgrade also resolves Hypothesis 6.168.3 +instead of 6.168.0. No hashes are edited by hand. + +Installing the entire Bandit lock alongside this lock is not the repair: their +Pygments pins differ. The common quality environment instead resolves its own +complete, consistent dependency closure. Hash checking remains mandatory. +PyYAML's repository reports MIT. GitHub reports NOASSERTION for defusedxml, +so its installed hash-pinned 0.7.1 wheel's LICENSE was read directly and confirms +Python Software Foundation License Version 2; a missing SPDX detection is not +interpreted as missing permission. + +A contract regression requires both parser packages in the installed lock. +RED: one failing test because both packages were absent. GREEN: 36 tests passed +and two optional document-format tests skipped after hash-locked installation. +The full repository suite is a separate verification step, not implied by that +focused result. On code head `41e95af9dd1ac95dea716d53c44df1eacbd81d9d`, the final +isolated run finished with 5,165 passed, five skipped and 40 subtests passed in +356.08 seconds. It used only the common quality lock plus the project-pinned +pip seed, disabled incidental pytest plugin autoload and cleared live event/token +environment variables. An inherited ResourceWarning from a synthetic HTTPError +fixture remains visible; it is not suppressed or treated as a product failure. + +## Verification environment + +The first YAML-present full run on source-repair head `deb47f8db` finished with +5,232 passed, seven failed, five skipped and 40 subtests passed. Two failures +were a locally unseeded virtualenv lacking pip; five used pytest temporary +paths beneath the host home, which the sandbox correctly refuses. Installing +project-pinned pip 26.2.1 and using an isolated `/tmp` basetemp made all seven +reproductions pass. These local environment corrections do not weaken sandbox +validation and are not changes to production code. + +An initial clean-lock run had one further environment failure: macOS inherited +`/tmp` group 0, which this user does not belong to, and silently cleared the +setgid bit when the test requested mode 2600. A direct mode probe observed 600, +while a staff-owned directory retained 2600. The final run used a unique +staff-owned parent outside the host home; the security check itself was not +changed. Both the targeted permission test and the complete suite passed there. + +Source-repair's own two scripts separately measured 100% statement/branch +coverage and 100% docstrings. Neither those measurements nor the parser repair +constitute hosted exact-head approval or merge authorization. + +## Consumers and limits + +Quality workflows install `requirements-opencode-review-ci-hashes.txt` directly. +The central OpenCode coverage image also consumes it in +`.github/workflows/opencode-review-dispatch.yml`, together with the existing +Noema document lock. No exact-head fast-mlsirm Noema HTTP 400 or Strix report +scope failure is claimed resolved by this dependency change. Those incidents +require their own current-head transport/report evidence and independent review. + +## References + +Python Packaging Authority. (n.d.). *Secure installs*. Retrieved September 30, +2026, from https://pip.pypa.io/en/stable/topics/secure-installs/ + +The pip documentation requires every dependency in hash-checking mode to be +pinned and hashed. The repair preserves that contract rather than adding an +unhashed installation fallback. diff --git a/docs/doctoring/github-api-http-error-response-lifecycle.md b/docs/doctoring/github-api-http-error-response-lifecycle.md new file mode 100644 index 0000000000..e26110c0b8 --- /dev/null +++ b/docs/doctoring/github-api-http-error-response-lifecycle.md @@ -0,0 +1,86 @@ +# GitHub API HTTP error response lifecycle + +Status: Proposed source repair; exact-head hosted security Checks and independent review remain mandatory. + +## Incident and exact owner + +Protected `ContextualWisdomLab/.github` `main` at +`37b10243cec3d160ecc9c1be75c71428b160a703` reproduced eight failures in +`tests/test_github_api_url_boundary.py` under Python 3.14.7 with warnings promoted +to errors. Both authenticated GitHub REST clients correctly refused a synthetic +302 without sending a second request, but the resulting `HTTPError` response was +left open and Python reported `ResourceWarning: Implicitly cleaning up +`. + +The exact RED command was: + +```console +GITHUB_ACTIONS=true python -m pytest tests/test_github_api_url_boundary.py -q -W error +``` + +Result on the protected revision: `8 failed, 26 passed`. The same result in a +clean protected-revision worktree proves this is a pre-existing central supplier +defect rather than a consumer or `.github#2040` branch-only failure. + +## Root cause and repair + +Python documents `urllib.error.HTTPError` as both an exception and the same kind +of file-like response returned by `urlopen()`, with a readable error-body file +pointer. The clients converted the exception into their domain error but did not +close that response object. Success responses already used context managers; the +HTTP error path violated the same lifecycle boundary. + +The repair keeps the existing fail-closed redirect policy and error mapping. +The first exact-tree verification also exposed the same lifecycle defect in +other central HTTP boundaries, so the owner repair now covers every observed +path rather than leaving warning-fatal failures for a successor: + +- `codeql_ghas_configuration_identity._request_json` reads at most 400 bytes of + diagnostic body and closes the `HTTPError` in `finally`, including decode/read + failures; +- `strix_evidence_binding.default_github_opener` snapshots the status code, + closes the response, and then raises `EvidenceBindingError`; +- `noema_review_gate.call_llm` extracts only bounded allowlisted gateway + telemetry and then closes the response in `finally`; +- the Pingora artifact client closes JSON and raw-blob HTTP errors after + preserving its existing 404/domain-error mapping; +- the review preflight closes provider HTTP errors after recording only the + safe status and retry fields; +- Pages publication and sandbox readiness probes close rejected redirects + before returning their existing fail-closed result; +- the production-opener regression now asserts that the single synthetic 302 + response is closed as well as proving no redirected bearer request occurs, + and each newly discovered caller has an equivalent close contract. + +The 400-byte intake bound matches the pre-existing 400-character public +diagnostic limit without reading an arbitrarily large untrusted response first. +No warning filter, security suppression, redirect allowance, timeout, or check +threshold changed. + +## Acceptance + +1. The protected-revision RED becomes GREEN under Python 3.14.7 and `-W error`. +2. CodeQL identity, Strix evidence, Noema, Pingora, review-preflight, Pages, + sandbox readiness, and URL-authority suites remain GREEN with warnings + fatal. +3. Changed production behavior retains contract coverage and public docs. +4. Hosted Semgrep, Bandit, CodeQL, dependency, and independent review Checks pass + on the exact PR head before ordinary protected merge. +5. `.github#2040` then merges the protected repair normally and regenerates its + own exact-head evidence; stale predecessor failures are not rerun as proof. + +Local repair evidence on Python 3.14.7 is `35 passed` for the URL-authority +suite after adding the bounded-intake regression (`1 failed` before the repair), +`220 passed` for the original eight-file CodeQL/Strix impact suite, `69 passed` +for the sandbox E2E suite, and `5161 passed, 10 skipped, 40 subtests passed` +for the complete warning-fatal test tree. The original two production modules +each report 100% statement/branch coverage, while the repository-wide report +remains at its pre-existing 99% because unrelated production files outside this +delta retain uncovered branches. Hosted exact-head Checks, rather than this +local evidence, remain the merge authority. + +## Primary reference + +Python Software Foundation. (2026). *urllib.error — Exception classes raised by +urllib.request* (Python 3.14.7 documentation). +https://docs.python.org/3.14/library/urllib.error.html diff --git a/docs/doctoring/law-ai-agent-central-ci.md b/docs/doctoring/law-ai-agent-central-ci.md new file mode 100644 index 0000000000..4bb159440f --- /dev/null +++ b/docs/doctoring/law-ai-agent-central-ci.md @@ -0,0 +1,145 @@ +# Law AI Agent 중앙 CI 이관 기록 + +## 범위와 상태 + +- 2026-10-05 14:02 KST 사용자 승인. 중앙 소유자는 `ContextualWisdomLab/.github`다. +- 중앙 기준본: `main@37b10243cec3d160ecc9c1be75c71428b160a703`. +- 검증한 소비 기준본: `ContextualWisdomLab/law-ai-agent@301800bd9e2f1c0876ff3466b0de8013474c7d26`. +- 이 변경은 새 workflow, shell helper, 중앙 계약 테스트, 이 기록의 네 파일만 소유한다. + 기존 중앙 control/security/scanner와 소비 source는 수정하지 않는다. +- AGENTS, CLAUDE, master context, product directive, Project protocol, gap baseline, + `ContextualWisdomLab/naruon#974` spec을 읽었다. Project #1은 조회 당시 503개 항목이었다. + 조회한 앞 100개는 전체 인벤토리가 아니다. 이 작업의 통합 담당자는 Coordinator다. +- 소스 이관과 runner 운영은 다른 증거다. 보호된 병합, GitHub job 성공, 출시를 주장하지 않는다. + +## 신뢰 경계 + +`workflow_call`만 허용한다. 중앙 public PR 이벤트는 private 소비 code를 checkout하지 않는다. +첫 inline admission은 checkout 전에 exact caller repository, 이벤트와 SHA를 검사한다. +허용 이벤트는 같은 저장소의 main 대상 PR, main push, main 수동 실행이다. +Fork, 다른 저장소, `pull_request_target`, mutable 중앙 ref, 잘못된 SHA는 실패한다. +Fork를 성공한 skip으로 처리하지 않는다. + +소비 checkout은 이벤트의 PR head 또는 github.sha다. 중앙 helper checkout은 +`job.workflow_repository`와 `job.workflow_sha`다. `github.workflow_sha`를 중앙 SHA로 쓰지 않는다. +공식 문서는 job 필드가 “workflow file that defines the current job”을 가리킨다고 설명한다 +(GitHub, n.d.-a). 두 checkout의 실제 HEAD를 검사한다. 경로는 `source`와 `central`로 분리한다. +`persist-credentials: false`, `contents: read`, cache 비활성화를 적용한다. 별도 secret과 +`secrets: inherit`은 없다. 소비 concurrency prefix와 중앙 `law-ci` prefix는 달라야 한다. + +검증한 action 객체: + +- checkout commit: `fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09`. +- 소비 setup-uv의 `94527f2e458b27549849d47d273a16bec83a01e9`는 annotated tag 객체다. + REST git tag 응답에서 commit `37802adc94f370d6bfd71619e3f0bf239e1f3b78`을 확인했다. + 중앙은 이 commit을 pin한다. uv 버전은 0.12.5다. + +Runner selector는 group `CWL law CI`, labels `self-hosted/Linux/X64/law-ai-agent-ci`다. +Python 3.12/3.14 matrix는 `max-parallel: 1`, 각 job은 20분이다. +Reusable workflow의 runner 접근과 hosted billing은 caller context를 따른다 +(GitHub, n.d.-b). 중앙 소스 이관으로 private caller의 전역 admission 문제가 해결됐다는 주장은 하지 않는다. +격리 runner의 운영 권한·자원·network 검증은 Coordinator의 별도 영수증이 필요하다. + +기존 중앙 workflow에는 Harden-Runner가 있다. 이 lane에는 효과가 없는 action을 복사하지 않았다. +공급자 문서는 self-hosted runner에 Enterprise 구독과 runner infrastructure agent가 필요하며, +구독 없이 action만 추가하면 no-op이라고 설명한다 (StepSecurity, n.d.). Egress 강제와 +host 격리는 runner provisioning 경계다. 이 기록은 해당 제어의 실제 활성화를 증명하지 않는다. + +## 실행 계약 + +Helper는 BASH_SOURCE로 자기 경로를 찾는다. CWD에 의존하지 않는다. +정확한 consumer commit을 `git archive`로 새 scratch source에 복원한다. +이전 checkout의 dist, venv, cache를 사용하지 않는다. + +- native PostgreSQL은 provisioned `pg_config --bindir`에서 찾는다. 런타임 apt/sudo 설치는 없다. +- RUNNER_TEMP의 짧은 `mktemp` immediate-child root, uid 소유권, mode 0700, + socket 0700, TCP off, test database만 사용한다. 모든 경로 component의 symlink를 거부한다. +- `LAW_AGENT_ROOT`와 소비 안전 코드가 실제 읽는 `LAW_AGENT_POSTGRES_ROOT`는 같은 scratch다. +- 서버와 연결의 statement 10초, lock 2초 제한을 유지한다. 실제 consumer 승인 함수 + `connect(..., test_only=True)`를 사용하여 migration 001/002를 commit한다. + committed baseline fixture가 suite 앞부분에서 필요하므로 전체 pytest 전에 초기화한다. +- uv locked sync, ruff, format, mypy, 전체 pytest/coverage90/subprocess coverage를 유지한다. + JUnit의 모든 suite에서 skip/failure/error 0과 test count 양수를 확인한다. +- 새 outdir에서 정확히 wheel 1개와 sdist 1개를 요구한다. uv의 자동 `.gitignore`는 + `--no-create-gitignore`로 막는다 (Astral, n.d.). +- 새 lane-local cache를 online prime한 뒤 다른 새 venv에 실제 offline wheel 설치를 한다. + source 밖에서 `python -I`로 installed prefix 안의 module, 두 migration resource와 CLI help를 확인한다. +- 생성 직후 trap을 설치한다. 기존 state marker는 덮어쓰거나 삭제하지 않는다. + child 실패 상태를 보존하고 cleanup 실패를 성공으로 바꾸지 않는다. + 항상 실행하는 workflow cleanup은 private uid/mode와 canonical immediate-child 범위를 다시 검사한다. + +Shell은 Python interrogate 대상이 아니다. 모든 shell function 앞의 역할·실패 경계 주석과 +모든 embedded Python block의 module docstring을 이 파일의 문서화 계약으로 사용한다. +이는 전체 조직의 100% coverage/docstring gate가 실행됐다는 주장이 아니다. + +## 실제 검증 + +2026-10-05 KST, macOS 로컬의 실제 native PostgreSQL 18.6와 uv 0.12.5에서 실행했다. +소비 source는 위의 정확한 committed SHA다. 소비 작업 중 추가된 미커밋 테스트는 포함하지 않는다. +요청 당시 343개와 달리 현재 기준본의 실제 collection은 **345개**다. + +| 실행 | 실제 결과 | +|---|---| +| Python 3.12.14 전체 소비 suite | 345 passed, skipped 0, coverage 98.95% | +| Python 3.14.7 전체 소비 suite | 345 passed, skipped 0, coverage 98.87% | +| 두 Python의 ruff / format / mypy | 통과 | +| 두 Python의 fresh wheel/sdist + offline 새 venv 설치 | 통과 | +| 두 Python의 source 밖 isolated resource / CLI help | 통과 | +| 두 Python의 native cluster stop와 scratch 제거 | 통과 | +| 중앙 admission TDD | 15 RED → 15 GREEN | +| 중앙 helper evidence TDD | 7 RED → 23 GREEN | +| 실행 계약 TDD | 2 RED → 25 GREEN | +| 실제 빌드 `.gitignore` 회귀 | RED 확인 후 수정·두 lane 재실행 통과 | +| 독립 리뷰 cleanup/trap 회귀 | nested/public root와 trap 순서 3 RED → 31 GREEN | + +Synthetic tool fixture는 child exit 37의 전파, stop 실패, 재cleanup만 검사한다. +실제 DB 검증으로 계산하지 않는다. 실제 DB 결과는 위의 두 native 실행뿐이다. +독립 리뷰 뒤 강화한 최종 helper bytes는 아래 최종 checkpoint에서 다시 검증한다. + +`actionlint 1.7.12`는 공식 `job.workflow_*` 필드를 아직 모른다. 처음 실행은 이 필드와 +custom label에서 실패했다. 임시 custom-label config와 이 세 필드의 exact error regex만 +ignore한 실행은 통과했다. Workflow expression 검사가 완전히 지원된다는 주장은 하지 않는다. +공식 context 문서와 admission runtime 테스트를 별도 근거로 남긴다. +Shellcheck와 `bash -n`은 통과했다. 중앙 full-suite/100% coverage는 별도 실행 상태를 기록한다. + +## 최종 로컬 checkpoint — 14:42 KST + +독립 리뷰 수정 뒤 동일 helper bytes를 두 Python에서 다시 실행했다. +3.12.14는 345 passed / skip 0 / 98.95%, 3.14.7은 345 passed / skip 0 / 98.87%다. +양쪽 offline 실설치, prefix/resource/CLI help와 cleanup을 포함하여 exit 0이다. +중앙 subset은 일반 환경과 `GITHUB_ACTIONS=true`에서 각각 **33 passed**다. +Shellcheck, bash 구문, 6개 embedded Python block compile, 제한적 actionlint와 diff check는 통과했다. +중앙 전체 suite와 100% coverage/interrogate는 이 worker에서 실행하지 않았다. + +최종 core 파일 SHA-256: + +| 파일 | SHA-256 | +|---|---| +| `.github/workflows/law-ai-agent-ci.yml` | `3aac714edfb5438c1f7c0eb6420fcff2f43b39692324cd6d7e86c4c103cfc164` | +| `scripts/ci/law_ai_agent_ci.sh` | `7ee3d7d4f7083d87b921530f4ee3c7ce7dd8041e9611f4429390aa58f9e4e935` | +| `tests/test_law_ai_agent_ci.py` | `8afd78b82f0c2a270375192eb35454735b1bb192b573bc89027d951b50900b35` | + +이 문서 자체의 hash는 Coordinator handoff에서 별도로 전달한다. +다른 agent의 runner image/provisioning/review 파일은 shared worktree에 있지만 이 worker는 수정하지 않았다. + +## 남은 통합 확인 + +1. 독립 reviewer에게 final 네 파일 SHA-256을 전달한다. +2. Coordinator가 격리 Linux runner에서 final helper exact bytes를 실행한다. +3. 소비 thin SHA caller의 exact commit과 중앙 protected commit을 연결한다. +4. GitHub current-head required checks, 독립 승인, protected merge를 따로 검증한다. +5. 활성 기존 writer/cron의 재개는 Coordinator가 소유한다. 이 worker는 스케줄을 변경하지 않는다. + +## References (APA 7) + +Astral. (n.d.). *Commands: uv build*. Retrieved October 5, 2026, from +https://docs.astral.sh/uv/reference/cli/ + +GitHub. (n.d.-a). *Contexts reference*. Retrieved October 5, 2026, from +https://docs.github.com/en/actions/reference/workflows-and-actions/contexts + +GitHub. (n.d.-b). *Reusing workflow configurations*. Retrieved October 5, 2026, from +https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations + +StepSecurity. (n.d.). *Harden-Runner*. Retrieved October 5, 2026, from +https://github.com/step-security/harden-runner diff --git a/docs/doctoring/law-central-ci-independent-review.md b/docs/doctoring/law-central-ci-independent-review.md new file mode 100644 index 0000000000..becd4bc6cc --- /dev/null +++ b/docs/doctoring/law-central-ci-independent-review.md @@ -0,0 +1,89 @@ +# Law central CI — independent bounded review + +검토일: 2026-10-05 KST. 담당 dispatch: `ctx_03ac2f3ed053`. + +## 판정 + +**BOUNDED PASS — writer 완료 뒤 frozen 6파일 bytes와 전달 SHA256이 일치했다. 원격 gate, 소비 caller pin과 출시의 PASS가 아니다.** + +Coordinator는 writer_done `msg_87a489827bae`, outcome succeeded와 14:44 KST byte freeze를 확인했다. 검토자는 freeze SHA256 6개를 현재 파일에서 직접 다시 계산하고 일치시켰다. 조기 발견한 root 경계와 trap-before-marker 결함은 final helper에서 수정됐다. 현재 승인된 source/design 검토 범위에서 미해결 blocking 결함은 발견하지 않았다. + +검토 대상은 중앙 신규 reusable workflow, shell helper, 계약 테스트와 운영 설명이다. 제품 원문을 중앙에 복사하지 않는다. 검토자는 제품 소스, 자격 증명, 다른 private 자료를 읽지 않았다. 소스 수정, Git 변경, 빌드, 전체 테스트, 컨테이너 실행과 등록을 하지 않았다. 이 문서만 검토자가 소유한다. + +## 조기 발견 — 수정 중인 bytes 기준 + +1. **Cleanup 경로 경계** — 최초 helper의 `cleanup_root`는 최종 root의 디렉터리/비-symlink 여부와 basename만 검사했다. `cleanup_state`는 marker의 regular/non-symlink 여부와 lexical prefix만 검사했다. marker/root owner, mode, 정규화된 즉시 자식과 parent symlink 검증이 없었다. 삭제를 실행하지 않고 Bash 조건만 평가했다. `/scope/lawci.parent/lawci.child`와 `/scope/lawci.parent/../lawci.child` 모두 lexical prefix 조건에 통과했다. 기존 정리 경계 안에서 owner/mode와 canonical direct-child 경계를 확인하는 수정을 coordinator에 전달했다. +2. **Marker 생성 이전 실패** — 최초 helper는 `mktemp` 이후 state 부재 검사와 state 쓰기를 완료한 다음 EXIT trap을 설치했다. state 충돌/쓰기 실패 시 새 scratch 정리가 실행되지 않는다. 기존 marker와 이번 실행 marker의 소유 여부를 구분하면서 trap을 일찍 설치하는 좁은 수정이 필요하다. +3. **실행 증거의 강도** — 최초 helper는 PostgreSQL 도구 존재를 검사하나 16.15 runtime version을 검사하지 않았다. matrix Python은 uv 선택값이며 실행 interpreter version 검사가 없었다. 설치 wheel의 import는 source 밖임만 확인하고 이번 installed prefix 안임은 확인하지 않았다. image/runtime 영수증과 최종 수정 확인이 필요하다. 재현 없이 실제 image가 잘못됐다고 주장하지 않는다. + +## 현재 직접 관측 + +- 14:31 KST GitHub API: runner group `11`, `CWL law CI`, visibility `selected`, `allows_public_repositories=false`, 선택 repository ID는 `[1404539192]` 한 개다. workflow restriction은 설정되지 않았다. 이것은 선택된 private 저장소의 접근 범위이며 특정 중앙 workflow만 허용하는 ACL은 아니다. +- 같은 조회: runner `1134186`, Linux, `online`, `busy=false`; labels는 `self-hosted`, `Linux`, `X64`, `law-ai-agent-ci`다. 재등록, 영구 재사용 또는 컨테이너 폐기의 증거는 아니다. +- 최초 workflow는 `workflow_call`만 받는다. contents 권한은 read다. 두 checkout은 persist-credentials=false이며 consumer head와 called workflow SHA를 따로 검증한다. admission은 owned same-repository PR, main push, main dispatch만 인정하고 fork와 pull_request_target을 거부한다. +- `bash -n scripts/ci/law_ai_agent_ci.sh`는 exit 0이었다. 검토 Python은 `/Users/seonghobae/.pyenv/versions/3.14.5/bin/python3`, version `3.14.5`였다. 이것은 runner Python 3.12/3.14 실행 증거가 아니다. +- 중앙 기존 tracked 파일 수정은 최초 `git status --short`에 없었다. 신규 workflow/helper/test 3개가 untracked였다. 다른 writer가 작업 중이므로 이 관측을 최종 상태로 재사용하지 않는다. + +## 공식 설계 근거 + +- GitHub 2026-09-03 Changelog, “GitHub Actions: Early September 2026 updates”: `job.workflow_ref`, `job.workflow_sha`, `job.workflow_repository`, `job.workflow_file_path`가 해당 job을 정의한 workflow의 identity를 제공한다. github.workflow_*와 달리 reusable called workflow를 가리킨다. GitHub Enterprise Server에는 제공되지 않는다. +- GitHub Docs, “Contexts reference”, job context: 위 called-workflow identity 의미가 현재 reference에도 있다. +- GitHub Docs, “Reusing workflow configurations”, access/runner behavior: private caller는 public reusable workflow를 사용할 수 있다. Actions 허용 설정은 별도 전제다. 같은 조직 called workflow는 caller에게 제공된 조직 self-hosted runner를 사용할 수 있다. GitHub-hosted runner 배정과 billing은 caller context다. 중앙 이전을 billing 또는 현재 StartupFailure 해소로 판정하지 않는다. + +공식 페이지는 검토 세션에서 직접 검색했다. 운영 API 관측은 문서 일반론과 구분한다. + +## 중간 수정 확인 — 14:37 KST, 아직 freeze 전 + +- `validate_owned_root`가 marker/root uid와 0600/0700 mode, immediate-child, parent symlink 검증을 추가했다. 기존 lexical-only 문제는 읽은 revised bytes에서 해소됐다. +- trap이 marker 생성보다 먼저 설치되고 `state_owned`가 이전 marker 삭제를 막는다. 조기 lifecycle 문제는 읽은 revised bytes에서 해소됐다. +- source Python major/minor runtime 검사와 wheel import의 `sys.prefix` 내부 검사가 추가됐다. PostgreSQL 16.15는 여전히 Dockerfile의 major-only 패키지 지정과 실제 image receipt를 구분해야 한다. +- 4번째 파일은 `.github/runner-images/law-ai-agent/Dockerfile`이다. 공식 base digest는 dispatch와 일치한다. build 중 root는 apt 설치에만 사용하고 최종 USER는 runner다. 실제 배포 image와 final Dockerfile bytes 동일성은 미확인이다. + +## 소비 계약 테스트 — 명시적으로 승인된 한 파일 + +Coordinator가 `/Users/seonghobae/orca/projects/law-ai-agent-pg-moa/tests/test_central_ci_caller.py`만 읽도록 추가 승인했다. 42줄의 해당 파일만 읽었다. private product source와 실제 caller workflow는 읽지 않았다. + +- immutable 40-hex 중앙 참조 1개, 중앙 실행 중복 금지, contents read, events와 concurrency literal을 검사한다. +- 테스트 보강 권고: YAML 구조로 `uses`가 job-level인지, push와 pull_request 양쪽 branch 범위가 main인지 확인한다. 현재 branch literal 한 번만 있으면 통과한다. 이는 실제 caller 결함 판정이 아니라 테스트의 증명 범위다. +- consumer test의 현재 RED3/pass1은 coordinator 관측이며 검토자가 실행하지 않았다. 중앙 SHA pin을 쓰기 전 결과를 최종 gate로 주장하지 않는다. + +## 최종 frozen bytes와 직접 bounded probe + +14:44–14:46 KST, 다음 SHA256을 full bytes에서 계산했다. 모두 Coordinator freeze receipt와 일치했다. + +| 파일 | SHA256 | +|---|---| +| `.github/workflows/law-ai-agent-ci.yml` | `3aac714edfb5438c1f7c0eb6420fcff2f43b39692324cd6d7e86c4c103cfc164` | +| `scripts/ci/law_ai_agent_ci.sh` | `7ee3d7d4f7083d87b921530f4ee3c7ce7dd8041e9611f4429390aa58f9e4e935` | +| `tests/test_law_ai_agent_ci.py` | `8afd78b82f0c2a270375192eb35454735b1bb192b573bc89027d951b50900b35` | +| `docs/doctoring/law-ai-agent-central-ci.md` | `08f02306e9aee3d9ca2a4d5ada6b0cc50a492831de08e4712479eec13772e6e5` | +| `.github/runner-images/law-ai-agent/Dockerfile` | `17596881141c761c5e1a55a81d1b2b76559f6fac3aa8b66e1b705f4990808694` | +| `docs/doctoring/law-ci-runner-provisioning.md` | `c798d85cc1db6b92113b2463dbbdb9b12b7115c23dbdf02f7fc0937f041af281` | + +직접 실행 결과: + +- 실제 workflow admission parsed Python body: 정상 main push 허용, mutable 중앙 ref/target event/중앙 caller/uppercase SHA 거부, **5개 PASS**. +- 실제 helper의 root validator parsed Python body: owned direct-child 허용, nested root/symlink root/0644 marker 거부, **4개 PASS**. validator만 실행했고 stop/remove는 실행하지 않았다. +- embedded Python **6개 compile PASS**, `bash -n` exit **0**. +- 승인된 소비 테스트 44줄을 다시 읽었다. Coordinator가 event별 main branch와 job-level uses regex를 추가했음을 확인했다. 실제 caller는 중앙 commit 전이므로 별도 후속 검토다. + +## 실행 증거의 경계와 다음 단계 + +- writer 문서의 macOS native 두 lane/33 central tests 성공은 writer 관측이다. 검토자가 재실행하지 않았다. +- Coordinator는 final helper `7ee3d7d…`와 consumer baseline `301800bd…`를 사용한 Linux 격리 image에서 3.12.3: 345 pass, skip 0, coverage 98.95%; 3.14.7: 345 pass, skip 0, coverage 98.87%, full quality/offline wheel/cleanup exit 0을 직접 확인했다고 전달했다. 이는 producer 관측이며 검토자 자신의 실행과 구분한다. +- image SHA, container limits/mounts와 sudo 거부는 provisioning 문서와 Coordinator producer 영수증이다. 문서는 egress 방화벽 미검증, VM 수준 격리 아님, apt 재빌드 결과 가변, ephemeral 이후 새 등록 필요를 명확히 기록했다. 현재 source 검토가 이를 보안 인증으로 확대하지 않는다. +- inherited PGOPTIONS는 helper에서 제거된다. native server timeout 10s/2s를 유지한다. 단일 job 20분은 품질 lane이며 중앙 모델 검토 timeout 변경이 아니다. +- 실제 소비 thin SHA caller, 원격 current-head required checks, ordinary protected merge/publish가 남았다. 중앙 기존 review/security gate는 변경하지 않았다. 중앙 이관은 billing/permission gate 우회가 아니다. + + +## 후속 공백 경로 수리 — scratch 검증, 미게시 + +위 14:44–14:46의 helper `7ee3d7d…` 및 test `8afd78b8…` 값은 당시 frozen source의 역사적 receipt다. 이후 a55 source의 migration inventory·ancestor admission 수리 bytes와 같다는 주장이 아니다. 당시 producer의 DB/wheel 관측을 이 후속 후보의 실행으로 전이하지 않는다. + +현재 scratch는 a55 head의 source에 공백 경로용 두 줄 수정과 별도 회귀 테스트를 추가했다. helper SHA-256은 `67b7b11968eed60745537d9e000a5c834f1cfbc22409d8321f16191ac25355f6`이며, 기존 test SHA-256은 `cbc5fa07cb6f004ff32f4f8a89f91d16dbcb4e944f9850af14d64c10e7311a25`, 추가 test SHA-256은 `736eb442735e1d40744edb554a65c087f825079eb91f5dc7650a679d6d3b8800`다. + +- RED: 실제 helper에서 추출한 pg_ctl 호출과 conninfo 할당을 실행했다. 공백이 든 path를 shlex token seam으로 확인한 2개 assertion이 실패했다. +- 최소 GREEN: pg_ctl server option의 socket 값을 double quote하고 conninfo host 값을 single quote했다. 같은 두 assertion은 2 passed, Bash syntax exit 0이다. +- pg_ctl executable은 합성 capture stub이다. shlex token seam은 실제 libpq parser 또는 native PostgreSQL startup이 아니다. 현재 host PATH에는 PostgreSQL 실행 파일이 없어 실제 native startup은 NOT_RUN이다. +- 이 후보는 공백 경로만 검증했다. quote/backslash 등 별도 경로 문자에 대한 전체 escaping acceptance를 주장하지 않는다. 실제 native startup과 독립 검토 전 canonical adoption·게시·App 승인으로 확대하지 않는다. +- 새 base362fed와의 조합은 아직 실행하지 않았다. a55의 이전 full gate를 이 수정이나 새 base의 결과로 전이하지 않는다. diff --git a/docs/doctoring/law-ci-runner-provisioning.md b/docs/doctoring/law-ci-runner-provisioning.md new file mode 100644 index 0000000000..1fa421ab41 --- /dev/null +++ b/docs/doctoring/law-ci-runner-provisioning.md @@ -0,0 +1,57 @@ +# Law CI 전용 runner 제공 기록 + +## 목표와 책임 + +ContextualWisdomLab/law-ai-agent#1의 품질 실행을 중앙 reusable workflow로 옮긴다. +중앙 issue는 ContextualWisdomLab/.github#2580, GitHub Project #1은 In Progress다. +사용자는 2026-10-05 KST에 중앙 .github 이관과 self-hosted 전환을 명시했다. +제품 source와 source 기록은 private consumer에 유지한다. 중앙 public repo에 원문을 복사하지 않는다. + +## 2026-10-05 14:25 KST 실측 + +- 새 group 11, `CWL law CI`: selected repository는 `ContextualWisdomLab/law-ai-agent` (1404539192) 하나. +- public repository 허용 false, 기존 control/security/signing/GPU/health group 변경 0. +- 새 runner 1134186 `law-ai-agent-ci-isolated-20261005`: Linux X64, label `law-ai-agent-ci`, online, busy false. +- 별도 컨테이너: non-root UID1001, mounts [], cap-drop ALL, no-new-privileges, memory2147483648, nanoCPUs2000000000, pids512. +- host Docker socket, host home/credential, production database volume, published port, host network 사용 0. +- 컨테이너에서 sudo는 no-new-privileges 때문에 거부된다. 이는 helper에 sudo install이 없다는 전제와 일치한다. +- immutable base: `ghcr.io/actions/actions-runner@sha256:e5496277be5d09bc968b3d64911b74e219ac4a3f2edce956a3ecf9271bea1ef4`. +- 로컬 제공 image SHA256 `7eef6dcf639e556ab28c8970978f30b668dc9d21099b8c8d206d3c8d863f7068`. +- `.github/runner-images/law-ai-agent/Dockerfile`의 SHA256 `17596881141c761c5e1a55a81d1b2b76559f6fac3aa8b66e1b705f4990808694`. +- native PostgreSQL 16.15와 pg_config bindir `/usr/lib/postgresql/16/bin`를 직접 확인했다. +- runner 2.337.0과 `Listening for Jobs`를 확인했다. GitHub job 성공 증거는 아니다. +- 이미지 apt 해석 결과는 위 image에 고정돼 있다. Dockerfile의 apt package는 재빌드 시 바뀔 수 있다. registry 배포/이미지 서명은 수행하지 않았다. + +## 재현 운영 계약 + +이미지 build는 provisioning 때만 수행한다. CI job에서 sudo/apt 설치하지 않는다. +전용 image는 DB 바이너리를 제공하고 workflow가 uv0.12.5와 Python matrix를 제공한다. +각 runner는 `--ephemeral --disableupdate --runnergroup 'CWL law CI' --labels law-ai-agent-ci`로 등록한다. +단일 job 종료 후 등록이 해제된다. 다음 matrix job은 새 컨테이너와 새 등록이 필요하다. +등록 token은 승인된 GitHub API로 구하고 stdin으로 config.sh에 전달한다. +`ACTIONS_RUNNER_INPUT_TOKEN`은 등록 child에만 적용하며 stdout·로그·파일·명령 인자에 직접 넣지 않는다. +서버 .credentials 등의 generated credential 파일은 컨테이너 내부에만 존재한다. 컨테이너 제거 시 함께 회수한다. + +컨테이너 생성 옵션은 `--restart=no --cap-drop ALL --security-opt no-new-privileges --memory=2g --cpus=2 --pids-limit=512`다. +이미지 writable layer는 job 전용이다. host mount를 추가하거나 host 네트워크로 전환하지 않는다. +같은 명칭의 busy runner는 제거하지 않는다. API의 id/name/group/busy와 컨테이너 owner label을 대조한 뒤 owned idle resource만 회수한다. +초기 detached stdin 전달은 대기 상태로 남았다. 원인은 detached container의 stdin attachment였다. +owned unregistered container를 제거하고 `docker exec -i` 등록 → 별도 `docker exec -d` 실행으로 수리했다. +등록 성공, online, workflow 해석, job 시작, source checkout, 품질 verdict를 별도 확인한다. + +## 신뢰 한계와 실제 다음 gate + +이 개발용 컨테이너를 VM 수준 격리 또는 hostile workload 보안 인증으로 표현하지 않는다. +network는 GitHub/toolchain 접근을 위해 존재한다. host private network에 대한 별도 egress 방화벽은 이 기록에서 검증하지 않았다. +일반 CI가 signing/security runner나 운영 자격을 공유하지 않도록 repo별 group을 사용한다. +reusable workflow의 runner access와 billing은 caller context다. 중앙 파일 이관은 caller의 계정 admission을 자동 해결하지 않는다. +issue ContextualWisdomLab/law-ai-agent#2의 정책 정상화 증거와 실제 runner-bound job가 있어야 ordinary merge를 승인한다. +공급된 runner만으로 성과를 만들지 않는다. 실제 Linux helper/full-suite 기록과 exact-head GitHub job receipt를 별도로 남긴다. + +## 공식 근거 (APA 7) + +GitHub. (2026, September 3). *GitHub Actions: Early September 2026 updates*. GitHub Changelog. https://github.blog/changelog/2026-09-03-github-actions-early-september-2026-updates/ + +GitHub. (n.d.). *Reusing workflow configurations*. GitHub Docs. Retrieved October 5, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations + +GitHub. (n.d.). *Reuse workflows*. GitHub Docs. Retrieved October 5, 2026, from https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows diff --git a/docs/doctoring/law-ci-test-portability-fix.md b/docs/doctoring/law-ci-test-portability-fix.md new file mode 100644 index 0000000000..bb15ee74be --- /dev/null +++ b/docs/doctoring/law-ci-test-portability-fix.md @@ -0,0 +1,60 @@ +# Law CI test portability fix + +## 범위 + +2026-10-05 KST. Dispatch `ctx_df60f6521590`. +기준 HEAD는 `9e482e14c3d31a09e9167af2fafb2edab26fbf12`다. +수정 파일은 `tests/test_law_ai_agent_ci.py`와 이 새 보고서뿐이다. +Git commit, push, 원격 게시, runner 설정, 제품 helper 변경은 하지 않았다. + +GitHub에서 PR #2581의 head/base와 CodeRabbit comment `4181096101`을 직접 읽었다. +Finding은 유효했다. 기존 fixture는 `~/.hermes/cache/scratch`가 이미 있어야 했다. +해당 부모 디렉터리가 없으면 두 parametrize case 모두 child 실패 주입 이전에 실패했다. +CodeRabbit의 `/tmp` 고정 제안은 적용하지 않았다. 현재 TMPDIR와 canonical 경로 검증을 유지했다. + +## RED → GREEN + +실행 interpreter는 `/Users/seonghobae/.pyenv/versions/3.14.5/bin/python3`, Python **3.14.5**다. +환경 snapshot의 3.14.7과 구분한다. 실제 TMPDIR는 +`/var/folders/vq/nxtc622x2lg1xq3jpdktymdw0000gn/T/`였다. + +1. 새 subprocess 회귀 테스트를 먼저 추가했다. 빈 HOME, 긴 RUNNER_TEMP, 짧은 TMPDIR에서 + 기존 실제 `test_synthetic_child_failure_and_cleanup_are_not_success[False/True]`를 각각 실행했다. +2. `python3 -m pytest tests/test_law_ai_agent_ci.py -q -k without_hermes_home`는 + **2 failed, 33 deselected**였다. 두 child 모두 `.hermes/cache/scratch/lf.*`에서 + **FileNotFoundError**였다. 이는 추정이나 별도 mock 재현이 아니다. +3. 테스트 전용 `short_temp_root()`를 추가했다. `tempfile.gettempdir()`의 resolved 경로를 우선한다. + `l.` + 8자리 임시 이름과 `/lawci.XXXXXX`를 포함한 24자 예산을 적용한다. + 생성될 helper scratch는 기존 계약인 **65자 미만**을 유지한다. +4. 기본 temp 경로가 길면 HOME 아래 `.cache/law-ci-tests`를 canonical 경로로 선택한다. + 길이를 먼저 검사하고 mode 0700으로 생성한다. 기존 경로의 uid와 private mode도 검사한다. + 긴 HOME까지 허용하려고 `/tmp` alias나 제품 validator를 우회하지 않는다. +5. GREEN 회귀 child는 `--basetemp`를 지정해 pytest 자체 임시 파일과 helper scratch를 분리한다. + 두 case는 child exit **37**, stop 호출, stop 실패 시 보존, 재cleanup을 실제 helper로 확인한다. + 새 `keep` sentinel은 cleanup 이후에도 내용 `unrelated`를 유지한다. + 테스트 종료 시 TemporaryDirectory가 이번 실행 디렉터리만 제거한다. + +## 최종 검증 + +| 명령/계약 | 실제 결과 | +|---|---| +| 원래 collection | 33 tests | +| 두 no-Hermes-HOME subprocess + 기존 두 failure case | 4 passed, 31 deselected | +| `python3 -m pytest tests/test_law_ai_agent_ci.py -q` | **39 passed, 10.18s** | +| `GITHUB_ACTIONS=true python3 -m pytest tests/test_law_ai_agent_ci.py -q` | **39 passed, 10.88s** | +| `git diff --check` | exit 0 | +| 추가 portability 계약 | short temp, symlink alias canonicalization, long temp fallback, TMPDIR 없는 default | + +기존 33개와 새 6개가 모두 통과했다. Synthetic PostgreSQL 도구 결과를 실제 DB 실행으로 계산하지 않는다. +전체 중앙 suite/coverage는 중복 실행하지 않았다. Coordinator의 진행 중 baseline 결과와 별도다. +중앙 provider/model review timeout은 변경하지 않았다. 독립 리뷰와 보호된 원격 gate는 Coordinator가 소유한다. + +## Exact bytes + +| 파일 | SHA-256 | +|---|---| +| `tests/test_law_ai_agent_ci.py` | `e36b38d67befd4f591c2362fe2306430af8843d8028d7d13e3deb5429bfece79` | +| `scripts/ci/law_ai_agent_ci.sh` (변경 없음) | `7ee3d7d4f7083d87b921530f4ee3c7ce7dd8041e9611f4429390aa58f9e4e935` | +| `.github/workflows/law-ai-agent-ci.yml` (변경 없음) | `3aac714edfb5438c1f7c0eb6420fcff2f43b39692324cd6d7e86c4c103cfc164` | + +보고서 자체의 SHA-256은 handoff에서 따로 전달한다. diff --git a/docs/doctoring/maturin-download-response-lifecycle-20261001.md b/docs/doctoring/maturin-download-response-lifecycle-20261001.md new file mode 100644 index 0000000000..b6765daae8 --- /dev/null +++ b/docs/doctoring/maturin-download-response-lifecycle-20261001.md @@ -0,0 +1,44 @@ +# Maturin download response-lifecycle RCA + +## Incident + +Trusted uv Materializer run `36811202519`, job `110206427182`, checked out +`.github#1653@5cd141ec2c33b631d164af936cd1c9de70e4c9a4`. All 5,314 tests passed, +but the complete repository gate reported 99% coverage. The only incomplete +owner was `scripts/ci/verify_release_maturin_tool_assets.py`: five statements +and two branches in its non-200 and `HTTPError` response paths. + +## Root cause and boundary + +The bounded downloader belongs to the central `.github#2530` successor, not +the #1653 label-taxonomy delta. Its nullable `response` finalizer encoded a +false branch that cannot fall through: when `opener.open()` raises, control +re-raises from the `HTTPError` handler before the code following the finalizer. +That made honest 100% branch evidence impossible even though successful +responses were closed. The Trusted uv workflow path filter also omitted this +verifier and its tests, so fixing the canonical owner would not itself request +the complete gate that originally exposed the defect downstream. After the +path repair, owner head `8cf2ea5f73976d47b2267fb52ac28284323404b7` +still produced no gate because the workflow admitted only PRs whose base was +`main`, while #2530 is correctly stacked on canonical owner #2531. + +## Repair + +The downloader now closes every returned response in one unconditional nested +`finally` block. An opener-raised `HTTPError` remains independently closed by +its handler. Tests assert closure for both an HTTP 503 response and an HTTP 502 +exception. A workflow contract now requires both verifier paths in pull-request +and protected-branch triggers, and a separate contract admits stacked PR bases +while the push trigger remains restricted to protected `main`. The repair does +not change admitted hosts, the one-hop redirect +contract, credentials, request timeout, byte bounds, digests, or error mapping. + +## Evidence and remaining gates + +- Hosted RED: 5,314 passed, 5 skipped, 40 subtests; 18,775 statements with 5 + missing, 7,652 branches with 2 partial; total 99%. +- Local owner GREEN: 17 focused tests; 107/107 statements and 34/34 branches; + `git diff --check` clean. +- Required before acceptance: complete exact-head hosted suite, security and + CodeQL verdicts, qualifying independent approval, ordinary owner integration, + then ordinary merge-forward into #1653 and fresh consumer Checks. diff --git a/docs/doctoring/noema-document-reader-dependency-baseline-20261001.md b/docs/doctoring/noema-document-reader-dependency-baseline-20261001.md new file mode 100644 index 0000000000..4e723f2040 --- /dev/null +++ b/docs/doctoring/noema-document-reader-dependency-baseline-20261001.md @@ -0,0 +1,33 @@ +# Noema document-reader dependency baseline + +## Incident + +Security Scan run +[`36773087489`](https://github.com/ContextualWisdomLab/.github/actions/runs/36773087489), +Trivy job `110084194330`, found three medium-severity vulnerabilities in the +generated Noema document-reader lock at exact PR head +`a99784219305d1b6e14cf76f0acea30c5ee45e21`: + +- `fast-uri` 3.1.7: CVE-2026-86472 / GHSA-hrr3-gc8f-f4qj; +- `ip-address` 10.7.0: CVE-2026-101911 / GHSA-h3mg-xc3c-68pw; and +- `ip-address` 10.7.0: CVE-2026-101912 / GHSA-j6r3-76f7-8jcv. + +The upstream fast-uri advisory fixes the 3.x line in 3.1.8. The ip-address +advisories fix both findings in 10.7.1; this lock selects the first patched 10.7.1 +release within the existing `^10.2.0` transitive range. + +## Ownership and decision + +The vulnerable file is owned by the central `.github` Noema document-reader +runtime, so the repair stays in that owner instead of patching consumer +repositories. The direct manifest is unchanged. `npm update` regenerated only +the two transitive lock entries, including registry URL and integrity digest. + +## Verification and remaining gate + +`tests/test_noema_document_reader_runtime_dependencies.py` first failed on +3.1.7 and 10.7.0, then passed with exactly one safe entry for each package. +The focused Python context suite passed 14 tests with 2 skipped, `npm ci +--ignore-scripts` reproduced the lock, and `npm audit --omit=dev +--audit-level=moderate` reported zero vulnerabilities. Hosted Security Scan, +CodeQL, independent approval, and ordinary protected merge remain required. diff --git a/docs/doctoring/product-gap-baseline-source-integrity-20261001.md b/docs/doctoring/product-gap-baseline-source-integrity-20261001.md new file mode 100644 index 0000000000..a009f92bbb --- /dev/null +++ b/docs/doctoring/product-gap-baseline-source-integrity-20261001.md @@ -0,0 +1,28 @@ +# Product gap baseline source-integrity repair + +## Incident + +Repository Metadata Reconcile run `36777030872`, job `110097412873`, ran +against `.github#2530@61a3f2ebbf6adf9c237a647552ccffeb52b00f47`. +Five tests failed because `docs/product-technical-gap-baseline.md` began with +a connector display warning and contained only 433 lines. The previous complete +artifact contained 3,703 lines and 361,021 bytes. + +## Root cause + +Commit `bd3cfe8645844ec7b140a1b73d2b339f8e4283dc` attempted to change one +dependency-floor row, but persisted a truncated connector rendering instead of +the Git blob. This removed G-17 publication evidence, the 107-row live +inventory, APA 7th references, and buyer-facing PRD/TRD/UML/Gap evidence. + +## RED to GREEN + +- RED `c8d41f52962814d080a1c4947acbff2f3f9bb81e` rejects the connector warning + and output-line banner at the artifact boundary. +- GREEN restores the complete parent blob and reapplies only the intended + `ip-address` 10.7.1 dependency-floor row. +- The focused integrity, gap-baseline, and published-lineage tests must pass + before the full warnings-fatal repository suite is accepted. + +Hosted exact-head Metadata, authenticated agent verdicts, and independent +approval remain required; this record is not merge authorization. diff --git a/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md b/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md new file mode 100644 index 0000000000..d0f1cbbb2b --- /dev/null +++ b/docs/doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md @@ -0,0 +1,157 @@ +# Shared PyJWT and PyO3 security baseline repair + +## Status and decision + +**Proposed; release HOLD.** The central `.github` repository owns both affected +dependency surfaces, so the repair belongs on a protected-main foundation PR. +It must not be copied into PR #1026, which changes neither lock. The owner repair +must pass exact-head review and hosted security Checks, merge ordinarily, and +then reach #1026 through a non-force merge from protected `main`. + +## Exact incident evidence + +PR [#1026](https://github.com/ContextualWisdomLab/.github/pull/1026) was observed +at exact head `6f645a73502e159d5a229805afa34868ad9bb851` against protected +`main@37b10243cec3d160ecc9c1be75c71428b160a703`. + +- [Security Scan run 36495499815](https://github.com/ContextualWisdomLab/.github/actions/runs/36495499815), + job `109380628690`, reported PyO3 `0.22.6` in + `tests/fixtures/coverage-cargo/Cargo.lock`: GHSA-36hh-v3qg-5jq4 (High, 8.0) + and GHSA-chgr-c6px-7xpp (Medium, 5.5). Both advisories fix the defect in + PyO3 `0.29.0`; this repair selects and locally verifies `0.29.2`. +- [Python Security run 36495499871](https://github.com/ContextualWisdomLab/.github/actions/runs/36495499871), + job `109380725819`, reported PyJWT `2.13.0` in + `requirements-strix-ci-hashes.txt` as affected by CVE-2026-102274. PyJWT + `2.14.0` is the fixed release. +- The exact #1026 diff changes neither vulnerable file. The same lock bytes + were present on protected `main`, establishing a shared baseline defect rather + than a PR-specific regression. + +## Root cause and operational scenarios + +PyJWT was only a transitive MCP dependency, so the generated Strix lock could +select a newly vulnerable release without an explicit reviewed source pin. A +malformed RSA JWK can raise a plain `ValueError` and abort processing of the +whole JWK Set. An operator can therefore lose otherwise valid signing keys and +fail authentication or review-agent startup because one untrusted key is bad. + +The offline Rust coverage fixture intentionally pins exact crate releases, but +its PyO3 pin was not advanced when the two 2026 advisories were published. One +defect permits an out-of-bounds read from iterator methods; the other omits a +required `Sync` bound and permits a data race. Even though this is a test +fixture, the central scanner correctly treats its lock as executable supply +chain material. + +## RED to GREEN contract + +RED commit `cd84d887` introduced two fail-closed contracts: + +1. `requirements-strix-ci.txt` must explicitly select PyJWT `2.14.0`, and the + generated hash lock must contain the same version. +2. The Rust coverage fixture manifest and lock must both select PyO3 `0.29.2`. + +The implementation adds the direct PyJWT input, regenerates the Python 3.13 +manylinux hash lock with the repository command, advances the exact PyO3 +manifest pin, and regenerates the Cargo lock with Rust `1.97.1`. The +cryptography override remains single-purpose; it does not become a general +security-version overlay. + +## Ownership, release, and failure recovery + +The fixed-source release workflows consume `requirements-strix-ci-hashes.txt` +from immutable central revisions. This PR repairs the canonical owner bytes but +does not rewrite those workflows to an open branch. After ordinary protected +merge, a separate consumer change must advance their exact source commit and +rerun API/schema, security, SBOM, and provenance evidence. If any exact-head +scanner, build, or independent review fails, the PR remains HOLD and the root +cause is repaired here; no bypass or mutable source reference is permitted. + +## Local verification on the repaired tree + +- Focused dependency, fixed-source, Maturin asset, and Rust toolchain contracts: + 48 passed, 1 skipped. +- Repository regression suite: 5,160 passed, 11 skipped, 40 subtests passed. +- Rust `1.97.1` `cargo check --locked`: passed for the coverage fixture. +- Python lock regeneration with `uv 0.12.18`, seeded with the existing reviewed + output, was byte-identical. Input SHA-256 values were `c3812261…` for + `requirements-strix-ci.txt` and `3b745514…` for the override; the output was + `8f8318d4…`. A hash-enforced installation loaded PyJWT `2.14.0`. A fresh + unseeded solve is intentionally not claimed to be byte-identical because it + may select newer allowed transitive releases. +- `pip-audit`: no known vulnerabilities in the Strix lock. OSV's direct + `pyo3@0.29.2` query returned no vulnerability records. + +No production Python module changes in this repair. The repository-wide +coverage and docstring commands expose separate protected-main baseline debt: +coverage is 99% (178 statements missing) and `interrogate scripts/ci` is 97% +(43 docstrings missing). Those failures are not waived or called green here; +they require their own bounded owner repair before the repository can claim the +100% global gates. + +## References + +GitHub. (2026, June 12). *Out-of-bounds read in PyO3 iterator methods* +(GHSA-36hh-v3qg-5jq4). GitHub Advisory Database. +https://github.com/advisories/GHSA-36hh-v3qg-5jq4 + +GitHub. (2026, June 12). *PyO3 missing Sync bound can lead to a data race* +(GHSA-chgr-c6px-7xpp). GitHub Advisory Database. +https://github.com/advisories/GHSA-chgr-c6px-7xpp + +Open Source Vulnerabilities. (2026). *CVE-2026-102274: PyJWT RSA JWK Set +availability failure*. +https://osv.dev/vulnerability/CVE-2026-102274 + +## 2026-10-01 urllib3 audit follow-up + +Python Security run `36733279716`, job `109949358063`, found two newly +published vulnerabilities in urllib3 2.7.0: CVE-2026-97687 permits target TLS +policy to weaken or replace HTTPS proxy TLS policy, and CVE-2026-97689 permits +an unbounded chunk-size line to consume memory in streaming clients. Both are +fixed in urllib3 2.8.0. The same vulnerable transitive pin appeared in the +pip-audit and Strix hash locks, so this remains one central security-owner +repair rather than two consumer workarounds. + +The repair adds urllib3 2.8.0 to both source inputs and regenerates both locks +with their recorded uv commands. A contract requires exactly one 2.8.0 row in +each source input and generated lock. Comparison against exact predecessor +`d1aa3659fca527a6c7330151f3ab4df3d7578391` shows no unrelated package-version +movement. Repeated compilation produced identical SHA-256 digests, and +pip-audit 2.10.1 reported no known vulnerabilities for either generated lock. +These local results are not merge authority: exact-head hosted security Checks, +terminal authenticated CodeQL evidence, independent approval, and ordinary +protected merge remain required. + +## 2026-10-01 PyJWT recursion denial-of-service follow-up + +Security Scan run [36741151937](https://github.com/ContextualWisdomLab/.github/actions/runs/36741151937) +found GHSA-42vr-xj54-vc7v / +CVE-2026-101918 in PyJWT 2.14.0. Dependency Review job `109975641239` and OSV +job `109975641271` both rejected that shared Strix lock. An attacker-controlled, +deeply nested unsigned JWT payload can exhaust Python recursion during unverified +payload parsing in `PyJWKClient.get_signing_key_from_jwt`, before key lookup, +and raise an uncaught request-level exception; +the available evidence does not establish a process crash or authentication +bypass. PyJWT 2.15.0 contains the upstream fix. + +The canonical-owner repair advances the explicit source pin and generated lock +to 2.15.0. Lock regeneration changes only the PyJWT version and its wheel/sdist +hashes; urllib3 2.8.0, PyO3 0.29.2, and the single-purpose cryptography override +remain unchanged. The existing parity test was first changed to require 2.15.0 +and failed against the 2.14.0 source and lock before implementation. Hosted +exact-head Security, CodeQL, independent approval, ordinary protected merge, +and immutable consumer-pin advancement remain release gates. + +Local verification used the hosted-workflow Python 3.12 line. The focused +source/lock contract passed 5 tests and the warnings-as-errors repository suite +passed 5,167 tests, 6 skips, and 40 subtests. Repeating the recorded `uv 0.12.18` +compile command was byte-identical at lock SHA-256 `76443a3300d0…`; a +hash-enforced, no-dependency installation loaded PyJWT 2.15.0 and urllib3 2.8.0. +`pip-audit 2.10.1` reported no known vulnerabilities. The repository's existing +97% docstring baseline remains a separate HOLD and is not represented as green. +An independent review found no remaining Critical, Important, or Minor finding +after correcting the advisory's attack-vector wording. + +GitHub. (2026). *PyJWT has a denial of service vulnerability via maliciously +crafted JWT token with deeply nested payload* (GHSA-42vr-xj54-vc7v). +https://github.com/jpadilla/pyjwt/security/advisories/GHSA-42vr-xj54-vc7v diff --git a/docs/doctoring/shared-security-litellm-credential-exfiltration-20261001.md b/docs/doctoring/shared-security-litellm-credential-exfiltration-20261001.md new file mode 100644 index 0000000000..fc573fc8e1 --- /dev/null +++ b/docs/doctoring/shared-security-litellm-credential-exfiltration-20261001.md @@ -0,0 +1,53 @@ +# Shared Strix LiteLLM credential-exfiltration RCA + +## Incident binding + +On 2026-10-01, `.github` PR #2531 exact head +`516471fbe7d4e93a50c7bbba20402447f06f8d8b` failed Python Security run +`36799069276`, job `110169140365`. The unmodified +`requirements-strix-ci-hashes.txt` selected LiteLLM 1.94.1, and `pip-audit` +reported CVE-2026-84377 / GHSA-3cv6-jpf6-8222. The advisory describes an +authenticated request-body routing override that can redirect an upstream call, +exfiltrate configured provider credentials, and reach internal services. + +This is a canonical control-plane dependency defect, not a product-PR finding +and not an audit-service transient. The same exact head also failed Trivy on the +Noema document reader's `fast-uri` 3.1.7 and `ip-address` 10.7.0. Stacked PR +#2545 already owns and proves the minimal Node transitive repair, so its ordinary +commit is preserved in the repaired #2531 ancestry instead of being copied or +reimplemented. + +## Test-first repair + +The RED contract +`test_strix_litellm_security_pin_is_an_explicit_lock_input` first failed because +the source input did not own a LiteLLM pin. The minimal repair: + +1. selects `litellm==1.94.3` in `requirements-strix-ci.txt`, the first patched + release in the retained 1.94 line; +2. regenerates `requirements-strix-ci-hashes.txt` with the repository's declared + `uv pip compile --generate-hashes` command; +3. requires exact source/lock parity so a future resolver run cannot silently + restore an affected release; and +4. preserves #2545's `fast-uri==3.1.8` and `ip-address==10.7.1` source overrides, + lock, and nested-copy regression contract as unchanged ancestry. + +No scanner finding is ignored or suppressed. `pip-audit` over the repaired +hash lock reports no known vulnerabilities. At repaired exact head +`fe879f7b7f48f729f757e03851bf61149470ccb5`, Python Security run `36800615364`, +Security Scan run `36800615435`, SAST run `36800615456`, and runtime-quality run +`36800615444` are terminal GREEN. CodeQL run `36800615319` remains fail-closed: +both language jobs recorded `verdict=pending` while the exact-head dispatch job +succeeded. A qualifying independent approval and ordinary protected merge are +still required; local or partial hosted evidence is not merge authorization. + +## References + +BerriAI. (2026, August 26). *Authenticated SSRF and provider-credential +exfiltration via unvalidated request-body routing parameters* +(GHSA-3cv6-jpf6-8222) [Security advisory]. GitHub. +https://github.com/BerriAI/litellm/security/advisories/GHSA-3cv6-jpf6-8222 + +National Institute of Standards and Technology. (2026). *CVE-2026-84377*. +National Vulnerability Database. +https://nvd.nist.gov/vuln/detail/CVE-2026-84377 diff --git a/docs/doctoring/trusted-review-archive-transient-retry.md b/docs/doctoring/trusted-review-archive-transient-retry.md new file mode 100644 index 0000000000..341f79323d --- /dev/null +++ b/docs/doctoring/trusted-review-archive-transient-retry.md @@ -0,0 +1,37 @@ +# Trusted review archive transient retry + +## Incident + +Bounded Noema continuation run +[`36760921156`](https://github.com/ContextualWisdomLab/.github/actions/runs/36760921156), +job `110043067331`, resolved the trusted workflow source to protected +`main@37b10243cec3d160ecc9c1be75c71428b160a703` and then failed before model +setup. The GitHub archive API returned HTTP 502 while the workflow executed a +single `curl -fsSL` request. The source revision, target pull request, and model +route were not the cause. + +## Decision + +The three central workflows that materialize the same immutable trusted archive +use `curl --retry 3 --retry-all-errors --retry-delay 1 --retry-max-time 30`. +This is a bounded transport repair at the canonical `.github` owner. It does +not select a mutable ref, bypass archive extraction checks, change credentials, +or convert a terminal failure into success. + +## Alternatives + +- Manual rerun was rejected because it would not repair the repeated control + path. +- A custom retry helper was rejected because native `curl` already provides the + bounded behavior and another abstraction would enlarge the trusted surface. +- An unbounded loop was rejected because it could occupy review capacity without + a terminal result. + +## Verification and remaining gate + +`tests/test_trusted_archive_retry_behavior.py` runs the production command from +each workflow against a local server that returns one 502 followed by a 200. +The test was RED on the predecessor and GREEN after the bounded retry options. +Hosted exact-head Checks, unresolved-thread review, qualifying independent +approval, and ordinary protected merge remain required. A local GREEN result is +not merge authorization. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 6e5f1c549a..88bdaf0b30 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,5 +1,73 @@ # Product and Technical Gap Baseline +## 2026-10-09 reusable R matrix conservation + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| `.github#2565@6e924f32a2dbaa0950709c6d8f1391fc1058ef43` printed `STOP` for unsupported R platform legs but returned exit 0 whenever at least one Ubuntu leg remained; the workflow then expanded only the filtered output, so a required macOS, Windows, ARM, or unknown check could disappear while the reusable workflow passed | RED changed the existing executable contract to require the mixed Ubuntu+macOS+Windows+ARM matrix to fail; exact-parent execution failed because the script returned 0 and emitted only Ubuntu. The inline workflow copy had identical behavior | **PRD:** requested required checks are conserved or the request fails; partial silent execution is forbidden. **TRD/RCA:** reject the entire matrix on the first unsupported leg and return the original rows only when every row is admitted; keep the owner script byte-equivalent to the workflow inline copy. **Context Map:** central `.github` owns reusable R admission; product callers consume the released contract. **Flow:** `caller matrix → validate every row → all admitted → fixed isolated Linux/x64 execution`, otherwise `STOP / nonzero`. DB/UI/ERD changes are N/A | **Proposed / focused RED→GREEN complete; full exact-tree verification, hosted exact-head Checks, isolated-capacity canary, and qualifying independent approval remain required** | + +## 2026-10-07 security scanner runner isolation + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| `.github#2565@3b35d3d427d17f71472d3bf52ba179338011c17a` restored the pinned Trivy binary cache on one self-hosted runner but could not execute `trivy`, while the completed OSV comparison was reclassified as failed only because optional debug-artifact storage quota was exhausted | Security run `37242578328`; Trivy job `112314054734` logged cache hit followed by `entrypoint.sh: line 88: trivy: command not found` (exit 127). OSV job `112314054619` found no PR-introduced issue and uploaded SARIF before `Upload OSV debug artifacts` failed with `Artifact storage quota has been hit` | **PRD:** a buyer-visible security verdict must represent scanner findings, not runner-local cache state or optional retention quota. **TRD/RCA:** disable `trivy-action` binary caching across the heterogeneous isolated runner group and make only OSV debug retention non-blocking; the base/head reporter and SARIF path remain fail-closed. **Context Map:** central `.github` owns security workflow execution; product repositories consume the released required-workflow contract. **Flow:** `exact head checkout → pinned scanner install → finding gate → SARIF → optional debug retention`. DB/UI/ERD changes are N/A. RED `cc42969b…` failed 2/2 contracts; GREEN `26f993c0…` passes focused tests normally and under `GITHUB_ACTIONS=true`, plus 5,488 passed, 10 skipped, 40 subtests repository-wide | **Proposed / source and contract repair published; fresh exact-head hosted security evidence, isolated-capacity canary, unresolved-thread zero, and qualifying independent approval required** | + +## 2026-10-03 isolated self-hosted runner authority boundary + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| `.github#2565@d6ba56d04e22476b2ea41bfe7bb26e2cbc98f98f` routed ordinary jobs and non-main fallbacks by the mutable `cwlab-ci-isolated` label alone, although the PR's own doctoring stated that a generic label does not attest separation; GitHub does not validate OS/architecture label truth, so a privileged or persistent runner with matching labels and repository access could receive untrusted work | RED contract `test_isolated_label_is_scoped_to_dedicated_runner_group` failed first at `.github/workflows/actions-queue-health.yml`; the reviewed tree contained 63 affected `runs-on` declarations across direct, conditional, CodeQL and R-matrix selectors | At the central `.github` owner, require both the dedicated `CWL CI isolated` runner group and existing labels for every isolated selector; preserve trusted-main groups and repository predicates; update exact workflow oracles and rollout doctoring; keep Draft/HOLD until the group, access policy, disposable capacity, cleanup proof, canary and exact-head Checks exist | **Proposed / local RED→GREEN complete; operator capacity, exact-head hosted Checks and qualifying independent approval required** | +| `.github#2565@cd49a60f1299fb8f7984de8d8ba223db673f7b84` wrapped the reusable R runner JSON in unescaped literal braces inside GitHub `format()`, so expression evaluation treated `"group"` as a replacement field and failed before any matrix job could request a runner; the prior test asserted that defective source literal instead of rendering it | RED `f9211898d83f039cdf7ff82c8f93c72c9c10dceb` added a behavior contract that renders and parses the exact template for Linux, Windows, macOS and an unsupported OS; focused result was 1 failed / 7 passed with `KeyError: '"group"'`. GREEN `7fc1e2e8ad5091adff6c56b501e93d1d4ac2f211` doubles only the outer JSON braces and passes 8/8 normally and with `GITHUB_ACTIONS=true` | Preserve `{0}`/`{1}` substitutions and the original matrix OS label, render one valid group-scoped selector for every supported/unsupported branch, and keep the PR Draft/HOLD until the independent runner-group prerequisites and hosted exact-head gates are satisfied | **Proposed / local RED→GREEN complete; no runner capacity or merge authority claimed** | +| `.github#2565@f0f7ed988fba5d94eda6513f35d171f23c4bd9a5` inserted caller-controlled `matrix.config.os` inside a quoted JSON string; a quote-bearing label could add duplicate `group` and `labels` members whose last values replaced the fixed isolation boundary | Exact-head RED updated the source contract to require an unquoted `{1}` JSON value plus `toJSON(matrix.config.os)`; before source repair the focused result was 2 failed / 6 passed, after repair 8/8 passed. The behavioral matrix includes benign Linux/Windows/macOS/unsupported labels, a duplicate-key injection string, and backslash/newline data. Full warning-fatal verification is 5,439 passed, 10 skipped, 40 subtests with 18,830/18,830 statements and 7,684/7,684 branches covered. Exact-head public-doc coverage initially exposed two inherited Maturin helper gaps; adding their public contracts restored 100%, and compileall/diff checks pass | **PRD:** untrusted reusable-workflow input must remain data and cannot select privileged capacity. **TRD/RCA:** serialize the OS label exactly once at the GitHub expression boundary, then parse one selector with fixed group and mandatory labels. **Context Map:** central `.github` owns runner admission; callers supply labels through the released workflow contract only. **Flow:** `r_matrix JSON → matrix.config.os → toJSON → format value slot → fromJSON → fixed runner group`. DB/UI/ERD changes are N/A. Require hosted exact-head security/quality Checks, isolated capacity canary, cleanup proof, and qualifying approval | **Proposed / local RED→GREEN and full verification complete; hosted/operator gates pending** | + +## 2026-10-01 Maturin response-lifecycle coverage closure + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| `.github#1653@5cd141ec2c33b631d164af936cd1c9de70e4c9a4` passed all 5,314 tests but failed the complete branch gate because the canonical Maturin downloader left five error-path statements and two branches unexecuted; the owner workflow omitted both verifier paths and stacked PR bases | Trusted uv Materializer run `36811202519`, job `110206427182`; `verify_release_maturin_tool_assets.py` 95%, missing lines 104 and 106-112 plus branch 114→116; no owner run at #2530 predecessor `8cf2ea5f73976d47b2267fb52ac28284323404b7` because its base was #2531 rather than `main` | Repair canonical successor `.github#2530`: exercise non-200 and opener-raised `HTTPError` closure, replace the impossible nullable-response finalizer with one unconditional response-owned close scope, add source/test and stacked-PR trigger contracts to the complete gate while retaining protected-main push scope, preserve all network and fail-closed boundaries, then ordinary-merge the accepted owner head into #1653 | **Proposed / hosted RED reproduced; focused verifier coverage GREEN locally; path and stacked-admission contracts RED→GREEN; exact-head hosted full-suite, security, CodeQL, and independent approval required** | + +## 2026-10-01 bounded Maturin release downloader SAST closure + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| The canonical Maturin asset verifier replaced a suppressed dynamic `urlopen` call with direct `HTTPSConnection`, but Semgrep's low-confidence certificate-validation audit still rejects every use of that low-level API | `.github#2531@e33d97d022e1c1a26b35d51f9fa4b695fe969547`; SAST run `36802256836`; job `110178968784`; rule `python.lang.security.audit.httpsconnection-detected.httpsconnection-detected` | At source-repair head `eede925e71e0f1526560a305f5219a13c6631227`, replace the low-level connection with a standard-library opener whose redirect handler admits one credential-free HTTPS hop only from the fixed GitHub release path to the exact release-assets host; preserve bounded reads and terminal response closure; prohibit scanner suppressions | **Proposed / RED scanner-contract reproduction and 31 tests plus 4 subtests GREEN locally; source-repair exact-head Security, Python Security, SAST, and runtime-quality GREEN; terminal CodeQL and independent approval required** | + +## 2026-10-01 gap baseline source integrity + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| A connector display truncation was committed as the baseline source, removing 3,270 lines of PRD/TRD/UML, Gap-register, APA 7th, and live-inventory evidence | Repository Metadata Reconcile run `36777030872`; job `110097412873`; exact broken head `.github#2530@61a3f2ebbf6adf9c237a647552ccffeb52b00f47`; RED `c8d41f52962814d080a1c4947acbff2f3f9bb81e` | Restore the last complete 3,703-line source blob, reapply only the intended first-patched dependency-floor row, and reject future connector truncation banners in the repository quality gate | **Proposed / local RED→GREEN complete; exact-head hosted Metadata and independent approval required** | + +## 2026-10-01 Noema document-reader transitive security baseline + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| The generated Noema document-reader lock selected `fast-uri` 3.1.7 and `ip-address` 10.7.0 after CVE-2026-86472, CVE-2026-101911, and CVE-2026-101912 were published | Security Scan run `36773087489`; Trivy job `110084194330`; exact predecessor `.github#2530@a99784219305d1b6e14cf76f0acea30c5ee45e21` | At the central `.github` owner, regenerate only the two transitive entries to `fast-uri` 3.1.8 and the first patched `ip-address` 10.7.1 release, scan every hoisted or nested lock entry in a regression contract, reproduce with `npm ci`, and require a zero-vulnerability npm audit | **Proposed / local RED→GREEN and audit complete; exact-head hosted security and independent approval required** | + +## 2026-10-01 shared Strix LiteLLM credential-exfiltration closure + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| The shared Strix hash lock selected LiteLLM 1.94.1, which is affected by CVE-2026-84377 / GHSA-3cv6-jpf6-8222 and can expose configured provider credentials through authenticated routing overrides | Failing predecessor `.github#2531@516471fbe7d4e93a50c7bbba20402447f06f8d8b`, Python Security run `36799069276`, job `110169140365`; repaired exact head `fe879f7b7f48f729f757e03851bf61149470ccb5`, Python Security run `36800615364`, Security Scan run `36800615435`, SAST run `36800615456`, and runtime-quality run `36800615444` | Preserve stacked #2545's Noema document-reader transitive repair, add a direct `litellm==1.94.3` source floor, regenerate the complete hash lock, bind source and lock with a RED-to-GREEN regression, and require authenticated CodeQL verdict evidence plus independent approval | **Proposed / dependency, security, SAST, and runtime-quality Checks GREEN; CodeQL run `36800615319` fail-closed pending authenticated verdicts; independent approval required** | + +## 2026-10-01 trusted review archive transient transport + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| Required Noema resolved the correct immutable trusted source but a single GitHub archive API HTTP 502 exhausted materialization before model setup | `.github` run `36760921156`, job `110043067331`; protected source `37b10243cec3d160ecc9c1be75c71428b160a703`; combined successor predecessor `.github#2530@5b3a76ea71d7ae2fa9b1719f4f82df8d52e98082` | At the central `.github` owner, add bounded native transport retries to Noema, OpenCode, and merge-scheduler trusted archive downloads; prove the actual commands against a deterministic 502→200 server without changing exact-SHA, credential, extraction, or fail-closed contracts | **Proposed / local RED→GREEN complete; exact-head hosted Checks and independent approval required** | + +## 2026-10-01 PyJWT recursion denial-of-service closure + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| The shared Strix source and hash lock retained PyJWT 2.14.0 after GHSA-42vr-xj54-vc7v / CVE-2026-101918 disclosed an unauthenticated recursion DoS | Security Scan run `36741151937`; dependency-review job `109975641239`; OSV job `109975641271`; dependent `.github#2540@612d8e77cf13eba84782a22587a72d3ffb4b6c6e`; canonical owner PR #2531 predecessor `dde3ea7876ceb1569db717975cc74f44cc8d18f9` | In canonical owner PR #2531, advance source and lock to 2.15.0 without unrelated package movement, preserve exact source/lock parity, and merge-forward dependent branches only after owner acceptance | **Proposed / exact-head Checks and independent approval required** | + +## 2026-10-01 shared urllib3 security closure + +| Gap | Exact evidence | Action | Status | +|---|---|---|---| +| pip-audit and Strix locks retained urllib3 2.7.0 after CVE-2026-97687 and CVE-2026-97689 were published | Python Security run `36733279716`, job `109949358063`; exact predecessor `d1aa3659fca527a6c7330151f3ab4df3d7578391` | In canonical owner PR #2531, pin urllib3 2.8.0 in both source inputs, regenerate both hash locks without unrelated version movement, and bind all four files with one contract | **Proposed / exact-head Checks and independent approval required** | + 작성 기준일: **2026-08-26 10:35 KST** 대상: **ContextualWisdomLab/.github** 중앙 거버넌스·자동화 레포지터리와 이를 소비하는 naruon 생태계 현재 보호된 `main`: `826b92394c63deb6981c3a8d16a724d71f85a0d7` @@ -7,10 +75,46 @@ 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +### 2026-09-30 central coverage owner stack delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-CENTRAL-COVERAGE-OWNER-01 | **Proposed — complete local integration GREEN; hosted exact-head acceptance pending** | Current coverage owner `.github#2521@61fb469a…`, parser/security/response integration successor `.github#2530@2510618f…`, GitHub API response-lifecycle owner `.github#2532@9d3ec75d…`를 ordinary two-parent merge로 보존했다. 첫 integrated warning-fatal run은 `5196 passed, 6 skipped` 뒤 queue/Strix/release prescreen/release dependency의 실제 미실행 분기 90개와 partial branch 29개를 드러냈다. Dummy/live-CLI tests를 bounded behavior contracts로 교체하고, exact Git blob·Cargo development lock·runtime receipt·final fanout cap·Python 3.10 TOML fallback을 검증했으며, 앞선 필수조건 때문에 도달 불가능했던 prescreener postcondition만 제거했다. 두 live head를 재수집·일반 병합한 combined successor는 `5291 passed, 5 skipped, 40 subtests passed`, owned production `18729/18729` statements 및 `7642/7642` branches, Docstring 100%, warning 0이다. | Canonical owner는 중앙 `ContextualWisdomLab/.github`이며 source delta는 ordinary merge ancestry로만 통합한다. #2530과 #2521의 live head 이동을 재수집해 force 없이 merge했고 새 integrated tree 전체를 재검증했다. 게시된 #2530 exact head의 hosted security/quality Checks 및 qualifying independent approval을 새로 확인한다. queued/skipped/pending을 성공으로 간주하지 않고 #2521/#2530/#2532를 단순 Close하지 않는다. [RCA와 검증 근거](doctoring/central-coverage-owner-stack-2521.md). | + +### 2026-09-30 full-suite parser-lock incident delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-QUALITY-FULL-SUITE-PARSER-LOCK-01 | **Proposed — `.github#2530` combined successor preserves live parser, security, response-lifecycle, and coverage heads; hosted acceptance pending** | Protected `main@37b10243…`의 common quality lock만 설치하는 전체 suite가 `defusedxml`을 찾지 못해 collection error 13건으로 중단됐고, 같은 suite의 신규 workflow 계약은 `yaml`을 import한다. Concurrent-head 재검증 뒤 live #2530 `2510618f…`와 live #2521 `61fb469a…`를 Force Push·rebase 없이 ordinary merge했다. 이 ancestry는 security prerequisite #2531 `d1aa3659…`, response-lifecycle prerequisite #2532 `9d3ec75d…`, parser lock, coverage 수리를 함께 보존한다. 잠금을 직접 설치하는 모든 workflow는 생성 lock, 두 source input, compiler 변경을 추적하며 선택형 runtime quality도 실제 consumer suite를 실행한다. Review-repair owner는 launcher runtime 두 suite를 직접 실행·계측한다. Combined local evidence는 Python 3.14 warnings-fatal 5,291 passed, 5 skipped, 40 subtests, production 18,729/18,729 statements·7,642/7,642 branches, Docstring 100%다. 이 행은 live Project #1 상태나 merge authorization을 주장하지 않는다. | Canonical owner는 중앙 `.github`의 source requirement, 생성 hash lock, compiler, 직접 소비 quality workflows다. 새 combined exact head의 hosted Checks, 미해결 thread 0, qualifying independent approval을 다시 수집해야 ordinary protected merge할 수 있다. #2531/#2532/#2521은 protected successor merge와 complete carryover를 확인하기 전 닫지 않는다. | +| CONTROL-REPOSITORY-BRANCH-COVERAGE-01 | **Proposed — `.github#2521`의 전체 유효 delta를 `.github#2530` combined successor가 ordinary merge로 승계; exact-head hosted acceptance pending** | `.github#2521`는 production exclusion을 제거해 전역 100% 주장을 정직하게 RED로 되돌렸다. 네 잔여 소유자인 `opencode_queue_priority`, `strix_unverified_dependency`, `prescreen_release_runtime_archives`, `release_dependency_gate`의 실제 분기를 test-first로 모두 실행했고, launcher production omission도 제거했다. `.github#2530`의 parser lock 없이는 전체 suite collection이 실패하고, parser-lock PR은 이 coverage gap 때문에 전역 gate가 실패하는 순환 선행조건이었다. predecessor의 current head `61fb469a…`까지 successor ancestry에 보존하며 predecessor는 successor의 보호 병합과 tree 동등성을 확인하기 전 닫지 않는다. | Canonical owner는 중앙 `.github`의 production CI modules와 complete repository gate다. coverage 제외·pragma·threshold 하향·샘플 축소는 허용하지 않는다. refreshed combined successor의 complete warnings-fatal suite, 100% statement/branch report, hosted Checks, independent approval, ordinary protected merge를 새 exact head에서 완료해야 한다. | + +### 2026-09-30 공유 보안 기준 exact-head delta + +이 delta는 아래 2026-08-26 인벤토리를 덮어쓰지 않는다. 2026-09-30 재수집한 +보호 `main`은 `37b10243cec3d160ecc9c1be75c71428b160a703`이고, live API의 첫 +페이지에는 열린 PR 50개가 있었다. 페이지 전체를 조직의 총 PR 수로 추론하지 않는다. + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-SHARED-SECURITY-LOCK-01 | **Source repair in progress — release HOLD** | `.github#1026@6f645a73502e159d5a229805afa34868ad9bb851`의 Security Scan run `36495499815`는 공통 Rust fixture의 PyO3 `0.22.6`에서 GHSA-36hh-v3qg-5jq4와 GHSA-chgr-c6px-7xpp를 검출했고, Python Security run `36495499871`은 공통 Strix hash lock의 PyJWT `2.13.0`에서 CVE-2026-102274를 검출했다. 두 파일은 #1026 변경 범위 밖이며 보호 `main`에도 동일하게 남아 있었다. RED commit `cd84d887`는 PyO3 `0.29.2`와 PyJWT `2.14.0` source/lock parity를 요구한다. | 중앙 `.github`가 공통 fixture와 Strix lock을 소유한다. [RCA와 검증 계약](doctoring/shared-security-baseline-pyjwt-pyo3-20260930.md)에 따라 owner PR의 exact-head Checks와 독립 승인, ordinary protected merge, immutable consumer source pin 갱신, 그리고 #1026의 비강제 main merge-forward가 순서대로 필요하다. 어떤 실패도 #1026 전용 패치나 bypass로 처리하지 않는다. | + +### 2026-09-30 GitHub API response lifecycle incident delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| CONTROL-GITHUB-API-HTTP-ERROR-CLOSE-01 | **Proposed — protected-main RED reproduced; source repair under hosted exact-head verification** | 보호된 `.github/main@37b10243cec3d160ecc9c1be75c71428b160a703`의 Python 3.14.7 `tests/test_github_api_url_boundary.py -W error`가 실제 CodeQL/Strix opener의 synthetic 302 여덟 경우에서 `ResourceWarning: Implicitly cleaning up `로 `8 failed, 26 passed`였다. 첫 repair의 warning-fatal full suite가 동일 defect를 Noema/Pingora/preflight/Pages/sandbox readiness에서 추가로 드러냈다. | Canonical owner는 중앙 `.github`이다. 각 caller가 기존 bounded status/telemetry와 fail-closed mapping을 보존한 뒤 file-like error response를 명시적으로 닫는다. `5161 passed, 10 skipped, 40 subtests passed`로 complete warning-fatal local tree가 GREEN이다. [RCA와 acceptance](doctoring/github-api-http-error-response-lifecycle.md)를 따라 exact-head hosted security, independent review, ordinary protected merge를 완료한 뒤 `.github#2040`과 review-transport stack이 새 protected head를 정상 병합해 downstream 증거를 재생성해야 한다. | + +### 2026-10-03 Dependency Review transport-completion delta + +| Gap ID / 상태 | PRD / TRD / Context Map / evidence / next gate | +|---|---| +| `CONTROL-DEPENDENCY-REVIEW-TRANSPORT-EXIT-01` — **Complete owner carryover integrated into `.github#2565`; Proposed/HOLD pending exact-head hosted evidence and independent approval** | **PRD:** 구매자는 불완전한 dependency comparison이 보안 gate를 통과하지 않음을 보장받아야 한다. **TRD/RCA:** reusable preflight가 curl의 nonzero 종료를 `|| true`로 숨기고 출력된 HTTP `200`만 검사해 partial transfer에도 `available=true`를 발행할 수 있었다. **Context Map:** 중앙 `.github` Dependency Review admission owner가 thin callers 앞에서 exact base/head와 authenticated transport completion을 소유하며, bundled Security Scan은 같은 identity/transport invariant를 적용한다. **실행 흐름:** `exact identity validation → authenticated curl → curl exit == 0 AND HTTP 200 → available=true → pinned Dependency Review`; DB/UI/ERD 변화는 N/A다. **RED→GREEN:** #1725 RED `fe9d1933…`의 fake curl `HTTP 200 + exit 18` 회귀는 defect head에서 실패했다. #2565 published ordinary two-parent merge `650bc6e35409d52d24fec890c2ab74063794e9b8` (tree `9522f0b9c96247b3412e147bc6f89fd2595c0776`)가 #1725 exact `229027280e8bce6cc4f13e722b2b0c280a1ac17d`의 source/tests/ADR/doctoring/CHANGELOG/Gap delta를 runner-group stack에 보존했고, 결합 focused suite는 일반 및 `GITHUB_ACTIONS=true`에서 각각 53/53, warning-fatal 전체 suite는 5,437 passed, 10 skipped, 40 subtests다. Review에서 발견된 ADR-0025/0032 충돌은 security decision을 ADR-0034로, owned CodeQL decision을 ADR-0033으로 이동하고 numeric identity uniqueness 계약을 추가해 제거했다. **다음 gate:** fresh exact-head hosted security/review Checks, unresolved thread 0, qualifying independent approval, runner-group/capacity/cleanup canary, ordinary protected merge가 필요하다. | + ### 2026-09-19 exact-head incident delta | Gap ID | 상태 | exact-head evidence | causal owner / next gate | |---|---|---|---| +| CONTROL-LAW-CI-CLUSTER-SYMLINK-01 | **Proposed — `.github#2581` owner repair; local GREEN, hosted acceptance pending** | Exact parent `7558db6749e6f73a83cefd5f67dfaca380c8ef7d` allowed symlinks at `data`, `private`, `postgres`, `cluster`, and `postmaster.pid` to reach the synthetic `pg_ctl` boundary. Five RED cases now exercise the real helper; the minimal shared `cleanup_root` guard makes all five GREEN and preserves the root/marker/foreign PID evidence. | Canonical owner is `scripts/ci/law_ai_agent_ci.sh`. Keep `.github#2581` Draft until the final exact head passes the complete local suite, source-executing hosted Checks, and qualifying independent review; never substitute a skipped/zero-step run or bypass. | | CONTROL-OPENCODE-COVERAGE-LOCK-CONTEXT-01 | **Proposed — PR-bound incident register; GitHub Project #1 roadmap item이 아님; `.github#2385@950ab885…` source convergence, hosted acceptance pending** | Required OpenCode run `35370902053`의 `coverage-evidence` job `105778600365`은 PR source 실행 전에 `COPY requirements-opencode-review-ci-hashes.txt requirements-noema-document-ci-hashes.txt /tmp/`에서 두 번째 파일을 찾지 못해 종료했다. RED `9b9f5edcd`는 Dockerfile의 모든 lock input이 trusted build context에 존재해야 한다는 계약을 고정했다. 이 행은 live Project 상태를 주장하지 않고 exact-head PR evidence만 추적하며, protected integration 뒤 제거 여부를 재평가한다. | Canonical owner는 중앙 `.github/.github/workflows/opencode-review-dispatch.yml`이고 complete successor는 `.github#2385`이다. 두 lockfile을 각각 regular non-symlink로 검증하고 build context로 복사한 뒤 exact-head focused/full suite와 새 hosted `coverage-evidence`를 통과해야 한다. PR 제품 source나 coverage 비율의 결함으로 오인하지 않으며 synthetic status·manual rerun·bypass를 사용하지 않는다. | ### 2026-09-13 current-head incident delta diff --git a/opencode.jsonc b/opencode.jsonc index 3b5f34e2a6..2663186084 100644 --- a/opencode.jsonc +++ b/opencode.jsonc @@ -1,11 +1,11 @@ { "$schema": "https://opencode.ai/config.json", // Default routed through the vendored contextual-orchestrator gateway - // (orchestrator/free: fail-closed zero-cost pool, ZDR-first). The gateway + // (orchestrator/auto: fail-closed zero-cost pool, ZDR-first). The gateway // must be provisioned before a review starts; there is no direct-provider // fallback for review content. - "model": "contextual-orchestrator/orchestrator/free", - "small_model": "contextual-orchestrator/orchestrator/free", + "model": "contextual-orchestrator/orchestrator/auto", + "small_model": "contextual-orchestrator/orchestrator/auto", "enabled_providers": ["contextual-orchestrator"], "lsp": false, "mcp": {}, @@ -290,7 +290,7 @@ // vendored contextual-orchestrator LLM gateway. It auto-discovers models // across Bytez/NVIDIA NIM (x2 keys)/OpenRouter/OpenAI from KV-registered // credentials and serves the fail-closed zero-cost pool under the virtual - // model id `orchestrator/free`, with ZDR-compliant (zero-data-retention) + // model id `orchestrator/auto`, with ZDR-compliant (zero-data-retention) // routes prioritized by scripts/ci/zdr_policy.py. Requires // CONTEXTUAL_ORCHESTRATOR_BASE_URL and CONTEXTUAL_ORCHESTRATOR_TOKEN, // which scripts/ci/contextual_orchestrator_review_sidecar.sh provisions on @@ -306,8 +306,8 @@ "apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}" }, "models": { - "orchestrator/free": { - "name": "Orchestrator Free (ZDR-first zero-cost pool)", + "orchestrator/auto": { + "name": "Orchestrator Auto (cost-fenced dynamic selection)", "tool_call": true, "reasoning": true, "options": { diff --git a/pyproject.toml b/pyproject.toml index ff6353c164..cebf24066b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,16 +19,8 @@ pythonpath = ["."] [tool.coverage.run] branch = true source = ["scripts/ci"] -# contextual_orchestrator_review_launcher.py imports the vendored -# contextual-orchestrator library, which is installed only inside the review -# sidecar's runtime (scripts/ci/contextual_orchestrator_review_sidecar.sh). -# It cannot be imported by this repository's test suite, so it is omitted from -# coverage exactly as contextual-orchestrator omits its own runtime-only -# __main__.py and server.py. The policy and ZDR decision logic it consumes are -# fully tested offline. omit = [ "tests/*", - "scripts/ci/contextual_orchestrator_review_launcher.py", ] [tool.coverage.report] diff --git a/requirements-opencode-review-ci-hashes.txt b/requirements-opencode-review-ci-hashes.txt index 116009874b..f58dc7f4d3 100644 --- a/requirements-opencode-review-ci-hashes.txt +++ b/requirements-opencode-review-ci-hashes.txt @@ -137,88 +137,96 @@ coverage==7.15.4 \ # via # -r requirements-opencode-review-ci.txt # pytest-cov -hypothesis==6.168.0 \ - --hash=sha256:046fe4bcfce2a2fa186ba9d96bbb62c25c2f6c2e4071f0783ed6b5cc481d0669 \ - --hash=sha256:076a2096c34448931c3cfeb2eb7a6b843a56ffdce5e4e3a025bfdf8f935666d9 \ - --hash=sha256:085c9aa246487c56a40ca89003d285cbffdbb5be4097ba6d0139f9c21003c04a \ - --hash=sha256:0ba3838c4a92e0b9730d1ed7e67e4950c152ad79d0a0c7594065262db84c55c4 \ - --hash=sha256:112b0900059bf9d7d6528ed729770629ab146e0d133c4143b9bd4a01dc002bcc \ - --hash=sha256:16864797de4b024e4c6cebd44598af932f870aad811341bc5bc24c738801ff76 \ - --hash=sha256:1894782fae5d9a7bb44e6dcf848ccb09ccb5babab48d8b5c31a0a7fc025b82a1 \ - --hash=sha256:1d1aa5b3484e329295d88488a5ba06243909e65c2ab616513c2d36721de4ed1d \ - --hash=sha256:1f4cd0ff11bd470a1a846296ed5fe55e84214194850370994fd1370fe73d3099 \ - --hash=sha256:2085ee74ac3ab6b70e2f7ffae9b4cb74c246da2f574b2de81a0818a8a30f659f \ - --hash=sha256:2264f15a1c80329e3ad48e39c44bd5c9429b7b04c9ee62cdd72f4b10aaac9f29 \ - --hash=sha256:24b52a2b1c8db6e1e516f9295c8e4ef7ef63303ff24fbbc5b35f4ff71dcd732c \ - --hash=sha256:283eda952bcb1987ccba1c8b634db0e8a960e1e92e2daa7003bc2392f19cea01 \ - --hash=sha256:2a380b521b5a76a9e8917d64adcf7f861a45a4360a34b1579af14c5df8eb0377 \ - --hash=sha256:2a838218ff1eab8d7b4bf66b96037fce0a802f61f2fa5fd4b784696cac365ce7 \ - --hash=sha256:348d9b93fd4129f67f9bab94f3d70709a9372bbe0e0d22731325ce85d5eb409f \ - --hash=sha256:34e3c8b66047ba92f8b8df5e427074058d92db58038f007da4bf9d14e934ad3c \ - --hash=sha256:35f1262831b5acc74ded15f629965daffcd657f6016ee04fc9605f6eb2b334c0 \ - --hash=sha256:3b3ce1cce70b25a37ed1a38a53ce7204785726c675c0f41a0f83c338a7e47b3d \ - --hash=sha256:3bc00fd8cda04b58e37a1163e8a65389b247b4f5ee547ae37d244a4960995517 \ - --hash=sha256:3f6dcf66270278d078bed01b401f47db4e26456cd909d8e23c6b9366a6c0b131 \ - --hash=sha256:3f7486bed33225d02f6aa78a4c4ba2b6f84992a82571cdda1bf08dce41d13507 \ - --hash=sha256:4085b61e25d3dcc6c9151d4115269870aee8cdb921611ee5c989b2786449be09 \ - --hash=sha256:45fcfa05f746e253350f55f216bcef59754f5f2b85745f1fc2bb8ba81dd517a9 \ - --hash=sha256:47b89491ff02e3ae9b302c440457938e87b47a45b9a1d98ff5575b6910d779e2 \ - --hash=sha256:489d5c060f49f495b64215cae627c71730cffd5ef59dc4d7f431932e6e6d2e67 \ - --hash=sha256:4d7d29dd63ad9fdc4aa1d65fa272449e14aaf6c6bb8451091818c2945533a43a \ - --hash=sha256:527452b43e79e6dfbf9cb69145a940547a3cd177c556698a3fc939ed2354c4b3 \ - --hash=sha256:53469a1a7c4861b12c9a8622f762d7d1fd7bcf171884e1018ed5a8f063a5c063 \ - --hash=sha256:5427a3c951080c18170486f775df6a82153882b819eca6b8e7ed77693634e5ab \ - --hash=sha256:5920d267f7d8cfd376672f2bde5905cdf284d47519582e41ce7c142d48ee46c4 \ - --hash=sha256:5b54769033b84477931d2072e7133a7555e0de5c53fd5ca3bbde960762d7d31b \ - --hash=sha256:5f099b1c8fc49ec2d9d7944e661addb97d7c38e818fb8d1f78073c43895a87f6 \ - --hash=sha256:6b750390dac4429da0cb70ab3fe758457f0cea3d9c843d48c59d0690d1189fda \ - --hash=sha256:6de30e559eb151de14a5f74bceb4d97792a9315ada2a1816b5da825cd7d28edc \ - --hash=sha256:6f0dd437ec01140676192422b61f2f833b3ce6a3213da9b7e196ad6b3777e795 \ - --hash=sha256:6ff259260015f9be3756dcd4bc11c08e007314dec6b43d9a89084c4f34f94475 \ - --hash=sha256:719b45b0512e3535a6a0077c2f7c6053b02ac0e72d60693f66f98790a33855b2 \ - --hash=sha256:72af51087b7b5ab21c49f0d502f803c20897678652835596bd2a8b169a39135e \ - --hash=sha256:73084b76e4a79cd0f7883ce80fc60c9f374ce7dcad8f520b39db40470ce1852f \ - --hash=sha256:732ae5d47482f99d8028cca096729625f05690a83f5e7ce31466e266155792f4 \ - --hash=sha256:754016594fe78cef91790e0922f60d183c52f531255fbfa30dac495b813e2128 \ - --hash=sha256:76d4d36ed2fd62de11382f1d608169c1ffa9a49d3b9351146d8ff87cb81a66f7 \ - --hash=sha256:7d55562bf8d41cfa18559c33f30cadf44ceac8e517509d7a022a9feace621f28 \ - --hash=sha256:8067e6b4b48e5cfdc849a1a20c9d4972b3f532b3e3edb5e2b5dfd106045a5236 \ - --hash=sha256:812a84c4cc7f7ae4fcb39a5647cc2698e6c18254f8423126425578f1dcdac782 \ - --hash=sha256:891b2d281ede45130e7fa0a22fd65336cc77ef2f780ec3792e8de6fc274a02c8 \ - --hash=sha256:8e4b2d434e0dd134f3d31ac1efc1825bf99730dfe70fec005ff66d7211836d79 \ - --hash=sha256:9018b20acdb061b2ef4b2fa7f558ca5db97ffea316e0a528bc003a24b2ac996e \ - --hash=sha256:91e3de666a6c4f7543000d1710e25055d63ef3032c98bd2ab338b3087bdaa780 \ - --hash=sha256:92cff497b92e2285ff6a94193fdee04aba483a4115d501c1f9a570bd103fcd20 \ - --hash=sha256:93413d1b0af50a7b165d66278c529174bf2fd1773c78027735dc0b50d1d3fd27 \ - --hash=sha256:990026952d5b2eca290c88f639ac639233f47e13dae338c6dfb6e4774bcab349 \ - --hash=sha256:9a2079cd09919956dd388f1a1f8ea5a79f2b2437650fbeda31d8661217ffefef \ - --hash=sha256:9a72ed7afa1f7e30488b8a5754fca0ad9755518bdb77d6f0b003cadf7437a5f9 \ - --hash=sha256:9ba679f183c67adcb6f4ad93694beafb6da99fe691757f4e57b04ae77e581ba8 \ - --hash=sha256:9d9a8574f80fc859313aee56167d202e8625c0eedd200971130f0839f06d1c93 \ - --hash=sha256:a0d28418c104d7268fdebcc09bc49f7b6569b5eb942430c6859f53ec8d4edf63 \ - --hash=sha256:a4956f41ab1ec6e6ef9262a35970e9f3e2caaaa1cdafe0d413156c6934dd99d8 \ - --hash=sha256:a74b0945acbbd552c7c2d0a99a3b5232962b8848c8eed1829451800a9bfcf00b \ - --hash=sha256:a9650c4882fdbdd8e90bdae602a8bfa8c6f09dc5d06afec5b9b23982e8f60a04 \ - --hash=sha256:b5449a64eb37d9a4aa6ac9cd2ab0fd1a24145adf421ef1536884f73f39824887 \ - --hash=sha256:bc935a5d5f86fd8f5af951b8fbe00307f6f7c596f82a9a27c17d974f6ab0a26c \ - --hash=sha256:bfef4d46dbf1704a7b8fa3a78778651a2cb18870ca0a70da19c381646822b149 \ - --hash=sha256:c3af200b322f710c76c2189866246cdcff2039165dd77edff1a7bf1157162fb0 \ - --hash=sha256:cb10aa59b0af45badca76911f5323f40d24fdbe00d01b7b67fef8648c99411b5 \ - --hash=sha256:cd0c1dcf308e919c8ae708054d0ad61921ae87634a9aea574a9851da584cebc1 \ - --hash=sha256:d0620fa320fa66649e6bfd71e94f3f86115fffebb7e3c6dcece19d1aaff8e07f \ - --hash=sha256:d0bdb77f976740b8cd5ec697327ea343d02d052b9916d213b5d4c65d823415cd \ - --hash=sha256:db2751c27bffc8491a96d72969649089d5400115e4b7c49bf7167ebbdcc84193 \ - --hash=sha256:deb02de608268928d779aa889b0a9d67794b1cc0c54a322cf19e386be8a46ca7 \ - --hash=sha256:e21e30b76b6d3adb87c550576132a3204f4c257ec43353f6c09b9d59bb762abc \ - --hash=sha256:e2df8afacf9261070795db36db4a394e3ccdbb663fd2d38c7a9fba0c836dcecc \ - --hash=sha256:e86820053afad84677f301c0b892a226be1df49790800a65668ae7cc8a1ac571 \ - --hash=sha256:ec0886fe0be9091669937989f9a662beca42ae14a4a6dab25491c2c63365f88d \ - --hash=sha256:ecf0ab13cef899efb816ffdd7963e0679f372520884ce06756c7642f3df94213 \ - --hash=sha256:f62bdabf278db9ff61df5f3203d608949f0d893d0e30cdac3f2330e67e41ae68 \ - --hash=sha256:f77af7721ff35a58fa8797decd14c932c350a2548686c6e9b844db710a3a2441 \ - --hash=sha256:f89d8e998d3c936ffbbd1c3686c96f0378f6558aecc5967a3035a857f2bab0ad \ - --hash=sha256:fb8cdf45361e259df86e19f8cd042ce2d6c7e6ad88fa631b78a4e3a83c2e572d \ - --hash=sha256:fcc5bad4300a751804ce41f0e10d77f85272668160708ce39ec579bca8984843 +defusedxml==0.7.1 \ + --hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \ + --hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61 + # via -r requirements-noema-document-ci.txt +hypothesis==6.168.3 \ + --hash=sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408 \ + --hash=sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89 \ + --hash=sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62 \ + --hash=sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897 \ + --hash=sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef \ + --hash=sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b \ + --hash=sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102 \ + --hash=sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575 \ + --hash=sha256:140fe3cc7ba98a4ffeeb6492c318b43c326110a4d7b4f56bb759d5c264e2dc1c \ + --hash=sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa \ + --hash=sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7 \ + --hash=sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595 \ + --hash=sha256:26084ef31653108da9e5eb171278a1d930e90d8631604391f828219f5c1f6cb2 \ + --hash=sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef \ + --hash=sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f \ + --hash=sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7 \ + --hash=sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16 \ + --hash=sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2 \ + --hash=sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96 \ + --hash=sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074 \ + --hash=sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90 \ + --hash=sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4 \ + --hash=sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a \ + --hash=sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766 \ + --hash=sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462 \ + --hash=sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca \ + --hash=sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5 \ + --hash=sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd \ + --hash=sha256:558625ff28e415f2f770cad618c2c9e05a045a6586949397f178d537bec2f083 \ + --hash=sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef \ + --hash=sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc \ + --hash=sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026 \ + --hash=sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f \ + --hash=sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd \ + --hash=sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5 \ + --hash=sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c \ + --hash=sha256:7311d8ff296806ea41513d52edc832fcf43a1dc8d5c5f308cc386fc31b253a05 \ + --hash=sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa \ + --hash=sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e \ + --hash=sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d \ + --hash=sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa \ + --hash=sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b \ + --hash=sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a \ + --hash=sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d \ + --hash=sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018 \ + --hash=sha256:836eda971f25ddfce274107ed113297e8acede8d9b267775742b8ba8a404d0f0 \ + --hash=sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b \ + --hash=sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24 \ + --hash=sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5 \ + --hash=sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696 \ + --hash=sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b \ + --hash=sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b \ + --hash=sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75 \ + --hash=sha256:aac5889ea7b0b86cf2bf5709250b7576c44859dd249d1be23c009a7d6fb9f742 \ + --hash=sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2 \ + --hash=sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11 \ + --hash=sha256:b1872d2dc3f3758dfbf17cc39838b8e6b6a5c39a37811c067245e87b12d50f74 \ + --hash=sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba \ + --hash=sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a \ + --hash=sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128 \ + --hash=sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed \ + --hash=sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95 \ + --hash=sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068 \ + --hash=sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96 \ + --hash=sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7 \ + --hash=sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb \ + --hash=sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957 \ + --hash=sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38 \ + --hash=sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86 \ + --hash=sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499 \ + --hash=sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4 \ + --hash=sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248 \ + --hash=sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2 \ + --hash=sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd \ + --hash=sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c \ + --hash=sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483 \ + --hash=sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0 \ + --hash=sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580 \ + --hash=sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe \ + --hash=sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675 \ + --hash=sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6 \ + --hash=sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09 \ + --hash=sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab \ + --hash=sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e \ + --hash=sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837 # via -r requirements-opencode-review-ci.txt iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ @@ -272,6 +280,81 @@ pytest-cov==7.1.0 \ --hash=sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2 \ --hash=sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678 # via -r requirements-opencode-review-ci.txt +pyyaml==6.0.3 \ + --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ + --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ + --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ + --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \ + --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ + --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \ + --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ + --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \ + --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \ + --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \ + --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ + --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \ + --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \ + --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \ + --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \ + --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ + --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \ + --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \ + --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \ + --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \ + --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ + --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ + --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ + --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ + --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ + --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \ + --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \ + --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \ + --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \ + --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \ + --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \ + --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \ + --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \ + --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \ + --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ + --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ + --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ + --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ + --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \ + --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \ + --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \ + --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ + --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ + --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \ + --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \ + --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \ + --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \ + --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ + --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ + --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \ + --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ + --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \ + --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \ + --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ + --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \ + --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \ + --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ + --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ + --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ + --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ + --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ + --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \ + --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ + --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ + --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 + # via -r requirements-opencode-review-ci.txt sortedcontainers==2.4.0 \ --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \ --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0 diff --git a/requirements-opencode-review-ci.txt b/requirements-opencode-review-ci.txt index bf2112ed68..452ce2f074 100644 --- a/requirements-opencode-review-ci.txt +++ b/requirements-opencode-review-ci.txt @@ -12,4 +12,7 @@ interrogate==1.7.0 maturin==1.15.0 pytest==9.1.1 pytest-cov==7.1.0 +# Full-suite collection imports the Noema document parser and workflow YAML. +-r requirements-noema-document-ci.txt +PyYAML==6.0.3 uv==0.12.7 diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index 7a41a3d2d8..9a331a0d62 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -391,7 +391,9 @@ tomli-w==1.2.0 \ --hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \ --hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021 # via pip-audit -urllib3==2.7.0 \ - --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ - --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 - # via requests +urllib3==2.8.0 \ + --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \ + --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63 + # via + # -r requirements-pip-audit-ci.txt + # requests diff --git a/requirements-pip-audit-ci.txt b/requirements-pip-audit-ci.txt index 684087ba51..bec9276178 100644 --- a/requirements-pip-audit-ci.txt +++ b/requirements-pip-audit-ci.txt @@ -1 +1,2 @@ pip-audit==2.10.1 +urllib3==2.8.0 diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index eb83beda17..ba3198c876 100644 --- a/requirements-strix-ci-hashes.txt +++ b/requirements-strix-ci-hashes.txt @@ -144,6 +144,7 @@ anyio==4.14.2 \ --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via + # -r requirements-strix-ci.txt # google-genai # gql # httpx @@ -1119,24 +1120,45 @@ jsonschema-specifications==2025.9.1 \ --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d # via jsonschema -litellm==1.94.1 \ - --hash=sha256:001be1cde7950f2ae484e450ab2f8e93ab8791e5e8d4da560d21f2fb456b0b47 \ - --hash=sha256:07c1771315d7d26e242ef90b9336bcbc49a52158ff72ee640b4f8160cc963147 \ - --hash=sha256:156c62022320bccab7c3507b6b13400b26e55b74c799e5a4a2d5bf904a77368f \ - --hash=sha256:1b0bc4a2373e54f2bd4c13f8ef9fda3839bfb2e1173fb4bcea3150b07d4c59bc \ - --hash=sha256:2103e9b155d6545b48936d2ac2e614661613adb9e3d081c58c7303ca5dd6c656 \ - --hash=sha256:44e55a55270dee8bb85e063940c368d32040e6db66765c55db4b884fc002d4ef \ - --hash=sha256:66bc95498af3ab687ce7570704cb274bcf1d78049afa87a9f5f64db45b72847d \ - --hash=sha256:a6f5274876f20dd5c9e53ba3da502e94f5b3c681c5027a0398231d0caae4aacd \ - --hash=sha256:af37356cf5b325a2887c40ff772b39e1e0865b988297c544b29123ffb13fd1b9 \ - --hash=sha256:b0145d6b9fb718d12b7242ce5c975123f4dbfecd7b8ed1eb6a6939b0e506c946 \ - --hash=sha256:c5c9247d9fea8fe7cda851f7b15db560ee547a8325a0af86048967edf3ccfa15 \ - --hash=sha256:cfef0468bda9c1ba8f554bebc2966f08436f1ead98017e7ed2d7663ece77f1c2 \ - --hash=sha256:d14e5812b5f36af2ab45461ee0c925251bc07daf65c33b8f2ce3fd3ec1235eae \ - --hash=sha256:e9b6d92e305d96bdadb8a5ccd343b1ac188de142fbd6c91f72c75416b8c25c48 \ - --hash=sha256:e9effe4c1e9206740b4bb4c98142ea1f71bae57e49df007cd25ef24b0ce4563f \ - --hash=sha256:ffa9a6cd9b6205d60b02ffc0b7f077a03693d835b06d2a34bfeaabb4f073c08a +litellm==1.94.3 \ + --hash=sha256:0ddb60a439cc361e69bb4f550a78d1656c8edad6753f8fece14f60bbd0fcd054 \ + --hash=sha256:1017f29a6f72a16ef1b026208b3e52c7197b91ae848b396c817bfb051f86c300 \ + --hash=sha256:136960922878cbc66065346583a60fd0d35f3b1dbe853d636d10da39f049399f \ + --hash=sha256:14f06f1486aa4eef0684a2088539548e3647495fa119edc2b8145c5cfee3ca24 \ + --hash=sha256:1594d231e005f6a99e319310f2e3c0d33f26d8c746e90ec42fedc2b413f16573 \ + --hash=sha256:1ec0ff6e81acbfa85b4af90f9460c46a7872a28d53aaeee956ad7d289f47edc4 \ + --hash=sha256:2098c818ce5d8139eb46a851f26d1ac5caac1502e19d5223af4930b31682c461 \ + --hash=sha256:45b9f56f02a5417b249ecaf6f01f2531e744b17b701aaea441741ed9431ba35e \ + --hash=sha256:4a21e3f5cc72b18bd424009e5ad9addcee5ebdd726c7b26cadb4c87f865f5f1b \ + --hash=sha256:56bb6b303e2e341eb55570fd0cfa5734e5d89e8a1a5ee0096e7bb0bd9f27f9e2 \ + --hash=sha256:57a82dbbf1528aa530f021f1e6fe7528331c7d5f95fa755a17f984623eaf29ae \ + --hash=sha256:584b705ece98e7b7a2fc3cf4b720711d0b786e4d131de5c4469c34cabc3f55e0 \ + --hash=sha256:5b7ef13f56e167c76ca5dfc2e6bba8e52faf6371a540c00585ee7cff81af4ba0 \ + --hash=sha256:68b5b8c4ec8d51e4a2d02cf5405b6dd85be06b8ccf0205b7b0b30c1d3c02aebe \ + --hash=sha256:6ae267172e1c615346523faf6de6cac2f21937bda2e15e0af8c5d3291476a4e5 \ + --hash=sha256:87a343c86304daa6be5f96493f1eb0796d57142886f21f8d0815e1712d2bfc18 \ + --hash=sha256:93e13c1919f8eaeb3bee9a9160c6339d1fc3913254981a1e152023c5815667cc \ + --hash=sha256:9d0fabd73ca571771773a892de1c57e208349c99a3bbc51cd049a326f7a6f6e2 \ + --hash=sha256:9e339bea866f313487b1d7e150374360700f7ed80cac41722f7ddb072b9b685e \ + --hash=sha256:9f0529505cd5cceed220f89b311f4403fa076beae18069e9df31c4e59b226871 \ + --hash=sha256:a18386278e02a2b7f8d73bf836f1c7945133becca7894ee0641dbacc6473322d \ + --hash=sha256:ac40df3e6f412a2e87f61553640922d63c8114e433c85992461317b5f67ce78f \ + --hash=sha256:b6984b3b8032ab6807aa7c56423e961a5c4220d6b93a348dbf37ec1162b68e2d \ + --hash=sha256:b6ba10b699165a9a46811f2d3abe92967dc1c2d16f972d1057dae5c5c9cd460b \ + --hash=sha256:c0eee30a6d7c38fd4702339a5a53648f140ed8e5307baa321215d0d10dd9e535 \ + --hash=sha256:c526656e85ee1be21038c75774c094b97e7439e649dc13f61cb434700199990a \ + --hash=sha256:ccf58062430fdfd53d1ebb02d55d2316ad1068005dbb4186a522039077489f9b \ + --hash=sha256:d2027a4e492fd85702ffdbcc43be770a450c521b8c8c0c995e6f7d2fb6df1d49 \ + --hash=sha256:d8eedd2dc2e504ea8452392c4c66c102ed7ccd204307d72420fd073552b59ea9 \ + --hash=sha256:de2b3cc1a5cb5b9c7e393b55a0da58cdbe46f0b373b215d4422e30b2d5dec29b \ + --hash=sha256:deac3f3efdb5373729f37f612d2469696bbc928baef5f7c80ae7a2b0940b7939 \ + --hash=sha256:e05cabfc94633e47b725bed0851ecf18809117d23e8d6ef9abaa6059c2712261 \ + --hash=sha256:e4255bc09a27d99e8c1526baf76ff307e16d3543f51fda9c87c51a71c99c8ff7 \ + --hash=sha256:f8913c9f10ef3fb1baf4ab16fc77e110f92c1c0a9a16fa0f86d1b90b417e063f \ + --hash=sha256:f8c9a92d6165570065349b8d324e7b58800cb993c39b85a5f3c7458b5d926f3d \ + --hash=sha256:fafb59618a85f3c96d38a123ac53187c75e722a6b4e34d305063661262f62ea8 # via + # -r requirements-strix-ci.txt # openai-agents # strix-agent markdown-it-py==4.2.0 \ @@ -1811,17 +1833,19 @@ pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via rich -pyjwt==2.13.0 \ - --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ - --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 - # via mcp +pyjwt==2.15.0 \ + --hash=sha256:7a3742debf6b879e912dbb9819ceec1594be812452b78c5f2e2dfc56564954f8 \ + --hash=sha256:b11c5f9791d7bf51c2b39a81ed669f6b2dbbd669df2942f6c60167e9e3d1abe4 + # via + # -r requirements-strix-ci.txt + # mcp pyopenssl==26.4.0 \ --hash=sha256:28dfcce0162b9211413e26dfbfdf1d24317fbeba18fc93c12400a1856b2a0bc7 \ --hash=sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c # via google-auth -pypdf==6.16.1 \ - --hash=sha256:63fec31c4092ae50b6729beedcb469055b60d20c834bde1c402df241f371f644 \ - --hash=sha256:c4d1b43ddae921387321cf63936cd16a7743b91d2da92f165c149a195c972ba9 +pypdf==6.19.0 \ + --hash=sha256:7e5d6e730e7dae87d560a2cee218b852f6498c8be61966f3cd02ead971e48d14 \ + --hash=sha256:bbc43aca292369ccc6cbc8a921991ecf2538a3587ab5a116eff06c321d647155 # via strix-agent python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ @@ -2346,10 +2370,11 @@ typing-inspection==0.4.4 \ # mcp # pydantic # pydantic-settings -urllib3==2.7.0 \ - --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ - --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 +urllib3==2.8.0 \ + --hash=sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3 \ + --hash=sha256:63bf2ead4c879426ebf22ef2a781eeb4aa3b4ae798a0435506f8687fd5bb9b63 # via + # -r requirements-strix-ci.txt # docker # requests uvicorn==0.49.0 \ diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index 50e8a05f9b..3a31a6c2c1 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -1,6 +1,9 @@ strix-agent==1.5.3 +litellm==1.94.3 anyio==4.14.2 +pyjwt==2.15.0 openai[httpx2]==2.54.0 +urllib3==2.8.0 aiohttp==3.14.3 google-cloud-aiplatform==1.133.0 protobuf<8.0.0 diff --git a/scripts/ci/codeql_ghas_configuration_identity.py b/scripts/ci/codeql_ghas_configuration_identity.py index 53e00c41c6..527afa12e3 100644 --- a/scripts/ci/codeql_ghas_configuration_identity.py +++ b/scripts/ci/codeql_ghas_configuration_identity.py @@ -19,6 +19,7 @@ import argparse import json import os +import re import sys import time import urllib.error @@ -29,6 +30,8 @@ DEFAULT_SETUP_ANALYSIS_KEY = "dynamic/github-code-scanning/codeql:analyze" CODEQL_TOOL_NAME = "CodeQL" GITHUB_API_AUTHORITY = "api.github.com" +MAX_ANALYSES_PAGES = 1000 +MAX_HTTP_ERROR_DIAGNOSTIC_BYTES = 400 class ConfigurationIdentityError(RuntimeError): @@ -182,8 +185,11 @@ def _require_github_api_url(url: str) -> str: return url -def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: - """GET one canonical GitHub REST URL without redirects, or fail closed.""" +def _request_json( + url: str, *, token: str, timeout_seconds: int, + response_headers: dict[str, str] | None = None, +) -> Any: + """GET canonical REST JSON; optionally expose Link without changing the result.""" url = _require_github_api_url(url) request = urllib.request.Request( url, @@ -198,8 +204,21 @@ def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: try: with _GITHUB_API_OPENER.open(request, timeout=timeout_seconds) as response: payload = response.read().decode("utf-8") + if response_headers is not None: + headers: Any = getattr(response, "headers", {}) + if hasattr(headers, "get_all"): + response_headers["Link"] = ", ".join(headers.get_all("Link", [])) + else: + response_headers["Link"] = headers.get("Link", "") + except UnicodeDecodeError as exc: + raise ConfigurationIdentityError("GitHub API returned invalid UTF-8") from exc except urllib.error.HTTPError as exc: - body = exc.read().decode("utf-8", errors="replace")[-400:] + try: + body = exc.read(MAX_HTTP_ERROR_DIAGNOSTIC_BYTES).decode( + "utf-8", errors="replace" + ) + finally: + exc.close() raise ConfigurationIdentityError( f"GitHub API GET failed with HTTP {exc.code}: {body}" ) from exc @@ -208,6 +227,8 @@ def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: f"GitHub API transport failed: {type(exc).__name__}" ) from exc if not payload.strip(): + if response_headers is not None: + raise ConfigurationIdentityError("GitHub analyses API returned an empty body; evidence incomplete") return [] try: return json.loads(payload) @@ -215,6 +236,61 @@ def _request_json(url: str, *, token: str, timeout_seconds: int) -> Any: raise ConfigurationIdentityError("GitHub API returned invalid JSON") from exc +def _next_analyses_page( + link: str, *, endpoint: str, params: Mapping[str, str], page: int, + token: str, timeout_seconds: int, repository_ids: dict[str, int], +) -> str: + """Validate GitHub's Link grammar and bind next to the same sequential query. + + Only canonical HTTPS, this repository's analyses endpoint, and unchanged + ref/tool/per_page filters may receive the bearer header. A terminal last + relation must name the current page and preserve those same invariants. + Unknown, duplicate, malformed or non-sequential relations abort rather + than prove completeness. + """ + if not link: + return "" + relations: dict[str, str] = {} + for part in link.split(","): + match = re.fullmatch(r'\s*<([^<>\s]+)>\s*;\s*rel="(next|prev|first|last)"\s*', part) + if match is None or match.group(2) in relations: + raise ConfigurationIdentityError("malformed or duplicate analyses pagination Link") + relations[match.group(2)] = match.group(1) + target = relations.get("next", "") or relations.get("last", "") + if not target: + return "" + _require_github_api_url(target) + parsed = urllib.parse.urlsplit(target) + try: + query = urllib.parse.parse_qs(parsed.query, keep_blank_values=True, strict_parsing=True) + except ValueError as exc: + raise ConfigurationIdentityError("malformed analyses pagination query") from exc + expected = {key: [value] for key, value in params.items()} + expected["page"] = [str(page + 1 if "next" in relations else page)] + if query != expected: + raise ConfigurationIdentityError("analyses pagination changed filters or page sequence") + if parsed.path != urllib.parse.urlsplit(endpoint).path: + alias = re.fullmatch(r"/repositories/([1-9][0-9]*)/code-scanning/analyses", parsed.path) + if alias is None: + raise ConfigurationIdentityError("analyses pagination changed endpoint") + if "id" not in repository_ids: + metadata = _request_json( + endpoint.removesuffix("/code-scanning/analyses"), + token=token, timeout_seconds=timeout_seconds, + ) + repo_id = metadata.get("id") if isinstance(metadata, dict) else None + if type(repo_id) is not int or repo_id <= 0: + raise ConfigurationIdentityError("analyses pagination repository ID could not be verified") + repository_ids["id"] = repo_id + if alias.group(1) != str(repository_ids["id"]): + raise ConfigurationIdentityError("analyses pagination changed repository ID") + if "next" not in relations: + return "" + if page >= MAX_ANALYSES_PAGES: + raise ConfigurationIdentityError("analyses pagination page limit exceeded; evidence incomplete") + return target + + def list_codeql_analyses( repository: str, *, @@ -223,20 +299,43 @@ def list_codeql_analyses( per_page: int = 100, timeout_seconds: int = 30, ) -> list[dict[str, Any]]: - """List code-scanning analyses for a repository, optionally filtered by ref.""" - if not repository or "/" not in repository: + """Collect complete CodeQL history (at most 1000 pages), optionally by ref. + + Follow only sequential Link targets bound to the original repository and + ref/tool/page-size query. Transport, malformed links or the page budget + abort the collection: callers never receive partial identity evidence. + """ + if not repository or re.fullmatch(r"[A-Za-z0-9_-]+/[A-Za-z0-9_.-]+", repository) is None: raise ConfigurationIdentityError("repository must be owner/name") + if repository.split("/")[1] in {".", ".."}: + raise ConfigurationIdentityError("repository must be owner/name") + if type(per_page) is not int or not 1 <= per_page <= 100: + raise ConfigurationIdentityError("per_page must be an integer from 1 to 100") if not token: raise ConfigurationIdentityError("token is required to list analyses") params: dict[str, str] = {"per_page": str(per_page), "tool_name": CODEQL_TOOL_NAME} if ref: params["ref"] = ref query = urllib.parse.urlencode(params) - url = f"https://api.github.com/repos/{repository}/code-scanning/analyses?{query}" - payload = _request_json(url, token=token, timeout_seconds=timeout_seconds) - if not isinstance(payload, list): - raise ConfigurationIdentityError("code-scanning analyses response was not a list") - return [row for row in payload if isinstance(row, dict)] + endpoint = f"https://api.github.com/repos/{repository}/code-scanning/analyses" + url = f"{endpoint}?{query}" + rows: list[dict[str, Any]] = [] + page = 1 + repository_ids: dict[str, int] = {} + while url: + headers: dict[str, str] = {} + payload = _request_json( + url, token=token, timeout_seconds=timeout_seconds, response_headers=headers, + ) + if not isinstance(payload, list): + raise ConfigurationIdentityError("code-scanning analyses response was not a list") + rows.extend(row for row in payload if isinstance(row, dict)) + url = _next_analyses_page( + headers["Link"], endpoint=endpoint, params=params, page=page, + token=token, timeout_seconds=timeout_seconds, repository_ids=repository_ids, + ) + page += 1 + return rows def wait_for_language_pairing( diff --git a/scripts/ci/collect_release_strix_bindings.py b/scripts/ci/collect_release_strix_bindings.py index c4478fdeff..8162ba36c1 100644 --- a/scripts/ci/collect_release_strix_bindings.py +++ b/scripts/ci/collect_release_strix_bindings.py @@ -337,6 +337,8 @@ def collect_bindings( def main() -> None: + """Collect and print the full release dependency review from CLI inputs.""" + parser = argparse.ArgumentParser() for name in ( "capture", "license-report", "plan", "metadata", "attempt", "repository", diff --git a/scripts/ci/contextual_orchestrator_review_launcher.py b/scripts/ci/contextual_orchestrator_review_launcher.py index 811dc7d3f8..65911430c2 100644 --- a/scripts/ci/contextual_orchestrator_review_launcher.py +++ b/scripts/ci/contextual_orchestrator_review_launcher.py @@ -33,6 +33,7 @@ import sys from pathlib import Path from typing import Any, Callable +from urllib.error import HTTPError from scripts.ci.contextual_orchestrator_review_policy import ( FREE_POOL_CREDENTIAL_NAMES, @@ -329,10 +330,15 @@ def _safe_retry_after_seconds(exc: Exception) -> int | None: # runs inside the probe walk's exception handler, so a ValueError here # would escape ``_preflight_review_agents`` -- whose callers catch only # ``ReviewPreflightError`` -- and kill the boot before any evidence file - # is written. Every ``isdecimal`` string is accepted by ``int``. - if not isinstance(raw, str) or not raw.strip().isdecimal(): + # is written. Reject more than five decimal characters before conversion: + # the accepted ceiling is 86400, and Python deliberately rejects very + # long integer strings before ``int`` can return a value. + if not isinstance(raw, str): return None - seconds = int(raw.strip()) + normalized = raw.strip() + if len(normalized) > 5 or not normalized.isdecimal(): + return None + seconds = int(normalized) return seconds if 0 <= seconds <= 86400 else None @@ -389,19 +395,25 @@ def _record_provider_exception(row: dict[str, object], exc: Exception) -> None: row: The in-progress per-route evidence row to update. exc: The exception a probe attempt raised. """ - row["status"] = "rejected" - error_type = type(exc).__name__ - row["error_type"] = ( - error_type if error_type.isidentifier() and len(error_type) <= 64 else "provider_error" - ) - http_status = _safe_http_status(exc) - if http_status is not None: - row["http_status"] = http_status - retry_after = _safe_retry_after_seconds(exc) - if retry_after is not None: - row["retry_after_s"] = retry_after - row.pop("finish_reason", None) - row.pop("reasoning_without_content", None) + try: + row["status"] = "rejected" + error_type = type(exc).__name__ + row["error_type"] = ( + error_type + if error_type.isidentifier() and len(error_type) <= 64 + else "provider_error" + ) + http_status = _safe_http_status(exc) + if http_status is not None: + row["http_status"] = http_status + retry_after = _safe_retry_after_seconds(exc) + if retry_after is not None: + row["retry_after_s"] = retry_after + row.pop("finish_reason", None) + row.pop("reasoning_without_content", None) + finally: + if isinstance(exc, HTTPError): + exc.close() def _demote_agent(agent: object, penalty: int) -> object: @@ -1207,6 +1219,7 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--zdr-endpoints", default=None, help="Optional OpenRouter /api/v1/endpoints/zdr JSON path") parser.add_argument("--require-zdr", action="store_true") parser.add_argument("--pool", choices=("free", "auto"), default="free") + parser.add_argument("--zero-cost-only", action="store_true", help="Exclude priced routes and priced fallback even for auto") args = parser.parse_args(argv) from contextual_orchestrator.credentials import get_credential @@ -1242,7 +1255,7 @@ def main(argv: list[str] | None = None) -> int: try: discovered, discovery_errors = discover_all_models() - except Exception as exc: # pragma: no cover - provider/networking failure is runtime-only + except Exception as exc: raise SystemExit(f"review sidecar discovery failed: {exc}") from exc _log_discovery_errors(discovery_errors) routable_discovered = _routable_discovered_models(discovered) @@ -1253,7 +1266,7 @@ def main(argv: list[str] | None = None) -> int: model_id = getattr(model, "model_id", "") if not is_general_chat_agent_model_id(model_id) or not _has_text_output(model): continue - if args.pool == "free" and _route_identity(model) not in free_route_identities: + if (args.pool == "free" or args.zero_cost_only) and _route_identity(model) not in free_route_identities: continue selected_models.append(model) if not selected_models: @@ -1298,8 +1311,10 @@ def main(argv: list[str] | None = None) -> int: account_cap=_catalog_account_cap(DEFAULT_ACCOUNT_CAP), zdr_endpoints=zdr_endpoints, require_zdr=args.require_zdr, - pool=args.pool, + pool="free" if args.zero_cost_only else args.pool, ) + result["report"]["requested_pool"] = f"orchestrator/{args.pool}" + result["report"]["zero_cost_only"] = args.zero_cost_only result["report"] = _with_discovery_counts( result["report"], normalized_rows, provider_account=provider_account ) @@ -1318,6 +1333,7 @@ def main(argv: list[str] | None = None) -> int: ) if ( args.pool == "auto" + and not args.zero_cost_only and admitted_free_rows and admitted_priced_rows and fallback_limit @@ -1388,5 +1404,5 @@ def main(argv: list[str] | None = None) -> int: return 0 -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": raise SystemExit(main()) diff --git a/scripts/ci/contextual_orchestrator_review_policy.py b/scripts/ci/contextual_orchestrator_review_policy.py index 241f84bef7..2169dcc027 100644 --- a/scripts/ci/contextual_orchestrator_review_policy.py +++ b/scripts/ci/contextual_orchestrator_review_policy.py @@ -143,6 +143,29 @@ def _normalize_cost_evidence( ) +def normalize_input_modalities(value: object) -> list[str]: + """Preserve explicit input evidence as normalized catalog-tag tokens. + + Match the pinned contextual-orchestrator ``input:`` grammar. + Missing evidence remains empty, never guessed text. Malformed nonempty + evidence fails closed rather than silently dropping provider exclusions. + """ + if value is None: + return [] + if not isinstance(value, (list, tuple)): + raise PolicyError("input_modalities must be a list or tuple of tokens") + normalized: list[str] = [] + for item in value: + if not isinstance(item, str): + raise PolicyError("input_modalities contains a non-string token") + token = item.strip().casefold() + if not re.fullmatch(r"[a-z][a-z0-9_-]*", token): + raise PolicyError("input_modalities contains an invalid token") + if token not in normalized: + normalized.append(token) + return normalized + + def _bytez_non_token_price_evidence( *, is_free: bool, @@ -240,6 +263,7 @@ def parse_discovery_report(report: Mapping[str, Any]) -> list[dict[str, Any]]: "provider": provider, "model": model, "agent_id": str(candidate_id), + "input_modalities": normalize_input_modalities(row.get("input_modalities")), "is_free": is_free, "cost_evidence": cost_evidence, "prompt_price_per_1k": prompt_price, @@ -395,6 +419,9 @@ def build_zdr_prioritized_catalog( "review", f"cost:{evidence}", "zdr" if zdr else "non-zdr", + *(f"input:{value}" for value in normalize_input_modalities( + row.get("input_modalities") + )), ], "priority": -rank, "disabled": False, diff --git a/scripts/ci/contextual_orchestrator_review_sidecar.sh b/scripts/ci/contextual_orchestrator_review_sidecar.sh index 46dd07c3e0..c9fa4ef81e 100755 --- a/scripts/ci/contextual_orchestrator_review_sidecar.sh +++ b/scripts/ci/contextual_orchestrator_review_sidecar.sh @@ -321,14 +321,12 @@ case "$orchestrator_pool" in free) pool_args=(--pool "$orchestrator_pool") ;; + auto) + # Auto resolves the gateway alias; the explicit fence excludes priced candidates and fallback. + pool_args=(--pool "$orchestrator_pool" --zero-cost-only) + ;; *) - # GitHub Actions Workflow usage of contextual-orchestrator is pinned to - # orchestrator/free: the org has not solved cost-safe free+ZDR routing - # well enough yet to justify a priced-inclusive "auto" pool in central CI, - # so "auto" is rejected here even though the launcher's own --pool flag - # (a general-purpose CLI also used outside GitHub Actions) still accepts - # it. - fail "CONTEXTUAL_ORCHESTRATOR_POOL must be free" + fail "CONTEXTUAL_ORCHESTRATOR_POOL must be free or auto" ;; esac diff --git a/scripts/ci/law_ai_agent_ci.sh b/scripts/ci/law_ai_agent_ci.sh new file mode 100755 index 0000000000..542359159c --- /dev/null +++ b/scripts/ci/law_ai_agent_ci.sh @@ -0,0 +1,257 @@ +#!/usr/bin/env bash +# Law CI: isolate the consumer, native test database, tool cache and built artifacts. +# Shell function comments are the docstring contract; Python blocks use docstrings. +set -euo pipefail +SELF="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/$(basename -- "${BASH_SOURCE[0]}")" + +# Verify pytest's JUnit evidence, including every nested suite and executed count. +verify_junit() { + python3 - "$1" <<'PY' +"""Reject skips, failures, errors and empty JUnit evidence.""" +import sys +import xml.etree.ElementTree as ET +suites = list(ET.parse(sys.argv[1]).getroot().iter("testsuite")) +valid = bool(suites) and sum(int(s.get("tests", "0")) for s in suites) > 0 +valid &= all(int(s.get(k, "0")) == 0 for s in suites for k in ("skipped", "failures", "errors")) +valid &= not any(ET.parse(sys.argv[1]).getroot().iter("skipped")) +sys.exit(0 if valid else 1) +PY +} + +# Refuse ambiguous or stale artifact directories rather than selecting a glob match. +verify_dist() { + python3 - "$1" <<'PY' +"""Require exactly one regular non-symlink wheel and source archive.""" +import sys +from pathlib import Path +entries = list(Path(sys.argv[1]).iterdir()) +valid = len(entries) == 2 and all(p.is_file() and not p.is_symlink() for p in entries) +valid &= sum(p.name.endswith(".whl") for p in entries) == 1 +valid &= sum(p.name.endswith(".tar.gz") for p in entries) == 1 +sys.exit(0 if valid else 1) +PY +} + +# Validate owned canonical immediate-child paths and refuse every symlink component. +validate_owned_root() { + python3 - "$RUNNER_TEMP" "$1" "${2:-}" <<'PY' +"""Authorize only private current-user scratch roots and optional state markers.""" +import os +import stat +import sys +from pathlib import Path +base, root = map(Path, sys.argv[1:3]) +valid = base.is_absolute() and root.is_absolute() +for path in (base, root): + for component in (path, *path.parents): + valid &= not component.is_symlink() +valid &= root.parent == base and root.name.startswith("lawci.") +valid &= root.is_dir() and root.resolve().parent == base.resolve() +if root.exists(): + info = root.stat() + valid &= info.st_uid == os.getuid() and stat.S_IMODE(info.st_mode) == 0o700 +if sys.argv[3]: + marker = Path(sys.argv[3]) + valid &= marker.parent == base and not marker.is_symlink() and marker.is_file() + if marker.is_file(): + info = marker.stat() + valid &= info.st_uid == os.getuid() and stat.S_IMODE(info.st_mode) == 0o600 +sys.exit(0 if valid else 1) +PY +} + +# Remove only this lane's private scratch cluster; failed stop leaves evidence for retry. +cleanup_root() { + local root="$1" pg_bin="$2" cluster="$1/data/private/postgres/cluster" boundary_path + validate_owned_root "$root" || return 1 + for boundary_path in \ + "$root/data" "$root/data/private" "$root/data/private/postgres" \ + "$cluster" "$cluster/postmaster.pid"; do + if [[ -L "$boundary_path" ]]; then + printf '%s\n' 'law-ci: PostgreSQL cluster boundary is symlinked; scratch retained' >&2 + return 1 + fi + done + if [[ -e "$cluster" && ! -d "$cluster" ]]; then + printf '%s\n' 'law-ci: PostgreSQL cluster path is not a directory; scratch retained' >&2 + return 1 + fi + if [[ -d "$cluster" ]]; then + if [[ ! -f "$cluster/postmaster.pid" ]]; then + printf '%s\n' 'law-ci: PostgreSQL cluster PID metadata is missing; shutdown is inconclusive; scratch retained' >&2 + return 1 + fi + "$pg_bin/pg_ctl" -D "$cluster" -m fast -w stop || return 1 + if [[ -e "$cluster/postmaster.pid" ]]; then + printf '%s\n' 'law-ci: PostgreSQL stop returned success but PID metadata remains; scratch retained' >&2 + return 1 + fi + fi + rm -rf -- "$root" +} + +# Preserve the failing child status and turn a failed database cleanup into failure. +on_exit() { + local status=$? + trap - EXIT INT TERM + if ! cleanup_root "$scratch" "$pg_bin"; then + printf '%s\n' 'law-ci: cleanup failed; owned scratch retained' >&2 + [[ "$status" != 0 ]] || status=1 + else + if [[ "$state_owned" == 1 ]]; then rm -f -- "$state"; fi + fi + exit "$status" +} + +# Execute committed source in a fresh short scratch root, never mutate the checkout. +run_ci() { + [[ "${LAW_CI_SOURCE_SHA:-}" =~ ^[0-9a-f]{40}$ ]] && + [[ "$(git -C "${LAW_CI_SOURCE:-}" rev-parse HEAD)" == "$LAW_CI_SOURCE_SHA" ]] || { + printf '%s\n' 'law-ci: source identity refused' >&2; return 1; + } + [[ "${LAW_CI_PYTHON_VERSION:-}" == 3.12 || "${LAW_CI_PYTHON_VERSION:-}" == 3.14 ]] + [[ "$(uv --version)" == 'uv 0.12.5 '* ]] + pg_bin="$(pg_config --bindir)" + for tool in initdb pg_ctl createdb psql; do test -x "$pg_bin/$tool"; done + test -d "${RUNNER_TEMP:?}" + umask 077 + scratch="$(mktemp -d "$RUNNER_TEMP/lawci.XXXXXX")" + state_owned=0 + state="$RUNNER_TEMP/law-ci-state-${LAW_CI_PYTHON_VERSION}" + trap on_exit EXIT + trap 'exit 130' INT + trap 'exit 143' TERM + test ! -e "$state" && test ! -L "$state" + (set -o noclobber; printf '%s\n' "$scratch" > "$state") + state_owned=1 + # Native Unix socket paths must remain shorter than the platform socket limit. + test "${#scratch}" -lt 65 + export LAW_AGENT_ROOT="$scratch" + export LAW_AGENT_POSTGRES_ROOT="$LAW_AGENT_ROOT" + export LAW_AGENT_POSTGRES_USER + LAW_AGENT_POSTGRES_USER="$(id -un)" + export LAW_AGENT_POSTGRES_PORT=55439 + local private="$scratch/data/private/postgres" + mkdir -p "$private/socket" "$scratch/source" "$scratch/out" "$scratch/outside" + chmod 700 "$scratch" "$private" "$private/socket" + # Consumer safety code requires this exact cluster/socket identity and test database. + export LAW_AGENT_TEST_DATABASE_URL="dbname=law_agent_test user=$LAW_AGENT_POSTGRES_USER host='$private/socket' port=$LAW_AGENT_POSTGRES_PORT" + unset LAW_AGENT_DATABASE_URL PGHOSTADDR PGSERVICE PGSERVICEFILE PGSYSCONFDIR PGOPTIONS PYTHONPATH PYTHONHOME VIRTUAL_ENV + export UV_CACHE_DIR="$scratch/uv-cache" UV_PROJECT_ENVIRONMENT="$scratch/venv" + export UV_PYTHON="$LAW_CI_PYTHON_VERSION" UV_NO_CACHE=1 + printf '%s\n' 'law-ci: fresh native test cluster' + "$pg_bin/initdb" -D "$private/cluster" -A trust -U "$LAW_AGENT_POSTGRES_USER" --encoding=UTF8 --no-locale > "$scratch/initdb.log" + "$pg_bin/pg_ctl" -D "$private/cluster" -l "$scratch/server.log" \ + -o "-k \"$private/socket\" -p $LAW_AGENT_POSTGRES_PORT -c listen_addresses='' -c shared_buffers=32MB -c max_connections=15 -c statement_timeout=10s -c lock_timeout=2s" -w start + "$pg_bin/createdb" -h "$private/socket" -p "$LAW_AGENT_POSTGRES_PORT" law_agent_test + git -C "$LAW_CI_SOURCE" archive "$LAW_CI_SOURCE_SHA" | tar -x -C "$scratch/source" + cd "$scratch/source" + printf '%s\n' 'law-ci: locked source quality gates' + uv sync --locked + uv run --locked ruff check . + uv run --locked ruff format --check . + uv run --locked mypy + # Fixtures need the complete archived migration inventory before suite collection. + # Retain source evidence outside the checkout for the independent installed smoke. + # Use the consumer's real test_only approval, not the broader development CLI path. + uv run --locked python - "$scratch/source-migrations.json" <<'PY' +"""Commit the verified migrations to the fresh, approved test-only database.""" +from hashlib import sha256 +import json +import os +from pathlib import Path +import re +import sys +if sys.version_info[:2] != tuple(map(int, os.environ["LAW_CI_PYTHON_VERSION"].split("."))): + raise SystemExit("law-ci: Python version mismatch") +# Check lexical ancestors before traversal or importing the archived product package. +resources = Path("src/law_ai_agent/migrations").absolute() +for component in reversed((resources, *resources.parents)): + if component.is_symlink() or not component.is_dir(): + raise SystemExit("law-ci: source migration inventory invalid") +inventory = [] +versions = set() +for resource in sorted(resources.iterdir(), key=lambda path: path.name): + match = re.fullmatch(r"([0-9]{3})_[a-z][a-z0-9]*(?:_[a-z0-9]+)*\.sql", resource.name) + if not match or not resource.is_file() or resource.is_symlink(): + raise SystemExit("law-ci: source migration inventory invalid") + version = int(match[1]) + script = resource.read_text() + if version == 0 or version in versions or not script.strip(): + raise SystemExit("law-ci: source migration inventory invalid") + versions.add(version) + # Preserve PostgresStore.migrate's read_text()/script.encode() checksum semantics. + inventory.append({"name": resource.name, "version": version, + "checksum": sha256(script.encode()).hexdigest()}) +if not inventory: + raise SystemExit("law-ci: source migration inventory invalid") +from law_ai_agent.postgres import PostgresStore, connect +with connect(os.environ["LAW_AGENT_TEST_DATABASE_URL"], test_only=True) as connection: + PostgresStore(connection).migrate() + connection.commit() + actual = [{"version": row["version"], "checksum": row["checksum"]} + for row in connection.execute( + "SELECT version, checksum FROM law_agent.migration ORDER BY version")] + expected = [{"version": row["version"], "checksum": row["checksum"]} for row in inventory] + if actual != expected: + raise SystemExit("law-ci: database migration inventory mismatch") +Path(sys.argv[1]).write_text(json.dumps(inventory)) +PY + uv run --locked pytest --cov --cov-fail-under=90 --cov-report=term-missing --junitxml="$scratch/junit.xml" + bash "$SELF" verify-junit "$scratch/junit.xml" + printf '%s\n' 'law-ci: fresh build and installed artifact smoke' + uv build --out-dir "$scratch/out" --no-create-gitignore + bash "$SELF" verify-dist "$scratch/out" + # Prime dependency wheels into a new lane-local cache, then prove real offline install. + uv venv --python "$LAW_CI_PYTHON_VERSION" "$scratch/prime" + UV_NO_CACHE=0 uv pip install --python "$scratch/prime/bin/python" "$scratch/out/"*.whl + uv venv --python "$LAW_CI_PYTHON_VERSION" "$scratch/installed" + UV_NO_CACHE=0 uv pip install --offline --python "$scratch/installed/bin/python" "$scratch/out/"*.whl + cd "$scratch/outside" + "$scratch/installed/bin/python" -I - "$scratch/source-migrations.json" <<'PY' +"""Verify packaged migration resources outside the source checkout.""" +from hashlib import sha256 +from importlib.resources import files +import json +from pathlib import Path +import sys +import law_ai_agent +if not Path(law_ai_agent.__file__).resolve().is_relative_to(Path(sys.prefix).resolve()): + raise SystemExit("law-ci: installed package prefix mismatch") +expected = json.loads(Path(sys.argv[1]).read_text()) +resources = sorted(files("law_ai_agent").joinpath("migrations").iterdir(), key=lambda path: path.name) +if not expected or [resource.name for resource in resources] != [row["name"] for row in expected]: + raise SystemExit("law-ci: installed migration inventory mismatch") +for resource, row in zip(resources, expected): + if not resource.is_file(): + raise SystemExit("law-ci: installed migration inventory mismatch") + script = resource.read_text() + if not script.strip() or sha256(script.encode()).hexdigest() != row["checksum"]: + raise SystemExit("law-ci: installed migration inventory mismatch") +PY + "$scratch/installed/bin/python" -I -m law_ai_agent --help > "$scratch/installed-help.log" + printf '%s\n' 'law-ci: quality, zero-skip database and installed artifact gates passed' +} + +# Recover a lane-local marker on GitHub cancellation; never source marker content. +cleanup_state() { + local version state root pg_bin + for version in 3.12 3.14; do + state="${RUNNER_TEMP:?}/law-ci-state-$version" + test -e "$state" || continue + test ! -L "$state" && test -f "$state" + IFS= read -r root < "$state" + validate_owned_root "$root" "$state" + pg_bin="$(pg_config --bindir)" + cleanup_root "$root" "$pg_bin" + rm -f -- "$state" + done +} + +case "${1:-}" in + verify-junit) verify_junit "$2" ;; + verify-dist) verify_dist "$2" ;; + run) run_ci ;; + cleanup) cleanup_state ;; + *) printf '%s\n' 'law-ci: unknown operation' >&2; exit 1 ;; +esac diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 3ddc126604..99d228d5e1 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -22,6 +22,7 @@ import urllib.parse import urllib.request from typing import Any +from urllib.error import HTTPError try: import tomllib @@ -376,8 +377,11 @@ def _download_trusted_uv_archive() -> bytes: break payload.extend(chunk) except OSError as exc: + error_type = type(exc).__name__ + if isinstance(exc, HTTPError): + exc.close() raise RuntimeError( - f"trusted uv archive download failed: {type(exc).__name__}" + f"trusted uv archive download failed: {error_type}" ) from exc if len(payload) > TRUSTED_UV_DOWNLOAD_MAX_BYTES: diff --git a/scripts/ci/noema-document-reader/package-lock.json b/scripts/ci/noema-document-reader/package-lock.json index 1026fd79a3..fe692cdf0e 100644 --- a/scripts/ci/noema-document-reader/package-lock.json +++ b/scripts/ci/noema-document-reader/package-lock.json @@ -458,9 +458,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", - "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "funding": [ { "type": "github", @@ -669,9 +669,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz", - "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.1.tgz", + "integrity": "sha512-4OUAqU9Z1i3vCnS05hzGiFnEMDpQ+62pAD/MVQOp83fYyNC8GleCqaS0QikQBmcWCrKFiUs/B8ztRRiYOAXuCA==", "license": "MIT", "engines": { "node": ">= 12" diff --git a/scripts/ci/noema-document-reader/package.json b/scripts/ci/noema-document-reader/package.json index aa4fc0d3ff..2408b5854b 100644 --- a/scripts/ci/noema-document-reader/package.json +++ b/scripts/ci/noema-document-reader/package.json @@ -5,5 +5,9 @@ "dependencies": { "@rhwp/core": "0.7.7", "hwp-mcp": "0.3.0" + }, + "overrides": { + "fast-uri": "3.1.8", + "ip-address": "10.7.1" } } diff --git a/scripts/ci/noema_bootstrap_producer.py b/scripts/ci/noema_bootstrap_producer.py new file mode 100644 index 0000000000..5a891b8ff4 --- /dev/null +++ b/scripts/ci/noema_bootstrap_producer.py @@ -0,0 +1,409 @@ +"""Preserve authenticated phase issuance bytes in explicit private custody. + +The request and revalidation collaborators belong to the existing trusted App +issuer/runtime owner. This module neither authenticates arbitrary dictionaries +nor admits native execution from source acceptance or token-action metadata. +""" +from __future__ import annotations + +import json +import os +import stat +import sys +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from scripts.ci.premerge_review_tooling import _unique_object + + +MAX_RESPONSE_BYTES = 2 * 1024 * 1024 + + +def materialize_tooling(source_root, target_root, raw_manifest, *, accepted_digest, + admission, private_parent): + """Copy exactly externally admitted inert source into a separate namespace. + + Supplied acceptance must be independently authenticated by the runtime owner. + This checks its contract and bytes; it never creates acceptance evidence. + The parent is held exclusively by that owner, not defended against same-UID + or privileged namespace mutation by directory permission bits alone. + """ + import hashlib + import sys + from scripts.ci.premerge_review_tooling import load_noema_tooling + parent, source, target = map(Path, (private_parent, source_root, target_root)) + if (not parent.is_absolute() or not source.is_absolute() or not target.is_absolute() + or target.parent != parent or source == target or parent.is_relative_to(source)): + raise ValueError('bootstrap namespace refused') + if any(path.is_symlink() for path in (parent, *parent.parents, source)): + raise ValueError('bootstrap namespace refused') + info = parent.lstat() + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077 or os.path.lexists(target)): + raise ValueError('bootstrap namespace refused') + if admission.get('namespace') != str(parent): + raise ValueError('bootstrap namespace differs from independent admission') + for field in ('isolated_clean_job', 'native_custody', 'supplier_cost', + 'supplier_retention', 'issuer_phase_separation'): + if admission.get(field) is not True: + raise ValueError('bootstrap execution evidence is not accepted') + _validate_runtime(admission, parent) + tooling = load_noema_tooling(source, raw_manifest, accepted_digest=accepted_digest, + interpreter=Path(sys.executable).resolve(), phase_environment=lambda _: {}) + inventory = json.loads(raw_manifest)['files'] + target.mkdir(mode=0o700) + for name, digest in inventory.items(): + raw = (source / name).read_bytes() + if hashlib.sha256(raw).hexdigest() != digest: + raise ValueError('bootstrap source changed during delivery') + destination = target / name + destination.parent.mkdir(mode=0o700, parents=True, exist_ok=True) + with os.fdopen(os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o400), 'wb') as stream: + stream.write(raw) + tooling._verify() + load_noema_tooling(target, raw_manifest, accepted_digest=accepted_digest, + interpreter=Path(sys.executable).resolve(), phase_environment=lambda _: {}) + return target + + +def _validate_runtime(admission, parent): + """Rebind explicit namespace, interpreter, dependencies and required admission.""" + import hashlib + parent = Path(parent) + if not parent.is_absolute() or any(path.is_symlink() for path in (parent, *parent.parents)): + raise ValueError('bootstrap namespace refused') + info = parent.lstat() + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077 or admission.get('namespace') != str(parent)): + raise ValueError('bootstrap namespace refused') + for field in ('isolated_clean_job', 'native_custody', 'supplier_cost', + 'supplier_retention', 'issuer_phase_separation'): + if admission.get(field) is not True: + raise ValueError('bootstrap execution evidence unavailable') + interpreter = admission.get('interpreter') + actual_interpreter = Path(sys.executable).resolve() + if (not isinstance(interpreter, dict) or set(interpreter) != {'path', 'sha256'} + or interpreter['path'] != str(actual_interpreter) + or hashlib.sha256(actual_interpreter.read_bytes()).hexdigest() != interpreter['sha256']): + raise ValueError('bootstrap interpreter differs from independent admission') + dependencies = admission.get('dependencies') + if not isinstance(dependencies, dict): + raise ValueError('bootstrap dependency inventory unavailable') + for name, digest in dependencies.items(): + path = Path(name) + if (not path.is_absolute() or path.is_symlink() or not path.is_file() + or hashlib.sha256(path.read_bytes()).hexdigest() != digest): + raise ValueError('bootstrap dependency differs from independent admission') + + +def _response(raw): + """Parse bounded original API bytes without reflecting credential payloads.""" + if not isinstance(raw, bytes) or not 0 < len(raw) <= MAX_RESPONSE_BYTES: + raise ValueError('issuer response size or type refused') + try: + value = json.loads(raw, object_pairs_hook=_unique_object) + except (UnicodeError, ValueError): + raise ValueError('issuer response schema refused') from None + if not isinstance(value, dict): + raise ValueError('issuer response schema refused') + return value + + +def run_bootstrap_review(source_root, raw_manifest, *, accepted_digest, admission, + private_parent, fetch_live, request, revalidate, + inference_key): + """Compose actual admitted tooling, original App response and two children. + + Trusted runtime callbacks are externally held authority; this function only + checks their supplied admission and orders preparation before the separately + minted write token. No supplier policy is inferred from source or fixtures. + """ + import hashlib + import subprocess + from scripts.ci.premerge_review_tooling import ( + load_noema_tooling, phase_environment_from_issuance, + require_prepared_envelope, require_review_scope, + ) + scope = admission['scope'] + parent = Path(private_parent) + source = Path(source_root) + target = materialize_tooling(source, parent / 'tooling', raw_manifest, + accepted_digest=accepted_digest, admission=admission, private_parent=parent) + if not isinstance(inference_key, str) or not inference_key.strip(): + raise ValueError('existing inference authority unavailable') + envs = {} + def fence(): + """Revalidate supplier, source, admitted scope and external authority.""" + revalidate() + if hashlib.sha256(raw_manifest).hexdigest() != accepted_digest: + raise ValueError('bootstrap source pin changed') + live = fetch_live() + require_review_scope(scope, live) + from scripts.ci.premerge_review_tooling import admit_tooling + inventory = json.loads(raw_manifest)['files'] + files = {name: (target / name).read_bytes() for name in inventory} + admit_tooling(raw_manifest, accepted_digest=accepted_digest, source_files=files) + def issue(phase): + """Store exact authenticated response privately and consume its grant.""" + fence() + path = parent / (phase + '.json') + produce_issuance(phase, scope=scope, installation_id=admission['installation_id'], + app_id=admission['app_id'], output=path, private_parent=parent, + request=request, revalidate=fence) + fence() + response = _response(path.read_bytes()) + return phase_environment_from_issuance(phase, response, + scope=scope, reviewer_actor=scope['reviewer'], + installation_id=admission['installation_id'], + inference_key=inference_key if phase == 'prepare' else None) + fence() + envs['prepare'] = issue('prepare') + tooling = load_noema_tooling(target, raw_manifest, + accepted_digest=accepted_digest, interpreter=Path(__import__('sys').executable), + phase_environment=lambda phase: envs[phase]) + verdict = parent / 'verdict.json' + if tooling.prepare_verdict(scope['repository'], scope['number'], scope['head'], verdict) != 0: + raise ValueError('model preparation failed') + require_prepared_envelope(verdict, scope) + fence() + # No write grant can be requested until preparation and live revalidation. + envs['publish'] = issue('publish') + fence() + require_prepared_envelope(verdict, scope) + return tooling.publish_verdict(scope['repository'], scope['number'], scope['head'], verdict) + + +def produce_issuance(phase, *, scope, installation_id, app_id, output, + private_parent, request, revalidate): + """Request exact phase grant and exclusively store the unchanged response. + + Authenticated request supplies original response bytes from the same App + transport. The caller must independently bind that collaborator, namespace, + and installations; structural checks alone do not establish issuer authority. + Publish revalidation must include the prepared envelope and live head/base. + No inference credential is consumed here or attached to an issuer request. + """ + if phase not in ('prepare', 'publish'): + raise ValueError('issuer phase refused') + if any(type(value) is not int or value < 1 for value in (installation_id, app_id)): + raise ValueError('issuer identity refused') + repository = scope['repository'] + # Validate repository grammar with the same canonical controller contract. + import re + if not isinstance(repository, str) or not re.fullmatch( + r'ContextualWisdomLab/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+', repository): + raise ValueError('issuer repository refused') + if scope['reviewer'] != 'cwl-noema-review[bot]': + raise ValueError('issuer actor refused') + parent, target = Path(private_parent), Path(output) + if not parent.is_absolute() or not target.is_absolute() or target.parent != parent: + raise ValueError('issuer output namespace refused') + for ancestor in (parent, *parent.parents): + if ancestor.is_symlink(): + raise ValueError('issuer output namespace refused') + info = parent.lstat() + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077: + raise ValueError('issuer output namespace refused') + if os.path.lexists(target): + raise ValueError('issuer output already exists') + revalidate() + app = _response(request('GET', '/app', None)) + if type(app.get('id')) is not int or app.get('id') != app_id or app.get('slug') != 'cwl-noema-review': + raise ValueError('issuer App identity refused') + revalidate() + installation = _response(request('GET', '/repos/' + repository + '/installation', None)) + if (type(installation.get('id')) is not int or type(installation.get('app_id')) is not int + or installation.get('id') != installation_id or installation.get('app_id') != app_id): + raise ValueError('issuer installation refused') + revalidate() + permissions = {'contents': 'read', 'metadata': 'read', + 'pull_requests': 'read' if phase == 'prepare' else 'write'} + raw = request('POST', f'/app/installations/{installation_id}/access_tokens', + {'repositories': [repository.split('/', 1)[1]], 'permissions': permissions}) + response = _response(raw) + if response.get('permissions') != permissions: + raise ValueError('issuer phase grant refused') + repositories = response.get('repositories') + if (not isinstance(repositories, list) or len(repositories) != 1 + or not isinstance(repositories[0], dict) + or repositories[0].get('full_name') != repository): + raise ValueError('issuer response repository refused') + if not isinstance(response.get('token'), str) or not response['token'].strip(): + raise ValueError('issuer token missing') + revalidate() + current = parent.lstat() + if (current.st_dev, current.st_ino, current.st_mode, current.st_uid) != ( + info.st_dev, info.st_ino, info.st_mode, info.st_uid): + raise ValueError('issuer output custody changed') + fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + try: + with os.fdopen(fd, 'wb') as stream: + stream.write(raw) + stream.flush() + os.fsync(stream.fileno()) + except BaseException: + # Retain incomplete private bytes for the existing custody owner; + # never overwrite/retry a secret-bearing output after ambiguous I/O. + raise + return target + + +def _private_bytes(path, parent): + """Read one bounded private single-link credential input without diagnostics.""" + path, parent = Path(path), Path(parent) + if not path.is_absolute() or path.parent != parent or parent.is_symlink(): + raise ValueError('private credential namespace refused') + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW) + try: + info = os.fstat(fd) + if (not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 + or info.st_uid != os.getuid() or info.st_mode & 0o077): + raise ValueError('private credential custody refused') + raw = os.read(fd, MAX_RESPONSE_BYTES + 1) + if not 0 < len(raw) <= MAX_RESPONSE_BYTES: + raise ValueError('private credential size refused') + return raw + finally: + os.close(fd) + + +def authenticated_transport(jwt_file, private_parent, *, opener=None): + """Use the existing externally supplied App JWT on fixed GitHub routes only. + + The runtime owner provides and refreshes this private credential; this helper + neither signs a JWT nor reads an App private key. Injected openers are trusted + test collaborators, not PR inputs. No redirect or ambient proxy is admitted. + """ + import re + import urllib.request + jwt = _private_bytes(jwt_file, private_parent).decode('ascii') + if not jwt or any(character.isspace() for character in jwt): + raise ValueError('existing App authentication unavailable') + class NoRedirect(urllib.request.HTTPRedirectHandler): + """Reject credential forwarding before any redirected request dispatch.""" + def redirect_request(self, req, fp, code, msg, headers, newurl): + """Keep every response on the independently fixed issuer origin.""" + return None + if opener is None: + opener = urllib.request.build_opener( + urllib.request.ProxyHandler({}), NoRedirect()).open + def request(method, route, payload): + """Return unchanged bounded body bytes from an exact authenticated request.""" + get = method == 'GET' and (route == '/app' or re.fullmatch( + r'/repos/ContextualWisdomLab/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+/installation', route)) + post = method == 'POST' and re.fullmatch(r'/app/installations/[1-9][0-9]*/access_tokens', route) + if not (get or post) or (get and payload is not None): + raise ValueError('issuer endpoint refused') + body = json.dumps(payload, allow_nan=False).encode() if post else None + req = urllib.request.Request('https://api.github.com' + route, data=body, + method=method, headers={'Authorization': 'Bearer ' + jwt, + 'Accept': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', + 'Content-Type': 'application/json'}) + with opener(req, timeout=60) as response: + headers = getattr(response, 'headers', {}) + length = headers.get('Content-Length') + raw = response.read(MAX_RESPONSE_BYTES + 1) + if (not isinstance(raw, bytes) or not 0 < len(raw) <= MAX_RESPONSE_BYTES + or (length is not None and (not str(length).isdigit() or int(length) != len(raw)))): + raise ValueError('issuer response framing refused') + return raw + return request + + +def main(argv=None): + """Consume independently pinned bootstrap inputs, refusing absent admission.""" + import argparse + import hashlib + import re + parser = argparse.ArgumentParser(description=__doc__) + modes = parser.add_mutually_exclusive_group(required=True) + modes.add_argument('--materialize', action='store_true') + modes.add_argument('--issue-prepare', action='store_true') + modes.add_argument('--issue-publish', action='store_true') + modes.add_argument('--revalidate-publish', action='store_true') + modes.add_argument('--revalidate-prepare', action='store_true') + parser.add_argument('--private-parent', type=Path, required=True) + parser.add_argument('--source-root', type=Path, required=True) + parser.add_argument('--manifest', type=Path, required=True) + parser.add_argument('--accepted-digest', required=True) + parser.add_argument('--admission', type=Path, required=True) + parser.add_argument('--admission-digest', required=True) + parser.add_argument('--repo', required=True) + parser.add_argument('--pr-number', type=int, required=True) + parser.add_argument('--expected-head', required=True) + parser.add_argument('--app-jwt-file', type=Path) + parser.add_argument('--verdict-file', type=Path) + args = parser.parse_args(argv) + try: + if not all(re.fullmatch(r'[0-9a-f]{64}', value) for value in + (args.accepted_digest, args.admission_digest)): + raise ValueError('bootstrap independent pins unavailable') + raw = args.manifest.read_bytes() + admission_raw = args.admission.read_bytes() + if hashlib.sha256(admission_raw).hexdigest() != args.admission_digest: + raise ValueError('bootstrap admission pin refused') + admission = _response(admission_raw) + scope = admission['scope'] + if (scope['repository'], scope['number'], scope['head']) != ( + args.repo, args.pr_number, args.expected_head): + raise ValueError('bootstrap target refused') + if args.materialize: + materialize_tooling(args.source_root, args.private_parent / 'tooling', raw, + accepted_digest=args.accepted_digest, admission=admission, + private_parent=args.private_parent) + return 0 + from scripts.ci.premerge_review_tooling import ( + require_prepared_envelope, require_review_scope, load_noema_tooling, + ) + def fence(): + """Bind original pins, actual admitted tooling and fresh live metadata.""" + if (args.manifest.read_bytes() != raw + or args.admission.read_bytes() != admission_raw): + raise ValueError('bootstrap external pins changed') + tooling = args.private_parent / 'tooling' + _validate_runtime(admission, args.private_parent) + load_noema_tooling(tooling, raw, accepted_digest=args.accepted_digest, + interpreter=Path(sys.executable), phase_environment=lambda _: {}) + pr = json.loads(subprocess.run(['gh', 'api', f'repos/{args.repo}/pulls/{args.pr_number}'], + capture_output=True, text=True, check=True, timeout=60).stdout) + require_review_scope(scope, {'repository':pr['base']['repo']['full_name'], + 'number':pr['number'],'head':pr['head']['sha'],'base':pr['base']['sha'], + 'author':pr['user']['login'],'reviewer':scope['reviewer'], + 'state':str(pr['state']).upper(),'draft':pr['draft']}) + if (args.manifest.read_bytes() != raw + or args.admission.read_bytes() != admission_raw): + raise ValueError('bootstrap external pins changed during live read') + load_noema_tooling(tooling, raw, accepted_digest=args.accepted_digest, + interpreter=Path(sys.executable), phase_environment=lambda _: {}) + for field in ('isolated_clean_job', 'native_custody', 'supplier_cost', + 'supplier_retention', 'issuer_phase_separation'): + if admission.get(field) is not True: + raise ValueError('bootstrap execution evidence unavailable') + _validate_runtime(admission, args.private_parent) + if args.issue_publish or args.revalidate_publish: + if args.verdict_file != args.private_parent / 'verdict.json': + raise ValueError('bootstrap envelope namespace refused') + require_prepared_envelope(args.verdict_file, scope) + if args.revalidate_publish or args.revalidate_prepare: + fence() + return 0 + if args.app_jwt_file is None: + raise ValueError('authenticated issuer transport unavailable') + fence() + transport = authenticated_transport(args.app_jwt_file, args.private_parent) + phase = 'publish' if args.issue_publish else 'prepare' + produce_issuance(phase, scope=scope, installation_id=admission['installation_id'], + app_id=admission['app_id'], output=args.private_parent / (phase+'.json'), + private_parent=args.private_parent, request=transport, revalidate=fence) + return 0 + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError): + print('Noema bootstrap refused: independent issuer or custody input unavailable', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/ci/noema_existing_app_issuer.cjs b/scripts/ci/noema_existing_app_issuer.cjs new file mode 100644 index 0000000000..9e43907388 --- /dev/null +++ b/scripts/ci/noema_existing_app_issuer.cjs @@ -0,0 +1,255 @@ +/* Existing-App phase issuer. Only trusted workflow steps supply Secret inputs. + * No key/JWT/token is projected into stdout, argv, artifacts or child env. + * Callbacks below are trusted fixture seams, never PR-provided code. + */ +'use strict'; +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const https = require('node:https'); +const {spawnSync} = require('node:child_process'); +const LIMIT = 2 * 1024 * 1024; + +function signApp(clientId, pem, now) { + if (typeof clientId !== 'string' || !/^[A-Za-z0-9_.-]+$/.test(clientId) || + typeof pem !== 'string' || !Number.isSafeInteger(now)) throw new Error('signer refused'); + const key = crypto.createPrivateKey(pem); + if (key.asymmetricKeyType !== 'rsa' || key.asymmetricKeyDetails.modulusLength < 2048) + throw new Error('signer refused'); + const encode = value => Buffer.from(JSON.stringify(value)).toString('base64url'); + const body = encode({alg:'RS256',typ:'JWT'}) + '.' + encode({iat:now-60,exp:now+300,iss:clientId}); + return body + '.' + crypto.sign('RSA-SHA256',Buffer.from(body),key).toString('base64url'); +} + +function parse(raw, captureToken) { + if (!Buffer.isBuffer(raw) || !raw.length || raw.length > LIMIT) throw new Error('response refused'); + const text = new TextDecoder('utf-8',{fatal:true}).decode(raw); + const value = JSON.parse(text); + if (!value || Array.isArray(value) || typeof value !== 'object') throw new Error('response refused'); + // Native JSON.parse validates syntax but silently keeps the last duplicate. + // Walk that validated grammar to reject duplicates at every object depth. + let cursor=0,duplicate=false,duplicateToken=false; + function whitespace(){while(/\s/.test(text[cursor]||'') && cursor128)throw new Error('response refused');whitespace(); + const c=text[cursor]; + if(c==='"'){string();return;} + if(c==='{' || c==='['){ + cursor++;whitespace();const keys=new Set(),end=c==='{'?'}':']'; + if(text[cursor]===end){cursor++;return;} + for(;;){ + if(c==='{'){ + const key=string();if(keys.has(key)){duplicate=true;if(depth===0 && key==='token')duplicateToken=true;}keys.add(key); + whitespace();cursor++; // colon: syntax was already validated above + } + walk(depth+1);whitespace();const separator=text[cursor++]; + if(separator===end)break;whitespace(); + } + return; + } + const m=text.slice(cursor).match(/^(?:-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null)/); + if(!m)throw new Error('response refused');cursor+=m[0].length; + } + walk(0); + if(captureToken && !duplicateToken && typeof value.token==='string' && value.token.trim() && !/\s/.test(value.token)) + captureToken(value.token); + if(duplicate)throw new Error('response duplicate refused'); + return value; +} + +function transport(method, route, payload, credential) { + if (!['GET','POST','DELETE'].includes(method) || + !/^\/(app|repos\/ContextualWisdomLab\/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+\/installation|app\/installations\/[1-9][0-9]*\/access_tokens|installation\/token)$/.test(route)) + return Promise.reject(new Error('endpoint refused')); + return new Promise((resolve,reject) => { + const body = payload === null ? null : Buffer.from(JSON.stringify(payload)); + const req = https.request({hostname:'api.github.com',port:443,path:route,method, + headers:{Authorization:'Bearer '+credential,Accept:'application/vnd.github+json', + 'X-GitHub-Api-Version':'2022-11-28','User-Agent':'cwl-noema-issuer', + ...(body ? {'Content-Type':'application/json','Content-Length':body.length}: {})}}, res => { + const chunks=[];let size=0; + res.on('data',chunk=>{size+=chunk.length;if(size>LIMIT){res.destroy();reject(new Error('response refused'));}else chunks.push(chunk);}); + res.on('aborted',()=>reject(new Error('response refused'))); + res.on('error',()=>reject(new Error('response refused'))); + res.on('end',()=>{ + const length=res.headers['content-length']; + if (res.statusCode !== (method==='DELETE'?204:method==='POST'?201:200) || + !res.complete || (length!==undefined && (!/^[0-9]+$/.test(length) || Number(length)!==size))) + {const error=new Error('response refused');error.statusCode=res.statusCode;reject(error);} + else resolve(Buffer.concat(chunks)); + }); + }); + req.setTimeout(60000,()=>req.destroy(new Error('request refused'))); + req.on('error',()=>reject(new Error('request refused'))); + if(body)req.write(body);req.end(); + }); +} + +function privateParent(parent) { + if (!path.isAbsolute(parent) || path.resolve(parent)!==parent) throw new Error('custody refused'); + for(let p=parent;;p=path.dirname(p)){ + if(fs.lstatSync(p).isSymbolicLink())throw new Error('custody refused'); + if(path.dirname(p)===p)break; + } + const s=fs.lstatSync(parent); + if(!s.isDirectory() || s.uid!==process.getuid() || (s.mode&0o077))throw new Error('custody refused'); + return s; +} + +async function runPhase(options) { + const {phase,scope,appId,installationId,privateParent:parent,clientId,privateKey, + fence,controller,transport:request=transport,now=()=>Math.floor(Date.now()/1000)}=options; + if (!['prepare','publish'].includes(phase) || !Number.isSafeInteger(appId) || appId<1 || + !Number.isSafeInteger(installationId) || installationId<1 || + !/^ContextualWisdomLab\/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+$/.test(scope.repository) || + scope.reviewer!=='cwl-noema-review[bot]')throw new Error('identity refused'); + const initial=privateParent(parent);const target=path.join(parent,phase+'.json'); + if(fs.existsSync(target) || fs.lstatSync(parent).ino!==initial.ino)throw new Error('delivery refused'); + const tokens=[];const expiries=new Map();let delivered=false;let primary;let result;let cleanupFailed=false; + async function issue(grant, readToken) { + // Short-lived App authentication is created inside each issuer phase, not + // supplied by an absent external JWT producer or retained across the model. + const jwt=signApp(clientId,privateKey,now()); + const app=parse(await request('GET','/app',null,jwt)); + if(app.id!==appId || app.slug!=='cwl-noema-review')throw new Error('identity refused'); + if(readToken)await fence(readToken,phase); + const install=parse(await request('GET','/repos/'+scope.repository+'/installation',null,jwt)); + if(install.id!==installationId || install.app_id!==appId)throw new Error('identity refused'); + if(readToken)await fence(readToken,phase); + const permissions={contents:'read',metadata:'read',pull_requests:grant}; + const raw=await request('POST','/app/installations/'+installationId+'/access_tokens', + {repositories:[scope.repository.split('/')[1]],permissions},jwt); + const response=parse(raw, token => tokens.push(token)); + // Capture a safely parsed token before checking grant/expiry/delivery so + // rejected issuance is still revocation-owned. Never use it for fallback. + if(!tokens.includes(response.token))throw new Error('token refused'); + if(Object.keys(response.permissions||{}).length!==3 || + Object.entries(permissions).some(([k,v])=>response.permissions[k]!==v) || + !Array.isArray(response.repositories) || response.repositories.length!==1 || + response.repositories[0].full_name!==scope.repository)throw new Error('grant refused'); + const expires=Date.parse(response.expires_at); + if(!Number.isFinite(expires) || expires<=now()*1000+60000)throw new Error('expiry refused'); + expiries.set(response.token,expires); + return {raw,token:response.token}; + } + try { + const read=await issue('read'); + await fence(read.token,phase); + let selected=read; + if(phase==='publish'){ + selected=await issue('write',read.token); + await fence(read.token,phase); + } + const current=privateParent(parent); + if(current.dev!==initial.dev || current.ino!==initial.ino || current.mode!==initial.mode) + throw new Error('custody refused'); + const fd=fs.openSync(target,fs.constants.O_WRONLY|fs.constants.O_CREAT|fs.constants.O_EXCL|fs.constants.O_NOFOLLOW,0o600); + delivered=true; + try{fs.writeFileSync(fd,selected.raw);fs.fsyncSync(fd);}finally{fs.closeSync(fd);} + const env={PATH:'/usr/bin:/bin',GH_TOKEN:read.token}; + // The controller's live metadata read uses the read grant; its publisher + // child receives the separately issued write grant from the original file. + result=await controller(target,env); + if(result!==0)throw new Error('controller refused'); + } catch(error){primary=error;} + finally { + for(const token of tokens){ + try{await request('DELETE','/installation/token',null,token);} + catch(error){ + if(!(error.statusCode===401 && expiries.has(token) && expiries.get(token)<=now()*1000))cleanupFailed=true; + } + } + if(delivered){try{fs.unlinkSync(target);}catch{cleanupFailed=true;}} + } + if(cleanupFailed)throw new Error('issuer cleanup unconfirmed'); + if(primary)throw primary; + return result; +} + +module.exports={signApp,runPhase,transport}; + +async function main(argv=process.argv.slice(2), environment=process.env) { + const values={}; + const names=['phase','private-parent','source-root','manifest','accepted-digest','admission', + 'admission-digest','repo','pr-number','expected-head','verdict-file']; + if(argv.length%2)throw new Error('arguments refused'); + for(let i=0;i!values[n]) || !['prepare','publish'].includes(values.phase) || + !/^[1-9][0-9]*$/.test(values['pr-number']) || !/^[a-f0-9]{40}$/.test(values['expected-head'])) + throw new Error('arguments refused'); + const phase=values.phase,parent=values['private-parent'];privateParent(parent); + const admissionRaw=fs.readFileSync(values.admission); + if(!/^[a-f0-9]{64}$/.test(values['admission-digest']) || + crypto.createHash('sha256').update(admissionRaw).digest('hex')!==values['admission-digest']) + throw new Error('admission refused'); + const admission=parse(admissionRaw),scope=admission.scope; + if(scope.repository!==values.repo || scope.number!==Number(values['pr-number']) || + scope.head!==values['expected-head'] || values['verdict-file']!==path.join(parent,'verdict.json')) + throw new Error('scope refused'); + const python=admission.interpreter?.path; + if(!python || !path.isAbsolute(python) || + !/^[a-f0-9]{64}$/.test(admission.interpreter.sha256) || + crypto.createHash('sha256').update(fs.readFileSync(python)).digest('hex')!==admission.interpreter.sha256) + throw new Error('interpreter refused'); + const pythonIdentity=fs.statSync(python); + const pythonRealpath=fs.realpathSync(python); + const pinnedNode=admission.dependencies?.[fs.realpathSync(process.execPath)]; + if(!pinnedNode || crypto.createHash('sha256').update(fs.readFileSync(process.execPath)).digest('hex')!==pinnedNode) + throw new Error('issuer interpreter refused'); + const root=path.resolve(__dirname,'../..'); + const common=['--private-parent',parent,'--source-root',values['source-root'], + '--manifest',values.manifest,'--accepted-digest',values['accepted-digest'], + '--admission',values.admission,'--admission-digest',values['admission-digest'], + '--repo',values.repo,'--pr-number',values['pr-number'],'--expected-head',values['expected-head']]; + function child(script,args,env){ + // Revalidate after every issuer wait, before either credential-bearing child. + // This is not atomic pathname execution; exclusive runtime custody remains required. + const current=fs.statSync(python); + if(!current.isFile() || fs.realpathSync(python)!==pythonRealpath || + ['dev','ino','mode','uid','gid'].some(key=>current[key]!==pythonIdentity[key]) || + crypto.createHash('sha256').update(fs.readFileSync(python)).digest('hex')!==admission.interpreter.sha256) + throw new Error('interpreter refused'); + const result=spawnSync(python,['-I','-B',path.join(root,'scripts/ci',script),...args], + {env,cwd:root,stdio:'ignore'}); + if(result.error || result.signal || result.status===null)throw new Error('child refused'); + return result.status; + } + const runtimePath=environment.PATH; + if(!runtimePath)throw new Error('runtime refused'); + const fence=async(token,requested)=>{ + const mode=requested==='publish'?'--revalidate-publish':'--revalidate-prepare'; + const args=[mode,...common]; + if(requested==='publish')args.push('--verdict-file',values['verdict-file']); + if(child('noema_bootstrap_producer.py',args,{PATH:runtimePath,GH_TOKEN:token})!==0) + throw new Error('scope refused'); + }; + const controller=async(file,metadataEnvironment)=>{ + // Do not inherit Secret/JWT, workflow write token or inference into publisher. + const env={PATH:runtimePath,GH_TOKEN:metadataEnvironment.GH_TOKEN}; + if(phase==='prepare'){ + if(!environment.LLM_GATEWAY_API_KEY)throw new Error('inference refused'); + env.LLM_GATEWAY_API_KEY=environment.LLM_GATEWAY_API_KEY; + } + const args=['--'+phase+'-production','--tooling-root',path.join(parent,'tooling'), + '--manifest',values.manifest,'--accepted-digest',values['accepted-digest'], + '--admission',values.admission,'--admission-digest',values['admission-digest'], + '--repo',values.repo,'--pr-number',values['pr-number'],'--expected-head',values['expected-head'], + '--verdict-file',values['verdict-file'],'--phase-issuance',file]; + return child('premerge_review_tooling.py',args,env); + }; + return runPhase({phase,scope,appId:admission.app_id,installationId:admission.installation_id, + privateParent:parent,clientId:environment.NOEMA_GITHUB_APP_CLIENT_ID, + privateKey:environment.NOEMA_GITHUB_APP_PRIVATE_KEY,fence,controller}); +} +module.exports.main=main; +if(require.main===module){ + main().then(status=>{process.exitCode=status;}).catch(()=>{ + // Fixed, credential-free disposition also covers unconfirmed revocation. + console.error('Noema existing-App issuer refused; issuance or cleanup unconfirmed');process.exitCode=1; + }); +} diff --git a/scripts/ci/noema_review_document.py b/scripts/ci/noema_review_document.py index 17d3ca603c..4594f5434b 100644 --- a/scripts/ci/noema_review_document.py +++ b/scripts/ci/noema_review_document.py @@ -158,7 +158,7 @@ def _extract_hwp_with_reviewed_reader(path: str, raw: bytes) -> str: handle.flush() try: completed = subprocess.run( - ["node", reader, source, handle.name], + [os.environ.get("NOEMA_NODE_EXECUTABLE") or "node", reader, source, handle.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 380ee22675..156f5aafa3 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -357,6 +357,7 @@ def graphql(query: str, **fields: str | int) -> dict[str, Any]: number title body + author { login } isDraft state headRefOid @@ -526,6 +527,18 @@ def current_actor() -> str: return "" +def require_distinct_pr_author(pr: dict[str, Any], actor: str) -> str: + """Require a known PR author distinct from the active reviewer actor.""" + author = pr.get("author") + login = author.get("login") if isinstance(author, dict) else None + if not isinstance(login, str) or not login.strip(): + raise RuntimeError("Noema pull request author identity could not be verified") + normalized = login.strip().casefold() + if not actor or actor.strip().casefold() == normalized: + raise RuntimeError("Noema reviewer actor must be distinct from the pull request author") + return normalized + + def fetch_diff(repo: str, number: int) -> tuple[str, bool]: """Fetch the PR diff and truncate it to the bounded LLM prompt size.""" diff = run(["gh", "api", f"repos/{repo}/pulls/{number}", "-H", "Accept: application/vnd.github.v3.diff"]) @@ -1606,7 +1619,7 @@ def call_llm( """ api_url = os.environ.get("NOEMA_LLM_API_URL", "").strip() api_key = os.environ.get("NOEMA_LLM_API_KEY", "").strip() - model = os.environ.get("NOEMA_LLM_MODEL", "").strip() or "orchestrator/free" + model = os.environ.get("NOEMA_LLM_MODEL", "").strip() or "orchestrator/auto" if not api_url or not api_key: raise RuntimeError( "Noema LLM review unavailable: NOEMA_LLM_API_URL or NOEMA_LLM_API_KEY is not configured." @@ -1719,12 +1732,15 @@ def call_llm( http_status: int | None = None retry_after_seconds: int | None = None if isinstance(exc, urllib.error.HTTPError): - active_phase = "response_error" - http_status = exc.code if type(exc.code) is int else None - retry_after_seconds = parse_http_retry_after_seconds(exc.headers) - gateway_telemetry = _extract_http_error_telemetry(exc) - model_value = gateway_telemetry.get("served_model") - served_model = model_value if isinstance(model_value, str) else None + try: + active_phase = "response_error" + http_status = exc.code if type(exc.code) is int else None + retry_after_seconds = parse_http_retry_after_seconds(exc.headers) + gateway_telemetry = _extract_http_error_telemetry(exc) + model_value = gateway_telemetry.get("served_model") + served_model = model_value if isinstance(model_value, str) else None + finally: + exc.close() elapsed = time.monotonic() - attempt_started current_failure = _stable_failure_diagnostic(exc) model_note = served_model or "unknown" @@ -1879,6 +1895,7 @@ def inspect_and_review(repo: str, number: int, expected_head: str) -> int: actor = current_actor() if not actor: raise RuntimeError("Noema reviewer identity could not be verified") + require_distinct_pr_author(pr, actor) if actor in PRIMARY_REVIEW_AUTHORS: raise RuntimeError( f"Current token actor {actor!r} is already a primary review actor; " @@ -1901,6 +1918,7 @@ def inspect_and_review(repo: str, number: int, expected_head: str) -> int: except RuntimeError: print("Pull request closed or its head changed during review; stale verdict was not published.") return 0 + require_distinct_pr_author(current_pr, actor) submit_review(repo, number, current_pr, actor, verdict) return 0 diff --git a/scripts/ci/noema_review_handoff.py b/scripts/ci/noema_review_handoff.py index a112e1e225..3f4a8a20a7 100644 --- a/scripts/ci/noema_review_handoff.py +++ b/scripts/ci/noema_review_handoff.py @@ -195,7 +195,7 @@ def dispatch_noema( *, runner: GhRunner = run_gh, ) -> None: - """Dispatch the target repository's default-branch Noema workflow.""" + """Dispatch the central handler while retaining the exact consumer identity.""" payload = { "event_type": "noema-review", "client_payload": { @@ -205,7 +205,7 @@ def dispatch_noema( }, } runner( - ["api", "-X", "POST", f"repos/{repo}/dispatches", "--input", "-"], + ["api", "-X", "POST", "repos/ContextualWisdomLab/.github/dispatches", "--input", "-"], json.dumps(payload), ) diff --git a/scripts/ci/opencode_queue_priority.py b/scripts/ci/opencode_queue_priority.py index 7024099b62..75ec11c58b 100755 --- a/scripts/ci/opencode_queue_priority.py +++ b/scripts/ci/opencode_queue_priority.py @@ -38,6 +38,8 @@ @dataclass(frozen=True) class QueuedRun: + """Identify one queued OpenCode review run.""" + run_id: int created_at: datetime repo: str @@ -47,6 +49,8 @@ class QueuedRun: @dataclass(frozen=True) class PrState: + """Describe the live pull request state used to classify a queued run.""" + state: str head: str priority: bool @@ -54,6 +58,8 @@ class PrState: @dataclass(frozen=True) class Plan: + """Partition ordered queued runs by priority and validity.""" + keep: tuple[QueuedRun, ...] cancel_current: tuple[QueuedRun, ...] cancel_stale: tuple[QueuedRun, ...] @@ -94,10 +100,14 @@ def metrics(p: Plan, *, now: datetime) -> dict: def _gh(*args: str, stdin: str | None = None) -> str: + """Run a GitHub CLI command and return its standard output.""" + return subprocess.run(["gh", *args], input=stdin, capture_output=True, text=True, check=True).stdout def fetch_queued() -> list[QueuedRun]: + """Return queued dispatch runs with parseable pull request identities.""" + out = _gh("api", "--paginate", f"repos/{CENTRAL}/actions/workflows/{WORKFLOW}/runs?status=queued&per_page=100", "--jq", ".workflow_runs[]|[.id,.created_at,.display_title]|@json") runs = [] @@ -111,6 +121,8 @@ def fetch_queued() -> list[QueuedRun]: def fetch_live(keys: set[tuple[str, int]], label: str) -> dict[tuple[str, int], PrState]: + """Fetch live pull request states and trusted priority labels for keys.""" + live: dict[tuple[str, int], PrState] = {} perms: dict[tuple[str, str], str] = {} keys_sorted = sorted(keys) @@ -144,6 +156,8 @@ def fetch_live(keys: set[tuple[str, int]], label: str) -> dict[tuple[str, int], def main(argv: list[str] | None = None) -> int: + """Print queue metrics and optionally record and cancel selected runs.""" + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("--label", default="review-priority") ap.add_argument("--include-current", action="store_true", help="also cancel non-priority current-head runs") diff --git a/scripts/ci/opencode_repository_dispatch_targets.json b/scripts/ci/opencode_repository_dispatch_targets.json index dd82dd1fd0..db995c9bae 100644 --- a/scripts/ci/opencode_repository_dispatch_targets.json +++ b/scripts/ci/opencode_repository_dispatch_targets.json @@ -51,6 +51,7 @@ "ContextualWisdomLab/saju-caldav", "ContextualWisdomLab/scopeweave", "ContextualWisdomLab/semantic-data-portal", + "ContextualWisdomLab/tokyo-travel-2026", "ContextualWisdomLab/wardnet", "ContextualWisdomLab/xtrm-lead-pi-outbound", "ContextualWisdomLab/xtrmLLMBatchPython" diff --git a/scripts/ci/opencode_review_receipt_gate.py b/scripts/ci/opencode_review_receipt_gate.py index 4dcb24af88..5c86b1fc8e 100644 --- a/scripts/ci/opencode_review_receipt_gate.py +++ b/scripts/ci/opencode_review_receipt_gate.py @@ -149,6 +149,7 @@ def evaluate_receipts( if not SHA_RE.fullmatch(head_sha): return None, "receipt gate requires a 40-character head SHA" stale_hits = 0 + dismissed_publishers: set[str] = set() for review in reversed(list(reviews)): if not isinstance(review, Mapping): continue @@ -156,6 +157,17 @@ def evaluate_receipts( if commit and commit.lower() != head_sha.lower(): stale_hits += 1 continue + author = review_author(review) + if author in dismissed_publishers: + continue + if ( + author in FORMAL_AUTHORS + and str(review.get("state") or "").upper() == "DISMISSED" + and review_matches_head(review, head_sha) + and review.get("id") + ): + dismissed_publishers.add(author) + continue ok, reason = is_formal_receipt(review, head_sha, is_draft=is_draft) if ok: return review, reason @@ -166,6 +178,8 @@ def evaluate_receipts( if reason.startswith("stale"): stale_hits += 1 continue + if dismissed_publishers: + return None, "current-head OpenCode publisher review was dismissed" if stale_hits: return ( None, diff --git a/scripts/ci/personal_review_route.py b/scripts/ci/personal_review_route.py new file mode 100644 index 0000000000..b417285813 --- /dev/null +++ b/scripts/ci/personal_review_route.py @@ -0,0 +1,53 @@ +"""Trusted personal LiteLLM route; never infer cost/privacy from auto or HTTP200.""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +BASE_URL = 'https://litellm.poinnetworks.net/v1' +MODEL = 'personal-litellm/auto' + + +def require_admission(*, zero_cost: bool, zero_retention: bool, private_target: bool) -> None: + """Reject missing supplier evidence; existing key possession is not policy.""" + if zero_cost is not True: + raise RuntimeError('STOP: personal auto zero-cost supplier is not attested') + if private_target and zero_retention is not True: + raise RuntimeError('STOP: private personal auto zero-retention supplier is not attested') + + +def configure_opencode(path: Path) -> None: + """Reset only provider/model routing, preserving read-only agent permissions.""" + source = path.read_text(encoding='utf-8') + # Generated workflow configs are JSON; tracked JSONC uses full-line comments. + data = json.loads('\n'.join(line for line in source.splitlines() + if not line.lstrip().startswith('//'))) + data.update(model=MODEL, small_model=MODEL, enabled_providers=['personal-litellm']) + data['provider'] = {'personal-litellm': { + 'npm': '@ai-sdk/openai-compatible', 'name': 'Personal LiteLLM auto', + 'options': {'baseURL': BASE_URL, 'apiKey': '{env:LLM_GATEWAY_API_KEY}'}, + 'models': {'auto': {'name': 'Auto', 'tool_call': True, + 'limit': {'context': 200000, 'output': 32768}}}, + }} + path.write_text(json.dumps(data, indent=2) + '\n', encoding='utf-8') + + +def main(argv: list[str] | None = None) -> int: + """Validate trusted nonsecret admission inputs and optionally reset config.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--zero-cost', choices=('true', 'false'), required=True) + parser.add_argument('--zero-retention', choices=('true', 'false'), required=True) + parser.add_argument('--private-target', choices=('true', 'false'), required=True) + parser.add_argument('--opencode-config', type=Path) + args = parser.parse_args(argv) + require_admission(zero_cost=args.zero_cost == 'true', + zero_retention=args.zero_retention == 'true', + private_target=args.private_target == 'true') + if args.opencode_config is not None: + configure_opencode(args.opencode_config) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/ci/pingora_edge_policy.py b/scripts/ci/pingora_edge_policy.py index 5c1e39d9e3..4c9697f1d7 100644 --- a/scripts/ci/pingora_edge_policy.py +++ b/scripts/ci/pingora_edge_policy.py @@ -467,11 +467,17 @@ def _github_open_json(url: str, token: str) -> object: with github_opener.open(request, timeout=30) as response: payload = response.read(MAX_RESPONSE_BYTES + 1) except (HTTPError, URLError, TimeoutError) as exc: - if isinstance(exc, HTTPError) and exc.code == 404: - raise ArtifactDeclarationNotFoundError( - f"GitHub API reported no resource for policy evidence at {url}" + try: + if isinstance(exc, HTTPError) and exc.code == 404: + raise ArtifactDeclarationNotFoundError( + f"GitHub API reported no resource for policy evidence at {url}" + ) from exc + raise PolicyError( + f"GitHub API request failed for policy evidence: {type(exc).__name__}" ) from exc - raise PolicyError(f"GitHub API request failed for policy evidence: {type(exc).__name__}") from exc + finally: + if isinstance(exc, HTTPError): + exc.close() if len(payload) > MAX_RESPONSE_BYTES: raise PolicyError("GitHub API policy response exceeded the bounded response size") try: @@ -497,7 +503,13 @@ def _github_open_raw_bytes(url: str, token: str, max_bytes: int) -> bytes: with github_opener.open(request, timeout=30) as response: raw = response.read(max_bytes + 1) except (HTTPError, URLError, TimeoutError) as exc: - raise PolicyError(f"GitHub raw blob request failed: {type(exc).__name__}") from exc + try: + raise PolicyError( + f"GitHub raw blob request failed: {type(exc).__name__}" + ) from exc + finally: + if isinstance(exc, HTTPError): + exc.close() if len(raw) > max_bytes: raise PolicyError("GitHub raw blob exceeded the bounded response size") return raw @@ -614,8 +626,6 @@ def _load_raw_file_bytes( return raw if encoding == "none": raise PolicyError(f"GitHub content evidence for {path} has no inline content and no verifiable oversized size") - if encoding != "base64": - raise PolicyError(f"GitHub content evidence for {path} is not a regular base64 file") encoded = payload.get("content") if not isinstance(encoded, str): raise PolicyError(f"GitHub content evidence for {path} has a malformed size or content field") diff --git a/scripts/ci/place_maturin_extension.py b/scripts/ci/place_maturin_extension.py index 45ff27aba5..c6fd690216 100755 --- a/scripts/ci/place_maturin_extension.py +++ b/scripts/ci/place_maturin_extension.py @@ -19,10 +19,14 @@ def _inside(path: pathlib.Path, root: pathlib.Path) -> bool: + """Return whether path is root or is contained by root.""" + return path == root or root in path.parents def place(wheel: pathlib.Path, project_dir: pathlib.Path) -> list[pathlib.Path]: + """Copy wheel extensions into existing project package directories.""" + project = project_dir.resolve() pyproject = tomllib.loads((project / "pyproject.toml").read_text(encoding="utf-8")) python_source = pyproject.get("tool", {}).get("maturin", {}).get("python-source", ".") @@ -48,6 +52,8 @@ def place(wheel: pathlib.Path, project_dir: pathlib.Path) -> list[pathlib.Path]: def main(argv: list[str]) -> int: + """Run extension placement and return its process status.""" + if len(argv) != 2: print("usage: place_maturin_extension.py WHEEL PROJECT_DIR", file=sys.stderr) return 2 diff --git a/scripts/ci/pr_review_merge_scheduler_core.py b/scripts/ci/pr_review_merge_scheduler_core.py index 5a86bd24c8..b2990236f1 100644 --- a/scripts/ci/pr_review_merge_scheduler_core.py +++ b/scripts/ci/pr_review_merge_scheduler_core.py @@ -2249,28 +2249,78 @@ def has_current_head_approval(pr: dict[str, Any]) -> bool: return current_head_review_state(pr, "APPROVED") +def is_substantive_noema_app_approval(review: dict[str, Any], head: str) -> bool: + """Recognize only the known second-review App's actual publisher contract. + + API author and commit identity remain authoritative. Body markers alone + cannot authorize an arbitrary bot, a personal-token fallback or OpenCode. + """ + if ( + review_author_login(review) not in {"cwl-noema-review", "cwl-noema-review[bot]"} + or not is_bot_review_author(review) + or (review.get("commit") or {}).get("oid") != head + or not re.fullmatch(r"[0-9a-f]{40}", head) + ): + return False + body = str(review.get("body") or "") + footer_marker = "" + if body.count(footer_marker) != 1: + return False + evidence, footer = body.split(footer_marker, 1) + expected_footers = { + ( + f"\n- Result: APPROVE\n- Head SHA: `{head}`\n" + f"- Reviewer credential: `{source}`\n" + "- Actor: `cwl-noema-review[bot]`\n\n" + f"" + ) + for source in ("noema-review-github-app", "noema-review-github-app-refresh") + } + if footer not in expected_footers: + return False + return bool( + evidence.startswith("## Noema LLM review\n") + and re.search( + r"### Reviewed changed lines\n- `[^`\n]+:[1-9][0-9]* \((?:LEFT|RIGHT)\)`: [^\s][^\n]*", + evidence, + ) + and re.search( + r"### Adversarial validation\n- `[^`\n]+:[1-9][0-9]* \((?:LEFT|RIGHT)\)` " + r"falsified: [^\s][^\n]* — [^\s][^\n]*", + evidence, + ) + and re.search(r"(?m)^- Residual risk: [^\s][^\n]*$", evidence) + ) + + def has_independent_current_head_approval(pr: dict[str, Any]) -> bool: - """Return whether an eligible reviewer's latest exact-head policy state approves.""" - author = ((pr.get("author") or {}).get("login") or "").lower() + """Require a non-author human or trusted, substantive second-review App.""" + author = ((pr.get("author") or {}).get("login") or "").lower().removesuffix("[bot]") if not author: return False seen_reviewers: set[str] = set() for review in reversed((pr.get("reviews") or {}).get("nodes") or []): - reviewer = review_author_login(review) + reviewer = review_author_login(review).removesuffix("[bot]") state = (review.get("state") or "").upper() if ( not reviewer or reviewer == author or is_automated_opencode_review(review) or reviewer == "github-actions" - or is_bot_review_author(review) or not review_matches_current_head(review, pr) or state not in {"APPROVED", "CHANGES_REQUESTED", "DISMISSED"} or reviewer in seen_reviewers ): continue + # Even a later malformed approval must retire that actor's older one. seen_reviewers.add(reviewer) - if state == "APPROVED": + if state == "APPROVED" and ( + ( + not is_bot_review_author(review) + and reviewer != "cwl-noema-review" + ) + or is_substantive_noema_app_approval(review, str(pr.get("headRefOid") or "")) + ): return True return False diff --git a/scripts/ci/premerge_review_tooling.py b/scripts/ci/premerge_review_tooling.py new file mode 100644 index 0000000000..0961f15652 --- /dev/null +++ b/scripts/ci/premerge_review_tooling.py @@ -0,0 +1,510 @@ +"""Admit independently pinned reviewer source before loading any candidate code. + +This source-only boundary does not issue credentials or admit a runner. Its +caller must obtain accepted_digest independently of PR data and supply the +complete regular-file inventory from the selected immutable Git tree. Merely +hashing a caller's own manifest is not independent source acceptance. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import stat +import re +import tempfile +import subprocess +from pathlib import Path +from collections.abc import Mapping + + +def _unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]: + """Reject duplicate keys before any manifest interpretation.""" + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + raise ValueError("manifest contains duplicate fields") + result[key] = value + return result + + +def _safe_path(value: object) -> bool: + """Accept canonical relative inventory paths without traversal or controls.""" + return ( + isinstance(value, str) + and bool(value) + and "\\" not in value + and not any(ord(character) < 32 for character in value) + and all(part not in {"", ".", ".."} for part in value.split("/")) + ) + + +def admit_tooling( + raw_manifest: bytes, *, accepted_digest: str, source_files: Mapping[str, bytes] +) -> None: + """Reject a manifest that does not match independently accepted bytes.""" + if not isinstance(raw_manifest, bytes) or not 0 < len(raw_manifest) <= 2 * 1024 * 1024: + raise ValueError("manifest size is outside the bounded contract") + if hashlib.sha256(raw_manifest).hexdigest() != accepted_digest: + raise ValueError("tooling does not match the accepted manifest digest") + try: + manifest = json.loads(raw_manifest, object_pairs_hook=_unique_object) + except (UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError("manifest JSON is malformed") from exc + keys = {"schema_version", "repository", "commit", "tree", "entrypoint", "files"} + if not isinstance(manifest, dict) or set(manifest) != keys: + raise ValueError("manifest fields do not match the trusted schema") + if type(manifest["schema_version"]) is not int or manifest["schema_version"] != 1: + raise ValueError("manifest schema version is unsupported") + if manifest["repository"] != "ContextualWisdomLab/.github": + raise ValueError("manifest tooling repository is untrusted") + for field in ("commit", "tree"): + if not isinstance(manifest[field], str) or not re.fullmatch(r"[0-9a-f]{40}", manifest[field]): + raise ValueError("manifest requires immutable commit and tree identities") + inventory = manifest["files"] + if not isinstance(inventory, dict) or not inventory: + raise ValueError("manifest source inventory must be nonempty") + for name, digest in inventory.items(): + if not _safe_path(name) or not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest): + raise ValueError("manifest source inventory contains invalid paths or digests") + if not _safe_path(manifest["entrypoint"]) or manifest["entrypoint"] not in inventory: + raise ValueError("manifest entrypoint is not in the accepted source inventory") + actual = {name: hashlib.sha256(data).hexdigest() for name, data in source_files.items()} + if manifest["files"] != actual: + raise ValueError("tooling source inventory differs from the accepted manifest") + + +def require_review_scope(expected: Mapping[str, object], live: Mapping[str, object]) -> None: + """Refuse changed PR scope before model preparation or App publication.""" + fields = {"repository", "number", "head", "base", "author", "reviewer"} + if set(expected) != fields or set(live) != fields | {"state", "draft"}: + raise ValueError("review scope fields are incomplete or unexpected") + if type(live["number"]) is not int or type(expected["number"]) is not int or expected["number"] < 1: + raise ValueError("review scope PR number is invalid") + repository = expected["repository"] + if not isinstance(repository, str) or not re.fullmatch( + r"ContextualWisdomLab/(?!.*(?:\.\.|\.$))[A-Za-z0-9_.-]+", repository + ): + raise ValueError("review scope repository is outside the organization") + for field in ("head", "base"): + if not isinstance(expected[field], str) or not re.fullmatch(r"[0-9a-f]{40}", expected[field]): + raise ValueError("review scope requires immutable head and base") + for field in ("author", "reviewer"): + if not isinstance(expected[field], str) or not expected[field].strip(): + raise ValueError("review scope identity is missing") + if expected["author"].strip().casefold() == expected["reviewer"].strip().casefold(): + raise ValueError("review scope requires an independent reviewer") + if live["state"] != "OPEN" or live["draft"] is not False: + raise ValueError("review scope is closed or draft") + if any(expected[field] != live[field] for field in fields): + raise ValueError("review scope changed after admission") + + +def require_prepared_envelope(path: Path, scope: Mapping[str, object]) -> None: + """Validate compatible eight-field model output before publisher credentials. + + This is same-invocation runner-local custody, not cross-run authenticity. + The independently admitted tooling is responsible for substantive model + validation; no externally supplied envelope is accepted by run_review. + """ + try: + with os.fdopen(os.open(path, os.O_RDONLY | os.O_NOFOLLOW), "rb") as stream: + info = os.fstat(stream.fileno()) + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_mode & 0o077: + raise ValueError("prepared envelope is not a private single-link regular file") + raw = stream.read(2 * 1024 * 1024 + 1) + if not 0 < len(raw) <= 2 * 1024 * 1024: + raise ValueError("prepared envelope exceeds its bounded size") + payload = json.loads(raw, object_pairs_hook=_unique_object) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise ValueError("prepared envelope is unavailable or malformed") from exc + keys = {"schema_version", "repository", "pull_request_number", "expected_head", + "expected_base", "reviewer_actor", "pull_request_author", "verdict"} + if not isinstance(payload, dict) or set(payload) != keys: + raise ValueError("prepared envelope does not match compatible producer schema") + if type(payload["schema_version"]) is not int or payload["schema_version"] != 1: + raise ValueError("prepared envelope schema version is invalid") + bound = {"repository": "repository", "number": "pull_request_number", + "head": "expected_head", "base": "expected_base", + "author": "pull_request_author", "reviewer": "reviewer_actor"} + if any(payload[key] != scope[field] for field, key in bound.items()): + raise ValueError("prepared envelope identity differs from admitted scope") + verdict = payload["verdict"] + if not isinstance(verdict, dict) or verdict.get("decision") not in {"approve", "request_changes", "comment"}: + raise ValueError("prepared envelope is missing a model verdict") + + +def require_phase_environment(phase: str, environment: Mapping[str, str]) -> None: + """Reject loader overrides and inference capability in publisher children. + + Metadata alone cannot distinguish read/write GitHub tokens; exact token + scope must still be verified by the trusted runtime admission adapter. + Never include values in refusal diagnostics. + """ + forbidden = {"PYTHONPATH", "PYTHONHOME", "LD_PRELOAD", "LD_LIBRARY_PATH", + "DYLD_INSERT_LIBRARIES", "BASH_ENV", "ENV"} + if any(key in environment for key in forbidden): + raise ValueError("phase environment contains executable-loader overrides") + if phase == "publish": + if any(key in environment for key in ("NOEMA_LLM_API_KEY", "LLM_GATEWAY_API_KEY")): + raise ValueError("phase environment publisher contains inference capability") + elif phase == "prepare": + if environment.get("NOEMA_LLM_MODEL") != "auto": + raise ValueError("phase environment must request literal auto") + else: + raise ValueError("phase environment has an unsupported phase") + + +def accepted_prepare_transport(context): + """Validate only independently pinned workflow/document transport capabilities. + + The enclosing execution admission digest binds these fields, not ambient + environment values. Runtime admission still owns their namespace and supplier. + """ + if context is None: + return {} + if not isinstance(context, dict) or not context or set(context) - { + 'GITHUB_OUTPUT', 'NOEMA_TRANSPORT_RETRY_ATTEMPT', + 'NOEMA_HWP_MCP_SOURCE', 'NOEMA_NODE_EXECUTABLE', 'node_sha256', 'hwp_files', + }: + raise ValueError('prepare transport has unsupported fields') + for field in ('GITHUB_OUTPUT', 'NOEMA_HWP_MCP_SOURCE'): + if field in context: + value = context[field] + if (not isinstance(value, str) or value != value.strip() + or any(ord(character) < 32 or ord(character) == 127 for character in value)): + raise ValueError('prepare transport path differs from consumer identity') + environment = {} + if 'GITHUB_OUTPUT' in context or 'NOEMA_TRANSPORT_RETRY_ATTEMPT' in context: + attempt = context.get('NOEMA_TRANSPORT_RETRY_ATTEMPT') + if not isinstance(attempt, str) or not re.fullmatch(r'[0-2]', attempt): + raise ValueError('prepare retry context is outside the existing bound') + output = context.get('GITHUB_OUTPUT') + if not isinstance(output, str) or not Path(output).is_absolute(): + raise ValueError('prepare output requires an accepted absolute path') + info = Path(output).lstat() + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_mode & 0o077: + raise ValueError('prepare output is not private regular custody') + environment.update(GITHUB_OUTPUT=output, NOEMA_TRANSPORT_RETRY_ATTEMPT=attempt) + document_keys = {'NOEMA_HWP_MCP_SOURCE', 'NOEMA_NODE_EXECUTABLE', 'node_sha256', 'hwp_files'} + if document_keys & set(context): + if not document_keys <= set(context): + raise ValueError('prepare document transport is incomplete') + source, node = context['NOEMA_HWP_MCP_SOURCE'], context['NOEMA_NODE_EXECUTABLE'] + if not all(isinstance(value, str) and Path(value).is_absolute() for value in (source, node)): + raise ValueError('prepare document supplier paths are not absolute') + root, executable = Path(source), Path(node) + if root.is_symlink() or not root.is_dir() or executable.is_symlink(): + raise ValueError('prepare document supplier is not regular custody') + info = executable.stat() + if not stat.S_ISREG(info.st_mode) or not info.st_mode & 0o111: + raise ValueError('prepare Node supplier is not executable') + digest = context['node_sha256'] + if not isinstance(digest, str) or not re.fullmatch(r'[0-9a-f]{64}', digest): + raise ValueError('prepare Node digest is missing') + if hashlib.sha256(executable.read_bytes()).hexdigest() != digest: + raise ValueError('prepare Node supplier bytes changed') + inventory = context['hwp_files'] + if not isinstance(inventory, dict) or not inventory or any( + not _safe_path(name) or not isinstance(value, str) or not re.fullmatch(r'[0-9a-f]{64}', value) + for name, value in inventory.items() + ): + raise ValueError('prepare HWP inventory is malformed') + actual = {} + for path in root.rglob('*'): + if path.is_symlink() or not (path.is_dir() or path.is_file()): + raise ValueError('prepare HWP supplier has unsupported custody') + if path.is_file(): + actual[path.relative_to(root).as_posix()] = hashlib.sha256(path.read_bytes()).hexdigest() + if actual != inventory: + raise ValueError('prepare HWP supplier bytes changed') + environment.update(NOEMA_HWP_MCP_SOURCE=source, NOEMA_NODE_EXECUTABLE=node) + return environment + + +def phase_environment_from_issuance( + phase, response, *, scope, reviewer_actor, installation_id, inference_key=None, + prepare_transport=None, +): + """Consume a trusted installation-token issuance response without logging it. + + The response must come directly from the existing authenticated App issuer, + not PR data or an artifact. The controller does not mint a token, read a + secret, or establish issuer authenticity by validating this dictionary. + """ + expected_permissions = {"contents": "read", "metadata": "read", + "pull_requests": "read" if phase == "prepare" else "write"} + if phase not in {"prepare", "publish"} or not isinstance(response, dict): + raise ValueError("credential issuance has unsupported phase or shape") + if response.get("permissions") != expected_permissions: + raise ValueError("credential issuance permissions do not match the phase") + repositories = response.get("repositories") + if not isinstance(repositories, list) or len(repositories) != 1: + raise ValueError("credential issuance must be single-repository scoped") + if not isinstance(repositories[0], dict) or repositories[0].get("full_name") != scope["repository"]: + raise ValueError("credential issuance repository does not match the review") + token = response.get("token") + if not isinstance(token, str) or not token.strip(): + raise ValueError("credential issuance returned no usable token") + if reviewer_actor != scope["reviewer"] or not re.fullmatch(r"[a-z0-9][a-z0-9-]*\[bot\]", reviewer_actor): + raise ValueError("credential issuance reviewer actor is not the admitted independent App") + if type(installation_id) is not int or installation_id < 1: + raise ValueError("credential issuance installation identity is invalid") + environment = {"PATH": "/usr/bin:/bin", "GH_TOKEN": token, + "NOEMA_REVIEW_ACTOR": reviewer_actor, + "NOEMA_REVIEW_INSTALLATION_ID": str(installation_id), + "NOEMA_REVIEW_TOKEN_SOURCE": "noema-review-github-app" if phase == "prepare" else "noema-review-github-app-refresh"} + if phase == "prepare": + if not isinstance(inference_key, str) or not inference_key.strip(): + raise ValueError("credential issuance preparation requires existing inference authority") + environment.update(NOEMA_LLM_MODEL="auto", NOEMA_LLM_API_KEY=inference_key, + NOEMA_LLM_API_URL="https://litellm.poinnetworks.net/v1/chat/completions") + environment.update(accepted_prepare_transport(prepare_transport)) + return environment + + +class _NoemaTooling: + """Execute one compatible producer/publisher from a sealed tooling namespace. + + The trusted caller owns the namespace exclusively through both phases and + supplies phase-scoped environments, including separately attested read or + write authority. This adapter does not authenticate an external envelope. + Interpreter and installed third-party dependencies need independent runtime + admission; the manifest binds repository files only. + """ + + def __init__(self, root, raw, accepted_digest, interpreter, phase_environment, child_runner): + """Retain explicit controller dependencies without loading candidate code.""" + self.root = Path(root) + self.raw = raw + self.accepted_digest = accepted_digest + self.interpreter = Path(interpreter) + self.phase_environment = phase_environment + self.child_runner = child_runner + + def _verify(self): + """Re-read complete regular source files before each child invocation.""" + if not self.root.is_absolute() or self.root.is_symlink() or not self.root.is_dir(): + raise ValueError("source inventory root is not an absolute owned directory") + files = {} + for path in self.root.rglob("*"): + if path.is_symlink(): + raise ValueError("source inventory contains a symbolic link") + if path.is_file(): + files[path.relative_to(self.root).as_posix()] = path.read_bytes() + elif not path.is_dir(): + raise ValueError("source inventory contains an unsupported file kind") + admit_tooling(self.raw, accepted_digest=self.accepted_digest, source_files=files) + manifest = json.loads(self.raw) + if manifest["entrypoint"] != ".github/actions/noema-review/two_phase.py": + raise ValueError("source inventory entrypoint is not the compatible Noema tooling") + return self.root / manifest["entrypoint"] + + def _run(self, phase, repo, number, head, path): + """Run a fresh isolated-import child with explicit phase environment only.""" + entrypoint = self._verify() + if not self.interpreter.is_absolute() or not self.interpreter.is_file(): + raise ValueError("trusted interpreter must be an absolute executable file") + environment = self.phase_environment(phase) + require_phase_environment(phase, environment) + # Credential acquisition can yield to a trusted issuer; bind source + # again afterwards so those credentials never reach stale tooling. + entrypoint = self._verify() + command = [str(self.interpreter), "-I", "-B", str(entrypoint), + "--repo", repo, "--pr-number", str(number), "--expected-head", head, + f"--{phase}-verdict-file", str(path)] + result = self.child_runner(command, cwd=self.root, env=environment, check=False) + return result.returncode + + def prepare_verdict(self, repo, number, head, path): + """Consume PR bytes as data through the admitted model producer.""" + return self._run("prepare", repo, number, head, path) + + def publish_verdict(self, repo, number, head, path): + """Publish only through the same compatible trusted tooling source.""" + return self._run("publish", repo, number, head, path) + + +def load_noema_tooling( + root, raw_manifest, *, accepted_digest, interpreter, phase_environment, + child_runner=subprocess.run, +): + """Bind the real two-phase CLI without importing caller-selected modules.""" + tooling = _NoemaTooling(root, raw_manifest, accepted_digest, interpreter, + phase_environment, child_runner) + tooling._verify() + return tooling + + +def run_review( + raw_manifest, *, accepted_digest, source_files, scope, fetch_live, + require_execution_admission, load_tooling, refresh_publisher, private_parent, +): + """Admit source and current scope before invoking trusted runtime collaborators. + + Callbacks are trusted controller dependencies, never PR payload. Their + authority, isolation, supplier policy and source binding require separate + operational acceptance; these checks do not manufacture such authority. + """ + admit_tooling(raw_manifest, accepted_digest=accepted_digest, source_files=source_files) + require_review_scope(scope, fetch_live()) + require_execution_admission() + tooling = load_tooling() + with tempfile.TemporaryDirectory(prefix="premerge-review-", dir=private_parent) as directory: + envelope = Path(directory) / "verdict.json" + result = tooling.prepare_verdict(scope["repository"], scope["number"], scope["head"], envelope) + if result != 0 or not envelope.is_file(): + raise RuntimeError("model preparation did not produce a verdict") + require_prepared_envelope(envelope, scope) + require_review_scope(scope, fetch_live()) + require_execution_admission() + refresh_publisher() + require_review_scope(scope, fetch_live()) + require_prepared_envelope(envelope, scope) + return tooling.publish_verdict(scope["repository"], scope["number"], scope["head"], envelope) + + +def production_main(argv=None): + """Validate independently pinned custody inputs and live scope before setup. + + Files must be supplied by the existing custody owner outside PR data. This + validates supplied evidence, not its authority: independent acceptance of + the digest is an explicit operational prerequisite, never generated here. + """ + import argparse + import sys + + parser = argparse.ArgumentParser(description=__doc__) + modes = parser.add_mutually_exclusive_group(required=True) + modes.add_argument('--admit-production', action='store_true') + modes.add_argument('--run-production', action='store_true') + modes.add_argument('--prepare-production', action='store_true') + modes.add_argument('--publish-production', action='store_true') + parser.add_argument('--phase-issuance', type=Path) + parser.add_argument('--verdict-file', type=Path) + parser.add_argument('--prepare-issuance', type=Path) + parser.add_argument('--publish-issuance', type=Path) + parser.add_argument('--private-parent', type=Path) + parser.add_argument('--tooling-root', type=Path, required=True) + parser.add_argument('--manifest', type=Path, required=True) + parser.add_argument('--accepted-digest', required=True) + parser.add_argument('--admission', type=Path, required=True) + parser.add_argument('--admission-digest', required=True) + parser.add_argument('--repo', required=True) + parser.add_argument('--pr-number', type=int, required=True) + parser.add_argument('--expected-head', required=True) + args = parser.parse_args(argv) + try: + if not all(re.fullmatch(r'[0-9a-f]{64}', value) for value in + (args.accepted_digest, args.admission_digest)): + raise ValueError('independent admission pins are missing or malformed') + raw = args.manifest.read_bytes() + tooling = _NoemaTooling(args.tooling_root, raw, args.accepted_digest, + Path(sys.executable), None, None) + tooling._verify() + admission_raw = args.admission.read_bytes() + if hashlib.sha256(admission_raw).hexdigest() != args.admission_digest: + raise ValueError('execution admission differs from independent pin') + admission = json.loads(admission_raw, object_pairs_hook=_unique_object) + scope = admission['scope'] + if scope['repository'] != args.repo or scope['number'] != args.pr_number or scope['head'] != args.expected_head: + raise ValueError('execution admission target differs from request') + for field in ('isolated_clean_job', 'native_custody', 'supplier_cost', 'supplier_retention', 'issuer_phase_separation'): + if admission.get(field) is not True: + raise ValueError('execution admission required evidence is not accepted') + fetched = subprocess.run(['gh', 'api', f'repos/{args.repo}/pulls/{args.pr_number}'], + check=True, capture_output=True, text=True, timeout=60) + pr = json.loads(fetched.stdout) + live = {'repository': pr['base']['repo']['full_name'], 'number': pr['number'], + 'head': pr['head']['sha'], 'base': pr['base']['sha'], + 'author': pr['user']['login'], 'reviewer': scope['reviewer'], + 'state': str(pr['state']).upper(), 'draft': pr['draft']} + require_review_scope(scope, live) + # Live metadata retrieval may yield while custody is revoked. Refuse + # stale admission before either phase obtains credentials or executes. + if args.admission.read_bytes() != admission_raw or args.manifest.read_bytes() != raw: + raise ValueError('independent custody changed during live scope retrieval') + if args.prepare_production or args.publish_production: + phase = 'prepare' if args.prepare_production else 'publish' + if args.phase_issuance is None or args.verdict_file is None: + raise ValueError('phase issuer output and private verdict custody required') + info = args.phase_issuance.lstat() + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_mode & 0o077: + raise ValueError('issuer output custody is not private') + response = json.loads(args.phase_issuance.read_bytes(), object_pairs_hook=_unique_object) + environment = phase_environment_from_issuance( + phase, response, scope=scope, reviewer_actor=scope['reviewer'], + installation_id=admission['installation_id'], + inference_key=os.environ.get('LLM_GATEWAY_API_KEY') if phase == 'prepare' else None, + prepare_transport=admission.get('prepare_transport') if phase == 'prepare' else None) + phase_tooling = load_noema_tooling(args.tooling_root, raw, + accepted_digest=args.accepted_digest, interpreter=Path(sys.executable), + phase_environment=lambda requested: environment if requested == phase else {}) + if phase == 'publish': + require_prepared_envelope(args.verdict_file, scope) + status = getattr(phase_tooling, phase + '_verdict')( + args.repo, args.pr_number, args.expected_head, args.verdict_file) + if phase == 'prepare' and status == 0 and args.verdict_file.exists(): + require_prepared_envelope(args.verdict_file, scope) + return status + if args.run_production: + if not args.prepare_issuance or not args.publish_issuance or not args.private_parent: + raise ValueError('phase issuance and private custody are required') + environments = {} + + def phase_environment(phase): + """Expose only the admitted credential for the selected phase.""" + return environments[phase] + + def read_issuance(path, phase): + """Consume private issuer output without logging credential bytes.""" + info = path.lstat() + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or info.st_mode & 0o077: + raise ValueError('issuer output is not private regular custody') + response = json.loads(path.read_bytes(), object_pairs_hook=_unique_object) + return phase_environment_from_issuance( + phase, response, scope=scope, reviewer_actor=scope['reviewer'], + installation_id=admission['installation_id'], + inference_key=os.environ.get('LLM_GATEWAY_API_KEY') if phase == 'prepare' else None, + prepare_transport=admission.get('prepare_transport') if phase == 'prepare' else None, + ) + + def revalidate_admission(): + """Refuse evidence or source drift at every controller boundary.""" + if args.admission.read_bytes() != admission_raw or args.manifest.read_bytes() != raw: + raise ValueError('independent custody changed after admission') + tooling._verify() + + def fetch_live(): + """Re-fetch current scope before each admitted phase.""" + current = subprocess.run(['gh', 'api', f'repos/{args.repo}/pulls/{args.pr_number}'], + check=True, capture_output=True, text=True, timeout=60) + document = json.loads(current.stdout) + return {'repository': document['base']['repo']['full_name'], 'number': document['number'], + 'head': document['head']['sha'], 'base': document['base']['sha'], + 'author': document['user']['login'], 'reviewer': scope['reviewer'], + 'state': str(document['state']).upper(), 'draft': document['draft']} + + def refresh_publisher(): + """Consume separately issued publication authority only after model completion.""" + environments['publish'] = read_issuance(args.publish_issuance, 'publish') + + environments['prepare'] = read_issuance(args.prepare_issuance, 'prepare') + files = {name: (args.tooling_root / name).read_bytes() for name in json.loads(raw)['files']} + return run_review(raw, accepted_digest=args.accepted_digest, source_files=files, + scope=scope, fetch_live=fetch_live, + require_execution_admission=revalidate_admission, + load_tooling=lambda: load_noema_tooling(args.tooling_root, raw, + accepted_digest=args.accepted_digest, interpreter=Path(sys.executable), + phase_environment=phase_environment), + refresh_publisher=refresh_publisher, private_parent=args.private_parent) + return 0 + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError): + # No credential values, response bodies or exception payloads in logs. + print('premerge admission refused: independent admission pins or custody/scope evidence unavailable', file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(production_main()) diff --git a/scripts/ci/prescreen_release_runtime_archives.py b/scripts/ci/prescreen_release_runtime_archives.py index 7631a38a06..7211b2acf8 100644 --- a/scripts/ci/prescreen_release_runtime_archives.py +++ b/scripts/ci/prescreen_release_runtime_archives.py @@ -109,6 +109,8 @@ def _build_packages(item: Mapping[str, Any], folder: Path) -> list[dict[str, Any raise gate.GateError(gate.CAPTURE_INCOMPLETE, f"{leg}: {name} metadata is ambiguous") metadata_root = PurePosixPath(metadata[0]).parent def read_file(path: str) -> bytes: + """Read one bounded package file from the captured build snapshot.""" + entry = members.get(f"{name}/{path}") if entry is None or entry.file_size > 4 * 1024 * 1024: raise gate.GateError(gate.CAPTURE_INCOMPLETE, f"{leg}: {name} text file is missing or oversized") @@ -391,17 +393,14 @@ def prescreen(scope: Any, root: Path) -> dict[str, list[dict[str, Any]]]: "native_properties": native_properties, "fixture": fixture, "fixture_sha256": gate.fixture_digest(fixture), "legs": [leg]} - if (len(seen_legs) != 13 or "sdist" not in seen_legs - or seen_variants != {f"universal2-apple-darwin-py{version}" - for version in ("3.12", "3.13", "3.14")} - or not rows): - raise gate.GateError(gate.SCOPE_UNVERIFIABLE, "runtime archive coverage is incomplete") return {"archives": sorted(rows.values(), key=lambda row: (row["key"], row["source_sha256"])), "build_packages": sorted(build_rows.values(), key=lambda row: row["key"]), "build_tools": sorted(tool_rows.values(), key=lambda row: row["key"])} def main() -> None: + """Write the prescreened runtime archive license report.""" + parser = argparse.ArgumentParser() parser.add_argument("--verified-scope", required=True) parser.add_argument("--scope-root", required=True) diff --git a/scripts/ci/r_isolated_matrix.py b/scripts/ci/r_isolated_matrix.py new file mode 100644 index 0000000000..02f44f5574 --- /dev/null +++ b/scripts/ci/r_isolated_matrix.py @@ -0,0 +1,27 @@ +"""Bound the R matrix to supported Linux/x64 image aliases, never runner labels.""" +import json +import sys + + +def admit(raw): + """Reject unsupported matrix legs before any R job can be scheduled.""" + rows = json.loads(raw) + if not isinstance(rows, list) or not 1 <= len(rows) <= 64: + raise ValueError('Expected 1..64 matrix objects') + for row in rows: + if not isinstance(row, dict) or not isinstance(row.get('os'), str): + raise ValueError('Expected an OS string in each matrix object') + if not isinstance(row.get('r'), str) or not row['r'].strip(): + raise ValueError('Expected a nonempty R version') + if row['os'] in ('ubuntu-latest', 'ubuntu-24.04'): + continue + raise ValueError('STOP unsupported platform leg: ' + repr(row['os'])) + return rows + + +if __name__ == '__main__': + try: + print(json.dumps(admit(sys.argv[1]), separators=(',', ':'))) + except (ValueError, TypeError, IndexError) as error: + print(str(error), file=sys.stderr) + sys.exit(1) diff --git a/scripts/ci/reconcile_repository_metadata.py b/scripts/ci/reconcile_repository_metadata.py index 1570455818..a78bd09725 100644 --- a/scripts/ci/reconcile_repository_metadata.py +++ b/scripts/ci/reconcile_repository_metadata.py @@ -16,7 +16,7 @@ import sys from pathlib import Path from typing import Any -from urllib.error import URLError +from urllib.error import HTTPError, URLError from urllib.request import HTTPRedirectHandler, Request, build_opener @@ -38,8 +38,6 @@ class _NoPagesRedirects(HTTPRedirectHandler): def redirect_request(self, req, fp, code, msg, headers, newurl): """Raise an HTTPError instead of following the redirect.""" - from urllib.error import HTTPError - raise HTTPError(req.full_url, code, msg, headers, fp) @@ -247,7 +245,13 @@ def _pages_publication_ready(repository: str, current: dict[str, Any]) -> None: if not response.read(1): raise RuntimeError(f"GitHub Pages returned empty content for {repository}") except (URLError, TimeoutError, OSError) as exc: - raise RuntimeError(f"GitHub Pages is not reachable for {repository}") from exc + try: + raise RuntimeError( + f"GitHub Pages is not reachable for {repository}" + ) from exc + finally: + if isinstance(exc, HTTPError): + exc.close() def _repository_file_exists(repository: str, default_branch: str, path: str) -> bool: diff --git a/scripts/ci/release_dependency_gate.py b/scripts/ci/release_dependency_gate.py index 96347278f6..f11178f107 100644 --- a/scripts/ci/release_dependency_gate.py +++ b/scripts/ci/release_dependency_gate.py @@ -48,6 +48,7 @@ import ast import email.parser import hashlib +import importlib import io import json import os @@ -63,10 +64,15 @@ from pathlib import Path, PurePosixPath from typing import Any, Iterable, Mapping, Sequence -try: - import tomllib -except ModuleNotFoundError: # Python 3.10; already declared in the dev group. - import tomli as tomllib +def _import_toml_parser(): + """Return the stdlib TOML parser, or the declared Python 3.10 backport.""" + try: + return importlib.import_module("tomllib") + except ModuleNotFoundError: # Python 3.10; already declared in the dev group. + return importlib.import_module("tomli") + + +tomllib = _import_toml_parser() try: from scripts.ci.spdx_license_policy import ( @@ -1835,6 +1841,8 @@ def _enumerate_cargo(capture: Path, *, source_root: Path | None = None, raise GateError(CAPTURE_INCOMPLETE, "Cargo source checkout cannot be bound") from error def source_blob(path: Path) -> bytes: + """Read and verify one regular source file from the selected commit.""" + try: relative = path.relative_to(bound_root) if any((bound_root / parent).is_symlink() for parent in (relative, *relative.parents)): @@ -2126,6 +2134,8 @@ def _source_license_notice(source: Path | None, source_sha: str, subject: str, raise GateError(CAPTURE_INCOMPLETE, "source notice needs an exact release commit") def blob(path: str) -> bytes: + """Read one bounded regular blob from the selected release commit.""" + entry = subprocess.check_output( ["git", "-C", str(source), "ls-tree", source_sha, "--", path], text=True) if not entry.startswith("100644 blob "): diff --git a/scripts/ci/resolve_base_rust_toolchain.py b/scripts/ci/resolve_base_rust_toolchain.py index 6dcd1642c2..7861bc336a 100755 --- a/scripts/ci/resolve_base_rust_toolchain.py +++ b/scripts/ci/resolve_base_rust_toolchain.py @@ -23,6 +23,8 @@ def _base_blob(repo_root: pathlib.Path, base_sha: str, path: str) -> str | None: + """Read path from the base commit, returning None when absent.""" + completed = subprocess.run( ["git", "-C", str(repo_root), "show", f"{base_sha}:{path}"], check=False, @@ -34,6 +36,8 @@ def _base_blob(repo_root: pathlib.Path, base_sha: str, path: str) -> str | None: def _channel(content: str) -> str | None: + """Return the Rust channel declared by TOML or a legacy bare file.""" + try: toolchain = tomllib.loads(content).get("toolchain") except tomllib.TOMLDecodeError: @@ -47,6 +51,8 @@ def _channel(content: str) -> str | None: def resolve(repo_root: pathlib.Path, base_sha: str) -> str: + """Resolve an exact base Rust release, its fallback, or no override.""" + if not SHA_RE.fullmatch(base_sha): raise ValueError("base SHA must be a full 40-character commit id") for path in PIN_FILES: @@ -66,6 +72,8 @@ def resolve(repo_root: pathlib.Path, base_sha: str) -> str: def main(argv: list[str] | None = None) -> int: + """Print the resolved base Rust toolchain and return a process status.""" + parser = argparse.ArgumentParser() parser.add_argument("--repo-root", required=True, type=pathlib.Path) parser.add_argument("--base-sha", required=True) diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index b0376c0822..fb5702f0c2 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -584,6 +584,9 @@ def wait_for_url(url: str, timeout: int, service: Service) -> bool: if 200 <= response.status < 500: return True time.sleep(1) + except urllib.error.HTTPError as exc: + exc.close() + time.sleep(1) except (urllib.error.URLError, TimeoutError): time.sleep(1) return False diff --git a/scripts/ci/scan_release_native_links.py b/scripts/ci/scan_release_native_links.py index 92af3d3c98..ad302d7894 100644 --- a/scripts/ci/scan_release_native_links.py +++ b/scripts/ci/scan_release_native_links.py @@ -69,6 +69,8 @@ def _reader() -> dict[str, str]: def _links(binary: bytes, target: str, reader: str, *, allow_subset: bool = False) -> list[dict]: + """Inspect a native binary and return validated dynamic-link rows.""" + if len(binary) > 128 * 1024 * 1024: raise ValueError("release native extension exceeds inspection limit") with tempfile.NamedTemporaryFile() as temporary: @@ -101,6 +103,8 @@ def _links(binary: bytes, target: str, reader: str, *, allow_subset: bool = Fals def scan(verified: dict, root: Path, source_sha: str, reader: dict[str, str]) -> dict: + """Build a native-link report from the exact verified distributions.""" + rows = verified.get("verified_distributions") if isinstance(verified, dict) else None if (not re.fullmatch(r"[0-9a-f]{40}", source_sha) or not isinstance(rows, list) or len(rows) != 13 @@ -168,6 +172,8 @@ def scan(verified: dict, root: Path, source_sha: str, reader: dict[str, str]) -> def main() -> None: + """Write the verified release native-link report.""" + parser = argparse.ArgumentParser() parser.add_argument("--verified-distributions", required=True) parser.add_argument("--distribution-root", required=True) diff --git a/scripts/ci/strix_evidence_binding.py b/scripts/ci/strix_evidence_binding.py index 7319040df2..aed54ded1a 100644 --- a/scripts/ci/strix_evidence_binding.py +++ b/scripts/ci/strix_evidence_binding.py @@ -305,8 +305,10 @@ def default_github_opener(url: str, token: str) -> Any: with _GITHUB_API_OPENER.open(request, timeout=30) as response: payload = response.read() except HTTPError as exc: + status_code = exc.code + exc.close() raise EvidenceBindingError( - f"GitHub changed-file request failed with HTTP {exc.code}" + f"GitHub changed-file request failed with HTTP {status_code}" ) from exc except URLError as exc: raise EvidenceBindingError( diff --git a/scripts/ci/strix_model_utils.sh b/scripts/ci/strix_model_utils.sh index 9f20eae670..16b32de179 100755 --- a/scripts/ci/strix_model_utils.sh +++ b/scripts/ci/strix_model_utils.sh @@ -77,6 +77,16 @@ normalize_model() { return 0 fi + # Literal auto is a gateway request, never a guessed Vertex/direct model. + if [ "$model" = "auto" ]; then + if [ "${DEFAULT_PROVIDER:-}" != "contextual_orchestrator" ]; then + echo "ERROR: literal auto requires the explicit contextual_orchestrator provider." >&2 + return 2 + fi + printf '%s\n' 'auto' + return 0 + fi + if is_vertex_resource_path "$model"; then local provider provider="$(sanitize_provider_name "${DEFAULT_PROVIDER:-}")" || { diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index cb97fd8eb8..5f1568e824 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -379,7 +379,7 @@ is_vertex_model() { is_contextual_orchestrator_model() { case "$1" in - orchestrator/free | contextual-orchestrator/orchestrator/free) + auto | orchestrator/free | contextual-orchestrator/orchestrator/free) return 0 ;; *) @@ -2651,8 +2651,8 @@ child_model_for_api_base() { # LiteLLM requires an explicit provider prefix even when the gateway is an # OpenAI-compatible local endpoint. Strip only the connector-facing alias so - # the selected orchestrator/free virtual pool reaches contextual-orchestrator - # unchanged. + # literal auto or a standalone legacy virtual pool reaches the gateway + # unchanged; the openai/ connector prefix is not part of the model ID. if is_contextual_orchestrator_model "$model" && is_contextual_orchestrator_api_base "$llm_api_base_value"; then local contextual_orchestrator_model diff --git a/scripts/ci/strix_report_scope.py b/scripts/ci/strix_report_scope.py index cc374c5852..08fab1bef2 100644 --- a/scripts/ci/strix_report_scope.py +++ b/scripts/ci/strix_report_scope.py @@ -10,7 +10,15 @@ # The PR scan target is a private directory holding only the changed files, so # a path under it (as Strix reports it) is bound to this invocation's scope. -SCOPE_PATH_RE = re.compile(r"/workspace/strix-pr-scope\.[A-Za-z0-9]+/([A-Za-z0-9_./-]+)") +PATH_TOKEN_END = ( + r"(?=$|[\s`\"'<>,;!?)]" + r"|:(?=[0-9]+(?::[0-9]+)?(?:$|[\s`\"'<>,;!?)]|\.(?![\w./-])))" + r"|\.(?![\w./-]))" +) +SCOPE_PATH_RE = re.compile( + r"(? bool: @@ -22,7 +30,35 @@ def _names_scoped_ancestor(report: str, changed_paths: list[str]) -> bool: return False +def _token(text: str) -> re.Pattern[str]: + """Match a path token only at whole-segment boundaries.""" + return re.compile(rf"(? re.Pattern[str]: + """Match a filename token without accepting suffixes or child paths.""" + return re.compile(rf"(? bool: + """Match exact file paths or a named directory plus its exact filename.""" + if _file_token(path).search(report) is not None: + return True + directory, _, name = path.rpartition("/") + if not directory or _file_token(name).search(report) is None: + return False + if _token(directory).search(report) is not None: + return True + parts = directory.split("/") + return any( + _token("/".join(parts[:depth])).search(report) + for depth in range(2, len(parts) + 1) + ) + + def validate(output: Path, changed_paths: list[str]) -> None: + """Raise ValueError unless one completed, unlinked report names changed scope.""" + if not output.is_dir() or output.is_symlink(): raise ValueError("scan output directory is missing") runs = [path for path in output.iterdir() if path.is_dir() and not path.is_symlink()] @@ -42,7 +78,7 @@ def validate(output: Path, changed_paths: list[str]) -> None: if metadata.get("status") != "completed" or results.get("scan_completed") is not True or results.get("success") is not True: raise ValueError("scan report is incomplete") report = report_path.read_text(encoding="utf-8") - if not any(path in report for path in changed_paths) and not _names_scoped_ancestor(report, changed_paths): + if not any(names_changed_path(report, path) for path in changed_paths) and not _names_scoped_ancestor(report, changed_paths): raise ValueError("scan report does not identify a changed source file") diff --git a/scripts/ci/strix_required_workflow_smoke.sh b/scripts/ci/strix_required_workflow_smoke.sh index 0b5a4a54d4..d1f192ea6f 100755 --- a/scripts/ci/strix_required_workflow_smoke.sh +++ b/scripts/ci/strix_required_workflow_smoke.sh @@ -180,9 +180,9 @@ assert_file_contains "$full_gate_test" "assert_strix_workflow_pr_trigger_hardene assert_file_contains "$workflow_file" "Provision contextual-orchestrator Strix sidecar" "Strix workflow provisions the trusted contextual-orchestrator gateway" assert_file_contains "$workflow_file" "CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR" "Strix workflow binds target visibility to the gateway ZDR policy" active_strix_models="$(sed -n -E 's/^[[:space:]]*STRIX_MODEL:[[:space:]]*([^#[:space:]]+)[[:space:]]*$/\1/p' "$workflow_file")" -[ "$active_strix_models" = "contextual-orchestrator/orchestrator/free" ] || record_failure "Strix must define exactly one active zero-cost free default model" -assert_file_not_contains "$workflow_file" "STRIX_MODEL: contextual-orchestrator/orchestrator/auto" "Strix must not retain the paid-inclusive auto default route" -assert_file_contains "$decision_record" "2026-08-30 amendment: Strix uses \`orchestrator/free\`" "The binding ADR amendment records the owner's explicit free-only override" +[ "$active_strix_models" = "auto" ] || record_failure "Strix must define exactly one literal auto default model" +assert_file_not_contains "$workflow_file" "STRIX_MODEL: contextual-orchestrator/orchestrator/free" "Strix must not silently restore a free default route" +assert_file_contains "$workflow_file" "CONTEXTUAL_ORCHESTRATOR_POOL: auto" "Strix sidecar pool matches the literal auto request" assert_file_contains "$decision_record" "Zero Data Retention (ZDR)-compliant routes remain mandatory for private targets" "The binding ADR preserves private-target privacy" assert_file_contains "$workflow_file" "provider_mode=contextual_orchestrator" "Strix workflow selects the contextual-orchestrator provider mode" assert_file_contains "$workflow_file" "STRIX_FALLBACK_MODELS: \"\"" "Strix delegates provider discovery and failover to the gateway" diff --git a/scripts/ci/strix_unverified_dependency.py b/scripts/ci/strix_unverified_dependency.py index bc4c781708..29563d2045 100755 --- a/scripts/ci/strix_unverified_dependency.py +++ b/scripts/ci/strix_unverified_dependency.py @@ -51,10 +51,14 @@ def named_packages(report: str) -> set[str]: def _is_requirements(name: str) -> bool: + """Return whether a filename is a supported requirements manifest.""" + return name.startswith("requirements") and name.endswith((".txt", ".in")) def _manifest_text(repo_root: Path) -> str: + """Return lower-cased text from bounded dependency manifests.""" + chunks = [] for path in repo_root.rglob("*"): if any(part in SKIP_DIRS for part in path.relative_to(repo_root).parts[:-1]): @@ -82,6 +86,8 @@ def unverified_dependency_finding(report: str, repo_root: Path) -> bool: def main(argv: list[str]) -> int: + """Classify command-line report paths and return a process status.""" + if len(argv) != 3: print(__doc__, file=sys.stderr) return 2 diff --git a/scripts/ci/verify_release_distribution_set.py b/scripts/ci/verify_release_distribution_set.py index 1779d5dfde..21e685b891 100644 --- a/scripts/ci/verify_release_distribution_set.py +++ b/scripts/ci/verify_release_distribution_set.py @@ -40,6 +40,7 @@ class DistributionSetError(ValueError): def _strict_object(pairs: Iterable[tuple[str, Any]]) -> dict[str, Any]: + """Build a JSON object while rejecting duplicate keys.""" result: dict[str, Any] = {} for key, value in pairs: if key in result: @@ -49,10 +50,12 @@ def _strict_object(pairs: Iterable[tuple[str, Any]]) -> dict[str, Any]: def _reject_constant(value: str) -> Any: + """Reject a non-finite JSON constant.""" raise DistributionSetError(f"non-finite JSON value: {value}") def _json_bytes(data: bytes) -> Any: + """Parse bounded UTF-8 JSON with strict object and number handling.""" if len(data) > MAX_CONTROL_BYTES: raise DistributionSetError("control JSON is too large") try: @@ -63,6 +66,7 @@ def _json_bytes(data: bytes) -> Any: def _timestamp(value: Any) -> datetime: + """Parse a canonical UTC timestamp.""" if not isinstance(value, str) or not value.endswith("Z"): raise DistributionSetError("missing canonical UTC timestamp") try: @@ -75,6 +79,7 @@ def _timestamp(value: Any) -> datetime: def _digest(value: Any) -> str: + """Validate and return a canonical SHA-256 artifact digest.""" if not isinstance(value, str) or DIGEST_RE.fullmatch(value) is None: raise DistributionSetError("missing canonical artifact digest") return value @@ -82,6 +87,7 @@ def _digest(value: Any) -> str: def _artifact(artifacts: Mapping[str, Mapping[str, Any]], name: str, artifact_id: int, digest: str, run_id: int, control_sha: str, started: datetime) -> None: + """Require an artifact to match the expected workflow-run identity.""" item = artifacts.get(name) if item is None or type(artifact_id) is not int or artifact_id <= 0: raise DistributionSetError(f"{name}: missing immutable artifact identity") @@ -99,6 +105,7 @@ def _artifact(artifacts: Mapping[str, Mapping[str, Any]], name: str, artifact_id @contextmanager def _archive(repository: str, artifact_id: int, digest: str, fetch: Callable[[str, int, BinaryIO], None]): + """Yield an artifact ZIP after verifying its bounded bytes and digest.""" with tempfile.TemporaryFile() as archive: fetch(repository, artifact_id, archive) if archive.tell() > MAX_ARCHIVE_BYTES: @@ -113,6 +120,7 @@ def _archive(repository: str, artifact_id: int, digest: str, def _members(archive: zipfile.ZipFile, expected: set[str]) -> dict[str, zipfile.ZipInfo]: + """Return the exact safe archive members required by the caller.""" entries = archive.infolist() if len(entries) != len(expected) or {entry.filename for entry in entries} != expected: raise DistributionSetError("artifact ZIP members differ from the expected set") @@ -126,6 +134,7 @@ def _members(archive: zipfile.ZipFile, expected: set[str]) -> dict[str, zipfile. def _record_rows(data: bytes, source_sha: str) -> dict[str, tuple[str, str]]: + """Parse source-bound reproducibility rows keyed by release leg.""" if len(data) > MAX_CONTROL_BYTES: raise DistributionSetError("reproducibility record is too large") try: @@ -269,6 +278,7 @@ def fetch_artifact(repository: str, artifact_id: int, output: BinaryIO) -> None: def main() -> None: + """Verify the CLI-supplied release distribution set.""" parser = argparse.ArgumentParser() for option in ("repository", "source-sha", "control-sha", "run-id", "run-attempt", "record-artifact-id", "record-artifact-digest", "wheel-filename", diff --git a/scripts/ci/verify_release_maturin_tool_assets.py b/scripts/ci/verify_release_maturin_tool_assets.py index 5554f15bd4..ed5afc90c3 100644 --- a/scripts/ci/verify_release_maturin_tool_assets.py +++ b/scripts/ci/verify_release_maturin_tool_assets.py @@ -9,9 +9,11 @@ import json import sys import tarfile +import urllib.error +import urllib.request import zipfile from pathlib import Path -from urllib.request import Request, urlopen +from urllib.parse import urlsplit try: from scripts.ci.release_dependency_gate import classify_platform_link @@ -29,21 +31,96 @@ } MAX_ASSET_BYTES = 16 * 1024 * 1024 MAX_BINARY_BYTES = 32 * 1024 * 1024 +ASSET_FILENAMES = frozenset({ + "maturin-aarch64-unknown-linux-musl.tar.gz", + "maturin-x86_64-unknown-linux-musl.tar.gz", + "maturin-aarch64-apple-darwin.tar.gz", + "maturin-x86_64-apple-darwin.tar.gz", + "maturin-x86_64-pc-windows-msvc.zip", +}) +GITHUB_RELEASE_HOST = "github.com" +GITHUB_RELEASE_CDN_HOST = "release-assets.githubusercontent.com" +GITHUB_RELEASE_PATH = "/PyO3/maturin/releases/download/v1.15.0/" + + +class _ExactReleaseRedirect(urllib.request.HTTPRedirectHandler): + """Admit one credential-free redirect to the exact GitHub release CDN.""" + + def http_error_302(self, request, response, code, message, headers): + """Validate and follow one exact release redirect, closing its response.""" + try: + location = headers.get("Location", "") + try: + redirect = urlsplit(location) + redirect_port = redirect.port + except ValueError as error: + raise ValueError("maturin release redirect is not trusted") from error + source = urlsplit(request.full_url) + if ( + getattr(request, "_cwl_release_redirected", False) + or source.scheme != "https" + or source.hostname != GITHUB_RELEASE_HOST + or redirect.scheme != "https" + or redirect.hostname != GITHUB_RELEASE_CDN_HOST + or redirect_port not in {None, 443} + or redirect.username is not None + or redirect.password is not None + or not redirect.path.startswith("/") + or redirect.fragment + ): + raise ValueError("maturin release redirect is not trusted") + redirected = urllib.request.Request( + location, + headers={"User-Agent": "cwl-release-gate"}, + method="GET", + ) + redirected._cwl_release_redirected = True + finally: + response.close() + return self.parent.open(redirected, timeout=request.timeout) + + http_error_301 = http_error_302 + http_error_303 = http_error_302 + http_error_307 = http_error_302 + http_error_308 = http_error_302 def _download(filename: str) -> bytes: - url = f"https://github.com/PyO3/maturin/releases/download/v1.15.0/{filename}" - # Fixed https origin and tag; verify_assets admits only five literal asset names. - with urlopen( # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310 - Request(url, headers={"User-Agent": "cwl-release-gate"}), timeout=60 - ) as response: - raw = response.read(MAX_ASSET_BYTES + 1) + """Download one admitted release asset through the fixed GitHub CDN hop.""" + if filename not in ASSET_FILENAMES: + raise ValueError("maturin asset name is unexpected") + request = urllib.request.Request( + f"https://{GITHUB_RELEASE_HOST}{GITHUB_RELEASE_PATH}{filename}", + headers={"User-Agent": "cwl-release-gate"}, + method="GET", + ) + opener = urllib.request.build_opener( + urllib.request.ProxyHandler({}), _ExactReleaseRedirect() + ) + try: + response = opener.open(request, timeout=60) + try: + if response.status != 200: + raise ValueError( + f"maturin release download returned HTTP {response.status}" + ) + raw = response.read(MAX_ASSET_BYTES + 1) + finally: + response.close() + except urllib.error.HTTPError as error: + try: + raise ValueError( + f"maturin release download returned HTTP {error.code}" + ) from error + finally: + error.close() if len(raw) > MAX_ASSET_BYTES: raise ValueError("maturin release asset exceeds inspection limit") return raw def _binary(raw: bytes, filename: str) -> bytes: + """Extract one bounded Maturin executable from its release archive.""" if filename.endswith(".zip"): with zipfile.ZipFile(io.BytesIO(raw)) as archive: members = archive.infolist() @@ -71,13 +148,7 @@ def verify_assets(evidence: dict, reader: str, fetch=_download) -> None: raise ValueError("maturin asset evidence is incomplete") for key, asset in sorted(evidence["assets"].items()): filename = asset["asset_filename"] - if filename not in { - "maturin-aarch64-unknown-linux-musl.tar.gz", - "maturin-x86_64-unknown-linux-musl.tar.gz", - "maturin-aarch64-apple-darwin.tar.gz", - "maturin-x86_64-apple-darwin.tar.gz", - "maturin-x86_64-pc-windows-msvc.zip", - }: + if filename not in ASSET_FILENAMES: raise ValueError(f"{key}: maturin asset name is unexpected") raw = fetch(filename) if len(raw) > MAX_ASSET_BYTES or hashlib.sha256(raw).hexdigest() != asset["asset_sha256"]: @@ -98,6 +169,7 @@ def verify_assets(evidence: dict, reader: str, fetch=_download) -> None: def main() -> None: + """Verify recorded Maturin assets from a local root or official release.""" parser = argparse.ArgumentParser() parser.add_argument("--asset-root", type=Path) args = parser.parse_args() diff --git a/scripts/ci/verify_release_scope_evidence_set.py b/scripts/ci/verify_release_scope_evidence_set.py index 094ecabcb2..7af41ec4cc 100644 --- a/scripts/ci/verify_release_scope_evidence_set.py +++ b/scripts/ci/verify_release_scope_evidence_set.py @@ -55,6 +55,7 @@ def _build_python_snapshot(folder: Path, leg: str, source_sha: str, members: Mapping[str, str]) -> None: + """Verify repeated build receipts and their exact Python snapshot.""" first = _json_bytes((folder / f"{leg}.build-first.json").read_bytes()) second = _json_bytes((folder / f"{leg}.build-second.json").read_bytes()) snapshot = folder / f"{leg}.build-python.zip" @@ -119,6 +120,7 @@ def _build_python_snapshot(folder: Path, leg: str, source_sha: str, def _wheel_identity(path: Path) -> tuple[str, str]: + """Read a runtime wheel's normalized project name and version.""" with zipfile.ZipFile(path) as archive: metadata = [item for item in archive.infolist() if item.filename.endswith(".dist-info/METADATA") @@ -224,6 +226,7 @@ def _runtime_target_architecture(runtime: Any, leg: str, *, intel: bool = False) def _runtime_archives(folder: Path, leg: str, source_sha: str, distribution: Mapping[str, Any], members: Mapping[str, str], *, intel: bool = False) -> list[dict[str, Any]]: + """Verify runtime dependency archives and their lock receipt.""" runtime = _json_bytes((folder / f"{leg}.runtime.json").read_bytes()) if (not isinstance(runtime, Mapping) or runtime.get("source_sha") != source_sha or runtime.get("leg") != leg @@ -492,6 +495,7 @@ def verify_scope_evidence_set( def main() -> None: + """Verify primary and Intel macOS scope evidence from CLI inputs.""" parser = argparse.ArgumentParser() for option in ("repository", "source-sha", "control-sha", "run-id", "run-attempt", "record-artifact-id", "record-artifact-digest", "verified-distributions", diff --git a/tests/fixture_github_reads.py b/tests/fixture_github_reads.py new file mode 100644 index 0000000000..1b44abda54 --- /dev/null +++ b/tests/fixture_github_reads.py @@ -0,0 +1,36 @@ +"""Explicit offline fixture-only GitHub CLI; all writes/endpoints denied.""" +import json +import os +from pathlib import Path +import sys + +fixture = json.loads(Path(os.environ["FIXTURE_FILE"]).read_text()) +args = sys.argv[1:] +with Path(fixture["calls"]).open("a") as handle: + handle.write(json.dumps(args) + "\n") +if any(value in args for value in ("POST", "PATCH", "PUT", "DELETE", "--method", "-X")): + raise SystemExit("fixture forbids mutations") +if not args or args[0] != "api": + raise SystemExit("fixture only permits api reads") +endpoints = [value for value in args[1:] if value.startswith("repos/")] +if len(endpoints) != 1: + raise SystemExit("fixture requires one endpoint") +endpoint = endpoints[0] +if endpoint == "repos/ContextualWisdomLab/example/pulls/1437": + print(json.dumps(fixture["pull"])) +elif endpoint == "repos/ContextualWisdomLab/fast-mlsirm/pulls/2018": + print(json.dumps(fixture["pull"])) +elif endpoint.startswith("repos/ContextualWisdomLab/fast-mlsirm/pulls/2018/reviews") or endpoint.startswith("repos/ContextualWisdomLab/example/pulls/1437/reviews"): + if fixture["lookup_fail"]: + print("fixture read failure", file=sys.stderr) + raise SystemExit(1) + if fixture["malformed"]: + print("not-json") + else: + print(json.dumps([fixture["reviews"]] if "--slurp" in args else fixture["reviews"])) +elif endpoint == "repos/ContextualWisdomLab/.github/contents/scripts/ci/opencode_review_receipt_gate.py?ref=6a37e4cdfbd8bf6f3a60645a0ad7c13e1b9c1ae3": + if args[-2:] != ["--jq", ".content"]: + raise SystemExit("fixture expects pinned content field") + print(fixture["helper_base64"]) +else: + raise SystemExit("fixture rejects endpoint outside exact bounded contract") diff --git a/tests/fixtures/coverage-cargo/Cargo.lock b/tests/fixtures/coverage-cargo/Cargo.lock index 9a54521941..20666db700 100644 --- a/tests/fixtures/coverage-cargo/Cargo.lock +++ b/tests/fixtures/coverage-cargo/Cargo.lock @@ -2,18 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "cfg-if" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" - [[package]] name = "coverage-cargo-fixtures" version = "0.0.0" @@ -29,15 +17,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "itoa" version = "1.0.15" @@ -50,15 +29,6 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -82,37 +52,32 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b" dependencies = [ - "cfg-if", - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327" dependencies = [ "libc", "pyo3-build-config", @@ -120,9 +85,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -132,13 +97,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.22.6" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] @@ -152,12 +116,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - [[package]] name = "ryu" version = "1.0.20" @@ -177,18 +135,12 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "unicode-ident" version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/tests/fixtures/coverage-cargo/Cargo.toml b/tests/fixtures/coverage-cargo/Cargo.toml index dbe13ab8ab..67af5592ca 100644 --- a/tests/fixtures/coverage-cargo/Cargo.toml +++ b/tests/fixtures/coverage-cargo/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" [dependencies] itoa = "=1.0.15" ryu = "=1.0.20" -pyo3 = { version = "=0.22.6", features = ["extension-module", "abi3-py310"] } +pyo3 = { version = "=0.29.2", features = ["extension-module", "abi3-py310"] } diff --git a/tests/fixtures/opencode_shell_literal_operands.json b/tests/fixtures/opencode_shell_literal_operands.json new file mode 100644 index 0000000000..5185ec4e88 --- /dev/null +++ b/tests/fixtures/opencode_shell_literal_operands.json @@ -0,0 +1,177 @@ +[ + { + "before": "'cd \"$1\" && corepack npm run build'", + "after": "\"cd \\\"\\$1\\\" && corepack npm run build\"", + "expected": "cd \"$1\" && corepack npm run build", + "literal_sha256": "5b75beac7d183f624302beeede5ef80ebdae5220c68d4e62e8fef76b1f7d95ae", + "count": 1 + }, + { + "before": "'cd \"$1\" && corepack pnpm run build'", + "after": "\"cd \\\"\\$1\\\" && corepack pnpm run build\"", + "expected": "cd \"$1\" && corepack pnpm run build", + "literal_sha256": "e322a3754c05bac3697ef5148b8d91fc821f772bbb825a65ef216789652671a9", + "count": 1 + }, + { + "before": "'cd \"$1\" && yarn build'", + "after": "\"cd \\\"\\$1\\\" && yarn build\"", + "expected": "cd \"$1\" && yarn build", + "literal_sha256": "7a32a0647a33e794473ad69fecd80c09b0ff0c9e07f48bbe4ac9fdc5985b0e57", + "count": 1 + }, + { + "before": "'set -eu\n cd \"$1\"\n dist_dir=\"$(mktemp -d)\"\n python3 -m maturin build --offline --release -o \"$dist_dir\"\n python3 -m pip install --user --no-index --no-deps --force-reinstall \"$dist_dir\"/*.whl\n python3 \"$2\" \"$dist_dir\"/*.whl .\n rm -rf \"$dist_dir\"'", + "after": "\"set -eu\n cd \\\"\\$1\\\"\n dist_dir=\\\"\\$(mktemp -d)\\\"\n python3 -m maturin build --offline --release -o \\\"\\$dist_dir\\\"\n python3 -m pip install --user --no-index --no-deps --force-reinstall \\\"\\$dist_dir\\\"/*.whl\n python3 \\\"\\$2\\\" \\\"\\$dist_dir\\\"/*.whl .\n rm -rf \\\"\\$dist_dir\\\"\"", + "expected": "set -eu\n cd \"$1\"\n dist_dir=\"$(mktemp -d)\"\n python3 -m maturin build --offline --release -o \"$dist_dir\"\n python3 -m pip install --user --no-index --no-deps --force-reinstall \"$dist_dir\"/*.whl\n python3 \"$2\" \"$dist_dir\"/*.whl .\n rm -rf \"$dist_dir\"", + "literal_sha256": "4bba65aefcb59292fa58d0c47228faa9de663c1be54f4fdcfc30e37e234ad82e", + "count": 1 + }, + { + "before": "'cd \"$1\" && PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest tests && python3 -m coverage report --show-missing'", + "after": "\"cd \\\"\\$1\\\" && PYTHONPATH=\\\"\\$([ -d src ] && printf src:. || printf .)\\\" python3 -m coverage run -m pytest tests && python3 -m coverage report --show-missing\"", + "expected": "cd \"$1\" && PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest tests && python3 -m coverage report --show-missing", + "literal_sha256": "8328238f7e8a9efdf50a7d5c1f7bf2e3e9d407b93736b459cc49a720d0dbe253", + "count": 1 + }, + { + "before": "'PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest && python3 -m coverage report --show-missing'", + "after": "\"PYTHONPATH=\\\"\\$([ -d src ] && printf src:. || printf .)\\\" python3 -m coverage run -m pytest && python3 -m coverage report --show-missing\"", + "expected": "PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest && python3 -m coverage report --show-missing", + "literal_sha256": "5b8f66d840e67514ded7204289d8d6544f8d9e9b51783bcc5e1f5e99ba45c103", + "count": 1 + }, + { + "before": "'cd \"$1\" && PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m pytest tests/test_docstrings.py'", + "after": "\"cd \\\"\\$1\\\" && PYTHONPATH=\\\"\\$([ -d src ] && printf src:. || printf .)\\\" python3 -m pytest tests/test_docstrings.py\"", + "expected": "cd \"$1\" && PYTHONPATH=\"$([ -d src ] && printf src:. || printf .)\" python3 -m pytest tests/test_docstrings.py", + "literal_sha256": "f6e4b346d6c2285ecff7172605110fedcd34b0e7119695918a3c683adc9edb83", + "count": 1 + }, + { + "before": "'- Preferred review language: `%s`\\n'", + "after": "\"- Preferred review language: \\`%s\\`\\\\n\"", + "expected": "- Preferred review language: `%s`\\n", + "literal_sha256": "a55f9a0314aca4f2919688735698b948e4eb3c6fb5b9a764343626111edff099", + "count": 1 + }, + { + "before": "'- PR title: `%s`\\n'", + "after": "\"- PR title: \\`%s\\`\\\\n\"", + "expected": "- PR title: `%s`\\n", + "literal_sha256": "990a7ab75683f7fc6ebb36792ac611d816bc67aa456f96ba5f40e33641c6c6d4", + "count": 1 + }, + { + "before": "'- PR body excerpt: `%s`\\n'", + "after": "\"- PR body excerpt: \\`%s\\`\\\\n\"", + "expected": "- PR body excerpt: `%s`\\n", + "literal_sha256": "068807903f94d5f4944b5dd5afc54a62face32538f66d1b09d177b986bebb1da", + "count": 1 + }, + { + "before": "'- PR body excerpt: `[empty]`\\n'", + "after": "\"- PR body excerpt: \\`[empty]\\`\\\\n\"", + "expected": "- PR body excerpt: `[empty]`\\n", + "literal_sha256": "3daa3390c09d66fccc34f340c3142b8b1f2de29a23c30e8caa73b62e1617ddab", + "count": 1 + }, + { + "before": "'Unable to collect %s from `%s` to `%s`; continue review from available changed-file evidence and direct file inspection.\\n'", + "after": "\"Unable to collect %s from \\`%s\\` to \\`%s\\`; continue review from available changed-file evidence and direct file inspection.\\\\n\"", + "expected": "Unable to collect %s from `%s` to `%s`; continue review from available changed-file evidence and direct file inspection.\\n", + "literal_sha256": "31eccf9ccdc69d89d9033eac2d0c10beaa644ecdab06f53305d0904a267de44a", + "count": 1 + }, + { + "before": "'Merge-base discovery failed for `%s` and `%s`; falling back to base SHA for bounded diff evidence.\\n\\n'", + "after": "\"Merge-base discovery failed for \\`%s\\` and \\`%s\\`; falling back to base SHA for bounded diff evidence.\\\\n\\\\n\"", + "expected": "Merge-base discovery failed for `%s` and `%s`; falling back to base SHA for bounded diff evidence.\\n\\n", + "literal_sha256": "3520ee4d10ac9a7f302555dabdc7f8370a0c821ffda3694ffd3aab3daf135c5d", + "count": 1 + }, + { + "before": "'All PR reviews and comments evidence is historical context only and may contain stale bot conclusions. Do not infer active failed checks, unresolved threads, or missing changed files from those comments unless current-head evidence corroborates the same claim for Head SHA `%s`.\\n\\n'", + "after": "\"All PR reviews and comments evidence is historical context only and may contain stale bot conclusions. Do not infer active failed checks, unresolved threads, or missing changed files from those comments unless current-head evidence corroborates the same claim for Head SHA \\`%s\\`.\\\\n\\\\n\"", + "expected": "All PR reviews and comments evidence is historical context only and may contain stale bot conclusions. Do not infer active failed checks, unresolved threads, or missing changed files from those comments unless current-head evidence corroborates the same claim for Head SHA `%s`.\\n\\n", + "literal_sha256": "a62108037c8187ed5baa230d394c788909c36cbb2489fbd292c8c6d7f74eca39", + "count": 1 + }, + { + "before": "'\n def self_check:\n (.name // \"\") as $n\n | [\"opencode-review\", \"coverage-evidence\", \"coverage-source-tree\", \"required-workflow-bootstrap\", \"metadata-only gate evaluation\", \"scan-pr-queue\"] | index($n);\n def latest_peer_checks:\n [\n (.check_runs // [])[]\n | select(self_check | not)\n | . + {\n checkedAt: (\n if ((.started_at // \"\") != \"\") then .started_at\n else (.completed_at // \"\")\n end\n )\n }\n ]\n | sort_by(.app.slug // \"\", .name // \"\", .checkedAt // \"\", .id // 0)\n | group_by([.app.slug // \"\", .name // \"\"])\n | map(last)\n | .[];\n '", + "after": "\"\n def self_check:\n (.name // \\\"\\\") as \\$n\n | [\\\"opencode-review\\\", \\\"coverage-evidence\\\", \\\"coverage-source-tree\\\", \\\"required-workflow-bootstrap\\\", \\\"metadata-only gate evaluation\\\", \\\"scan-pr-queue\\\"] | index(\\$n);\n def latest_peer_checks:\n [\n (.check_runs // [])[]\n | select(self_check | not)\n | . + {\n checkedAt: (\n if ((.started_at // \\\"\\\") != \\\"\\\") then .started_at\n else (.completed_at // \\\"\\\")\n end\n )\n }\n ]\n | sort_by(.app.slug // \\\"\\\", .name // \\\"\\\", .checkedAt // \\\"\\\", .id // 0)\n | group_by([.app.slug // \\\"\\\", .name // \\\"\\\"])\n | map(last)\n | .[];\n \"", + "expected": "\n def self_check:\n (.name // \"\") as $n\n | [\"opencode-review\", \"coverage-evidence\", \"coverage-source-tree\", \"required-workflow-bootstrap\", \"metadata-only gate evaluation\", \"scan-pr-queue\"] | index($n);\n def latest_peer_checks:\n [\n (.check_runs // [])[]\n | select(self_check | not)\n | . + {\n checkedAt: (\n if ((.started_at // \"\") != \"\") then .started_at\n else (.completed_at // \"\")\n end\n )\n }\n ]\n | sort_by(.app.slug // \"\", .name // \"\", .checkedAt // \"\", .id // 0)\n | group_by([.app.slug // \"\", .name // \"\"])\n | map(last)\n | .[];\n ", + "literal_sha256": "aee9423e1e806ca41f083a4ab8ae311c6c423c67a4750a05f6aceff961c29321", + "count": 1 + }, + { + "before": "'\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0))\n | group_by(.name // \"\")\n | map(last)\n | .[]?\n | select(((.name // \"\" | ascii_downcase) as $n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index($n)) | not)\n | select((.status // \"\") == \"completed\")\n | select((.conclusion // \"\" | ascii_upcase) as $c | [\"FAILURE\",\"TIMED_OUT\",\"ACTION_REQUIRED\",\"CANCELLED\",\"STARTUP_FAILURE\"] | index($c))\n | select((.name // \"\") != \"scan-pr-queue\")\n | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and ((.name // \"\") | contains(\"$\" + \"{{\"))) | not)\n | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and (.name // \"\") == \"noema-review\") | not)\n | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.conclusion // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end)\n '", + "after": "\"\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \\\"\\\"), (.id // 0))\n | group_by(.name // \\\"\\\")\n | map(last)\n | .[]?\n | select(((.name // \\\"\\\" | ascii_downcase) as \\$n | [\\\"opencode-review\\\",\\\"coverage-evidence\\\",\\\"metadata-only gate evaluation\\\"] | index(\\$n)) | not)\n | select((.status // \\\"\\\") == \\\"completed\\\")\n | select((.conclusion // \\\"\\\" | ascii_upcase) as \\$c | [\\\"FAILURE\\\",\\\"TIMED_OUT\\\",\\\"ACTION_REQUIRED\\\",\\\"CANCELLED\\\",\\\"STARTUP_FAILURE\\\"] | index(\\$c))\n | select((.name // \\\"\\\") != \\\"scan-pr-queue\\\")\n | select(((.conclusion // \\\"\\\" | ascii_downcase) == \\\"cancelled\\\" and ((.name // \\\"\\\") | contains(\\\"\\$\\\" + \\\"{{\\\"))) | not)\n | select(((.conclusion // \\\"\\\" | ascii_downcase) == \\\"cancelled\\\" and (.name // \\\"\\\") == \\\"noema-review\\\") | not)\n | \\\"- \\\" + (if (.name // \\\"\\\") == \\\"strix\\\" then \\\"Strix Security Scan/strix\\\" else ((.name // \\\"check\\\") + \\\" check run\\\") end) + \\\": \\\" + (.conclusion // \\\"unknown\\\") + (if (.details_url // .html_url // \\\"\\\") != \\\"\\\" then \\\" (\\\" + (.details_url // .html_url) + \\\")\\\" else \\\"\\\" end)\n \"", + "expected": "\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0))\n | group_by(.name // \"\")\n | map(last)\n | .[]?\n | select(((.name // \"\" | ascii_downcase) as $n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index($n)) | not)\n | select((.status // \"\") == \"completed\")\n | select((.conclusion // \"\" | ascii_upcase) as $c | [\"FAILURE\",\"TIMED_OUT\",\"ACTION_REQUIRED\",\"CANCELLED\",\"STARTUP_FAILURE\"] | index($c))\n | select((.name // \"\") != \"scan-pr-queue\")\n | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and ((.name // \"\") | contains(\"$\" + \"{{\"))) | not)\n | select(((.conclusion // \"\" | ascii_downcase) == \"cancelled\" and (.name // \"\") == \"noema-review\") | not)\n | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.conclusion // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end)\n ", + "literal_sha256": "6c4a46c21e26f6edff27c80ec673a1587677626d749ac929899db7d8c6c13613", + "count": 1 + }, + { + "before": "'\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0))\n | group_by(.name // \"\")\n | map(last)\n | .[]?\n | select(((.name // \"\" | ascii_downcase) as $n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index($n)) | not)\n | select((.name // \"\") != \"scan-pr-queue\")\n | select((.status // \"\") != \"completed\")\n | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.status // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end)\n '", + "after": "\"\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \\\"\\\"), (.id // 0))\n | group_by(.name // \\\"\\\")\n | map(last)\n | .[]?\n | select(((.name // \\\"\\\" | ascii_downcase) as \\$n | [\\\"opencode-review\\\",\\\"coverage-evidence\\\",\\\"metadata-only gate evaluation\\\"] | index(\\$n)) | not)\n | select((.name // \\\"\\\") != \\\"scan-pr-queue\\\")\n | select((.status // \\\"\\\") != \\\"completed\\\")\n | \\\"- \\\" + (if (.name // \\\"\\\") == \\\"strix\\\" then \\\"Strix Security Scan/strix\\\" else ((.name // \\\"check\\\") + \\\" check run\\\") end) + \\\": \\\" + (.status // \\\"unknown\\\") + (if (.details_url // .html_url // \\\"\\\") != \\\"\\\" then \\\" (\\\" + (.details_url // .html_url) + \\\")\\\" else \\\"\\\" end)\n \"", + "expected": "\n [.[].check_runs[]?]\n | sort_by((.started_at // .completed_at // .created_at // \"\"), (.id // 0))\n | group_by(.name // \"\")\n | map(last)\n | .[]?\n | select(((.name // \"\" | ascii_downcase) as $n | [\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"] | index($n)) | not)\n | select((.name // \"\") != \"scan-pr-queue\")\n | select((.status // \"\") != \"completed\")\n | \"- \" + (if (.name // \"\") == \"strix\" then \"Strix Security Scan/strix\" else ((.name // \"check\") + \" check run\") end) + \": \" + (.status // \"unknown\") + (if (.details_url // .html_url // \"\") != \"\" then \" (\" + (.details_url // .html_url) + \")\" else \"\" end)\n ", + "literal_sha256": "3c98f8ca91cdb37939293cd06808463e69bad6e1c015cafa05d07fb2d5a036ce", + "count": 1 + }, + { + "before": "'\n query($owner:String!,$name:String!,$number:Int!) {\n repository(owner:$owner,name:$name) {\n pullRequest(number:$number) {\n statusCheckRollup {\n contexts(first: 100) {\n nodes {\n __typename\n ... on CheckRun {\n name\n status\n conclusion\n completedAt\n detailsUrl\n checkSuite {\n workflowRun {\n workflow {\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n '", + "after": "\"\n query(\\$owner:String!,\\$name:String!,\\$number:Int!) {\n repository(owner:\\$owner,name:\\$name) {\n pullRequest(number:\\$number) {\n statusCheckRollup {\n contexts(first: 100) {\n nodes {\n __typename\n ... on CheckRun {\n name\n status\n conclusion\n completedAt\n detailsUrl\n checkSuite {\n workflowRun {\n workflow {\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n \"", + "expected": "\n query($owner:String!,$name:String!,$number:Int!) {\n repository(owner:$owner,name:$name) {\n pullRequest(number:$number) {\n statusCheckRollup {\n contexts(first: 100) {\n nodes {\n __typename\n ... on CheckRun {\n name\n status\n conclusion\n completedAt\n detailsUrl\n checkSuite {\n workflowRun {\n workflow {\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n ", + "literal_sha256": "a1f6ebee2275c7fb64e9c80219b8102ded1c712d19d40c867ee72f16f79839aa", + "count": 1 + }, + { + "before": "'query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id}}}'", + "after": "\"query(\\$owner:String!,\\$name:String!,\\$number:Int!){repository(owner:\\$owner,name:\\$name){pullRequest(number:\\$number){id}}}\"", + "expected": "query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id}}}", + "literal_sha256": "286a6960231d8dfb8ff7781de00eea84606a9da93dba3bacf96062e541f8ef77", + "count": 1 + }, + { + "before": "'\n [\n .[]\n | {\n number: (.number // 0),\n rule: (.rule.id // .rule.name // \"unknown\"),\n tool: (.tool.name // \"code-scanning\"),\n severity: (.rule.security_severity_level // .rule.severity // \"unknown\"),\n url: (.html_url // \"\")\n }\n | select((.severity | ascii_downcase) as $s | [\"medium\",\"high\",\"critical\",\"warning\",\"error\"] | index($s))\n ]\n | .[]\n | \"- \" + .tool + \"/\" + .rule + \": \" + .severity + \" alert #\" + (.number | tostring) + (if .url != \"\" then \" (\" + .url + \")\" else \"\" end)\n '", + "after": "\"\n [\n .[]\n | {\n number: (.number // 0),\n rule: (.rule.id // .rule.name // \\\"unknown\\\"),\n tool: (.tool.name // \\\"code-scanning\\\"),\n severity: (.rule.security_severity_level // .rule.severity // \\\"unknown\\\"),\n url: (.html_url // \\\"\\\")\n }\n | select((.severity | ascii_downcase) as \\$s | [\\\"medium\\\",\\\"high\\\",\\\"critical\\\",\\\"warning\\\",\\\"error\\\"] | index(\\$s))\n ]\n | .[]\n | \\\"- \\\" + .tool + \\\"/\\\" + .rule + \\\": \\\" + .severity + \\\" alert #\\\" + (.number | tostring) + (if .url != \\\"\\\" then \\\" (\\\" + .url + \\\")\\\" else \\\"\\\" end)\n \"", + "expected": "\n [\n .[]\n | {\n number: (.number // 0),\n rule: (.rule.id // .rule.name // \"unknown\"),\n tool: (.tool.name // \"code-scanning\"),\n severity: (.rule.security_severity_level // .rule.severity // \"unknown\"),\n url: (.html_url // \"\")\n }\n | select((.severity | ascii_downcase) as $s | [\"medium\",\"high\",\"critical\",\"warning\",\"error\"] | index($s))\n ]\n | .[]\n | \"- \" + .tool + \"/\" + .rule + \": \" + .severity + \" alert #\" + (.number | tostring) + (if .url != \"\" then \" (\" + .url + \")\" else \"\" end)\n ", + "literal_sha256": "0f75e1d5894b2b73171b4eb30786e20af5178dda107f415b3c80de0af1f69fe2", + "count": 1 + }, + { + "before": "'- Problem: Failed same-head checks remain for `%s`.\\n'", + "after": "\"- Problem: Failed same-head checks remain for \\`%s\\`.\\\\n\"", + "expected": "- Problem: Failed same-head checks remain for `%s`.\\n", + "literal_sha256": "f65b717a34c174fb8de45a0c7a004762a1e300a8010b3c7d0a3f816512bd5ccd", + "count": 1 + }, + { + "before": "'- Result: `EXISTING_CURRENT_HEAD_APPROVAL`\\n'", + "after": "\"- Result: \\`EXISTING_CURRENT_HEAD_APPROVAL\\`\\\\n\"", + "expected": "- Result: `EXISTING_CURRENT_HEAD_APPROVAL`\\n", + "literal_sha256": "8423de58f35ef3649d71f174e789b7fdde0c794d6c471f79a13eb5e9f18c828c", + "count": 1 + }, + { + "before": "'- Head SHA: `%s`\\n'", + "after": "\"- Head SHA: \\`%s\\`\\\\n\"", + "expected": "- Head SHA: `%s`\\n", + "literal_sha256": "840f8302400a82c3ba04fc0ed8e5b4d230fe277cb276c0a043d61a0a025a5b65", + "count": 2 + }, + { + "before": "'- Model-pool outcome: `%s`\\n'", + "after": "\"- Model-pool outcome: \\`%s\\`\\\\n\"", + "expected": "- Model-pool outcome: `%s`\\n", + "literal_sha256": "5b8ec084e8d6b976374a8332b42978c39f0222d6f12183d3683b001d44d83899", + "count": 1 + }, + { + "before": "'```json\\n%s\\n```\\n\\n'", + "after": "\"\\`\\`\\`json\\\\n%s\\\\n\\`\\`\\`\\\\n\\\\n\"", + "expected": "```json\\n%s\\n```\\n\\n", + "literal_sha256": "1fd318239f7f84df9676717e141bd7ca8ef672b4c585e01f942ce3aa15349fee", + "count": 1 + } +] \ No newline at end of file diff --git a/tests/test_actions_queue_health_contract.py b/tests/test_actions_queue_health_contract.py index 746aa93990..4958d18376 100644 --- a/tests/test_actions_queue_health_contract.py +++ b/tests/test_actions_queue_health_contract.py @@ -16,9 +16,12 @@ def test_queue_health_workflow_is_scheduled_read_only_and_pinned() -> None: assert "cancel-in-progress: false" in workflow assert "timeout-minutes: 30" in workflow assert "github.repository == 'ContextualWisdomLab/.github'" in workflow + assert "github.ref == 'refs/heads/main'" in workflow assert "endsWith(github.workflow_ref, '@refs/heads/main')" in workflow assert '"group":"CWL MCP remediation"' in workflow assert '"labels":["self-hosted","linux","x64"]' in workflow + assert '"group":"CWL CI isolated"' in workflow + assert '"labels":["self-hosted","linux","x64","cwlab-ci-isolated"]' in workflow assert "runs-on: ubuntu-24.04" not in workflow assert "runs-on: ubuntu-latest" not in workflow assert "actions: read" in workflow diff --git a/tests/test_adr_identifier_uniqueness.py b/tests/test_adr_identifier_uniqueness.py new file mode 100644 index 0000000000..c82662b79a --- /dev/null +++ b/tests/test_adr_identifier_uniqueness.py @@ -0,0 +1,30 @@ +"""Contracts for unambiguous architecture decision record identifiers.""" + +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[1] +ADR_DIRECTORY = ROOT / "docs" / "adr" +ADR_FILE_ID = re.compile(r"(?:^|[-_])(?P\d{4})(?:[-_]|$)") + + +def test_adr_file_identifiers_are_unique() -> None: + """Each numeric ADR identifier must name exactly one decision record.""" + records_by_identifier: dict[str, list[str]] = defaultdict(list) + + for path in sorted(ADR_DIRECTORY.glob("*.md")): + match = ADR_FILE_ID.search(path.stem) + if match is not None: + records_by_identifier[match.group("identifier")].append(path.name) + + duplicates = { + identifier: records + for identifier, records in records_by_identifier.items() + if len(records) > 1 + } + + assert duplicates == {}, f"duplicate ADR identifiers: {duplicates}" diff --git a/tests/test_agent_mention_downstream_idempotency.py b/tests/test_agent_mention_downstream_idempotency.py index 28c9084a9b..2be8080c7f 100644 --- a/tests/test_agent_mention_downstream_idempotency.py +++ b/tests/test_agent_mention_downstream_idempotency.py @@ -135,10 +135,10 @@ def test_forwarders_restrict_self_hosted_admission_to_trusted_main() -> None: """Branch workflows cannot select the main-only privileged runner group.""" for path in (NOEMA_WORKFLOW, OPENCODE_WORKFLOW): text = path.read_text(encoding="utf-8") - selector = next(line for line in text.splitlines() if "runs-on:" in line) + selector = next(line for line in text.splitlines() if line.startswith(" group:")) assert "github.repository == 'ContextualWisdomLab/.github'" in selector assert "endsWith(github.workflow_ref, '@refs/heads/main')" in selector assert '"group":"CWL MCP remediation"' in selector assert '"labels":["self-hosted","linux","x64"]' in selector - assert "|| fromJSON('[\"ubuntu-24.04\"]')" in selector + assert '|| fromJSON(\'{"group":"CWL CI isolated","labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}\')' in selector assert "actions/checkout@" not in text diff --git a/tests/test_agent_mention_workflow_contract.py b/tests/test_agent_mention_workflow_contract.py index e640121ad1..1cce19993e 100644 --- a/tests/test_agent_mention_workflow_contract.py +++ b/tests/test_agent_mention_workflow_contract.py @@ -52,6 +52,19 @@ def test_workflow_uses_local_event_and_central_sweep_with_job_scoped_writes() -> assert "agent_mention_sweep.py" in sweep +def test_full_suite_tooling_lock_includes_collection_dependencies() -> None: + """A quality install must provide both parsers imported during suite collection.""" + lock = (ROOT / "requirements-opencode-review-ci-hashes.txt").read_text( + encoding="utf-8" + ) + requirements = { + line.split("==", 1)[0].casefold() + for line in lock.splitlines() + if line and not line.startswith(("#", " ", "-")) and "==" in line + } + assert {"defusedxml", "pyyaml"} <= requirements + + def test_quality_workflow_measures_exact_files_without_module_name_warnings() -> None: """Coverage includes the two script paths instead of treating paths as modules.""" diff --git a/tests/test_agent_review_runtime_quality_consolidation.py b/tests/test_agent_review_runtime_quality_consolidation.py index 2b0e83312b..4fecc93656 100644 --- a/tests/test_agent_review_runtime_quality_consolidation.py +++ b/tests/test_agent_review_runtime_quality_consolidation.py @@ -6,8 +6,10 @@ workflow_level_cancels_in_progress, ) +import os import re import subprocess +import textwrap from pathlib import Path import pytest @@ -128,6 +130,116 @@ def test_consolidated_workflow_preserves_all_contract_suites() -> None: assert required_path in workflow +@pytest.mark.parametrize( + "changed_path", + ( + "scripts/ci/codeql_ghas_configuration_identity.py", + "tests/test_codeql_ghas_configuration_identity.py", + "tests/test_codeql_ghas_analyses_pagination.py", + "tests/test_github_api_url_boundary.py", + ), +) +def test_ghas_runtime_changes_start_the_quality_runner(changed_path: str) -> None: + """Admit the helper and each focused GHAS regression independently.""" + trigger = _workflow_text().split("on:\n", 1)[1].split("\nconcurrency:\n", 1)[0] + assert f' - "{changed_path}"' in trigger + + +@pytest.mark.parametrize( + ("changed_path", "expected_suites"), + ( + ("scripts/ci/codeql_ghas_configuration_identity.py", {"ghas"}), + ("tests/test_codeql_ghas_configuration_identity.py", {"ghas"}), + ("tests/test_codeql_ghas_analyses_pagination.py", {"ghas"}), + ("tests/test_github_api_url_boundary.py", {"ghas"}), + ("tests/test_agent_review_runtime_quality_consolidation.py", {"ghas"}), + ( + ".github/workflows/agent-review-runtime-quality-ci.yml", + {"noema", "opencode", "strix", "queue", "review_repair", + "commercial_readiness", "exact_artifact", "ghas"}, + ), + ("docs/doctoring/codeql-ghas-analyses-pagination.md", set()), + ), +) +def test_ghas_runtime_selector_executes_the_actual_shell( + changed_path: str, expected_suites: set[str], tmp_path: Path +) -> None: + """Execute the full selector with a synthetic diff and a real HEAD guard.""" + selector_step = _workflow_text().split( + "- name: Select affected contract suites", 1 + )[1].split(" - name:", 1)[0] + script = textwrap.dedent(selector_step.split(" run: |\n", 1)[1]) + output = tmp_path / "suites.txt" + head = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=REPOSITORY_ROOT, text=True + ).strip() + # Only changed-path input is synthetic; HEAD verification uses actual Git. + diff_fixture = ( + 'git() {\n' + ' if [[ "$1" == diff ]]; then\n' + ' printf "%s\\n" "$CHANGED_PATH"\n' + ' else\n' + ' command git "$@"\n' + ' fi\n' + '}\n' + ) + result = subprocess.run( + ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", + diff_fixture + script], + cwd=REPOSITORY_ROOT, + env={**os.environ, "HEAD_SHA": head, "BASE_SHA": head, + "CHANGED_PATH": changed_path, "GITHUB_OUTPUT": str(output)}, + text=True, capture_output=True, check=True, + ) + suites = dict(line.split("=", 1) for line in output.read_text().splitlines()) + assert suites == { + name: str(name in expected_suites).lower() + for name in ("noema", "opencode", "strix", "queue", "review_repair", + "commercial_readiness", "exact_artifact", "ghas") + } + assert result.stdout == result.stderr == "" + + +@pytest.mark.parametrize("pytest_exit", (0, 42)) +def test_ghas_runtime_step_runs_exact_tests_and_fails_closed( + pytest_exit: int, tmp_path: Path +) -> None: + """Run the actual step shell and capture its argv without invoking providers.""" + workflow = _workflow_text() + step_name = " - name: Verify GHAS identity, pagination, and API boundary contracts\n" + assert step_name in workflow, "focused GHAS test-command step is missing" + step = workflow.split(step_name, 1)[1].split(" - name:", 1)[0] + assert "if: steps.affected_suites.outputs.ghas == 'true'" in step + script = textwrap.dedent(step.split(" run: |\n", 1)[1]) + calls = tmp_path / "python-calls.txt" + recorder = ( + 'python() {\n' + ' printf "%s\\n" "$*" >> "$CALLS"\n' + ' if [[ "$*" == *pytest* ]]; then return "$PYTEST_EXIT"; fi\n' + '}\n' + ) + result = subprocess.run( + ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", + recorder + script], + cwd=REPOSITORY_ROOT, + env={**os.environ, "CALLS": str(calls), "PYTEST_EXIT": str(pytest_exit)}, + text=True, capture_output=True, check=False, + ) + tests = ( + "tests/test_codeql_ghas_configuration_identity.py " + "tests/test_codeql_ghas_analyses_pagination.py " + "tests/test_github_api_url_boundary.py" + ) + expected = [f"-m pytest -q {tests}"] + if pytest_exit == 0: + expected.append( + "-m compileall -q scripts/ci/codeql_ghas_configuration_identity.py " + tests + ) + assert calls.read_text().splitlines() == expected + assert result.returncode == pytest_exit + assert result.stdout == result.stderr == "" + + def test_exact_head_is_verified_before_selected_suites_run() -> None: """Reject a checkout that differs from the pull request's current head.""" @@ -154,6 +266,49 @@ def test_review_repair_suite_is_selected_and_conditionally_executed() -> None: assert workflow.count("runs-on:") == 1 +def test_review_launcher_runtime_is_owned_by_the_review_repair_suite() -> None: + """Trigger, execute, compile, and measure the launcher's runtime contracts.""" + + workflow = _workflow_text() + + assert workflow.count("tests/test_contextual_orchestrator_review_launcher.py") == 4 + assert workflow.count("tests/test_review_preflight_concurrency.py") == 4 + assert "--cov=scripts.ci.contextual_orchestrator_review_launcher" in workflow + + +@pytest.mark.parametrize( + "changed_path", + ( + "requirements-opencode-review-ci.txt", + "requirements-noema-document-ci.txt", + "scripts/ci/compile_opencode_review_lock.sh", + ), +) +def test_common_lock_sources_select_every_python_consumer_suite( + changed_path: str, +) -> None: + """A common-lock source change must execute both Python consumer suites.""" + + workflow = _workflow_text() + selector = workflow.split(' case "$changed_path" in\n', 1)[1].split( + " esac", 1 + )[0] + result = subprocess.run( + [ + "bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", + 'IFS= read -r changed_path\nnoema_suite=false\nopencode_suite=false\n' + 'case "$changed_path" in\n' + selector + + 'esac\nprintf "%s,%s" "$noema_suite" "$opencode_suite"\n', + ], + input=changed_path + "\n", + text=True, + capture_output=True, + check=True, + ) + assert result.stdout == "true,true" + assert result.stderr == "" + + @pytest.mark.parametrize( ("changed_path", "starts_runner", "review_repair", "queue"), ( diff --git a/tests/test_all_self_hosted_runner_contract.py b/tests/test_all_self_hosted_runner_contract.py new file mode 100644 index 0000000000..2833ddb09f --- /dev/null +++ b/tests/test_all_self_hosted_runner_contract.py @@ -0,0 +1,183 @@ +"""Require self-hosted CI without admitting PR code to privileged runner pools. + +The isolated label is an operator provisioning requirement, not evidence that an +isolated machine exists. Existing trusted control and review selectors retain +their main-ref predicates; their non-main fallback must never be GitHub-hosted. +""" +from pathlib import Path +import json +import re +import pytest +import yaml + + +STATIC_RUNNER_GROUP_LABELS = { + 'CWL central control': set(), + 'CWL central CodeQL': set(), + 'CWL central OpenCode': set(), + 'CWL CI isolated': {'cwlab-ci-isolated'}, + 'CWL law CI': {'law-ai-agent-ci'}, +} + + +def required_static_labels(group: object) -> set[str]: + """Return the exact additional labels admitted for a static runner group.""" + assert group in STATIC_RUNNER_GROUP_LABELS, ('unsupported static group', group) + return {'self-hosted', 'linux', 'x64'} | STATIC_RUNNER_GROUP_LABELS[group] + + +def test_every_runner_requires_self_hosted_and_no_hosted_fallback() -> None: + """Inspect every declaration, including expression and matrix selectors.""" + declarations = [] + for path in sorted(Path('.github/workflows').glob('*.yml')): + lines = path.read_text().splitlines() + for index, line in enumerate(lines): + if re.match(r'^ runs-on:', line): + block = line + if line.strip() == 'runs-on:': + block += '\n' + '\n'.join(lines[index + 1:index + 3]) + declarations.append((path, block)) + assert declarations + for path, block in declarations: + assert 'self-hosted' in block, (path, block) + assert not re.search(r'fromJSON\(\'\["ubuntu-|\|\| \'"ubuntu-', block), (path, block) + assert not re.search(r'runs-on: (?:ubuntu-|windows-|macos-)', block), (path, block) + + +def test_nonprivileged_runners_require_isolated_label() -> None: + """Check actual YAML selectors, including static labels and both extensions.""" + paths = sorted(set(Path('.github/workflows').glob('*.yml')) | + set(Path('.github/workflows').glob('*.yaml'))) + observed = 0 + groups = set(STATIC_RUNNER_GROUP_LABELS) + for path in paths: + for job_id, job in yaml.safe_load(path.read_text())['jobs'].items(): + if 'runs-on' not in job: + continue + observed += 1 + runner = job['runs-on'] + if isinstance(runner, dict): + group = runner.get('group') + if isinstance(group, str) and group.startswith('${{'): + # Dynamic predicates remain covered by the exact fallback, + # typed-pair and R-matrix contracts, not literal evaluation. + assert any(name in group for name in groups), (path, job_id, group) + continue + labels = runner.get('labels') + assert isinstance(labels, list) and all(isinstance(label, str) for label in labels), ( + path, job_id, 'static labels must be a string list', labels, + ) + required = required_static_labels(group) + assert required.issubset(label.lower() for label in labels), ( + path, job_id, 'missing required static labels', required.difference(labels), + ) + else: + assert isinstance(runner, str) and 'cwlab-ci-isolated' in runner, (path, job_id, runner) + assert observed, 'runner selector coverage is empty' + + +def test_static_runner_group_policy_is_explicit_and_fail_closed() -> None: + """Bind dedicated groups to their isolation labels and reject unknown groups.""" + assert required_static_labels('CWL law CI') == { + 'self-hosted', 'linux', 'x64', 'law-ai-agent-ci', + } + with pytest.raises(AssertionError, match='unsupported static group'): + required_static_labels('CWL ungoverned CI') + + +def test_isolated_fallback_never_selects_privileged_group() -> None: + """Inspect scalar and typed runner fallbacks, never an empty declaration set.""" + observed = 0 + expected = { + 'group': 'CWL CI isolated', + 'labels': ['self-hosted', 'linux', 'x64', 'cwlab-ci-isolated'], + } + paths = sorted(set(Path('.github/workflows').glob('*.yml')) | + set(Path('.github/workflows').glob('*.yaml'))) + for path in paths: + for job_id, job in yaml.safe_load(path.read_text())['jobs'].items(): + runner = job.get('runs-on') + fields = list(runner.values()) if isinstance(runner, dict) else [runner] + expressions = [field for field in fields if isinstance(field, str) and + field.startswith('${{') and '||' in field and + 'matrix.config.os' not in field] + if not expressions: + continue + observed += 1 + for expression in expressions: + fallback = expression.rsplit('||', 1)[1] + match = re.fullmatch( + r"\s*(?:fromJSON\(\s*)?'(\{[^']*\})'\s*\)*\s*" + r"(?:\.group|\.labels)?\s*\}\}", fallback, + ) + assert match is not None, (path, job_id, 'unsupported fallback', fallback) + assert json.loads(match.group(1)) == expected, ( + path, job_id, 'unsafe fallback', fallback, + ) + assert observed == 29, ('conditional runner fallback coverage', observed) + + +def test_isolated_label_is_scoped_to_dedicated_runner_group() -> None: + """A mutable label cannot replace the isolated runner access boundary.""" + for path in sorted(Path('.github/workflows').glob('*.yml')): + lines = path.read_text().splitlines() + for index, line in enumerate(lines): + if not re.match(r'^ runs-on:', line): + continue + block = line + if line.strip() == 'runs-on:': + block += '\n' + '\n'.join(lines[index + 1:index + 3]) + if 'cwlab-ci-isolated' in block: + assert 'CWL CI isolated' in block, (path, block) + + +TRUSTED_MAIN_UNION_SELECTORS = { + '.github/workflows/actions-queue-health.yml': {'collect'}, + '.github/workflows/audit-central-ruleset.yml': {'audit'}, + '.github/workflows/organization-commercial-readiness-loop.yml': {'coordinate'}, + '.github/workflows/pr-auto-rebase.yml': {'auto-rebase'}, + '.github/workflows/repository-metadata-reconcile.yml': {'validate', 'apply'}, + '.github/workflows/sbom-inventory-scheduler.yml': {'aggregate-sbom-inventory'}, +} + + +def test_trusted_main_union_selectors_preserve_control_and_isolated_fallback() -> None: + """Resolve main/head routing conflicts without restoring hosted fallback.""" + trusted_main = '{"group":"CWL MCP remediation","labels":["self-hosted","linux","x64"]}' + isolated_fallback = ( + '{"group":"CWL CI isolated",' + '"labels":["self-hosted","linux","x64","cwlab-ci-isolated"]}' + ) + for workflow_path, job_ids in TRUSTED_MAIN_UNION_SELECTORS.items(): + workflow = yaml.safe_load(Path(workflow_path).read_text()) + for job_id in job_ids: + mapping = workflow['jobs'][job_id]['runs-on'] + assert isinstance(mapping, dict), (workflow_path, job_id, mapping) + assert set(mapping) == {'group', 'labels'} + group = mapping['group'] + labels = mapping['labels'] + assert group.endswith(').group }}') + assert labels.endswith(').labels }}') + assert group.removesuffix(').group }}') == labels.removesuffix(').labels }}') + runner = '${{ ' + group[5:].removesuffix(').group }}') + ' }}' + expected = ( + "${{ github.repository == 'ContextualWisdomLab/.github' && " + "github.ref == 'refs/heads/main' && " + "endsWith(github.workflow_ref, '@refs/heads/main') && " + f"fromJSON('{trusted_main}') || fromJSON('{isolated_fallback}') }}" + "}" + ) + assert runner == expected, (workflow_path, job_id, runner) + condition = runner[4:].split(' && fromJSON(', 1)[0] + for repository in ('ContextualWisdomLab/.github', 'ContextualWisdomLab/example'): + for ref in ('refs/heads/main', 'refs/pull/2565/merge', 'refs/heads/candidate'): + for workflow_ref in ('main', 'candidate'): + terms = { + "github.repository == 'ContextualWisdomLab/.github'": repository == 'ContextualWisdomLab/.github', + "github.ref == 'refs/heads/main'": ref == 'refs/heads/main', + "endsWith(github.workflow_ref, '@refs/heads/main')": workflow_ref == 'main', + } + admitted = all(terms[term] for term in condition.split(' && ')) + wanted = repository == 'ContextualWisdomLab/.github' and ref == 'refs/heads/main' and workflow_ref == 'main' + assert admitted == wanted, (workflow_path, job_id, repository, ref, workflow_ref) + assert 'ubuntu-24.04' not in runner + assert 'ubuntu-latest' not in runner diff --git a/tests/test_audit_org_codeql_coverage.py b/tests/test_audit_org_codeql_coverage.py index 0ffc8fa749..8d95cdb0b1 100644 --- a/tests/test_audit_org_codeql_coverage.py +++ b/tests/test_audit_org_codeql_coverage.py @@ -262,7 +262,14 @@ def test_null_latest_analysis_is_not_coverage() -> None: ] -def test_audit_codeql_coverage_defaults_now_to_current_time() -> None: +def test_audit_codeql_coverage_defaults_now_to_current_time(monkeypatch) -> None: + class FixedClock(datetime): + @classmethod + def now(cls, tz=None): + assert tz is timezone.utc + return NOW + + monkeypatch.setattr(audit, "datetime", FixedClock) repositories = [covered_by_recent_analysis("DefaultNowRepo", days_ago=0)] assert audit.audit_codeql_coverage(repositories) == [] @@ -299,7 +306,14 @@ def test_main_fail_path_reports_gaps_from_stdin(monkeypatch, capsys) -> None: assert "FAIL: 1 repositories have no CodeQL coverage" in captured.err -def test_main_pass_path_reports_from_file_arg(tmp_path, capsys) -> None: +def test_main_pass_path_reports_from_file_arg(tmp_path, capsys, monkeypatch) -> None: + class FixedClock(datetime): + @classmethod + def now(cls, tz=None): + assert tz is timezone.utc + return NOW + + monkeypatch.setattr(audit, "datetime", FixedClock) payload_path = tmp_path / "repositories.json" payload_path.write_text( json.dumps([covered_by_default_setup("ELUNVERA"), covered_by_recent_analysis("Orgmetra")]), diff --git a/tests/test_codeql_ghas_analyses_pagination.py b/tests/test_codeql_ghas_analyses_pagination.py new file mode 100644 index 0000000000..db906964d6 --- /dev/null +++ b/tests/test_codeql_ghas_analyses_pagination.py @@ -0,0 +1,443 @@ +"""Complete, fail-closed GHAS analyses traversal at the real HTTP boundary.""" + +from __future__ import annotations + +from email.message import Message +from io import BytesIO +import json +from urllib.parse import urlencode + +import pytest + +from scripts.ci import codeql_ghas_configuration_identity as identity + + +ENDPOINT = "https://api.github.com/repos/ContextualWisdomLab/example/code-scanning/analyses" + + +class _Page: + """Serve one synthetic HTTP page without contacting GitHub.""" + + def __init__(self, payload, link=""): + """Keep the page body and its actual Link response header.""" + self.body = json.dumps(payload).encode() + self.headers: dict[str, str] | Message = {"Link": link} + + def __enter__(self): + """Open the response context.""" + return self + + def __exit__(self, *_args): + """Close the response context without suppressing failures.""" + return None + + def read(self): + """Return the serialized body.""" + return self.body + + +def _url(page=None, ref: str | None = "refs/heads/main", per_page=100): + """Construct the documented analyses query, with an optional page number.""" + params = {"per_page": str(per_page), "tool_name": "CodeQL"} + if ref: + params["ref"] = ref + if page is not None: + params["page"] = str(page) + return ENDPOINT + "?" + urlencode(params) + + +def test_separate_link_headers_keep_next_after_current_last(monkeypatch): + """Every physical Link field contributes to the complete page traversal.""" + first = _Page([{"id": 1}]) + first.headers = Message() + first.headers["Link"] = f'<{_url(1)}>; rel="last"' + first.headers["link"] = f'<{_url(2)}>; rel="next"' + pages = {_url(): first, _url(2): _Page([{"id": 2}])} + calls = [] + + def open_page(request, timeout): + """Serve both pages and record their unchanged authenticated requests.""" + calls.append((request.full_url, request.get_header("Authorization"), timeout)) + return pages[request.full_url] + + monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", open_page) + assert identity.list_codeql_analyses("ContextualWisdomLab/example", token="opaque", ref="refs/heads/main") == [ + {"id": 1}, {"id": 2} + ] + assert calls == [(url, "Bearer opaque", 30) for url in pages] + + +@pytest.mark.parametrize("links", [ + [f'<{_url(2)}>; rel="next"', f'<{_url(2)}>; rel="next"'], + [f'<{_url(2)}>; rel="next"', '; rel="next"'], + [f'<{_url(1)}>; rel="last"', f'<{_url(1)}>; rel="last"'], + [f'<{_url(2)}>; rel="next"', "garbage"], + [f'<{_url(2)}>; rel="next"', ""], + [f'<{_url(2)}>; rel="next"', f'<{_url(1)}>; rel="unknown"'], + [f'<{_url(1)}>; rel="last"', '; rel="next"'], + [f'<{_url(1)}>; rel="last"', f'<{_url(2, ref="refs/pull/7/head")}>; rel="next"'], +]) +def test_separate_link_headers_reject_invalid_later_field_before_request(monkeypatch, links): + """A later duplicate, malformed or unsafe field cannot hide behind the first.""" + response = _Page([{"id": 1}]) + response.headers = Message() + for link in links: + response.headers["Link"] = link + calls = [] + + def open_page(request, timeout): + """Fail if any combined invalid header reaches another bearer request.""" + calls.append((request.full_url, request.get_header("Authorization"))) + assert len(calls) == 1, "invalid later Link field reached authenticated transport" + return response + + monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", open_page) + with pytest.raises(identity.ConfigurationIdentityError): + identity.list_codeql_analyses("ContextualWisdomLab/example", token="opaque", ref="refs/heads/main") + assert calls == [(_url(), "Bearer opaque")] + + +@pytest.mark.parametrize("header_type", ["message", "dict"]) +@pytest.mark.parametrize("relation", [None, "", "last", "next"]) +def test_message_and_dict_headers_preserve_absent_single_link_behavior(monkeypatch, header_type, relation): + """Absent, empty and single Link fields preserve Message and dict compatibility.""" + response = _Page([{"id": 1}]) + response.headers = Message() if header_type == "message" else {} + if relation is not None: + response.headers["Link"] = ( + f'<{_url(2 if relation == "next" else 1)}>; rel="{relation}"' if relation else "" + ) + terminal = _Page([{"id": 2}]) + terminal.headers = Message() if header_type == "message" else {} + calls = [] + + def open_page(request, timeout): + """Serve a single optional next page without inventing extra headers.""" + calls.append((request.full_url, request.get_header("Authorization"), timeout)) + assert len(calls) <= (2 if relation == "next" else 1) + return response if len(calls) == 1 else terminal + + monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", open_page) + expected_urls = [_url(), _url(2)] if relation == "next" else [_url()] + assert identity.list_codeql_analyses("ContextualWisdomLab/example", token="opaque", ref="refs/heads/main") == [ + {"id": page} for page in range(1, len(expected_urls) + 1) + ] + assert calls == [(url, "Bearer opaque", 30) for url in expected_urls] + + +def test_complete_history_keeps_active_identity_after_first_hundred(monkeypatch): + """A base identity on page three must still block an unpaired head.""" + base_sha, head_sha = "a" * 40, "b" * 40 + active = {"commit_sha": base_sha, "analysis_key": "active-workflow:analyze", + "category": "/language:rust", "tool": {"name": "CodeQL"}} + historical = [{"id": n, "commit_sha": "c" * 40} for n in range(200)] + pages = { + _url(): _Page(historical[:100], f'<{_url(2)}>; rel="next", <{_url(3)}>; rel="last"'), + _url(2): _Page(historical[100:], f'<{_url(3)}>; rel="next", <{_url(1)}>; rel="prev"'), + _url(3): _Page([active], f'<{_url(1)}>; rel="first", <{_url(2)}>; rel="prev"'), + } + calls = [] + + def open_page(request, timeout): + """Record every authenticated request and serve its exact page.""" + calls.append((request.full_url, request.get_header("Authorization"), timeout)) + return pages[request.full_url] + + monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", open_page) + rows = identity.list_codeql_analyses("ContextualWisdomLab/example", token="opaque", ref="refs/heads/main") + assert rows == historical + [active] + assert identity.pairing_ready(rows, [], base_sha=base_sha, head_sha=head_sha, language="rust") == ( + False, [("active-workflow:analyze", "/language:rust")] + ) + assert calls == [(url, "Bearer opaque", 30) for url in pages] + + +@pytest.mark.parametrize("target", [ + "https://evil.example/analyses?page=2", + _url(2).replace("api.github.com", "api.github.com.evil.example"), + _url(2).replace("https://", "http://"), + _url(2).replace("api.github.com", "api.github.com:443"), + _url(2).replace("api.github.com", "user@api.github.com"), + _url(2) + "#fragment", + _url(2).replace("/example/", "/other/"), + _url(2).replace("/analyses?", "/alerts?"), + _url(2, ref="refs/pull/7/head"), + _url(2, ref=None), + _url(2, per_page=50), + _url(2).replace("tool_name=CodeQL", "tool_name=Semgrep"), + _url(2) + "&ref=refs%2Fheads%2Fmain", + _url(2) + "&extra=unreviewed", + _url(2).replace("page=2", "page=4"), + _url(2).replace("page=2", "page=0"), + _url(2).replace("page=2", "page=two"), + _url(2).replace("page=2", "page=02"), + _url(2).replace("&page=2", ""), + _url(2) + "&broken", + "/repos/ContextualWisdomLab/example/code-scanning/analyses?page=2", +]) +def test_next_link_cannot_change_authority_endpoint_or_query(monkeypatch, target): + """Reject poisoned next URLs before constructing a second bearer request.""" + calls = [] + + def open_page(request, timeout): + """Refuse a second request even when its authority still appears valid.""" + calls.append((request.full_url, request.get_header("Authorization"))) + assert len(calls) == 1, "poisoned link reached authenticated transport" + return _Page([{"id": 1}], f'<{target}>; rel="next"') + + monkeypatch.setattr(identity._GITHUB_API_OPENER, "open", open_page) + with pytest.raises(identity.ConfigurationIdentityError): + identity.list_codeql_analyses("ContextualWisdomLab/example", token="opaque", ref="refs/heads/main") + assert calls == [(_url(), "Bearer opaque")] + + +@pytest.mark.parametrize("link", [ + "garbage", '