diff --git a/.github/workflows/codeql-scan-dispatch.yml b/.github/workflows/codeql-scan-dispatch.yml index 45cfcc75fc..49873c2987 100644 --- a/.github/workflows/codeql-scan-dispatch.yml +++ b/.github/workflows/codeql-scan-dispatch.yml @@ -587,11 +587,56 @@ jobs: id: gate run: python3 "$RUNNER_TEMP/codeql_sarif_gate.py" codeql-results-dispatch + - name: Select target CodeQL analysis-read credential + id: ghas_analysis_token + if: steps.gate.outcome == 'success' + env: + TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} + TARGET_APP_TOKEN: ${{ steps.target_app_token.outputs.token || '' }} + PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || '' }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || '' }} + WORKFLOW_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + probe_analysis_read() { + token_label="$1" + token="$2" + if [ -z "$token" ]; then + return 1 + fi + if GH_TOKEN="$token" gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "repos/${TARGET_REPOSITORY}/code-scanning/analyses?per_page=1&tool_name=CodeQL" \ + >/dev/null 2>&1; then + echo "::add-mask::$token" + { + printf 'token=%s\n' "$token" + printf 'source=%s\n' "$token_label" + } >>"$GITHUB_OUTPUT" + echo "Selected ${token_label} after proving target CodeQL analysis-read access." + return 0 + fi + echo "::notice::${token_label} cannot read target CodeQL analyses; trying the next configured credential." + return 1 + } + + if probe_analysis_read "target-app-token" "$TARGET_APP_TOKEN" || + probe_analysis_read "pr-review-merge-token" "$PR_REVIEW_MERGE_TOKEN" || + probe_analysis_read "opencode-approve-token" "$OPENCODE_APPROVE_TOKEN" || + probe_analysis_read "github-token" "$WORKFLOW_TOKEN"; then + exit 0 + fi + + echo "::error::no configured credential can read target CodeQL analyses; GHAS configuration identity cannot be proven." + exit 1 + - name: Verify GHAS base/head CodeQL configuration identity id: ghas_configuration_identity if: steps.gate.outcome == 'success' env: - GH_TOKEN: ${{ steps.target_app_token.outputs.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || github.token }} + GH_TOKEN: ${{ steps.ghas_analysis_token.outputs.token }} TARGET_REPOSITORY: ${{ needs.validate-dispatch.outputs.target_repository }} PR_NUMBER: ${{ needs.validate-dispatch.outputs.pr_number }} BASE_REF: ${{ needs.validate-dispatch.outputs.base_ref }} diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index cbc8d21439..1f8e74bc86 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -632,12 +632,17 @@ jobs: coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" coverage_build_dir="${RUNNER_TEMP}/opencode-coverage-tool-build" trusted_ci_requirements="${GITHUB_WORKSPACE}/requirements-opencode-review-ci-hashes.txt" + trusted_noema_document_requirements="${GITHUB_WORKSPACE}/requirements-noema-document-ci-hashes.txt" trusted_base_python_installer="${GITHUB_WORKSPACE}/scripts/ci/install_base_python_locks.py" trusted_vcs_import_root_resolver="${GITHUB_WORKSPACE}/scripts/ci/resolve_opencode_base_vcs_import_root.sh" if [ ! -f "$trusted_ci_requirements" ] || [ -L "$trusted_ci_requirements" ]; then echo "::error::Trusted coverage requirements must be a regular non-symlink file." exit 1 fi + if [ ! -f "$trusted_noema_document_requirements" ] || [ -L "$trusted_noema_document_requirements" ]; then + echo "::error::Trusted Noema document requirements must be a regular non-symlink file." + exit 1 + fi if [ ! -f "$trusted_base_python_installer" ] || [ -L "$trusted_base_python_installer" ]; then echo "::error::Trusted base Python lock installer must be a regular non-symlink file." exit 1 @@ -651,6 +656,8 @@ jobs: chmod 0700 "$coverage_build_dir" install -m 0644 "$trusted_ci_requirements" \ "$coverage_build_dir/requirements-opencode-review-ci-hashes.txt" + install -m 0644 "$trusted_noema_document_requirements" \ + "$coverage_build_dir/requirements-noema-document-ci-hashes.txt" install -m 0755 "$trusted_base_python_installer" \ "$coverage_build_dir/install-base-python-locks.py" install -m 0755 "$trusted_vcs_import_root_resolver" \ diff --git a/CHANGELOG.d/20260920-strix-trusted-binder-runtime-fixture.md b/CHANGELOG.d/20260920-strix-trusted-binder-runtime-fixture.md new file mode 100644 index 0000000000..f3165fe979 --- /dev/null +++ b/CHANGELOG.d/20260920-strix-trusted-binder-runtime-fixture.md @@ -0,0 +1,17 @@ +### Strix keeps trusted evidence binding outside consumer workspaces + +- The Strix gate resolves its evidence binder beside the trusted gate source. + The executable core harness now materializes that trusted runtime under a + separate source directory, passes a binder-free consumer workspace through + `STRIX_REPO_ROOT`, and invokes the trusted gate by its absolute path. +- OpenCode coverage assertions follow the consolidated + `validate-pr-metadata` owner instead of the removed + `coverage-source-tree` job and failure-report step. +- The commercial-readiness receipt contract now compares the complete parsed + harden-runner endpoint set instead of treating an expected hostname as a URL + substring. This closes the exact CodeQL + `py/incomplete-url-substring-sanitization` finding without suppressing it or + widening egress. +- The branch adopts the current central dependency owner, including the + explicit AnyIO 4.14.2 source-to-hash pin required by the Python security + gate. diff --git a/CHANGELOG.md b/CHANGELOG.md index 34281625cb..c8e492a899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### Strix supplies bounded Job Analysis authority context from the trusted base + +- Orgmetra #63 changes `packages/hris-kernel/src/orgmetra_hris_kernel/job_analysis.py`, but the Strix scan workspace previously omitted the unchanged authorization, HTTP, snapshot, and persistence collaborators that establish its resource-ownership boundary. That incomplete context produced a false HIGH IDOR finding even though the product reconstructs owner scope and authorizes resource fields before port access. A source-first executable fixture now requires the changed PR-head module, exactly five unchanged Job Analysis authority files from the authenticated trusted base, and exclusion of an unrelated administration file. RED `1fd22f4e` failed because `auth.py` was absent; the gate now recognizes only the normalized Job Analysis trigger and adds the five fixed context paths through the existing trusted-base materialization boundary. No consumer source, provider/model policy, severity gate, timeout, or write authority changes. + +### OpenCode coverage image materializes every Dockerfile lock input + +- Required OpenCode run `35370902053` for `.github#2266@12621f75e` failed before executing PR code because its trusted Dockerfile copied `requirements-noema-document-ci-hashes.txt` while the isolated build context contained only the OpenCode lockfile. The coverage owner now validates both lockfiles as regular non-symlink files and copies both into the trusted build context before the networked image build. `tests/test_opencode_agent_contract.py` pins the complete input boundary. Hosted exact-head acceptance remains Proposed until the new run reaches the image-build and coverage steps. + ### Noema transport capacity schedules a bounded continuation re-dispatch - After gateway failover, HTTP 429/5xx no longer end only as a permanent required-check failure with `caller attempts=1`. ADR-0031 classifies that class as `provider_capacity_unavailable`, keeps the single gateway request per job, surfaces `provider_attempt_count` from the orchestrator error envelope, and authorizes at most two same-head `repository_dispatch` retries after a capped `Retry-After` or deterministic 60–180 s jitter. Review is never skipped. Refs #2165. diff --git a/docs/doctoring/opencode-exact-head-dispatch-audit-20260917.md b/docs/doctoring/opencode-exact-head-dispatch-audit-20260917.md new file mode 100644 index 0000000000..1d14cd04b5 --- /dev/null +++ b/docs/doctoring/opencode-exact-head-dispatch-audit-20260917.md @@ -0,0 +1,41 @@ +# Doctoring record: OpenCode exact-head dispatch audit (2026-09-17) + +- **Date:** 2026-09-17 +- **Subject:** Snapshot of an exact-head OpenCode Review Dispatch audit over open + `ContextualWisdomLab/.github` pull requests after the coalesce gap, captured in + `/tmp/docs-review-dispatch.json`. +- **Decision record:** none — audit trail only; no workflow or scheduler change. +- **Source artifact:** `/tmp/docs-review-dispatch.json` + (`generated_at=2026-09-17T21:19:25.528137+00:00`, + `event_type=opencode-review`, `coalesce_flag_touched=false`). + +## Counts + +| Outcome | Count | +|---|---| +| Dispatched | **2** | +| Skipped | **11** | +| Errors | **0** | +| Unconfirmed | 0 | +| Total PRs audited | 13 | + +## Redispatched (exact-head missing) + +| PR | Head SHA | Dispatch run | Dispatched at (UTC) | +|---|---|---|---| +| ContextualWisdomLab/.github#2215 | `ab04bfdb3cbd69cc7ef90dc6bb2104b88550b89c` | [35275996814](https://github.com/ContextualWisdomLab/.github/actions/runs/35275996814) | 2026-09-17T21:19:14Z | +| ContextualWisdomLab/.github#2226 | `ff16764ac374b23be2d8131a5d03c89d62cd0bc7` | [35275997215](https://github.com/ContextualWisdomLab/.github/actions/runs/35275997215) | 2026-09-17T21:19:17Z | + +## Skipped (exact-head dispatch already exists) + +All eleven skips used reason `exact-head dispatch already exists`: + +`#2249`, `#2250`, `#2251`, `#2252`, `#2253`, `#2254`, `#2166`, `#2170`, +`#2174`, `#2184`, `#2205`. + +## Reading + +Exact-head deduplication held for the majority of the open set: eleven PRs +already had a queued or pending OpenCode Review Dispatch bound to their live +head SHA. Only `#2215` and `#2226` lacked that binding and were redispatched. +Zero errors and zero unconfirmed results — the audit completed cleanly. diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index c617e3ad73..1c96b765ac 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -7,6 +7,12 @@ 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +### 2026-09-19 exact-head incident delta + +| Gap ID | 상태 | exact-head evidence | causal owner / next gate | +|---|---|---|---| +| 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 | Gap ID | 상태 | exact-head evidence | causal owner / next gate | @@ -3424,3 +3430,70 @@ alone -- it is a documented multi-PR hot-file collision zone. Contract: **Action.** Exact `57477289ebec5631b0c48f0bc419f336dbe19deb` adds a dependency-free synthetic-302 transport to `tests/test_github_api_url_boundary.py`. For both actual production openers, the case drives a canonical bearer request through the real HTTPS open/response chain, requires the typed HTTP-302 failure mapping, and proves transport receives exactly one original request; lookalike HTTPS, HTTP, `file:`, and same-authority redirect targets never receive a second request or bearer. Exact `e0b0b4d4fff5b6ea88236a1e91dcd7dbb3be09b5` repairs the doctoring claim so direct-handler coverage is not mislabeled as production-chain proof. **Evidence / remaining condition.** The standalone fixture mechanism was executed locally against Python stdlib and produced one canonical request followed by terminal HTTP 302 for every hostile target. This is mechanism evidence, not repository acceptance. Final authority requires focused/full exact-tree GREEN, fresh exact-head Security/SAST/Python Security/CodeQL/runtime-quality checks, no unresolved actionable review, ordinary protected-main integration, and downstream consumer validation. No scanner suppression, redirect allowlist widening, provider fallback, workflow gate weakening, or credential-boundary change is included. + +## 2026-09-20 Strix trusted-binder consumer-isolation gap + +**Status:** Proposed on `ContextualWisdomLab/.github#2291`; exact-head hosted +checks, independent review, and protected-main integration remain required. + +**Context Map / owner.** The central `.github` CI bounded context owns +`strix_quick_gate.sh`, its evidence binder, and the executable gate harness. +Consumer repositories supply only the scan workspace through +`STRIX_REPO_ROOT`; they do not copy or own the binder. + +**Gap / root cause.** The production gate incorrectly resolved the trusted +binder from the consumer root. The first repair correctly moved that lookup to +`SCRIPT_DIR`, but its test harness copied only the gate and model helper into +the isolated fixture. The current PR head therefore still reproduced the same +missing-binder exit in the `success` scenario. Three assertions in that harness +also described the removed standalone `coverage-source-tree` job after its +responsibility moved into `validate-pr-metadata`. + +**Action / evidence.** The production gate resolves +`strix_evidence_binding.py` beside its trusted source. RED `191bd630` +requires the generic executable consumer fixture to contain no binder. GREEN +`ef1a8667` materializes the gate, model helper, and binder under a separate +`trusted-source/scripts/ci` directory, passes only the binder-free consumer +workspace through `STRIX_REPO_ROOT`, and invokes the trusted gate by its +absolute path. This makes the core executable fixture reproduce the production +owner boundary instead of proving a co-located copy. The full exact-tree Strix +harness and hosted checks remain the release authority; no provider, model, +timeout, severity, or consumer ownership boundary changes. + +**2026-09-26 exact-head RCA / owner integration.** Exact Python-security job +`107750961662` on head `1794626af3473ef23b9c2e678c3f06fd6c11636f` +found AnyIO 4.14.0's CVE-2026-63374, CVE-2026-64847, and CVE-2026-63349 in +`requirements-strix-ci-hashes.txt`; this branch had not adopted the central +source-to-hash AnyIO 4.14.2 repair from `ContextualWisdomLab/.github#2385`. +Exact CodeQL dispatch run `36204821293`, Python job `108319933572`, separately +produced one Medium+ SARIF result: +`py/incomplete-url-substring-sanitization` at +`tests/test_organization_commercial_readiness_loop_receipt_contract.py:60`. +The receipt test parsed the complete YAML endpoint block but then expressed the +expected receiver hostname through a subset/membership-style assertion that +CodeQL correctly rejects on URL-security surfaces. The ordinary two-parent +owner integration adopts #2385's AnyIO contract; the test now compares the +complete seven-entry endpoint set exactly. This strengthens the egress oracle: +an unexpected endpoint fails rather than being tolerated. No CodeQL query, +severity, SARIF gate, dependency audit, or endpoint allowlist is suppressed or +widened. Fresh exact-head hosted Python Security and CodeQL remain mandatory. + +**2026-09-27 Job Analysis bounded-context repair.** Orgmetra #63 exact head +`d88800a5ca3ca15df332e8def5e25064c46e4005` changes the HRIS-kernel Job +Analysis aggregate module, while the trusted scan workspace previously omitted +the unchanged product-owned authority context that explains its ownership +checks. Strix consequently reported a HIGH IDOR finding against an incomplete +workspace even though the Job Analysis API reconstructs the canonical owner and +authorizes resource fields before snapshot or PostgreSQL port access. Source- +first RED `1fd22f4e1e86d0ebfe5dab932697e95593c9ad10` adds an executable +pull-request-target fixture whose fake scanner refuses to run unless the changed +PR-head `job_analysis.py` is accompanied by exactly the five fixed trusted-base +collaborators (`auth.py`, `authorization.py`, `http.py`, `postgres.py`, and +`snapshot.py`); it also proves an unrelated administration module is excluded. +The minimal GREEN recognizes only that normalized trigger and emits those five +paths through the existing trusted-base context materializer. This is a bounded +CI-context repair, not a transfer of product domain truth: no Orgmetra source, +authorization order, persistence boundary, model/provider policy, severity, +timeout, or write capability changes. Exact-head hosted Strix acceptance, +independent review, ordinary protected-main integration, and a fresh Orgmetra +#63 consumer run remain mandatory before the false-positive gap is complete. diff --git a/requirements-strix-ci-hashes.txt b/requirements-strix-ci-hashes.txt index 9e705850b5..eb83beda17 100644 --- a/requirements-strix-ci-hashes.txt +++ b/requirements-strix-ci-hashes.txt @@ -140,9 +140,9 @@ annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 # via pydantic -anyio==4.14.0 \ - --hash=sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89 \ - --hash=sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9 +anyio==4.14.2 \ + --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \ + --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f # via # google-genai # gql diff --git a/requirements-strix-ci.txt b/requirements-strix-ci.txt index 19093441e9..50e8a05f9b 100644 --- a/requirements-strix-ci.txt +++ b/requirements-strix-ci.txt @@ -1,4 +1,5 @@ strix-agent==1.5.3 +anyio==4.14.2 openai[httpx2]==2.54.0 aiohttp==3.14.3 google-cloud-aiplatform==1.133.0 diff --git a/scripts/ci/actions_queue_health.py b/scripts/ci/actions_queue_health.py index bb73698551..e01145b8e0 100644 --- a/scripts/ci/actions_queue_health.py +++ b/scripts/ci/actions_queue_health.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 """Queue-health CLI with stable identity and audit-provenance guarantees. -The shared collector implementation lives in ``actions_queue_health_core.py``. -This entrypoint owns the consistency boundary that binds active-run evidence to +Shared parsing and reporting primitives live in ``actions_queue_health_core.py``. +This entrypoint owns collection and the consistency boundary that binds active-run evidence to a stable pull-request view, carries stable workflow identity, and exports the exact timestamp used for queue-age calculations. """ @@ -163,16 +163,7 @@ def collect_snapshot( ), ) for workflow_run in workflow_runs: - workflow_run_id = workflow_run.get("id") - if ( - isinstance(workflow_run_id, bool) - or not isinstance(workflow_run_id, int) - or workflow_run_id <= 0 - ): - raise QueueHealthError( - "workflow run id must be a positive integer" - ) - active_snapshot[workflow_run_id] = workflow_run + active_snapshot[workflow_run["id"]] = workflow_run active_snapshots.append(active_snapshot) first_snapshot, second_snapshot = active_snapshots @@ -242,16 +233,7 @@ def collect_snapshot( TERMINAL_DIAGNOSTIC_STATUSES ): continue - workflow_run_id = workflow_run.get("id") - if ( - isinstance(workflow_run_id, bool) - or not isinstance(workflow_run_id, int) - or workflow_run_id <= 0 - ): - raise QueueHealthError( - "workflow run id must be a positive integer" - ) - terminal_diagnostic_snapshot[workflow_run_id] = workflow_run + terminal_diagnostic_snapshot[workflow_run["id"]] = workflow_run for terminal_status in TARGET_TERMINAL_DIAGNOSTIC_STATUSES: target_workflow_runs = _list_payload( diff --git a/scripts/ci/actions_queue_health_core.py b/scripts/ci/actions_queue_health_core.py index db3e5570ba..50cb3e0793 100644 --- a/scripts/ci/actions_queue_health_core.py +++ b/scripts/ci/actions_queue_health_core.py @@ -112,6 +112,13 @@ def _list_payload( declared_total_counts.append(payload["total_count"]) if not isinstance(values, list) or not all(isinstance(value, dict) for value in values): raise QueueHealthError(f"GitHub response field {key!r} must be an array of objects") + if key == "workflow_runs" and any( + isinstance(value.get("id"), bool) + or not isinstance(value.get("id"), int) + or value["id"] <= 0 + for value in values + ): + raise QueueHealthError("workflow run id must be a positive integer") if isinstance(payload, dict) and PAGINATED_PAGES_KEY in payload: record_identities: list[tuple[str, int]] = [] for value in values: @@ -360,133 +367,6 @@ def _normalise_run(repository: str, run: dict[str, Any], jobs: list[dict[str, An } -def collect_snapshot( - repositories: Sequence[str], - *, - runner: Runner = subprocess.run, - generated_at: str | None = None, -) -> dict[str, Any]: - """Collect bounded queued/in-progress run and job data using read-only API calls.""" - validated = sorted({_repository_name(repository) for repository in repositories}) - if len(validated) != len(repositories): - raise QueueHealthError("collection repository list contains duplicates") - collected_repositories: list[dict[str, Any]] = [] - collection_errors: list[dict[str, str]] = [] - for repository in validated: - try: - metadata = github_json(f"repos/{repository}", runner=runner) - if not isinstance(metadata, dict): - raise QueueHealthError(f"repository metadata for {repository} is not an object") - pulls_endpoint = f"repos/{repository}/pulls?state=open&per_page={MAX_API_PAGE_SIZE}" - pull_requests = _list_payload( - github_json(pulls_endpoint, paginate=True, runner=runner), - "pulls", - max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES, - ) - normalized_pull_requests = sorted( - (_normalise_pull_request(item) for item in pull_requests), - key=lambda item: item["number"], - ) - except IncompletePullRequestIdentity: - time.sleep(PULL_REQUEST_RETRY_DELAY_SECONDS) - try: - retry_pull_requests = _list_payload( - github_json(pulls_endpoint, paginate=True, runner=runner), - "pulls", - max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES, - ) - normalized_pull_requests = sorted( - (_normalise_pull_request(item) for item in retry_pull_requests), - key=lambda item: item["number"], - ) - except QueueHealthError as retry_exc: - collection_errors.append( - { - "repository": repository, - "error": f"pull-request identity validation failed: {retry_exc}", - } - ) - continue - except QueueHealthError as exc: - collection_errors.append({"repository": repository, "error": str(exc)}) - continue - pull_requests_by_number = {item["number"]: item for item in normalized_pull_requests} - runs_by_id: dict[int, dict[str, Any]] = {} - try: - active_statuses = ("in_progress", "pending", "queued", "requested", "waiting") - snapshots: list[dict[int, dict[str, Any]]] = [] - for status_order in (active_statuses, tuple(reversed(active_statuses))): - snapshot: dict[int, dict[str, Any]] = {} - for status in status_order: - runs = _list_payload( - github_json( - f"repos/{repository}/actions/runs?status={status}" - f"&per_page={WORKFLOW_RUN_PAGE_SIZE}", - paginate=True, - max_pages=ACTIVE_RUN_MAX_API_PAGES, - runner=runner, - ), - "workflow_runs", - max_items=WORKFLOW_RUN_PAGE_SIZE * ACTIVE_RUN_MAX_API_PAGES, - ) - for run in runs: - run_id = run.get("id") - if isinstance(run_id, bool) or not isinstance(run_id, int) or run_id <= 0: - raise QueueHealthError("workflow run id must be a positive integer") - snapshot[run_id] = run - snapshots.append(snapshot) - first_snapshot, second_snapshot = snapshots - first_states = { - run_id: str(run.get("status") or "").upper() - for run_id, run in first_snapshot.items() - } - second_states = { - run_id: str(run.get("status") or "").upper() - for run_id, run in second_snapshot.items() - } - if first_states != second_states: - raise QueueHealthError("active workflow run snapshot changed during collection") - for run_id, run in second_snapshot.items(): - run_id = run.get("id") - candidate = _normalise_run(repository, run, []) - identity, _ = _run_identity(candidate, pull_requests_by_number) - if identity != "current_head" or candidate["status"] not in { - "IN_PROGRESS", - "WAITING", - }: - runs_by_id[run_id] = candidate - continue - jobs_payload = github_json( - f"repos/{repository}/actions/runs/{run_id}/jobs?per_page={MAX_API_PAGE_SIZE}", - paginate=True, - runner=runner, - ) - jobs = _list_payload( - jobs_payload, - "jobs", - max_items=MAX_API_PAGE_SIZE * MAX_API_PAGES, - ) - runs_by_id[run_id] = _normalise_run(repository, run, jobs) - except QueueHealthError as exc: - collection_errors.append({"repository": repository, "error": str(exc)}) - continue - collected_repositories.append( - { - "full_name": repository, - "default_branch": str(metadata.get("default_branch") or ""), - "pull_requests": normalized_pull_requests, - "runs": sorted(runs_by_id.values(), key=lambda item: item["id"]), - } - ) - timestamp = generated_at or datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") - parse_timestamp(timestamp) - return { - "generated_at": timestamp, - "repositories": collected_repositories, - "collection_errors": collection_errors, - } - - def load_snapshot(path: Path) -> dict[str, Any]: """Load a JSON snapshot for offline, deterministic report generation.""" try: @@ -822,34 +702,3 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: parser.add_argument("--queue-age-slo-seconds", type=int, default=DEFAULT_QUEUE_AGE_SLO_SECONDS) parser.add_argument("--now", help="Explicit timezone-aware evaluation time for deterministic reports") return parser.parse_args(argv) - - -def main(argv: Sequence[str] | None = None, *, stderr: TextIO = sys.stderr) -> int: - """Collect or load a snapshot, write reports, and return a stable CLI status.""" - args = parse_args(argv) - try: - snapshot = load_snapshot(args.snapshot) if args.snapshot else collect_snapshot(load_allowlist(args.allowlist)) - now = parse_timestamp(args.now) if args.now else datetime.now(timezone.utc) - report = build_report( - snapshot, - now=now, - queue_age_slo_seconds=args.queue_age_slo_seconds, - ) - write_reports(report, args.output_json, args.output_html) - except (OSError, QueueHealthError, ValueError) as exc: - print(f"ERROR: queue-health report failed: {exc}", file=stderr) - return 2 - breaches = report["summary"]["unassigned_slo_breached_count"] - if breaches: - print(f"::warning::Actions queue-health found {breaches} unassigned current-head SLO breach(es).") - print( - "QUEUE_HEALTH_RESULT=" - f"observed={report['summary']['observed_job_count']} " - f"pending={report['summary']['pending_job_count']} " - f"slo_breaches={breaches}" - ) - return 0 - - -if __name__ == "__main__": # pragma: no cover - exercised through the CLI tests. - raise SystemExit(main()) diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index c7d3667465..8b0ff8aa17 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -243,7 +243,7 @@ PY sanitize_remediation_evidence_claims() { local log_file="$1" local report_root="$2" - local binder="$REPO_ROOT/scripts/ci/strix_evidence_binding.py" + local binder="$SCRIPT_DIR/strix_evidence_binding.py" local report_file if [ ! -f "$binder" ] || [ -L "$binder" ]; then @@ -1384,6 +1384,7 @@ pull_request_scope_context_files() { local needs_backend_app_python=0 local needs_contextual_orchestrator_python=0 local needs_frontend_email_api_context=0 + local needs_orgmetra_job_analysis_authority_context=0 local needs_deployment_context=0 local changed_file normalized_changed_file for changed_file in "$@"; do @@ -1400,6 +1401,9 @@ pull_request_scope_context_files() { contextual_orchestrator/*.py) needs_contextual_orchestrator_python=1 ;; + packages/hris-kernel/src/orgmetra_hris_kernel/job_analysis.py) + needs_orgmetra_job_analysis_authority_context=1 + ;; # The app shell, email components, threading URL builder, and API client can # shape frontend email retrieval flows; include backend auth context with them. frontend/src/components/EmailDetail.tsx | frontend/src/components/EmailList.tsx | frontend/src/app/page.tsx | frontend/src/lib/api-client.ts | frontend/src/lib/email-threading.ts) @@ -1549,6 +1553,16 @@ backend/services/threading_service.py EOF fi + if [ "$needs_orgmetra_job_analysis_authority_context" -eq 1 ]; then + cat <<'EOF' +services/job-analysis-api/src/orgmetra_job_analysis_api/auth.py +services/job-analysis-api/src/orgmetra_job_analysis_api/authorization.py +services/job-analysis-api/src/orgmetra_job_analysis_api/http.py +services/job-analysis-api/src/orgmetra_job_analysis_api/postgres.py +services/job-analysis-api/src/orgmetra_job_analysis_api/snapshot.py +EOF + fi + if [ "$needs_deployment_context" -eq 1 ]; then cat <<'EOF' Dockerfile diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 150b9102b3..349d8059c5 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -17,6 +17,16 @@ FAILURES=0 TIMEOUT_TEST_PROCESS_SECONDS="${STRIX_TEST_PROCESS_TIMEOUT_SECONDS:-30}" TIMEOUT_TEST_FAKE_SLEEP_SECONDS="${STRIX_TEST_FAKE_SLEEP_SECONDS:-60}" +materialize_trusted_gate_fixture() { + local fixture_script_dir="$1" + + mkdir -p "$fixture_script_dir" + cp "$GATE_SCRIPT" "$fixture_script_dir/strix_quick_gate.sh" + cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$fixture_script_dir/strix_model_utils.sh" + cp "$REPO_ROOT/scripts/ci/strix_evidence_binding.py" "$fixture_script_dir/strix_evidence_binding.py" + chmod +x "$fixture_script_dir/strix_quick_gate.sh" +} + if ! [[ "$TIMEOUT_TEST_PROCESS_SECONDS" =~ ^[1-9][0-9]*$ ]] || ! [[ "$TIMEOUT_TEST_FAKE_SLEEP_SECONDS" =~ ^[1-9][0-9]*$ ]] || [ "$TIMEOUT_TEST_FAKE_SLEEP_SECONDS" -le "$TIMEOUT_TEST_PROCESS_SECONDS" ]; then @@ -492,6 +502,8 @@ assert_changed_file_membership_uses_cached_normalized_paths() { assert_strix_evidence_binding_contract() { assert_file_contains "$GATE_SCRIPT" "sanitize_remediation_evidence_claims" "strix gate sanitizes false already-applied remediation claims" assert_file_contains "$GATE_SCRIPT" 'scripts/ci/strix_evidence_binding.py' "strix gate binds remediation evidence through the tested Python binder" + assert_file_contains "$GATE_SCRIPT" 'local binder="$SCRIPT_DIR/strix_evidence_binding.py"' "strix gate resolves its trusted evidence binder from the central script directory" + assert_file_not_contains "$GATE_SCRIPT" 'local binder="$REPO_ROOT/scripts/ci/strix_evidence_binding.py"' "strix gate never resolves the trusted binder from the consumer repository root" assert_file_contains "$GATE_SCRIPT" "evidence_scope=pr_delta" "strix gate labels PR-delta findings with authenticated provenance" assert_file_contains "$GATE_SCRIPT" "evidence_scope=repository_baseline" "strix gate labels unchanged-path findings as repository_baseline" assert_file_contains "$REPO_ROOT/scripts/ci/strix_evidence_binding.py" 'PR_DELTA = "pr_delta"' "strix evidence binder defines pr_delta scope" @@ -640,7 +652,7 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_not_contains "$workflow_file" 'ref: ${{ github.workflow_sha }}' "opencode trusted checkout never bypasses the validated ref output" assert_file_contains "$workflow_file" "target_repository:" "opencode repository_dispatch can target a repository whose PR does not inherit required workflows" assert_file_contains "$workflow_file" "Materialize pull request merge tree for coverage measurement" "opencode coverage measures the PR merge tree instead of exposing secrets to untrusted checkout actions" - assert_file_contains "$workflow_file" 'TARGET_REPOSITORY: ${{ needs.validate-pr-metadata.outputs.target_repository }}' "opencode coverage fetches exact validated base/head commits from the target repository" + assert_file_contains "$workflow_file" 'TARGET_REPOSITORY: ${{ steps.validate.outputs.target_repository }}' "opencode coverage fetches exact validated base/head commits from the target repository" assert_file_contains "$workflow_file" "Exchange OpenCode app token for target repository review reads" "opencode review can read private target repositories through the OpenCode app token before materializing review data" assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ steps.review_read_app_token.outputs.token || secrets.OPENCODE_APPROVE_TOKEN || github.token }}' "opencode materialization prefers the OpenCode app token for private target repository reads" assert_file_contains "$workflow_file" '[ "${GH_REPOSITORY:-}" != "${GITHUB_REPOSITORY:-}" ]' "opencode approval uses the app token for target-repository check lookup" @@ -968,13 +980,14 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "exponential backoff" "opencode model retry paths use exponential backoff instead of fixed sleeps" assert_file_contains "$workflow_file" '"enabled_providers": ["contextual-orchestrator"]' "opencode review keeps the generated provider set gateway-only" assert_file_contains "$workflow_file" '"model": "contextual-orchestrator/orchestrator/free"' "opencode review keeps the generated model on orchestrator/free" - assert_file_contains "$workflow_file" "coverage-source-tree:" "opencode workflow materializes coverage source before running PR-head tests" + assert_file_contains "$workflow_file" "validate-pr-metadata:" "opencode workflow validates metadata and materializes coverage source before running PR-head tests" assert_file_contains "$workflow_file" "coverage-evidence:" "opencode workflow measures coverage before review" assert_file_contains "$workflow_file" "Materialize pull request merge tree for coverage measurement" "required OpenCode reviews measure coverage instead of approving skipped coverage evidence" assert_file_contains "$workflow_file" "Exchange OpenCode app token for target repository coverage reads" "coverage source materialization can read private target repositories during central manual dispatch" assert_file_contains "$workflow_file" "Upload materialized pull request merge tree" "coverage source materialization passes only a prepared merge tree artifact to the PR-head coverage job" assert_file_contains "$workflow_file" "Download materialized pull request merge tree" "coverage evidence consumes the prepared merge tree artifact without target-repository credentials" - assert_file_contains "$workflow_file" "Report coverage source materialization failure" "coverage evidence logs source materialization failures as the coverage blocker" + assert_file_contains "$workflow_file" "Coverage fetch could not authenticate" "coverage source materialization reports target-repository read failures" + assert_file_contains "$workflow_file" "Coverage merge tree could not be materialized" "coverage source materialization reports merge failures" local coverage_merge_tree_step coverage_merge_tree_step="$( awk ' @@ -3291,12 +3304,18 @@ run_gate_case() { local untrusted_bin_dir="$tmp_dir/untrusted-bin" local workspace_dir="$tmp_dir/workspace" local repo_root_dir="$workspace_dir/smart-crawling-server" - mkdir -p "$bin_dir" "$untrusted_bin_dir" "$repo_root_dir/src" - mkdir -p "$repo_root_dir/scripts/ci" - local gate_under_test="$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$GATE_SCRIPT" "$gate_under_test" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$gate_under_test" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + mkdir -p "$bin_dir" "$untrusted_bin_dir" "$repo_root_dir/src" "$repo_root_dir/scripts/ci" + local gate_under_test="$trusted_script_dir/strix_quick_gate.sh" + materialize_trusted_gate_fixture "$trusted_script_dir" + if [ "$scenario" = "pr-changed-scope-includes-ci-dependency" ]; then + # These are consumer source files under scan, not the trusted runtime. + cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" + fi + if [ -e "$repo_root_dir/scripts/ci/strix_evidence_binding.py" ]; then + record_failure "scenario=$scenario consumer fixture must not own the trusted evidence binder" + fi local fake_strix="$bin_dir/strix" local path_hijack_log="$tmp_dir/path-hijack.log" cat >"$untrusted_bin_dir/strix" <<'EOF' @@ -5779,6 +5798,7 @@ PY STRIX_EXECUTABLE_PATH="$fake_strix" FAKE_STRIX_PATH_HIJACK_LOG="$path_hijack_log" STRIX_INPUT_FILE_ROOT="$tmp_dir" + STRIX_REPO_ROOT="$repo_root_dir" GITHUB_EVENT_NAME="" GITHUB_EVENT_PATH="" FAKE_STRIX_SCENARIO="$scenario" @@ -5947,7 +5967,7 @@ PY -u STRIX_OPENAI_FALLBACK_KEY_FILE \ -u STRIX_OPENAI_FALLBACK_API_BASE_FILE \ "${env_cmd[@]}" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + bash "$gate_under_test" >"$output_log" 2>&1 ) local rc=$? set -e @@ -6929,6 +6949,9 @@ run_filtered_gate_case_if_requested() { "1" \ "Container build manifest changed; materialized full PR-head blob scope" ;; + pull-request-target-job-analysis-authority-context) + run_pull_request_target_job_analysis_authority_context_scope_case + ;; repository-dispatch-pr-scope-uses-head-blob) run_pull_request_target_head_scope_case \ "repository-dispatch-pr-scope-uses-head-blob" \ @@ -7024,9 +7047,8 @@ run_pull_request_target_head_scope_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -7152,7 +7174,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="$target_path" \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7172,9 +7194,8 @@ run_pull_request_target_plaintext_runner_token_fails_closed_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -7272,7 +7293,7 @@ EOS LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7294,9 +7315,8 @@ run_pull_request_target_bounded_head_context_scope_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -7382,7 +7402,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7399,9 +7419,8 @@ run_pull_request_target_changed_context_scope_uses_pr_head_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -7525,7 +7544,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7561,7 +7580,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) rc=$? set -e @@ -7578,9 +7597,8 @@ run_pull_request_target_changed_backend_context_scope_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -7812,7 +7830,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -7837,9 +7855,8 @@ run_pull_request_target_frontend_email_context_scope_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -8008,7 +8025,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8019,6 +8036,143 @@ EOF rm -rf "$tmp_dir" } +run_pull_request_target_job_analysis_authority_context_scope_case() { + local changed_file="packages/hris-kernel/src/orgmetra_hris_kernel/job_analysis.py" + local case_name="pull-request-target-job-analysis-authority-context" + local tmp_dir + tmp_dir="$(mktemp -d)" + local bin_dir="$tmp_dir/bin" + local repo_root_dir="$tmp_dir/repo" + mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" + + local context_files=( + "services/job-analysis-api/src/orgmetra_job_analysis_api/auth.py" + "services/job-analysis-api/src/orgmetra_job_analysis_api/authorization.py" + "services/job-analysis-api/src/orgmetra_job_analysis_api/http.py" + "services/job-analysis-api/src/orgmetra_job_analysis_api/postgres.py" + "services/job-analysis-api/src/orgmetra_job_analysis_api/snapshot.py" + ) + local context_files_text + context_files_text="$(printf '%s\n' "${context_files[@]}")" + local fake_strix="$bin_dir/strix" + local output_log="$tmp_dir/output.log" + local strix_llm_file="$tmp_dir/strix_llm.txt" + local llm_api_key_file="$tmp_dir/llm_api_key.txt" + + cat >"$fake_strix" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail + +target_path="" +while [ "$#" -gt 0 ]; do + if [ "$1" = "-t" ] && [ "$#" -ge 2 ]; then + target_path="$2" + break + fi + shift +done + +changed_file="$target_path/${FAKE_STRIX_EXPECTED_CHANGED_FILE:?}" +if ! grep -Fq -- 'HEAD_JOB_ANALYSIS_KERNEL_SHOULD_BE_SCANNED' "$changed_file"; then + echo "Error: Job Analysis kernel PR-head content was not scanned" >&2 + exit 93 +fi + +while IFS= read -r context_file; do + [ -n "$context_file" ] || continue + context_path="$target_path/$context_file" + if [ ! -f "$context_path" ]; then + echo "Error: Job Analysis authorization context missing: $context_file" >&2 + exit 94 + fi + if ! grep -Fqx -- "BASE_JOB_ANALYSIS_AUTHORITY_CONTEXT:$context_file" "$context_path"; then + echo "Error: Job Analysis context did not use trusted base content: $context_file" >&2 + exit 95 + fi + if grep -Fq -- "HEAD_JOB_ANALYSIS_CONTEXT_SHOULD_NOT_BE_SCANNED:$context_file" "$context_path"; then + echo "Error: unchanged Job Analysis context leaked PR-head content: $context_file" >&2 + exit 96 + fi +done <<<"${FAKE_STRIX_EXPECTED_CONTEXT_FILES:?}" + +if [ -e "$target_path/services/job-analysis-api/src/orgmetra_job_analysis_api/unrelated_admin.py" ]; then + echo "Error: unrelated service source leaked into bounded Job Analysis scope" >&2 + exit 97 +fi + +echo "scan ok with trusted Job Analysis authorization and persistence context" +EOF + chmod +x "$fake_strix" + printf '%s' 'gemini/test-model' >"$strix_llm_file" + printf '%s' 'dummy' >"$llm_api_key_file" + + ( + cd "$repo_root_dir" + git init -q + git config user.name 'Strix Test' + git config user.email 'strix-test@example.invalid' + local context_file + for context_file in "${context_files[@]}"; do + mkdir -p "$(dirname -- "$context_file")" + printf 'BASE_JOB_ANALYSIS_AUTHORITY_CONTEXT:%s\n' "$context_file" >"$context_file" + done + mkdir -p "$(dirname -- "$changed_file")" \ + services/job-analysis-api/src/orgmetra_job_analysis_api + printf '%s\n' 'BASE_JOB_ANALYSIS_KERNEL_SHOULD_NOT_BE_SCANNED' >"$changed_file" + printf '%s\n' 'UNRELATED_SERVICE_SHOULD_NOT_BE_SCANNED' \ + >services/job-analysis-api/src/orgmetra_job_analysis_api/unrelated_admin.py + git add . + git commit -qm 'base commit' + ) + local base_sha + base_sha="$(git -C "$repo_root_dir" rev-parse HEAD)" + ( + cd "$repo_root_dir" + local context_file + for context_file in "${context_files[@]}"; do + printf 'HEAD_JOB_ANALYSIS_CONTEXT_SHOULD_NOT_BE_SCANNED:%s\n' "$context_file" >"$context_file" + done + printf '%s\n' 'HEAD_JOB_ANALYSIS_KERNEL_SHOULD_BE_SCANNED' >"$changed_file" + git add . + git commit -qm 'head commit' + ) + local head_sha + head_sha="$(git -C "$repo_root_dir" rev-parse HEAD)" + git -C "$repo_root_dir" checkout -q "$base_sha" + + set +e + ( + cd "$repo_root_dir" + env -u GITHUB_EVENT_PATH \ + PATH="$bin_dir:$PATH" \ + STRIX_EXECUTABLE_PATH="$bin_dir/strix" \ + STRIX_INPUT_FILE_ROOT="$tmp_dir" \ + GITHUB_EVENT_NAME="pull_request_target" \ + PR_BASE_SHA="$base_sha" \ + PR_HEAD_SHA="$head_sha" \ + STRIX_TEST_CHANGED_FILES_OVERRIDE="$changed_file" \ + STRIX_DISABLE_PR_SCOPING="0" \ + FAKE_STRIX_EXPECTED_CHANGED_FILE="$changed_file" \ + FAKE_STRIX_EXPECTED_CONTEXT_FILES="$context_files_text" \ + STRIX_LLM_FILE="$strix_llm_file" \ + LLM_API_KEY_FILE="$llm_api_key_file" \ + STRIX_TARGET_PATH="." \ + STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 + ) + local rc=$? + set -e + + assert_equals "0" "$rc" "case=$case_name exit code" + assert_file_contains "$output_log" \ + "scan ok with trusted Job Analysis authorization and persistence context" \ + "case=$case_name output" + + rm -rf "$tmp_dir" +} + run_pull_request_target_shallow_head_merge_base_fallback_case() { local tmp_dir tmp_dir="$(mktemp -d)" @@ -8027,9 +8181,8 @@ run_pull_request_target_shallow_head_merge_base_fallback_case() { local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$origin_repo_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -8103,7 +8256,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8142,9 +8295,8 @@ run_pull_request_target_aborts_on_pr_head_blob_failure_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local real_git real_git="$(command -v git)" @@ -8241,7 +8393,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8266,9 +8418,8 @@ run_pull_request_target_rejects_invalid_sha_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local call_log="$tmp_dir/calls.log" @@ -8333,7 +8484,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8359,9 +8510,8 @@ run_pull_request_target_irregular_head_entry_fails_closed_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local call_log="$tmp_dir/calls.log" @@ -8420,7 +8570,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8442,9 +8592,8 @@ run_pull_request_target_gitlink_is_explicitly_skipped_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local call_log="$tmp_dir/calls.log" @@ -8494,7 +8643,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8524,9 +8673,8 @@ run_full_head_scope_skips_gitlink_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" @@ -8617,7 +8765,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8638,9 +8786,8 @@ run_pull_request_target_rejects_unsafe_changed_path_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/repo" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local call_log="$tmp_dir/calls.log" @@ -8684,7 +8831,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ STRIX_TARGET_PATH="." \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8730,9 +8877,8 @@ run_timeout_cleanup_case() { local workspace_dir="$tmp_dir/workspace" local repo_root_dir="$workspace_dir/smart-crawling-server" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local child_pid_file="$tmp_dir/child.pid" local output_log="$tmp_dir/output.log" @@ -8768,7 +8914,7 @@ EOF STRIX_VERTEX_FALLBACK_MODELS="" \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ STRIX_TARGET_PATH="." \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8812,9 +8958,8 @@ run_vertex_model_ignores_untrusted_llm_api_base_file_case() { local llm_api_base_file="$outside_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" "$allowed_input_dir" "$outside_dir" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" cat >"$fake_strix" <<'EOF' #!/usr/bin/env bash @@ -8845,7 +8990,7 @@ EOF STRIX_LLM_FILE="$strix_llm_file" \ LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -8864,9 +9009,8 @@ run_total_timeout_case() { local workspace_dir="$tmp_dir/workspace" local repo_root_dir="$workspace_dir/smart-crawling-server" mkdir -p "$bin_dir" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local output_log="$tmp_dir/output.log" local call_count_file="$tmp_dir/calls.log" @@ -8902,7 +9046,7 @@ EOF STRIX_TRANSIENT_RETRY_BACKOFF_SECONDS="0" \ STRIX_REPORTS_DIR="$repo_root_dir/strix_runs" \ STRIX_TARGET_PATH="." \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9191,9 +9335,8 @@ run_llm_api_base_file_outside_input_root_fails_closed_case() { local llm_api_base_file="$outside_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" "$allowed_input_dir" "$outside_dir" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" cat >"$fake_strix" <<'EOF' #!/usr/bin/env bash @@ -9218,7 +9361,7 @@ EOF STRIX_LLM_FILE="$strix_llm_file" \ LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9246,9 +9389,8 @@ run_pr_scoped_llm_api_base_file_config_failure_exits_2_case() { local llm_api_base_file="$outside_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" "$repo_root_dir/src" "$allowed_input_dir" "$outside_dir" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" printf '%s\n' 'print("one")' >"$repo_root_dir/src/one.py" printf '%s\n' 'print("two")' >"$repo_root_dir/src/two.py" @@ -9277,7 +9419,7 @@ EOF STRIX_LLM_FILE="$strix_llm_file" \ LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9307,9 +9449,8 @@ run_required_input_file_outside_input_root_fails_closed_case() { local outside_file="$outside_dir/${file_env}.txt" mkdir -p "$repo_root_dir/scripts/ci" "$allowed_input_dir" "$outside_dir" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" cat >"$fake_strix" <<'EOF' #!/usr/bin/env bash @@ -9349,7 +9490,7 @@ EOF STRIX_LLM_FILE="$strix_llm_file" \ LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9377,9 +9518,8 @@ run_input_file_root_override_takes_precedence_over_runner_temp_case() { local llm_api_base_file="$explicit_input_root/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" "$explicit_input_root" "$inherited_runner_temp" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" cat >"$fake_strix" <<'EOF' #!/usr/bin/env bash @@ -9405,7 +9545,7 @@ EOF STRIX_LLM_FILE="$strix_llm_file" \ LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9431,9 +9571,8 @@ run_stale_report_case() { local llm_api_base_file="$tmp_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" mkdir -p "$stale_report_dir" cat >"$stale_report_dir/vuln-0001.md" <<'EOF' @@ -9463,7 +9602,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ STRIX_REPORTS_DIR="strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9486,9 +9625,8 @@ run_symlink_report_case() { local llm_api_base_file="$tmp_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" mkdir -p "$external_report_dir" "$repo_root_dir/strix_runs" cat >"$external_report_dir/vuln-0001.md" <<'EOF' @@ -9519,7 +9657,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ STRIX_REPORTS_DIR="strix_runs" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9542,9 +9680,8 @@ run_unsafe_target_path_case() { local llm_api_base_file="$tmp_dir/llm_api_base.txt" mkdir -p "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" cat >"$fake_strix" <<'EOF' #!/usr/bin/env bash @@ -9570,7 +9707,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ STRIX_TARGET_PATH="../../../../../etc/passwd" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9590,9 +9727,8 @@ run_absolute_outside_target_path_case() { local bin_dir="$tmp_dir/bin" local repo_root_dir="$tmp_dir/workspace/smart-crawling-server" mkdir -p "$bin_dir" "$repo_root_dir/src" "$repo_root_dir/scripts/ci" - cp "$GATE_SCRIPT" "$repo_root_dir/scripts/ci/strix_quick_gate.sh" - cp "$REPO_ROOT/scripts/ci/strix_model_utils.sh" "$repo_root_dir/scripts/ci/strix_model_utils.sh" - chmod +x "$repo_root_dir/scripts/ci/strix_quick_gate.sh" + local trusted_script_dir="$tmp_dir/trusted-source/scripts/ci" + materialize_trusted_gate_fixture "$trusted_script_dir" local fake_strix="$bin_dir/strix" local call_log="$tmp_dir/calls.log" local output_log="$tmp_dir/output.log" @@ -9622,7 +9758,7 @@ EOF LLM_API_KEY_FILE="$llm_api_key_file" \ LLM_API_BASE_FILE="$llm_api_base_file" \ STRIX_TARGET_PATH="$tmp_dir/strix-pr-scope.attacker" \ - bash "./scripts/ci/strix_quick_gate.sh" >"$output_log" 2>&1 + STRIX_REPO_ROOT="$repo_root_dir" bash "$trusted_script_dir/strix_quick_gate.sh" >"$output_log" 2>&1 ) local rc=$? set -e @@ -9839,6 +9975,8 @@ run_pull_request_target_frontend_email_context_scope_case \ run_pull_request_target_frontend_email_context_scope_case \ "frontend/src/lib/email-threading.ts" +run_pull_request_target_job_analysis_authority_context_scope_case + run_pull_request_target_aborts_on_pr_head_blob_failure_case \ "pull-request-target-added-file-pr-head-blob-read-failure" \ "src/new_module.py" \ diff --git a/tests/test_actions_queue_health_cancelled_before_runner.py b/tests/test_actions_queue_health_cancelled_before_runner.py index 1827275f68..850f3bd866 100644 --- a/tests/test_actions_queue_health_cancelled_before_runner.py +++ b/tests/test_actions_queue_health_cancelled_before_runner.py @@ -17,7 +17,7 @@ SPEC.loader.exec_module(queue_health) -def test_collect_snapshot_classifies_cancelled_job_before_runner_assignment() -> None: +def test_collect_snapshot_classifies_cancelled_job_before_runner_assignment(monkeypatch) -> None: """A cancelled current-head job with no runner or steps stays explicit evidence.""" repository_name = "owner/repo" pull_request = { @@ -144,6 +144,9 @@ def runner(args: list[str], **_: object) -> CompletedProcess[str]: "cancelled_before_runner_assignment" ) assert report["summary"]["cancelled_before_runner_assignment_count"] == 1 + actions = list(report["summary"]["external_actions"]) + monkeypatch.setattr(queue_health, "_CORE_BUILD_REPORT", lambda *_args, **_kwargs: report) + assert queue_health.build_report(snapshot)["summary"]["external_actions"] == actions def test_collect_snapshot_retains_cancelled_pull_request_target_current_head() -> None: diff --git a/tests/test_actions_queue_health_post_evidence_retry.py b/tests/test_actions_queue_health_post_evidence_retry.py index bc266fa96f..ae1c619cd0 100644 --- a/tests/test_actions_queue_health_post_evidence_retry.py +++ b/tests/test_actions_queue_health_post_evidence_retry.py @@ -5,6 +5,8 @@ from pathlib import Path from subprocess import CompletedProcess +import pytest + ROOT = Path(__file__).resolve().parents[1] MODULE_PATH = ROOT / "scripts/ci/actions_queue_health.py" @@ -82,3 +84,88 @@ def test_post_evidence_identity_read_fails_closed_after_retry_remains_incomplete assert len(snapshot["collection_errors"]) == 1 assert snapshot["collection_errors"][0]["repository"] == "owner/repo" assert "pull-request identity validation failed" in snapshot["collection_errors"][0]["error"] + +def _run(run_id: int, workflow_id: int) -> dict: + """Return one linked run for terminal-filter boundary tests.""" + return { + "id": run_id, + "workflow_id": workflow_id, + "name": "required-check", + "event": "pull_request", + "status": "queued", + "head_sha": "head", + "pull_requests": [{"number": 1, "head": {"sha": "head"}}], + } + + +@pytest.mark.parametrize( + ("active_runs", "completed_runs", "target_runs", "expected_error"), + [ + ([{"id": 0, "status": "queued"}], [], [], "workflow run id"), + ([], [{**_run(8, 501), "status": "completed", "conclusion": "failure", "id": 0}], [], "workflow run id"), + ([], [{**_run(8, 501), "status": "completed", "conclusion": "success"}], [], None), + ([], [], [{**_run(8, 501), "status": "completed", "conclusion": "cancelled", "pull_requests": []}], None), + ], +) +def test_collector_rejects_invalid_run_ids_and_ignores_unrelated_terminal_runs( + active_runs: list[dict], completed_runs: list[dict], target_runs: list[dict], expected_error: str | None, +) -> None: + """Bad identities fail closed; unrelated terminal runs do not become current-head evidence.""" + def runner(args: list[str], **_kwargs: object) -> CompletedProcess[str]: + path = args[-1] + if path == "repos/owner/repo": + payload: object = {"default_branch": "main"} + elif path == "repos/owner/repo/pulls?state=open&per_page=100": + payload = [_pull()] + elif "status=completed&head_sha=" in path: + payload = completed_runs + elif "status=cancelled&event=pull_request_target" in path: + payload = target_runs + elif "status=queued" in path: + payload = active_runs + elif "/actions/runs?status=" in path: + payload = [] + else: + raise AssertionError(f"unexpected endpoint: {path}") + return CompletedProcess(args, 0, json.dumps(payload), "") + + snapshot = queue_health.collect_snapshot( + ["owner/repo"], runner=runner, generated_at="2026-09-02T00:00:00Z" + ) + if expected_error: + assert snapshot["repositories"] == [] + assert expected_error in snapshot["collection_errors"][0]["error"] + else: + assert snapshot["collection_errors"] == [] + assert snapshot["repositories"][0]["runs"] == [] + + +def test_final_pull_identity_retry_failure_is_bounded(monkeypatch: pytest.MonkeyPatch) -> None: + """A repeatedly incomplete final PR view cannot certify current-head evidence.""" + monkeypatch.setattr(queue_health.time, "sleep", lambda _seconds: None) + pull_reads = 0 + + def runner(args: list[str], **_kwargs: object) -> CompletedProcess[str]: + nonlocal pull_reads + path = args[-1] + if path == "repos/owner/repo": + payload: object = {"default_branch": "main"} + elif path == "repos/owner/repo/pulls?state=open&per_page=100": + pull_reads += 1 + payload = [_pull()] if pull_reads == 1 else [{**_pull(), "head": {"sha": ""}}] + elif "/actions/runs?status=" in path: + payload = [] + else: + raise AssertionError(f"unexpected endpoint: {path}") + return CompletedProcess(args, 0, json.dumps(payload), "") + + snapshot = queue_health.collect_snapshot(["owner/repo"], runner=runner) + assert pull_reads == 3 + assert snapshot["repositories"] == [] + assert "pull-request identity validation failed" in snapshot["collection_errors"][0]["error"] + + +def test_core_run_normalization_rejects_non_object() -> None: + """A malformed workflow-run payload cannot be classified as a real run.""" + with pytest.raises(queue_health.QueueHealthError, match="workflow run entry must be an object"): + queue_health._CORE_NORMALISE_RUN("owner/repo", None, []) diff --git a/tests/test_actions_queue_health_snapshot_consistency.py b/tests/test_actions_queue_health_snapshot_consistency.py index b9711a09dd..8e939e2fc9 100644 --- a/tests/test_actions_queue_health_snapshot_consistency.py +++ b/tests/test_actions_queue_health_snapshot_consistency.py @@ -192,4 +192,5 @@ def test_invalid_present_workflow_id_fails_closed() -> None: def test_queue_health_workflow_does_not_grant_unused_pull_request_permission() -> None: """The scheduler token keeps only permissions used outside the cross-repository token.""" workflow = (ROOT / ".github/workflows/actions-queue-health.yml").read_text(encoding="utf-8") - assert "pull-requests: read" not in workflow + assert "\n pull-requests: read\n" not in workflow + assert "\n pull-requests: read\n" not in workflow diff --git a/tests/test_actions_queue_health_terminal_preexecution.py b/tests/test_actions_queue_health_terminal_preexecution.py index 05237ba340..c86c435e30 100644 --- a/tests/test_actions_queue_health_terminal_preexecution.py +++ b/tests/test_actions_queue_health_terminal_preexecution.py @@ -57,7 +57,7 @@ def _terminal_failure_job() -> dict: } -def test_terminal_preexecution_failure_survives_collection_and_is_not_product_failure() -> None: +def test_terminal_preexecution_failure_survives_collection_and_is_not_product_failure(monkeypatch) -> None: """Keep failed zero-step jobs as explicit non-passing admission evidence.""" failed_run = _terminal_failure_run() failed_job = _terminal_failure_job() @@ -106,3 +106,6 @@ def runner(args: list[str], **_: object) -> CompletedProcess[str]: assert row["recommended_action"] == "inspect_actions_control_plane_without_leaf_bypass" assert report["summary"]["terminal_pre_execution_failure_count"] == 1 assert report["summary"]["terminal_job_count"] == 1 + actions = list(report["summary"]["external_actions"]) + monkeypatch.setattr(queue_health, "_CORE_BUILD_REPORT", lambda *_args, **_kwargs: report) + assert queue_health.build_report(snapshot)["summary"]["external_actions"] == actions diff --git a/tests/test_codeql_scan_dispatch_ghas_credential_contract.py b/tests/test_codeql_scan_dispatch_ghas_credential_contract.py new file mode 100644 index 0000000000..d92bf3ef2c --- /dev/null +++ b/tests/test_codeql_scan_dispatch_ghas_credential_contract.py @@ -0,0 +1,111 @@ +"""Credential-routing contract for cross-repository GHAS CodeQL analysis reads.""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +from tests.test_opencode_workflow_shell_syntax import _extract_run_block + + +REPO_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_PATH = REPO_ROOT / ".github/workflows/codeql-scan-dispatch.yml" +SELECT_STEP_NAME = "Select target CodeQL analysis-read credential" +VERIFY_STEP_NAME = "Verify GHAS base/head CodeQL configuration identity" + + +def _run_selector(tmp_path: Path, *, succeeding_token: str | None) -> subprocess.CompletedProcess[str]: + """Execute the extracted selector with fixed Bash identity and a fake ``gh`` boundary.""" + assert Path("/bin/bash").is_file(), "/bin/bash is required to run this workflow-contract test" + + workflow_text = WORKFLOW_PATH.read_text(encoding="utf-8") + script = _extract_run_block(workflow_text, SELECT_STEP_NAME) + + fake_bin = tmp_path / "bin" + fake_bin.mkdir(parents=True) + call_log = tmp_path / "calls" + fake_gh = fake_bin / "gh" + fake_gh.write_text( + "#!/usr/bin/env bash\n" + "set -euo pipefail\n" + 'printf \'%s\\n\' "${GH_TOKEN:-}" >>"$FAKE_CALL_LOG"\n' + 'test "$1" = api\n' + 'test "$#" -eq 6\n' + 'test "$2" = -H\n' + 'test "$3" = "Accept: application/vnd.github+json"\n' + 'test "$4" = -H\n' + 'test "$5" = "X-GitHub-Api-Version: 2022-11-28"\n' + 'test "$6" = "repos/ContextualWisdomLab/OriginWeave/code-scanning/analyses?per_page=1&tool_name=CodeQL"\n' + 'if [ -n "${SUCCEEDING_TOKEN:-}" ] && [ "${GH_TOKEN:-}" = "$SUCCEEDING_TOKEN" ]; then\n' + " printf '[]\\n'\n" + " exit 0\n" + "fi\n" + "exit 1\n", + encoding="utf-8", + ) + fake_gh.chmod(0o755) + + output = tmp_path / "github-output" + env = { + **os.environ, + "PATH": f"{fake_bin}:{os.environ['PATH']}", + "GITHUB_OUTPUT": str(output), + "FAKE_CALL_LOG": str(call_log), + "SUCCEEDING_TOKEN": succeeding_token or "", + "TARGET_REPOSITORY": "ContextualWisdomLab/OriginWeave", + "TARGET_APP_TOKEN": "content-token", + "PR_REVIEW_MERGE_TOKEN": "security-token", + "OPENCODE_APPROVE_TOKEN": "approve-token", + "WORKFLOW_TOKEN": "workflow-token", + } + result = subprocess.run( + ["/bin/bash"], + input=script, + text=True, + capture_output=True, + check=False, + env=env, + ) + result.output_path = output # type: ignore[attr-defined] + result.call_log = call_log # type: ignore[attr-defined] + return result + + +def test_ghas_analysis_read_falls_through_content_only_target_app_token(tmp_path: Path) -> None: + """A content-capable app token must not mask a later GHAS-capable credential.""" + result = _run_selector(tmp_path, succeeding_token="security-token") + + assert result.returncode == 0, result.stdout + result.stderr + output = result.output_path.read_text(encoding="utf-8") + assert "token=security-token" in output + assert "source=pr-review-merge-token" in output + assert result.call_log.read_text(encoding="utf-8").splitlines() == [ + "content-token", + "security-token", + ] + + +def test_ghas_analysis_read_fails_closed_when_no_candidate_can_read_target(tmp_path: Path) -> None: + """Missing target code-scanning read authority must remain a hard prerequisite failure.""" + result = _run_selector(tmp_path, succeeding_token=None) + + assert result.returncode != 0 + assert "no configured credential can read target CodeQL analyses" in result.stdout + assert result.call_log.read_text(encoding="utf-8").splitlines() == [ + "content-token", + "security-token", + "approve-token", + "workflow-token", + ] + + +def test_ghas_identity_step_consumes_only_probed_analysis_read_token() -> None: + """The identity proof must not repeat the unprobed content-token precedence chain.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + verify_script = _extract_run_block(workflow, VERIFY_STEP_NAME) + verify_prefix = workflow.split(f" - name: {VERIFY_STEP_NAME}\n", 1)[1].split(" run: |", 1)[0] + + assert "GH_TOKEN: ${{ steps.ghas_analysis_token.outputs.token }}" in verify_prefix + assert "steps.target_app_token.outputs.token ||" not in verify_prefix + assert "codeql_ghas_configuration_identity.py" in verify_script diff --git a/tests/test_noema_document_review_context.py b/tests/test_noema_document_review_context.py index e6ec2e6d70..ffed1c8961 100644 --- a/tests/test_noema_document_review_context.py +++ b/tests/test_noema_document_review_context.py @@ -176,6 +176,13 @@ def test_forbidden_docx_entities_are_explicitly_rejected(): document.extract_review_document("docs/entity.docx", _docx_entity_bytes()) +def test_invalid_github_base64_content_fails_closed(monkeypatch): + """Malformed GitHub file data must not reach the document reader.""" + monkeypatch.setattr(noema, "run", lambda _args, stdin=None: "not/base64!") + with pytest.raises(RuntimeError, match="malformed base64"): + noema.fetch_file_content_at_ref("owner/repo", "docs/review.docx", "head") + + def test_hwp_reader_contract_is_local_and_fail_closed(monkeypatch): """HWP/HWPX use the configured local adapter and reject failed readers.""" monkeypatch.setenv(document.HWP_READER_ENV, "/trusted/hwp-mcp-source") diff --git a/tests/test_noema_review_document_boundaries.py b/tests/test_noema_review_document_boundaries.py new file mode 100644 index 0000000000..5680252b3f --- /dev/null +++ b/tests/test_noema_review_document_boundaries.py @@ -0,0 +1,128 @@ +"""Exercise document-reader failure boundaries used by protected review.""" + +from __future__ import annotations + +import io +import runpy +import sys +import zipfile +from pathlib import Path +from subprocess import CompletedProcess + +import pytest + +from scripts.ci import noema_review_document as document + + +def _docx(xml: str | None, *, extra_entries: int = 0) -> bytes: + """Build a small DOCX archive with optional missing document XML.""" + output = io.BytesIO() + with zipfile.ZipFile(output, "w") as archive: + if xml is not None: + archive.writestr("word/document.xml", xml) + for index in range(extra_entries): + archive.writestr(f"extra-{index}", "x") + return output.getvalue() + + +def _body(content: str) -> str: + """Wrap Word body content in the namespace expected by the reader.""" + return ( + f'' + f"{content}" + ) + + +def test_document_input_limits_and_unsupported_formats(monkeypatch: pytest.MonkeyPatch) -> None: + """Reject oversized, unsupported, and unconfigured reader inputs.""" + monkeypatch.setattr(document, "MAX_DOCUMENT_BYTES", 2) + with pytest.raises(document.DocumentReadError, match="8 MiB"): + document.extract_review_document("a.docx", b"long") + with pytest.raises(document.DocumentReadError, match="unsupported"): + document.extract_review_document("a.pdf", b"ok") + monkeypatch.delenv(document.HWP_READER_ENV, raising=False) + with pytest.raises(document.DocumentReadError, match="not configured"): + document.extract_review_document("a.hwp", b"ok") + + +def test_docx_archive_and_xml_boundaries(monkeypatch: pytest.MonkeyPatch) -> None: + """Reject partial archives and XML without visible document content.""" + valid = _docx(_body("ok")) + monkeypatch.setattr(document, "MAX_DOCUMENT_ZIP_ENTRIES", 0) + with pytest.raises(document.DocumentReadError, match="too many entries"): + document.extract_review_document("a.docx", valid) + monkeypatch.setattr(document, "MAX_DOCUMENT_ZIP_ENTRIES", 2048) + monkeypatch.setattr(document, "MAX_DOCUMENT_ZIP_UNCOMPRESSED_BYTES", 1) + with pytest.raises(document.DocumentReadError, match="unpacked size"): + document.extract_review_document("a.docx", valid) + monkeypatch.setattr(document, "MAX_DOCUMENT_ZIP_UNCOMPRESSED_BYTES", 64 * 1024 * 1024) + cases = ( + (_docx(None, extra_entries=1), "no word/document.xml"), + (_docx("'), "no document body"), + (_docx(_body("")), "no readable text"), + ) + for payload, message in cases: + with pytest.raises(document.DocumentReadError, match=message): + document.extract_review_document("a.docx", payload) + + +def test_docx_visible_controls_and_uneven_table() -> None: + """Keep tabs, line breaks, and uneven table cells in reviewer text.""" + xml = _body( + "ABC" + "X|Y" + "Z" + "" + "" + ) + text = document.extract_review_document("a.docx", _docx(xml)) + assert "A\tB\nC" in text + assert "X\\|Y" in text + assert "| Z | |" in text + assert "### Table 1 (2 rows x 2 columns)" in text + assert "Table 2" not in text + + +def test_hwp_reader_rejects_process_and_output_failures(monkeypatch: pytest.MonkeyPatch) -> None: + """Keep parser failures and untrusted output out of the review prompt.""" + monkeypatch.setenv(document.HWP_READER_ENV, "/reviewed/source") + + def unavailable(*_args: object, **_kwargs: object) -> None: + raise OSError("private process detail") + + monkeypatch.setattr(document.subprocess, "run", unavailable) + with pytest.raises(document.DocumentReadError, match="could not start"): + document.extract_review_document("a.hwpx", b"data") + + for stdout, message in ((b"abcd", "bounded output"), (b"\xff", "non-UTF-8"), (b" ", "empty text")): + monkeypatch.setattr(document, "MAX_DOCUMENT_TEXT_BYTES", 3) + completed = CompletedProcess(["node"], 0, stdout, b"") + monkeypatch.setattr( + document.subprocess, + "run", + lambda *_args, **_kwargs: completed, + ) + with pytest.raises(document.DocumentReadError, match=message): + document.extract_review_document("a.hwpx", b"data") + + +def test_document_text_truncation_and_cli(monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None: + """Bound UTF-8 output and preserve a useful local CLI failure exit.""" + monkeypatch.setattr(document, "MAX_DOCUMENT_TEXT_BYTES", 4) + assert document._bounded_text("ééé").startswith("éé\n[document text truncated;") + assert document._bounded_text("ok") == "ok" + + path = tmp_path / "review.docx" + path.write_bytes(_docx(_body("ok"))) + monkeypatch.setattr(sys, "argv", ["noema_review_document.py", str(path)]) + assert document._main() == 0 + assert "ok" in capsys.readouterr().out + monkeypatch.setattr(sys, "argv", ["noema_review_document.py", str(path.with_name("missing.docx"))]) + assert document._main() == 1 + assert capsys.readouterr().err + + monkeypatch.setattr(sys, "argv", ["noema_review_document.py", str(path)]) + with pytest.raises(SystemExit) as exit_status: + runpy.run_path(str(Path(document.__file__)), run_name="__main__") + assert exit_status.value.code == 0 diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 5a41cb7cdc..5cbc0a8cb4 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -743,6 +743,26 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"' in measure_step assert "The networked build context contains only this" in measure_step assert 'install -m 0644 "$trusted_ci_requirements"' in measure_step + assert ( + 'trusted_noema_document_requirements="${GITHUB_WORKSPACE}/requirements-noema-document-ci-hashes.txt"' + in measure_step + ) + assert ( + '[ ! -f "$trusted_noema_document_requirements" ]' + in measure_step + ) + assert ( + '[ -L "$trusted_noema_document_requirements" ]' + in measure_step + ) + assert ( + 'install -m 0644 "$trusted_noema_document_requirements"' + in measure_step + ) + assert ( + '"$coverage_build_dir/requirements-noema-document-ci-hashes.txt"' + in measure_step + ) assert 'install -m 0755 "$trusted_base_python_installer"' in measure_step assert "COPY install-base-python-locks.py" in measure_step assert "python3 -I /usr/local/libexec/install-base-python-locks.py" in measure_step diff --git a/tests/test_organization_commercial_readiness_loop_receipt_contract.py b/tests/test_organization_commercial_readiness_loop_receipt_contract.py index 6ae9dfa595..c3030dc8c3 100644 --- a/tests/test_organization_commercial_readiness_loop_receipt_contract.py +++ b/tests/test_organization_commercial_readiness_loop_receipt_contract.py @@ -57,7 +57,14 @@ def test_json_receipt_is_retained_as_an_immutable_short_lived_artifact() -> None assert "if-no-files-found: error" in source assert "retention-days: 3" in source endpoints = _harden_runner_allowed_endpoints(source) - assert "results-receiver.actions.githubusercontent.com:443" in endpoints - assert "*.actions.githubusercontent.com:443" in endpoints - assert "*.blob.core.windows.net:443" in endpoints + assert endpoints == { + "api.github.com:443", + "api.opencode.ai:443", + "github.com:443", + "objects.githubusercontent.com:443", + "release-assets.githubusercontent.com:443", + "results-receiver.actions.githubusercontent.com:443", + "*.actions.githubusercontent.com:443", + "*.blob.core.windows.net:443", + } assert "- name: Checkout exact trusted coordinator source" not in endpoints diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 8b7c55a4ef..c2d7c26082 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -17,7 +17,7 @@ DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") REVIEW_DISPATCH_WORKFLOW = Path(".github/workflows/opencode-review-dispatch.yml") -REVIEW_DISPATCH_BLOB_SHA = "cbc8d214394c4b7acbe82ce7fba11fd073b91c98" +REVIEW_DISPATCH_BLOB_SHA = "1f8e74bc8683ec54510fbabca71d8322b93fe9ad" def _workflow_text(path: Path) -> str: diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index 4b8715d361..b8afdfcd92 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -2363,19 +2363,19 @@ def fake_active_workflow_runs(repo, statuses, *, event=None, created=None, head_ assert created == ">=2026-09-17T11:50:00Z" return [ { - "path": ".github/workflows/opencode-review-coalesce-tick.yml", + "path": ".github/workflows/other.yml", "conclusion": "success", - "updated_at": "2026-09-17T11:55:00Z", + "updated_at": "2026-09-17T11:59:00Z", }, { "path": ".github/workflows/opencode-review-coalesce-tick.yml", "conclusion": "success", - "updated_at": "2026-09-17T11:40:00Z", + "updated_at": "2026-09-17T11:55:00Z", }, { - "path": ".github/workflows/other.yml", + "path": ".github/workflows/opencode-review-coalesce-tick.yml", "conclusion": "success", - "updated_at": "2026-09-17T11:59:00Z", + "updated_at": "2026-09-17T11:40:00Z", }, ] diff --git a/tests/test_strix_evidence_binder_trusted_path.py b/tests/test_strix_evidence_binder_trusted_path.py new file mode 100644 index 0000000000..308bbda0b7 --- /dev/null +++ b/tests/test_strix_evidence_binder_trusted_path.py @@ -0,0 +1,59 @@ +"""The Strix evidence binder resolves from the trusted gate, not the scanned repo.""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + +GATE = Path("scripts/ci/strix_quick_gate.sh") + + +def _function_source(name: str) -> str: + """Return one top-level bash function from the gate script.""" + text = GATE.read_text(encoding="utf-8") + match = re.search(rf"(?ms)^{name}\(\) \{{\n.*?^\}}\n", text) + assert match is not None, name + return match.group(0) + + +def test_binder_runs_when_the_scanned_repository_has_no_central_scripts(tmp_path: Path) -> None: + """A consumer repo need not contain the central Strix binder. + + ``strix.yml`` runs the trusted ``.github`` gate with ``STRIX_REPO_ROOT`` set + to the consumer checkout. The evidence binder must therefore resolve next + to the trusted gate rather than under the scanned repository root. + """ + consumer = tmp_path / "trusted-workspace" + consumer.mkdir() + reports = tmp_path / "strix_runs" + reports.mkdir() + report = reports / "penetration_test_report.md" + report.write_text("# Report\n\nNo vulnerabilities were identified.\n", encoding="utf-8") + log = tmp_path / "strix.log" + log.write_text("scan complete\n", encoding="utf-8") + script = ( + "set -u\n" + f'SCRIPT_DIR="{GATE.parent.resolve()}"\n' + f'REPO_ROOT="{consumer}"\n' + + _function_source("sanitize_remediation_evidence_claims") + + f'sanitize_remediation_evidence_claims "{log}" "{reports}"\n' + ) + result = subprocess.run(["bash", "-c", script], capture_output=True, text=True, check=False) + assert result.returncode == 0, result.stderr + assert "binder is missing" not in result.stderr + assert report.read_text(encoding="utf-8").startswith("# Report") + + +def test_binder_still_fails_closed_when_the_trusted_copy_is_absent(tmp_path: Path) -> None: + """Without the trusted binder the gate still refuses to continue.""" + script = ( + "set -u\n" + f'SCRIPT_DIR="{tmp_path}"\n' + f'REPO_ROOT="{GATE.parent.parent.resolve()}"\n' + + _function_source("sanitize_remediation_evidence_claims") + + 'sanitize_remediation_evidence_claims "" ""\n' + ) + result = subprocess.run(["bash", "-c", script], capture_output=True, text=True, check=False) + assert result.returncode == 2 + assert f"Strix evidence binder is missing: {tmp_path}/strix_evidence_binding.py" in result.stderr diff --git a/tests/test_strix_runtime_dependencies.py b/tests/test_strix_runtime_dependencies.py index fd66f8d452..5e67142958 100644 --- a/tests/test_strix_runtime_dependencies.py +++ b/tests/test_strix_runtime_dependencies.py @@ -15,3 +15,16 @@ def test_strix_installs_openai_httpx2_runtime() -> None: assert "openai[httpx2]==2.54.0" in requirements.splitlines() assert "openai==2.54.0 \\" in requirements_lock.splitlines() assert "httpx2==2.12.0 \\" in requirements_lock.splitlines() + + +def test_strix_anyio_security_pin_is_an_explicit_lock_input() -> None: + """Keep the audited AnyIO version reproducible from the source input.""" + requirements = (REPOSITORY_ROOT / "requirements-strix-ci.txt").read_text( + encoding="utf-8" + ) + requirements_lock = ( + REPOSITORY_ROOT / "requirements-strix-ci-hashes.txt" + ).read_text(encoding="utf-8") + + assert "anyio==4.14.2" in requirements.splitlines() + assert "anyio==4.14.2 \\" in requirements_lock.splitlines() diff --git a/tests/test_strix_trusted_fixture_boundary.py b/tests/test_strix_trusted_fixture_boundary.py new file mode 100644 index 0000000000..579b3da0d4 --- /dev/null +++ b/tests/test_strix_trusted_fixture_boundary.py @@ -0,0 +1,37 @@ +"""Regression contract for Strix trusted-runtime fixture isolation.""" + +from __future__ import annotations + +from pathlib import Path + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +HARNESS_PATH = REPOSITORY_ROOT / "scripts" / "ci" / "test_strix_quick_gate.sh" +CONSUMER_ROOT_MATERIALIZATION = ( + 'materialize_trusted_gate_fixture "$repo_root_dir/scripts/ci"' +) + + +def _consumer_root_materialization_owners(source: str) -> tuple[str, ...]: + """Return shell-function names that install trusted runtime in the consumer.""" + owners: list[str] = [] + current_function = "" + for raw_line in source.splitlines(): + stripped = raw_line.strip() + if stripped.endswith("() {"): + current_function = stripped.removesuffix("() {").strip() + if CONSUMER_ROOT_MATERIALIZATION in raw_line: + owners.append(current_function) + return tuple(owners) + + +def test_specialized_strix_fixtures_keep_trusted_runtime_outside_consumer() -> None: + """Fail while any fixture can mask consumer-root binder resolution.""" + source = HARNESS_PATH.read_text(encoding="utf-8") + offenders = _consumer_root_materialization_owners(source) + + assert not offenders, ( + "trusted Strix gate/model/binder must be materialized outside " + "repo_root_dir; consumer-root materialization remains in: " + + ", ".join(offenders) + )