From a509515fee4440899034cb33d17e379c9db87ea3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 26 Oct 2026 09:18:32 +0900 Subject: [PATCH 1/3] fix(strix): isolate trusted workspaces with owned cleanup --- .../agent-review-runtime-quality-ci.yml | 4 + .github/workflows/strix.yml | 70 ++++- tests/test_strix_workspace_isolation.py | 243 ++++++++++++++++++ 3 files changed, 311 insertions(+), 6 deletions(-) create mode 100644 tests/test_strix_workspace_isolation.py diff --git a/.github/workflows/agent-review-runtime-quality-ci.yml b/.github/workflows/agent-review-runtime-quality-ci.yml index 38f617e7d6..a6fff875e4 100644 --- a/.github/workflows/agent-review-runtime-quality-ci.yml +++ b/.github/workflows/agent-review-runtime-quality-ci.yml @@ -47,6 +47,7 @@ on: - "scripts/ci/test_strix_quick_gate.sh" - "tests/test_docs_only_pr_runner_admission.py" - "tests/test_strix_changed_path_policy.py" + - "tests/test_strix_workspace_isolation.py" - "tests/test_strix_evidence_binding.py" - "tests/test_strix_model_behavior_error.py" - "tests/test_strix_nvidia_nim_not_found_fallback.py" @@ -261,6 +262,7 @@ jobs: scripts/ci/test_strix_quick_gate.sh|\ tests/test_docs_only_pr_runner_admission.py|\ tests/test_strix_changed_path_policy.py|\ + tests/test_strix_workspace_isolation.py|\ tests/test_strix_evidence_binding.py|\ tests/test_strix_model_behavior_error.py|\ tests/test_strix_nvidia_nim_not_found_fallback.py|\ @@ -463,6 +465,7 @@ jobs: python -m pytest -q \ tests/test_docs_only_pr_runner_admission.py \ tests/test_strix_changed_path_policy.py \ + tests/test_strix_workspace_isolation.py \ tests/test_strix_evidence_binding.py \ tests/test_strix_model_behavior_error.py \ tests/test_strix_nvidia_nim_not_found_fallback.py \ @@ -472,6 +475,7 @@ jobs: python -m compileall -q \ scripts/ci/strix_evidence_binding.py \ tests/test_strix_changed_path_policy.py \ + tests/test_strix_workspace_isolation.py \ tests/test_strix_evidence_binding.py \ tests/test_strix_model_behavior_error.py \ tests/test_strix_nvidia_nim_not_found_fallback.py \ diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 53e704bcfb..af1573cfbb 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -790,6 +790,29 @@ jobs: esac echo "is_private=$is_private" >>"$GITHUB_OUTPUT" + - name: Allocate this job's target workspace + run: | + set -euo pipefail + python3 - <<'PY' + import os + from pathlib import Path + import tempfile + + parent = Path(os.environ["RUNNER_TEMP"]) + if not parent.is_absolute() or any(ord(c) < 32 for c in str(parent)): + raise RuntimeError("refusing invalid Strix temporary root") + for ancestor in (parent, *parent.parents): + if ancestor.is_symlink() or not ancestor.is_dir(): + raise RuntimeError("refusing changed Strix temporary ancestry") + # Never reinitialize the fixed path left by an earlier job. + root = Path(tempfile.mkdtemp(prefix="strix-workspace-", dir=parent)) + info = root.stat() + with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as output: + output.write(f"TRUSTED_WORKSPACE={root}\n") + output.write(f"TRUSTED_WORKSPACE_DEVICE={info.st_dev}\n") + output.write(f"TRUSTED_WORKSPACE_INODE={info.st_ino}\n") + PY + - name: Materialize target workspace if: github.event_name != 'repository_dispatch' env: @@ -798,8 +821,8 @@ jobs: TARGET_WORKSPACE_SHA: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.base.sha || github.sha }} run: | set -euo pipefail - trusted_workspace="$RUNNER_TEMP/trusted-workspace" - mkdir -p "$trusted_workspace" + trusted_workspace="$TRUSTED_WORKSPACE" + test -d "$trusted_workspace" git init -q "$trusted_workspace" gh auth setup-git git -C "$trusted_workspace" remote add origin "$GITHUB_SERVER_URL/$REPOSITORY.git" @@ -848,8 +871,8 @@ jobs: exit 1 fi - trusted_workspace="$RUNNER_TEMP/trusted-workspace" - mkdir -p "$trusted_workspace" + trusted_workspace="$TRUSTED_WORKSPACE" + test -d "$trusted_workspace" git init -q "$trusted_workspace" gh auth setup-git git -C "$trusted_workspace" remote add origin "$GITHUB_SERVER_URL/$REPOSITORY.git" @@ -1170,10 +1193,10 @@ jobs: # temporary scope with execute bits stripped, then scans that scope as # data. PR evidence uses the __PR_SCOPE__ sentinel so the scanner target # cannot accidentally remain the trusted base checkout. - working-directory: ${{ runner.temp }}/trusted-workspace + working-directory: ${{ env.TRUSTED_WORKSPACE }} env: STRIX_LLM_FILE: ${{ env.STRIX_LLM_FILE }} - STRIX_REPO_ROOT: ${{ runner.temp }}/trusted-workspace + STRIX_REPO_ROOT: ${{ env.TRUSTED_WORKSPACE }} LLM_API_BASE_FILE: ${{ env.LLM_API_BASE_FILE }} STRIX_LLM_DEFAULT_PROVIDER: contextual_orchestrator LLM_API_KEY_FILE: ${{ env.LLM_API_KEY_FILE }} @@ -1412,6 +1435,41 @@ jobs: fi echo "::warning::Could not publish manual Strix status from scan job; keeping scan evidence result authoritative in the workflow run." + - name: Clean up this job's target workspace + if: always() && env.TRUSTED_WORKSPACE != '' + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + import os + from pathlib import Path + import re + import shutil + import stat + + root = Path(os.environ["TRUSTED_WORKSPACE"]) + parent = Path(os.environ["RUNNER_TEMP"]) + if (not root.is_absolute() or root.parent != parent + or not re.fullmatch(r"strix-workspace-[A-Za-z0-9_-]+", root.name) + or any(ord(c) < 32 for c in str(root))): + raise RuntimeError("refusing unrelated Strix workspace cleanup path") + for ancestor in (parent, *parent.parents): + if ancestor.is_symlink() or not ancestor.is_dir(): + raise RuntimeError("refusing changed Strix workspace ancestry") + try: + info = root.lstat() + except FileNotFoundError: + print("Owned Strix workspace is already absent.") + else: + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_dev != int(os.environ["TRUSTED_WORKSPACE_DEVICE"]) + or info.st_ino != int(os.environ["TRUSTED_WORKSPACE_INODE"])): + raise RuntimeError("refusing changed Strix workspace identity") + # No shared-temp sweep, symlink following or old-workspace repair. + shutil.rmtree(root) + print("Removed this job's target workspace.") + PY + - name: Clean up this job's pinned GitHub CLI if: always() && env.CWL_GH_ROOT != '' shell: bash diff --git a/tests/test_strix_workspace_isolation.py b/tests/test_strix_workspace_isolation.py new file mode 100644 index 0000000000..3b4426737f --- /dev/null +++ b/tests/test_strix_workspace_isolation.py @@ -0,0 +1,243 @@ +"""Exercise the actual Strix materializers with real local Git repositories.""" +from __future__ import annotations + +import json +import os +from pathlib import Path +import subprocess +import textwrap + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".github/workflows/strix.yml" +ALLOCATE = "Allocate this job's target workspace" +CLEANUP = "Clean up this job's target workspace" +MATERIALIZERS = ( + "Materialize target workspace", + "Validate repository dispatch against live pull request metadata", +) + + +def _step(name: str) -> str: + """Read the exact shell, without substituting Git or workspace operations.""" + marker = f" - name: {name}\n" + source = WORKFLOW.read_text() + if marker not in source: + return "" + block = source.split(marker, 1)[1].split(" - name:", 1)[0] + return textwrap.dedent(block.split(" run: |\n", 1)[1]) + + +def _git(root: Path, env: dict[str, str], *args: str) -> str: + """Use real Git with fixture-local identity and no hooks/signing.""" + result = subprocess.run( + ["git", "-c", "core.hooksPath=/dev/null", "-c", "commit.gpgsign=false", + "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", + "-C", str(root), *args], + env=env, capture_output=True, text=True, check=True, + ) + return result.stdout.strip() + + +def _shell(script: str, env: dict[str, str]) -> subprocess.CompletedProcess[str]: + """Only GitHub auth/API are finite external IO; Git is never intercepted.""" + boundary = '''gh() { + if [[ "$*" == "auth setup-git" ]]; then return 0; fi + if [[ "$*" == "api repos/ContextualWisdomLab/.github/pulls/7" ]]; then + printf '%s\\n' "$FIXTURE_PR_JSON" + return 0 + fi + printf 'unexpected GitHub operation\\n' >&2 + return 97 + } + ''' + return subprocess.run( + ["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", + textwrap.dedent(boundary) + script], + env=env, text=True, capture_output=True, + ) + + +def _apply_environment(env: dict[str, str]) -> None: + """Emulate only the runner's next-step environment-file import.""" + for line in Path(env["GITHUB_ENV"]).read_text().splitlines(): + key, value = line.split("=", 1) + env[key] = value + + +@pytest.fixture +def workspace_fixture(tmp_path: Path) -> tuple[dict[str, str], Path, Path]: + """Build a real committed origin and a stale repo that must stay untouched.""" + env = {key: value for key, value in os.environ.items() + if not key.startswith("GIT_")} + env.update(GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_SYSTEM=os.devnull, + GIT_TERMINAL_PROMPT="0") + seed = tmp_path / "seed" + seed.mkdir() + _git(seed, env, "init", "--quiet") + (seed / "source.txt").write_text("exact trusted base\n") + _git(seed, env, "add", "source.txt") + _git(seed, env, "commit", "--quiet", "-m", "trusted base") + sha = _git(seed, env, "rev-parse", "HEAD") + remotes = tmp_path / "remotes" + remote = remotes / "ContextualWisdomLab/.github.git" + remote.parent.mkdir(parents=True) + _git(seed, env, "clone", "--bare", "--quiet", str(seed), str(remote)) + runner = tmp_path / "runner" + runner.mkdir() + stale = runner / "trusted-workspace" + stale.mkdir() + _git(stale, env, "init", "--quiet") + _git(stale, env, "remote", "add", "origin", "https://previous.invalid/repo.git") + (stale / "unrelated.txt").write_text("do not clean or reuse\n") + environment_file = tmp_path / "environment" + environment_file.touch() + env.update(RUNNER_TEMP=str(runner), GITHUB_ENV=str(environment_file), + GITHUB_SERVER_URL=remotes.as_uri(), REPOSITORY="ContextualWisdomLab/.github", + TARGET_WORKSPACE_SHA=sha, PR_NUMBER="7", SUPPLIED_BASE_REF="main", + SUPPLIED_BASE_SHA=sha, SUPPLIED_HEAD_SHA=sha) + env["FIXTURE_PR_JSON"] = json.dumps({ + "state": "open", "base": {"ref": "main", "sha": sha, + "repo": {"full_name": env["REPOSITORY"]}}, + "head": {"sha": sha, "repo": {"full_name": env["REPOSITORY"]}}, + }) + return env, stale, remote + + +def _materialize(env: dict[str, str], name: str) -> subprocess.CompletedProcess[str]: + """Execute old or new workflow bytes, preserving the original RED path.""" + allocation = _step(ALLOCATE) + if allocation: + result = _shell(allocation, env) + assert result.returncode == 0, result.stderr + _apply_environment(env) + return _shell(_step(name), env) + + +@pytest.mark.parametrize("name", MATERIALIZERS) +def test_repeated_materialization_is_job_private_and_cleanup_is_owned( + name: str, workspace_fixture: tuple[dict[str, str], Path, Path] +) -> None: + """An old origin cannot break a fresh scan or authorize a shared sweep.""" + env, stale, remote = workspace_fixture + stale_config = (stale / ".git/config").read_bytes() + workspaces = [] + for _ in range(2): + step_env = dict(env) + result = _materialize(step_env, name) + assert result.returncode == 0, (result.returncode, result.stdout, result.stderr) + _apply_environment(step_env) + workspace = Path(step_env["TRUSTED_WORKSPACE"]) + assert workspace.parent == Path(env["RUNNER_TEMP"]) + assert workspace.name.startswith("strix-workspace-") + assert workspace != stale + assert _git(workspace, step_env, "rev-parse", "HEAD") == env["TARGET_WORKSPACE_SHA"] + assert _git(workspace, step_env, "remote", "get-url", "origin") == remote.as_uri() + assert (workspace / "source.txt").read_text() == "exact trusted base\n" + assert not (workspace / "unrelated.txt").exists() + workspaces.append((workspace, step_env)) + assert workspaces[0][0] != workspaces[1][0] + assert all(path.is_dir() for path, _ in workspaces) + cleanup = _step(CLEANUP) + assert cleanup, "owned workspace cleanup is required" + first, first_env = workspaces[0] + result = _shell(cleanup, first_env) + assert result.returncode == 0, result.stderr + assert not first.exists() + assert workspaces[1][0].is_dir() + result = _shell(cleanup, first_env) + assert result.returncode == 0, result.stderr + result = _shell(cleanup, workspaces[1][1]) + assert result.returncode == 0, result.stderr + assert (stale / ".git/config").read_bytes() == stale_config + assert (stale / "unrelated.txt").read_text() == "do not clean or reuse\n" + + +@pytest.mark.parametrize("case", ("old-root", "symlink", "identity")) +def test_cleanup_refuses_unowned_or_replaced_workspace( + case: str, workspace_fixture: tuple[dict[str, str], Path, Path] +) -> None: + """A changed locator cannot delete a neighbor, replacement or symlink target.""" + env, stale, _ = workspace_fixture + result = _shell(_step(ALLOCATE), env) + assert result.returncode == 0, result.stderr + _apply_environment(env) + root = Path(env["TRUSTED_WORKSPACE"]) + sentinel = stale / "unrelated.txt" + if case == "old-root": + env["TRUSTED_WORKSPACE"] = str(stale) + elif case == "symlink": + root.rmdir() + root.symlink_to(stale, target_is_directory=True) + else: + env["TRUSTED_WORKSPACE_INODE"] = str(int(env["TRUSTED_WORKSPACE_INODE"]) + 1) + result = _shell(_step(CLEANUP), env) + assert result.returncode != 0 + assert "refusing" in result.stderr + assert sentinel.read_text() == "do not clean or reuse\n" + assert root.exists() + + +def test_materialization_failure_still_allows_only_owned_cleanup( + workspace_fixture: tuple[dict[str, str], Path, Path] +) -> None: + """Publish ownership before fetch so failure cannot strand a fixed repo.""" + env, stale, _ = workspace_fixture + env["TARGET_WORKSPACE_SHA"] = "f" * 40 + result = _materialize(env, MATERIALIZERS[0]) + assert result.returncode != 0 + root = Path(env["TRUSTED_WORKSPACE"]) + assert root.is_dir() + cleanup_block = WORKFLOW.read_text().split(f" - name: {CLEANUP}\n", 1)[1] + assert "if: always() && env.TRUSTED_WORKSPACE != ''" in cleanup_block.split(" run: |", 1)[0] + result = _shell(_step(CLEANUP), env) + assert result.returncode == 0, result.stderr + assert not root.exists() + assert (stale / "unrelated.txt").exists() + + +def test_dispatch_metadata_mismatch_still_fails_before_git_init( + workspace_fixture: tuple[dict[str, str], Path, Path] +) -> None: + """A private root does not relax the existing live PR identity fence.""" + env, stale, _ = workspace_fixture + env["SUPPLIED_HEAD_SHA"] = "f" * 40 + result = _materialize(env, MATERIALIZERS[1]) + assert result.returncode != 0 + assert "does not match live PR" in result.stdout + root = Path(env["TRUSTED_WORKSPACE"]) + assert not (root / ".git").exists() + assert (stale / "unrelated.txt").exists() + result = _shell(_step(CLEANUP), env) + assert result.returncode == 0, result.stderr + assert not root.exists() + + +def test_workspace_regression_is_enrolled_in_quality_ci() -> None: + """The real Git regression must not remain a manually invoked orphan.""" + quality = (ROOT / ".github/workflows/agent-review-runtime-quality-ci.yml").read_text() + path = "tests/test_strix_workspace_isolation.py" + trigger = quality.split("on:\n", 1)[1].split("\nconcurrency:\n", 1)[0] + assert f' - "{path}"' in trigger + selector = quality.split(' case "$changed_path" in\n', 1)[1].split(" esac", 1)[0] + result = _shell('changed_path=' + path + '\nstrix_suite=false\ncase "$changed_path" in\n' + + selector + 'esac\ntest "$strix_suite" = true\n', dict(os.environ)) + assert result.returncode == 0, result.stderr + verify = quality.split("- name: Verify exact-head path policy and syntax", 1)[1].split(" - name:", 1)[0] + assert "if: steps.affected_suites.outputs.strix == 'true'" in verify + assert path in verify.split("python -m compileall", 1)[0] + assert path in verify.split("python -m compileall", 1)[1] + + +def test_scan_uses_allocated_workspace_without_weakening_scope_or_threshold() -> None: + """Only the root locator changes; trusted execution and scan policy remain.""" + source = WORKFLOW.read_text() + step = source.split(" - name: Run Strix (quick)\n", 1)[1].split(" - name:", 1)[0] + assert "working-directory: ${{ env.TRUSTED_WORKSPACE }}" in step + assert "STRIX_REPO_ROOT: ${{ env.TRUSTED_WORKSPACE }}" in step + assert "STRIX_FAIL_ON_MIN_SEVERITY: MEDIUM" in step + assert "&& '__PR_SCOPE__' || './'" in step + assert 'bash "$TRUSTED_STRIX_GATE"' in step + assert "STRIX_FAIL_ON_PROVIDER_SIGNAL: \"1\"" in step From b9963d1a458be6e97372c90d4ce68927e61ecf8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 26 Oct 2026 10:52:24 +0900 Subject: [PATCH 2/3] [verified] test(strix): expect the allocated trusted workspace --- scripts/ci/test_strix_quick_gate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index e53741bd51..52b811d135 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -257,7 +257,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "TARGET_WORKSPACE_SHA" "strix workflow pins target workspace SHA" assert_file_contains "$workflow_file" "TRUSTED_WORKSPACE=\$trusted_workspace" "strix workflow exports a trusted workspace path" assert_file_contains "$workflow_file" "git -C \"\$TRUSTED_WORKSPACE\"" "strix workflow runs git only inside trusted workspace" - assert_file_contains "$workflow_file" 'working-directory: ${{ runner.temp }}/trusted-workspace' "strix workflow executes privileged steps from the trusted workspace" + assert_file_contains "$workflow_file" 'working-directory: ${{ env.TRUSTED_WORKSPACE }}' "strix workflow executes privileged steps from the allocated trusted workspace" assert_file_contains "$workflow_file" 'mkdir -p "$TRUSTED_WORKSPACE/scripts/ci"' "strix workflow creates the scheduler policy directory before materializing PR-head scheduler policy" assert_file_contains "$workflow_file" 'git -C "$TRUSTED_WORKSPACE" show "$PR_HEAD_SHA:.github/workflows/strix.yml" > "$TRUSTED_WORKSPACE/.github/workflows/strix.yml"' "strix workflow materializes the PR-head workflow for required-path self-test" assert_file_contains "$workflow_file" "STRIX_REPO_ROOT:" "strix workflow passes target repository root to the central Strix gate" From cb37802aba5c30d9832de3aef44c8ff76e302b75 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 26 Oct 2026 12:49:22 +0900 Subject: [PATCH 3/3] [verified] test(strix): align inherited assertions with current contracts --- scripts/ci/test_strix_quick_gate.sh | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 52b811d135..3548c5d372 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -337,8 +337,8 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$REPO_ROOT/tests/test_strix_repository_visibility_contract.py" "test_dispatch_api_visibility_preserves_internal_privacy" "strix visibility contract executes public, private, and internal dispatch fixtures" assert_file_contains "$workflow_file" 'STRIX_MODEL: ${{ steps.gate.outputs.strix_model }}' "strix workflow propagates the gate-selected fallback model to the scanner" assert_file_not_contains "$workflow_file" "secrets.STRIX_LLM ||" "strix workflow must not let the legacy STRIX_LLM secret override PR defaults" - assert_file_contains "$workflow_file" "Strix model overrides are limited to contextual-orchestrator/orchestrator/free" "strix workflow rejects non-gateway model overrides" - assert_file_contains "$workflow_file" "STRIX_LLM must select contextual-orchestrator/orchestrator/free" "strix workflow accepts only the gateway model" + assert_file_contains "$workflow_file" "Strix model overrides must select literal auto; free and direct-provider routes are not admitted." "strix workflow rejects non-gateway model overrides" + assert_file_contains "$workflow_file" "STRIX_LLM must remain literal auto; free fallback is not admitted." "strix workflow accepts only the gateway model" assert_file_contains "$workflow_file" 'STRIX_FALLBACK_MODELS: ""' "strix workflow disables external fallback models" assert_file_contains "$workflow_file" 'STRIX_FAIL_ON_PROVIDER_SIGNAL: "1"' "strix workflow fails closed on timeout, fatal, warning, denied, or provider failure signals" assert_file_contains "$workflow_file" 'NPM_CONFIG_IGNORE_SCRIPTS: "true"' "strix workflow disables npm lifecycle scripts for untrusted PR scan data" @@ -694,8 +694,8 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_contains "$workflow_file" 'NVIDIA_NIM_API_KEY: ${{ secrets.NVIDIA_NIM_API_KEY }}' "opencode review passes the scoped provider credentials only to sidecar bootstrap" assert_file_contains "$workflow_file" "CONTEXTUAL_ORCHESTRATOR_REQUIRE_ZDR" "opencode review passes repository privacy to the gateway ZDR policy" assert_file_contains "$workflow_file" 'is_private: ${{ steps.validate.outputs.is_private }}' "opencode review carries validated repository privacy into gateway routing" - assert_file_contains "$workflow_file" '"model": "contextual-orchestrator/orchestrator/free"' "opencode review uses the gateway free pool" - assert_file_contains "$workflow_file" '"small_model": "contextual-orchestrator/orchestrator/free"' "opencode review uses the gateway for the small model" + assert_file_contains "$workflow_file" '"model": "contextual-orchestrator/orchestrator/auto"' "opencode review uses the cost-fenced gateway auto pool" + assert_file_contains "$workflow_file" '"small_model": "contextual-orchestrator/orchestrator/auto"' "opencode review uses the gateway for the small model" assert_file_contains "$workflow_file" '"enabled_providers": ["contextual-orchestrator"]' "opencode review enables only the gateway provider" assert_file_contains "$workflow_file" '"baseURL": "{env:CONTEXTUAL_ORCHESTRATOR_BASE_URL}/v1"' "opencode review routes model traffic through the gateway origin" assert_file_contains "$workflow_file" '"apiKey": "{env:CONTEXTUAL_ORCHESTRATOR_TOKEN}"' "opencode review routes model credentials through the gateway token" @@ -825,8 +825,8 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_contains "$workflow_file" 'OPENCODE_MODEL_ATTEMPTS: "1"' "opencode fallback tries the catalog promptly instead of spending the entire review on one model" assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review includes a broad catalog fallback pool" assert_file_not_contains "$workflow_file" "steps.opencode_review_model_pool.outcome == 'success'" "opencode approval gate still runs after model pool failure to publish a reason" - assert_file_contains "$workflow_file" '"model": "contextual-orchestrator/orchestrator/free"' "opencode review starts the gateway model pool" - assert_file_contains "$workflow_file" '"small_model": "contextual-orchestrator/orchestrator/free"' "opencode review uses the gateway small model" + assert_file_contains "$workflow_file" '"model": "contextual-orchestrator/orchestrator/auto"' "opencode review starts the gateway model pool" + assert_file_contains "$workflow_file" '"small_model": "contextual-orchestrator/orchestrator/auto"' "opencode review uses the gateway small model" assert_file_contains "$workflow_file" '"enabled_providers": ["contextual-orchestrator"]' "opencode review generates a gateway-only provider set" assert_file_not_contains "$workflow_file" "opencode-free/" "opencode review has no direct anonymous-provider candidates" assert_file_not_contains "$workflow_file" "github-models/" "opencode review has no direct GitHub Models candidates" @@ -981,7 +981,7 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" "OpenCode %s attempt %s/%s failed" "opencode catalog fallback records per-model retry failures" 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" '"model": "contextual-orchestrator/orchestrator/auto"' "opencode review keeps the generated model on orchestrator/auto" assert_file_contains "$workflow_file" "validate-pr-metadata:" "opencode admission job 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" @@ -1092,7 +1092,10 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_contains "$workflow_file" "continue-on-error: true" "opencode post-approval scheduler dispatch failure does not fail a completed approval check" assert_file_contains "$workflow_file" "Merge scheduler follow-up failed after approval; leaving OpenCode review intact." "opencode post-approval scheduler failure is reported as a warning" assert_file_contains "$workflow_file" "--no-trigger-reviews" "opencode post-approval scheduler follow-up avoids duplicate OpenCode review runs" - assert_file_contains "$workflow_file" "--enable-auto-merge" "opencode post-approval scheduler follow-up enables approved-head merge handling" + assert_file_contains "$workflow_file" "--no-enable-auto-merge" "opencode post-approval scheduler follow-up does not grant merge authority" + assert_file_contains "$workflow_file" "--dry-run" "opencode post-approval scheduler follow-up is read-only" + assert_file_contains "$workflow_file" "--merge-mode disabled" "opencode post-approval scheduler follow-up disables merge mutations" + assert_file_not_contains "$workflow_file" "--enable-auto-merge" "opencode post-approval scheduler follow-up cannot enable auto-merge" assert_file_contains "$workflow_file" "--no-update-branches" "opencode post-approval scheduler follow-up preserves the approved head instead of mutating branches" merge_scheduler_workflow="$REPO_ROOT/.github/workflows/pr-review-merge-scheduler.yml" assert_file_contains "$merge_scheduler_workflow" "pull_request_review:" "merge scheduler receives OpenCode App review publication as a separate event" @@ -1122,9 +1125,9 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_contains "$workflow_file" 'safe_pytest_command.py" discover' "opencode coverage evidence discovers default CI workflow pytest commands through the trusted shell-free parser" assert_file_not_contains "$REPO_ROOT/scripts/ci/safe_pytest_command.py" "RUNNER_EXECUTABLES" "configured pytest evidence cannot invoke uv, poetry, or pipenv dependency resolution" assert_file_contains "$workflow_file" "Python configured CI test suite" "opencode coverage evidence labels repository-configured pytest evidence separately" - assert_file_contains "$workflow_file" 'cd "$1" && PYTHONPATH="$([ -d src ] && printf src:. || printf .)" python3 -m coverage run -m pytest tests' "opencode coverage runs Python tests with the trusted preinstalled src-layout-aware toolchain" + assert_file_contains "$workflow_file" 'cd \"\$1\" && PYTHONPATH=\"\$([ -d src ] && printf src:. || printf .)\" python3 -m coverage run -m pytest tests' "opencode coverage runs Python tests with the trusted preinstalled src-layout-aware toolchain" assert_file_contains "$workflow_file" 'python3 -m coverage report --show-missing' "opencode coverage preserves the missing-line report with the trusted toolchain" - assert_file_contains "$workflow_file" 'cd "$1" && PYTHONPATH="$([ -d src ] && printf src:. || printf .)" python3 -m pytest tests/test_docstrings.py' "opencode docstring tests use the trusted preinstalled src-layout-aware pytest" + assert_file_contains "$workflow_file" 'cd \"\$1\" && PYTHONPATH=\"\$([ -d src ] && printf src:. || printf .)\" python3 -m pytest tests/test_docstrings.py' "opencode docstring tests use the trusted preinstalled src-layout-aware pytest" assert_file_contains "$workflow_file" "missing project imports fail in pytest" "unavailable project dependencies fail closed with their import error" assert_file_contains "$workflow_file" "JavaScript/TypeScript dependencies (npm offline ci, lifecycle hooks disabled)" "opencode coverage evidence installs the trusted materialized npm lock offline without lifecycle hooks before JS coverage" assert_file_contains "$workflow_file" "coverage/coverage-summary.json" "opencode coverage evidence reads JS coverage summaries instead of trusting test exit codes" @@ -1180,7 +1183,8 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_contains "$workflow_file" '(.workflow // "") == "CodeQL"' "opencode approval can distinguish CodeQL dynamic setup checks" assert_file_contains "$workflow_file" '((.isRequired // false) | not) and (.workflow // "") == "CodeQL"' "opencode approval ignores non-required cancelled CodeQL checks without source evidence" assert_file_contains "$workflow_file" 'select((.name // "") != "scan-pr-queue")' "opencode approval ignores scheduler queue self-checks for every failed or pending state" - scheduler_self_check_filter_count="$(grep -Fc 'select((.name // "") != "scan-pr-queue")' "$workflow_file")" + # Three literal jq filters plus the two shell-escaped REST failed/pending filters. + scheduler_self_check_filter_count="$(grep -Fc -e 'select((.name // "") != "scan-pr-queue")' -e 'select((.name // \"\") != \"scan-pr-queue\")' "$workflow_file")" if [ "$scheduler_self_check_filter_count" -lt 5 ]; then record_failure "opencode GraphQL and commit-check failed/pending paths all ignore scheduler queue self-checks (found ${scheduler_self_check_filter_count}, expected at least 5)" fi @@ -1226,8 +1230,8 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { if [ "$metadata_gate_filter_count" -lt 3 ]; then fail "opencode pre-model, failed-check, and pending-check collection all ignore metadata-only review-state gates (found ${metadata_gate_filter_count}, expected at least 3)" fi - assert_file_contains "$workflow_file" '["opencode-review", "coverage-evidence", "coverage-source-tree", "required-workflow-bootstrap", "metadata-only gate evaluation", "scan-pr-queue"]' "central fast approval ignores its dependent review and scheduler control-plane checks" - assert_file_contains "$workflow_file" '["opencode-review","coverage-evidence","metadata-only gate evaluation"]' "opencode supplemental check-run collection ignores review-state helper gates" + assert_file_contains "$workflow_file" '[\"opencode-review\", \"coverage-evidence\", \"coverage-source-tree\", \"required-workflow-bootstrap\", \"metadata-only gate evaluation\", \"scan-pr-queue\"]' "central fast approval ignores its dependent review and scheduler control-plane checks" + assert_file_contains "$workflow_file" '[\"opencode-review\",\"coverage-evidence\",\"metadata-only gate evaluation\"]' "opencode supplemental check-run collection ignores review-state helper gates" scheduler_pending_filter_count="$(grep -Fc 'select((.name // "") != "scan-pr-queue")' "$workflow_file")" if [ "$scheduler_pending_filter_count" -lt 3 ]; then fail "opencode pre-model, rollup, and commit-check pending collection all ignore the scheduler control-plane cycle (found ${scheduler_pending_filter_count}, expected at least 3)" @@ -1532,8 +1536,8 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() { assert_file_not_contains "$opencode_config" '"@upstash/context7-mcp' "opencode config does not install Context7 at runtime" assert_file_not_contains "$opencode_config" '"@guhcostan/web-search-mcp' "opencode config does not install web-search MCP at runtime" assert_file_not_contains "$opencode_config" '"serve"' "opencode config does not launch CodeGraph inside the credentialed model process" - assert_file_contains "$opencode_config" '"small_model": "contextual-orchestrator/orchestrator/free"' "opencode config routes the small model through the contextual-orchestrator free pool" - assert_file_contains "$opencode_config" '"model": "contextual-orchestrator/orchestrator/free"' "opencode config defaults review sessions to the contextual-orchestrator free pool" + assert_file_contains "$opencode_config" '"small_model": "contextual-orchestrator/orchestrator/auto"' "opencode config routes the small model through the contextual-orchestrator auto pool" + assert_file_contains "$opencode_config" '"model": "contextual-orchestrator/orchestrator/auto"' "opencode config defaults review sessions to the contextual-orchestrator auto pool" assert_file_not_contains "$opencode_config" '"small_model": "nvidia-nim/meta/llama-3.3-70b-instruct"' "opencode config no longer pins the NVIDIA NIM small model" assert_file_not_contains "$opencode_config" '"model": "nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5"' "opencode config no longer pins the NVIDIA NIM Nemotron Super default" assert_file_not_contains "$opencode_config" '"nvidia-nim"' "opencode config no longer defines a dormant nvidia-nim provider block"