diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 26e8555967..c03c5ae705 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -7626,8 +7626,35 @@ jobs: exit 1 fi if [ "${GH_REPOSITORY:-}" != "${GITHUB_REPOSITORY:-}" ] && - [ "${OPENCODE_STATUS_TOKEN_SOURCE:-}" = "github-token" ]; then - echo "::notice::OpenCode repository_dispatch status publication is unavailable because only the same-repository github.token can access cross-repository target ${GH_REPOSITORY}. The exact-head formal review remains authoritative; configure PR_REVIEW_MERGE_TOKEN or OPENCODE_APPROVE_TOKEN to publish the optional commit status." + { [ "${OPENCODE_STATUS_TOKEN_SOURCE:-}" = "github-token" ] || + [ "${OPENCODE_STATUS_TOKEN_SOURCE:-}" = "opencode-app" ]; }; then + formal_review_file="$(mktemp)" + cleanup_formal_review_evidence() { + rm -f "$formal_review_file" + } + trap cleanup_formal_review_evidence EXIT + if ! gh api "repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate --slurp | + jq 'flatten' >"$formal_review_file"; then + echo "::error::OpenCode repository_dispatch status publication could not prove an exact-head formal OpenCode review before skipping unavailable cross-repository commit-status publication." + exit 1 + fi + if ! jq -e --arg head "$PR_HEAD_SHA" ' + any(.[]; + (.commit_id // "") == $head + and ( + (.user.login // "") == "opencode-agent[bot]" + or (.user.login // "") == "opencode-agent" + ) + and ( + ((.state // "") | ascii_upcase) == "APPROVED" + or ((.state // "") | ascii_upcase) == "CHANGES_REQUESTED" + ) + ) + ' "$formal_review_file" >/dev/null; then + echo "::error::OpenCode repository_dispatch status publication could not prove an exact-head formal OpenCode review; refusing to hide a cross-repository review-evidence gap." + exit 1 + fi + echo "::notice::OpenCode repository_dispatch status publication is unavailable because only the same-repository github.token can access cross-repository target ${GH_REPOSITORY}, and the OpenCode App token has no cross-repository commit-status permission in this deployment. A verified exact-head formal review remains authoritative; configure PR_REVIEW_MERGE_TOKEN or OPENCODE_APPROVE_TOKEN to publish the optional commit status." exit 0 fi diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 58ed3dab8d..3f0fcd5678 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -982,8 +982,9 @@ jobs: fi # Preserve configuration failures (exit 2) and any unexpected exit - # code as hard failures — only the scan-failure code (1) can be an - # infrastructure/backend-unavailability outcome. + # code as hard failures. A scan-failure code (1), including provider + # or backend unavailability, is incomplete security evidence and + # remains a hard failure for the required check. if [ "$strix_rc" -ne 1 ]; then exit "$strix_rc" fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 55a2a2f211..59f5a0c2c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1291,6 +1291,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Refreshed the read-only review-dispatch blob contract after the protected main workflow changed, keeping the cross-repository OpenCode evidence checks bound to the current canonical reviewer workflow. - Prefer the job-scoped `github.token` when the central OpenCode dispatch publishes a commit status back to the same `.github` repository. The job's declared `statuses: write` permission now reaches the endpoint instead of an @@ -1395,6 +1396,10 @@ Semantic Versioning where the repository publishes a release. - Published substantive OpenCode LLM probes when they already carried an independent proof and exact source-line digest but omitted a duplicated `path:line` citation, so NVIDIA NIM / OpenCode review evidence is no longer discarded as `NO_CONCLUSION`. - Refused a conflict-scope repository root whose immediate parent is a symbolic link, so a swapped parent cannot redirect the canonical worktree after the last-component check (CWE-367). - Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. +- Stopped cancelling in-flight required `scan-pr-queue` runs and stopped serializing the OpenCode required-workflow stub jobs, so a later same-head success is not hidden behind a cancelled or queued required check. +- Required an exact-head formal OpenCode review before skipping unavailable cross-repository commit-status publication, and kept Strix failed when provider evidence is incomplete instead of neutralizing an outage into a pass. +- Refused a conflict-scope repository root whose immediate parent is a symbolic link, so a swapped parent cannot redirect the canonical worktree after the last-component check (CWE-367). + - Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics. - Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed. - Bound each review-agent invocation key to the wrapper's complete canonical payload, including the base branch and requesting actor; altered fields with a valid-format key now fail before durable-leader election or forwarding, and wrapper write permission is job-scoped. diff --git a/docs/doctoring/base-requirements-lock-discovery.md b/docs/doctoring/base-requirements-lock-discovery.md new file mode 100644 index 0000000000..66e7efc9ee --- /dev/null +++ b/docs/doctoring/base-requirements-lock-discovery.md @@ -0,0 +1,59 @@ +# Base requirements lock discovery contract + +## Purpose + +This doctoring record defines how the central review and coverage workflows discover hash-pinned Python requirement locks from an authenticated pull-request base commit. It records the nested-path regression repaired in pull request #939 and preserves the security boundary already developed in pull request #785. + +## Incident + +The materializer intentionally recognizes two candidate forms: + +- conventional file names such as `requirements.txt`, `requirements-dev.txt`, and `requirements.lock`; and +- direct `.txt` children of any directory named `requirements`, such as `requirements/ci.txt` and `service/requirements/package.txt`. + +The path predicate implemented both forms, but `base_hash_locks()` still called the basename-only predicate. As a result, a direct child such as `requirements/ci.txt` was rejected before its authenticated base blob and hash-pinned content could be evaluated. The implementation advertised path-aware eligibility while the collector enforced only legacy basename eligibility. + +The repair changes the collector to call `_is_candidate_lock_path(candidate)` with the already parsed `PurePosixPath`. It does not broaden the accepted Git object types or relax content validation. + +## Trust boundary + +A candidate enters the generated build context only when every applicable condition holds: + +1. The base revision is an exact 40-character hexadecimal commit SHA. +2. `git ls-tree` reports a regular `100...` blob in that exact base tree. +3. The repository-relative path is non-absolute and contains no `..` component. +4. The path is either a conventional requirements lock name or a direct `.txt` child of a directory named `requirements`. +5. Every substantive requirement is an exact `==` pin with complete SHA-256 hashes, or a separately bounded relative requirements include. +6. Symlinks, gitlinks, malformed tree entries, unpinned files, unsafe includes, and pull-request-only content remain excluded. +7. `uv.lock` follows its separate trusted export path and still requires the corresponding base-owned `pyproject.toml`. + +Path eligibility is candidate discovery, not dependency trust. The existing hash, include, export, and downstream closure checks remain authoritative. + +## Test-first evidence + +Temporary repair workflow run `31787913977` executed the following sequence on head `912313ff92cdcee6f240e9584f79ca37615ee5a2`: + +1. Created a temporary Git repository containing hash-pinned `requirements/ci.txt` and `service/requirements/package.txt` blobs. +2. Confirmed the regression test failed before the implementation change because neither path was collected. +3. Replaced the basename-only collector predicate with the repository-relative path predicate. +4. Confirmed both paths were returned in deterministic repository order. +5. Compiled the implementation and regression test and ran `git diff --check`. +6. Deleted the temporary writer workflow before committing the production change. + +An earlier repair attempt failed before exercising the assertion because direct script execution omitted the repository root from `sys.path`. The corrected workflow ran both RED and GREEN phases with the same explicit `PYTHONPATH=.` environment, so the observed transition is attributable to the collector change rather than import setup. + +## Permanent regression command + +```bash +PYTHONPATH=. python3 tests/test_materialize_base_python_requirement_paths.py +python3 -m compileall -q \ + scripts/ci/materialize_base_python_requirements.py \ + tests/test_materialize_base_python_requirement_paths.py +git diff --check +``` + +The repository quality workflow must also run the full materializer and Strix regression suites on the exact pull-request head. Focused repair evidence cannot replace protected-branch checks, semantic review, or required independent approvals. + +## Change-management rule + +Future changes to candidate naming, path parsing, Git tree filtering, requirement includes, `uv.lock` export, or materialized manifests must update the path-discovery tests and the broader materializer suite together. A path predicate and its collector call site must not evolve independently. diff --git a/docs/doctoring/cross-repo-opencode-evidence.md b/docs/doctoring/cross-repo-opencode-evidence.md new file mode 100644 index 0000000000..8a3e18a6cb --- /dev/null +++ b/docs/doctoring/cross-repo-opencode-evidence.md @@ -0,0 +1,32 @@ +# Cross-repository OpenCode evidence + +## Incident and buyer impact + +Sibling-repo reviews (for example `ContextualWisdomLab/naruon#1317`) lost +coverage-evidence when the former Astral release endpoint rejected requests, +and the OpenCode App token could not publish a commit status across +repositories. A later Strix provider outage was also converted into a green +required check, so incomplete security evidence looked like a pass. + +## Decision + +1. Download uv from the literal GitHub Releases HTTPS URL with a fixed + `User-Agent`, disable proxies, reject redirects, and retain size, checksum, + and executable-version checks. Repository or user data cannot select the + network origin. +2. Before skipping cross-repository status publication, prove an exact-head + formal OpenCode review from an accepted author (`opencode-agent[bot]` or + `opencode-agent`) with state (`APPROVED` or `CHANGES_REQUESTED`). Missing + proof, an unaccepted author, or a mismatched head fails closed. +3. Keep Strix red when the backend is unavailable. Incomplete provider + evidence is not a clean scan. + +## References + +Fielding, R., Nottingham, M., & Reschke, J. (Eds.). (2022). *HTTP semantics* +(RFC 9110). Internet Engineering Task Force. +https://doi.org/10.17487/RFC9110 + +National Institute of Standards and Technology. (2020). *Security and privacy +controls for information systems and organizations* (NIST Special Publication +800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5 diff --git a/docs/doctoring/strix-bounded-pr-scope.md b/docs/doctoring/strix-bounded-pr-scope.md new file mode 100644 index 0000000000..6b27558796 --- /dev/null +++ b/docs/doctoring/strix-bounded-pr-scope.md @@ -0,0 +1,62 @@ +# Strix bounded pull-request scope and CI recursion contract + +## Purpose + +This doctoring record defines the trusted boundary used when Strix reviews a bounded set of pull-request changes from an organization-required workflow. It also records the GitHub Actions recursion behavior encountered while repairing the boundary so that future maintainers do not misclassify infrastructure state as a target-code defect. + +## Incident chain + +A downstream OpenCode review dispatch for `ContextualWisdomLab/pg-llm-batch#190` failed while the central workflow materialized its trusted `uv` executable. The trusted download retained a fixed Astral release URL, a no-proxy/no-redirect opener, a bounded response read, SHA-256 verification, and executable-version verification, but the request did not identify the organization client. Pull request #939 adds a fixed `User-Agent` and regression coverage without weakening those trust checks. + +During verification of the central repair, Strix received an intentionally bounded pull-request target. The GitHub Actions runner created that target below a host temporary directory, while the Strix sandbox mounted the same files below `/workspace/`. The original host path was intentionally absent inside the sandbox. Treating that absence as a missing-code vulnerability was therefore a scanner-orientation error, not a finding in the pull-request content. + +Repair workflow run `31784776654` established the regression test first, applied the trusted static scope guidance, ran shell syntax validation, ran the focused Python contract, and completed the full `scripts/ci/test_strix_quick_gate.sh` harness before committing the production change. Both temporary repair workflows were removed by the verified commit. + +## Trusted scope contract + +The following invariants apply: + +1. `pull_request_target` executes the protected-base workflow and trusted gate implementation. Pull-request content is materialized as data in a separate bounded directory; it is not executed with privileged credentials. +2. A target created under the runner host temporary directory may be mounted at `/workspace/` inside the Strix sandbox. Absence of the original host pathname inside the sandbox is expected. +3. For the internal bounded pull-request scope only, the trusted gate supplies a static instruction explaining the mount contract and directing Strix to inspect the files present in the current working directory. +4. No repository input, dispatch payload, pull-request field, environment override, or caller-supplied instruction is forwarded to the security model. The instruction is selected only when `TARGET_PATH_IS_INTERNAL_PR_SCOPE=1` was set by trusted scope materialization. +5. The bounded directory is the complete authorized target for the changed-path scan. Strix must continue to report actionable vulnerabilities in the workflow, shell, Python, configuration, and other eligible files that are actually present. +6. Scope orientation must not suppress provider failures, malformed reports, integrity failures, missing authorized files, or vulnerabilities in present content. Those conditions remain fail-closed. + +## GitHub Actions recursion behavior + +The verified repair commit was pushed by a workflow using the repository `GITHUB_TOKEN`. GitHub created the resulting pull-request workflow runs in an approval-required state and reported `action_required` without jobs. This is GitHub's recursion protection rather than test execution evidence. A maintainer-authenticated commit or explicit workflow approval is required before exact-head CI can run normally. + +This repository must not replace the recursion protection with a broadly privileged token merely to make a self-repair workflow recursively trigger CI. Temporary repair workflows must remain narrowly scoped, use least-privilege `contents: write`, verify that the remote branch has not advanced, run the full regression harness before pushing, and delete themselves from the resulting production commit. + +## Regression evidence + +The minimum local or CI evidence for this boundary is: + +```bash +bash -n scripts/ci/strix_quick_gate.sh +python3 -m unittest discover \ + --start-directory tests \ + --pattern 'test_strix_internal_scope_instruction_contract.py' \ + --verbose +bash scripts/ci/test_strix_quick_gate.sh +``` + +The exact pull-request head must additionally complete the trusted-uv materializer quality workflow, Strix changed-path quality workflow, repository security workflows, required OpenCode review, required Strix scan, and all protected-branch review requirements. A previous-head repair run, an approval-required run with no jobs, or a downstream repository's successful leaf checks cannot substitute for current-head central evidence. + +## Operational recovery sequence + +1. Confirm the downstream source head and reproduce the central failure against that exact SHA. +2. Repair the central trusted implementation; do not add unrelated downstream source changes. +3. Add a regression contract that fails before the central repair and passes after it. +4. Verify trusted URL, redirect, proxy, size, checksum, executable-version, and credential boundaries remain intact. +5. Run the full Strix gate harness before committing a scope-orientation change. +6. Remove temporary repair automation from the production diff. +7. Obtain exact-head central CI and independent approvals without dismissing reviews or bypassing branch protection. +8. Merge the central repair normally, then rerun the downstream review on the unchanged downstream head so the infrastructure-derived review is superseded through the standard review path. + +## References + +GitHub. (n.d.). *GITHUB_TOKEN*. GitHub Docs. Retrieved August 14, 2026, from https://docs.github.com/en/actions/concepts/security/github_token + +GitHub. (n.d.). *Securely using pull_request_target*. GitHub Docs. Retrieved August 14, 2026, from https://docs.github.com/en/actions/reference/security/securely-using-pull_request_target diff --git a/scripts/ci/strix_quick_gate.sh b/scripts/ci/strix_quick_gate.sh index 3a563d7020..6c413c3f75 100755 --- a/scripts/ci/strix_quick_gate.sh +++ b/scripts/ci/strix_quick_gate.sh @@ -70,6 +70,7 @@ NORMALIZED_CHANGED_FILES=() PULL_REQUEST_SCOPE_DIRS=() LAST_PULL_REQUEST_SCOPE_DIR="" TARGET_PATH_IS_INTERNAL_PR_SCOPE=0 +INTERNAL_PR_SCOPE_INSTRUCTION="This target is a deliberately bounded pull-request changed-file scope mounted by Strix under /workspace/. The original GitHub Actions runner host path is intentionally absent inside the sandbox, and that absence is not a vulnerability. Treat the files in the current working directory as the complete authorized target for this quick changed-path scan. Inspect the available workflow, shell, Python, and configuration files for actionable content vulnerabilities. Do not report the missing host path or intentional scope bounding as a target-code vulnerability." resolve_trusted_input_file() { local label="$1" @@ -2617,6 +2618,7 @@ run_strix_once() { local rc local llm_api_base_value local child_model + local child_instruction="" local child_reasoning_effort="${STRIX_REASONING_EFFORT:-}" local resolved_target_path local timeout_seconds="$STRIX_PROCESS_TIMEOUT_SECONDS" @@ -2651,6 +2653,9 @@ run_strix_once() { if ! resolved_target_path="$(resolve_current_target_path "$TARGET_PATH")"; then return 1 fi + if [ "$TARGET_PATH_IS_INTERNAL_PR_SCOPE" -eq 1 ]; then + child_instruction="$INTERNAL_PR_SCOPE_INSTRUCTION" + fi local start_epoch start_epoch="$(date +%s)" local child_llm_api_key="" @@ -2682,7 +2687,8 @@ run_strix_once() { STRIX_CHILD_EXECUTABLE_ROOT="$STRIX_EXECUTABLE_ROOT" \ STRIX_CHILD_EXECUTABLE_SHA256="$STRIX_EXECUTABLE_SHA256" \ STRIX_CHILD_REQUIRE_EXECUTABLE_INTEGRITY="${IS_PR_EVIDENCE_RUN:-false}" \ -python3 - "$timeout_seconds" "$resolved_target_path" "$SCAN_MODE" "$STRIX_LOG" "$STRIX_SCAN_WORKING_DIR" <<'PY' + STRIX_CHILD_INSTRUCTION="$child_instruction" \ + python3 - "$timeout_seconds" "$resolved_target_path" "$SCAN_MODE" "$STRIX_LOG" "$STRIX_SCAN_WORKING_DIR" <<'PY' import hashlib import hmac import os @@ -2856,6 +2862,9 @@ scan_output_dir.mkdir() # scan target. The target remains explicit and absolute, so changing cwd cannot # change which source tree is scanned. command = [resolved_strix_bin, "-n", "-t", str(target_cwd), "--scan-mode", scan_mode] +instruction = os.environ.get("STRIX_CHILD_INSTRUCTION", "").strip() +if instruction: + command.extend(["--instruction", instruction]) try: process = subprocess.Popen( diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b9b1c43de3..5d5bec834c 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -6621,7 +6621,7 @@ run_filtered_gate_case_if_requested() { "https://models.github.ai/inference" ;; github-models-fallback-provider-signal-tries-next) - run_gate_case "github-models-fallback-provider-signal-tries-next" \ + run_gate_case_allow_provider_signal "github-models-fallback-provider-signal-tries-next" \ "openai/gpt-5" \ "" \ "0" \ @@ -10366,7 +10366,7 @@ run_gate_case "github-models-primary-ratelimit-fallback-success" \ "deepseek/deepseek-r1-0528 deepseek/deepseek-v3-0324" \ "1" -run_gate_case "github-models-fallback-provider-signal-tries-next" \ +run_gate_case_allow_provider_signal "github-models-fallback-provider-signal-tries-next" \ "openai/gpt-5" \ "" \ "0" \ diff --git a/tests/test_materialize_base_python_requirement_paths.py b/tests/test_materialize_base_python_requirement_paths.py new file mode 100644 index 0000000000..fbf0e5633d --- /dev/null +++ b/tests/test_materialize_base_python_requirement_paths.py @@ -0,0 +1,62 @@ +"""Regression contracts for repository-relative requirements lock discovery.""" + +from __future__ import annotations + +import subprocess +import tempfile +import unittest +from pathlib import Path + +from scripts.ci import materialize_base_python_requirements as materializer + + +def _git(repo: Path, *args: str) -> str: + """Run one deterministic Git command in the fixture repository.""" + return subprocess.run( + ["git", "-C", str(repo), *args], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +class BaseHashLockPathTests(unittest.TestCase): + """Protect direct ``requirements`` directory child discovery.""" + + def test_collects_hash_locks_under_any_requirements_directory(self) -> None: + """Use the complete repository-relative path, not only the basename.""" + with tempfile.TemporaryDirectory() as temporary_directory: + repo = Path(temporary_directory) + _git(repo, "init") + _git(repo, "config", "user.name", "Test") + _git(repo, "config", "user.email", "test@example.invalid") + + root_requirements = repo / "requirements" + nested_requirements = repo / "service" / "requirements" + root_requirements.mkdir() + nested_requirements.mkdir(parents=True) + (root_requirements / "ci.txt").write_text( + "ci-demo==1 --hash=sha256:" + ("a" * 64) + "\n", + encoding="utf-8", + ) + (nested_requirements / "package.txt").write_text( + "service-demo==1 --hash=sha256:" + ("b" * 64) + "\n", + encoding="utf-8", + ) + _git(repo, "add", ".") + _git(repo, "commit", "-m", "base") + base_sha = _git(repo, "rev-parse", "HEAD") + + locks = materializer.base_hash_locks(repo, base_sha) + + self.assertEqual( + [path for path, _content in locks], + [ + "requirements/ci.txt", + "service/requirements/package.txt", + ], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 37ec068db9..93b7bc461f 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -2445,6 +2445,7 @@ def test_opencode_runs_merge_scheduler_after_review_without_repo_local_dispatch( "'PR_REVIEW_MERGE_TOKEN'" ) in status_step assert "steps.opencode_app_token.outputs.available == 'true' && 'opencode-app'" in status_step + assert '[ "${OPENCODE_STATUS_TOKEN_SOURCE:-}" = "opencode-app" ]' in status_step assert "OPENCODE_CHANGED_FILES_FILE" in status_step assert "OPENCODE_ARTIFACT_MANIFEST_SHA256" in status_step assert "OPENCODE_SOURCE_WORKDIR" in status_step @@ -2458,10 +2459,19 @@ def test_opencode_runs_merge_scheduler_after_review_without_repo_local_dispatch( assert "exit 1" in status_step cross_repository_guard = status_step.split( 'if [ "${GH_REPOSITORY:-}" != "${GITHUB_REPOSITORY:-}" ]', 1 - )[1].split("\n fi", 1)[0] + )[1].split("\n\n state=", 1)[0] assert "exact-head formal review remains authoritative" in cross_repository_guard + assert 'formal_review_file="$(mktemp)"' in cross_repository_guard + assert 'gh api "repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}/reviews"' in cross_repository_guard + assert 'gh api "repos/${GH_REPOSITORY}/pulls/${PR_NUMBER}/reviews" --paginate --slurp' in cross_repository_guard + assert "jq 'flatten'" in cross_repository_guard + assert '(.commit_id // "") == $head' in cross_repository_guard + assert 'opencode-agent[bot]' in cross_repository_guard + assert "APPROVED" in cross_repository_guard + assert "CHANGES_REQUESTED" in cross_repository_guard + assert "could not prove an exact-head formal OpenCode review" in cross_repository_guard assert "exit 0" in cross_repository_guard - assert "exit 1" not in cross_repository_guard + assert "exit 1" in cross_repository_guard assert "using %s token" in status_step assert "scripts/ci/opencode_dispatch_status.py" in status_step assert "COVERAGE_EVIDENCE_RESULT" in status_step diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 2d2304aaf1..92e3f92bcd 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 = "26e8555967171a5f3974602ac05700c27bddebf1" +REVIEW_DISPATCH_BLOB_SHA = "c03c5ae70598cf4bb20ec8d4486822ba478850ec" def _workflow_text(path: Path) -> str: diff --git a/tests/test_strix_internal_scope_instruction_contract.py b/tests/test_strix_internal_scope_instruction_contract.py new file mode 100644 index 0000000000..84c1dac997 --- /dev/null +++ b/tests/test_strix_internal_scope_instruction_contract.py @@ -0,0 +1,60 @@ +"""Protect Strix's interpretation of bounded pull-request scan targets.""" + +from pathlib import Path +import unittest + + +SCRIPT_PATH = Path("scripts/ci/strix_quick_gate.sh") + + +class InternalScopeInstructionContractTests(unittest.TestCase): + """Keep static sandbox guidance scoped to trusted PR materialization.""" + + @classmethod + def setUpClass(cls) -> None: + """Load the gate implementation once for contract assertions.""" + cls.script = SCRIPT_PATH.read_text(encoding="utf-8") + + def test_guidance_explains_the_sandbox_mount_contract(self) -> None: + """Tell Strix why the runner host path is absent without hiding code.""" + self.assertIn( + "deliberately bounded pull-request changed-file scope", + self.script, + ) + self.assertIn("/workspace/", self.script) + self.assertIn("host path is intentionally absent", self.script) + self.assertIn("complete authorized target", self.script) + self.assertIn("actionable content vulnerabilities", self.script) + + def test_guidance_is_only_selected_for_internal_pr_scope(self) -> None: + """Never relay caller-controlled instructions to the security agent.""" + expected = ( + 'if [ "$TARGET_PATH_IS_INTERNAL_PR_SCOPE" -eq 1 ]; then\n' + '\t\tchild_instruction="$INTERNAL_PR_SCOPE_INSTRUCTION"\n' + '\tfi' + ) + self.assertIn(expected, self.script) + self.assertIn('local child_instruction=""', self.script) + self.assertNotIn( + 'STRIX_CHILD_INSTRUCTION="${STRIX_INSTRUCTION', + self.script, + ) + + def test_child_process_receives_the_static_cli_instruction(self) -> None: + """Forward the trusted guidance through the stripped child environment.""" + self.assertIn( + 'STRIX_CHILD_INSTRUCTION="$child_instruction"', + self.script, + ) + self.assertIn( + 'instruction = os.environ.get("STRIX_CHILD_INSTRUCTION", "").strip()', + self.script, + ) + self.assertIn( + 'command.extend(["--instruction", instruction])', + self.script, + ) + + +if __name__ == "__main__": + unittest.main()