From 631cdc84b5344d78673e48356f70148b36fa86a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 20 Jul 2026 22:59:47 +0900 Subject: [PATCH 01/10] fix(review): try GPT-4.1 first --- .github/workflows/opencode-review.yml | 2 +- tests/test_opencode_agent_contract.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7010b262..ce7572db 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3524,7 +3524,7 @@ jobs: # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget # stretches further between top-ups. - OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-4.1 github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" + OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index c8eb1d34..3c8ade2d 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -103,9 +103,9 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert candidate_pairs assert candidate_pairs == [ + ["github-models", "openai/gpt-4.1"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], - ["github-models", "openai/gpt-4.1"], ["github-models", "openai/gpt-5"], ["github-models", "openai/gpt-5-chat"], ["github-models", "openai/o3"], @@ -115,8 +115,8 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert direct_openai_models == ["gpt-5.6-luna"] assert set(github_candidate_models).issubset(set(github_models)) assert github_candidate_models == [ - "deepseek/deepseek-v3-0324", "openai/gpt-4.1", + "deepseek/deepseek-v3-0324", "openai/gpt-5", "openai/gpt-5-chat", "openai/o3", @@ -1090,9 +1090,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' + 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' + "github-models/deepseek/deepseek-v3-0324 " "openai/gpt-5.6-luna " - "github-models/openai/gpt-4.1 " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " "github-models/openai/o3 " @@ -1213,9 +1213,9 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' not in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' + 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' + "github-models/deepseek/deepseek-v3-0324 " "openai/gpt-5.6-luna " - "github-models/openai/gpt-4.1 " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " "github-models/openai/o3 " From f8021828133f94b8d339b84691e17b23e874e0c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 20 Jul 2026 23:23:05 +0900 Subject: [PATCH 02/10] docs(review): align candidate-order guidance --- .github/workflows/opencode-review.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index ce7572db..f5417312 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3516,10 +3516,9 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - # High-sensitivity review candidates only. DeepSeek V3 has been the - # most reliable first-pass reviewer in the org queue, then the pool - # falls through to the direct GPT-5.6 Luna slot, then the full-size - # GPT-4.1 long-context endpoint and provider-specific GPT/o3 fallbacks. + # High-sensitivity review candidates only. GPT-4.1 is the first-pass + # long-context endpoint, then the pool falls through to DeepSeek V3, + # the direct GPT-5.6 Luna slot, and provider-specific GPT/o3 fallbacks. # The direct-OpenAI slot runs GPT-5.6 Luna: the newest family's # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget From 871db61579d7ed2652af2fba00fb225b32427278 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 06:06:07 +0900 Subject: [PATCH 03/10] fix(review): exclude GPT-4.1 approval candidate --- .github/workflows/opencode-review.yml | 9 +++++---- tests/test_opencode_agent_contract.py | 16 ++++++---------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index f5417312..29f67e7a 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3516,14 +3516,15 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - # High-sensitivity review candidates only. GPT-4.1 is the first-pass - # long-context endpoint, then the pool falls through to DeepSeek V3, - # the direct GPT-5.6 Luna slot, and provider-specific GPT/o3 fallbacks. + # High-sensitivity review candidates only. DeepSeek V3 is the + # first-pass reviewer, then the pool falls through to the direct + # GPT-5.6 Luna slot and provider-specific GPT-5/o3 fallbacks. GPT-4.1 + # and weaker review candidates are excluded from approval evidence. # The direct-OpenAI slot runs GPT-5.6 Luna: the newest family's # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget # stretches further between top-ups. - OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" + OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 3c8ade2d..6e86a065 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -21,9 +21,8 @@ def test_code_reviewer_subagent_contract_is_configured(): assert reviewer["color"] == "#7c3aed" # Reasoning effort is model-level only (see the model configs below and the # ci-autofix agent). An agent-level reasoningEffort is applied to every - # candidate the agent runs, including non-reasoning models like - # github-models/openai/gpt-4.1, whose OpenAI backend rejects the - # reasoning_effort request argument outright. + # candidate the agent runs, including non-reasoning candidates whose + # provider backends reject the reasoning_effort request argument outright. assert "reasoningEffort" not in reviewer assert "model" not in reviewer assert "Reviews only; never edits code" in reviewer["description"] @@ -42,7 +41,7 @@ def test_code_reviewer_subagent_contract_is_configured(): for primary_agent in ("ci-review", "ci-review-fallback"): # Reasoning effort must NOT be set at the agent level: it would be sent - # to every pool candidate, and non-reasoning models (gpt-4.1) reject the + # to every pool candidate, and non-reasoning candidates reject the # reasoning_effort argument. Reasoning models carry it per-model instead. assert "reasoningEffort" not in agents[primary_agent] permission = agents[primary_agent]["permission"] @@ -103,7 +102,6 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert candidate_pairs assert candidate_pairs == [ - ["github-models", "openai/gpt-4.1"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], ["github-models", "openai/gpt-5"], @@ -115,7 +113,6 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert direct_openai_models == ["gpt-5.6-luna"] assert set(github_candidate_models).issubset(set(github_models)) assert github_candidate_models == [ - "openai/gpt-4.1", "deepseek/deepseek-v3-0324", "openai/gpt-5", "openai/gpt-5-chat", @@ -125,6 +122,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ] banned_review_candidates = { "gpt-5-nano", + "openai/gpt-4.1", "openai/gpt-5-nano", "openai/o3-mini", } @@ -1090,8 +1088,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' - "github-models/deepseek/deepseek-v3-0324 " + 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' "openai/gpt-5.6-luna " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " @@ -1213,8 +1210,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' not in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' - "github-models/deepseek/deepseek-v3-0324 " + 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' "openai/gpt-5.6-luna " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " From 447e062bdd4e4c869ced6f7f6e387b02b3b73805 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 07:41:17 +0900 Subject: [PATCH 04/10] fix(review): restore evidence-backed GPT-4.1 lead --- .github/workflows/opencode-review.yml | 9 ++++----- scripts/ci/test_strix_quick_gate.sh | 6 +++--- tests/test_opencode_agent_contract.py | 16 ++++++++++------ 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 29f67e7a..f5417312 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3516,15 +3516,14 @@ jobs: SHARE: "false" NPM_CONFIG_IGNORE_SCRIPTS: "true" NO_COLOR: "1" - # High-sensitivity review candidates only. DeepSeek V3 is the - # first-pass reviewer, then the pool falls through to the direct - # GPT-5.6 Luna slot and provider-specific GPT-5/o3 fallbacks. GPT-4.1 - # and weaker review candidates are excluded from approval evidence. + # High-sensitivity review candidates only. GPT-4.1 is the first-pass + # long-context endpoint, then the pool falls through to DeepSeek V3, + # the direct GPT-5.6 Luna slot, and provider-specific GPT/o3 fallbacks. # The direct-OpenAI slot runs GPT-5.6 Luna: the newest family's # cost-efficient tier, cheaper than the legacy gpt-5 it replaced # ($1/$6 vs $1.25/$10 per 1M tokens) so the org OpenAI budget # stretches further between top-ups. - OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" + OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5 github-models/openai/gpt-5-chat github-models/openai/o3 github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" # One attempt per model, then fall through to the next model. Retrying # the SAME model 5x let a rate-limited/hung leader consume the whole # step, so the pool never reached a healthy fallback model. diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index aae63c5d..b51edd55 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -691,7 +691,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { 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" "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review starts with DeepSeek V3 before full-size GPT fallbacks" + assert_file_contains "$workflow_file" "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5" "opencode review starts with GPT-4.1 before provider fallbacks" assert_file_contains "$workflow_file" "The publish gate re-runs source-backed validation against PR-head data" "opencode review publish gate validates model output against the PR-head worktree" assert_file_contains "$workflow_file" '"openai/o3"' "opencode config declares OpenAI o3 fallback" assert_file_contains "$workflow_file" '"openai/o4-mini"' "opencode config declares OpenAI o4-mini fallback" @@ -838,7 +838,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "5400"' "opencode catalog fallback preserves legitimate full-hour provider sessions" 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" "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review tries DeepSeek V3 before OpenAI fallbacks" + assert_file_contains "$workflow_file" "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5" "opencode review tries GPT-4.1 before provider fallbacks" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528 github-models/deepseek/deepseek-r1" "opencode review keeps DeepSeek reasoning fallback coverage after OpenAI candidates" assert_file_contains "$workflow_file" "coverage-source-tree:" "opencode workflow materializes coverage source before running PR-head tests" assert_file_contains "$workflow_file" "coverage-evidence:" "opencode workflow measures coverage before review" @@ -1155,7 +1155,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'GH_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN || steps.review_read_app_token.outputs.token || github.token }}' "opencode manual dispatch uses the cross-repo approval token for target PR evidence lookups with app-token fallback" assert_file_contains "$workflow_file" 'repos/${GH_REPOSITORY}' "opencode review workflow uses env-backed repository context in shell commands" assert_file_contains "$workflow_file" "Run OpenCode PR Review model pool" "opencode review starts the central model pool" - assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-4.1 github-models/openai/gpt-5" "opencode review starts with DeepSeek V3 before full-size GPT fallbacks" + assert_file_contains "$workflow_file" "github-models/openai/gpt-4.1 github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna github-models/openai/gpt-5" "opencode review starts with GPT-4.1 before provider fallbacks" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-r1-0528" "opencode review keeps a reachable DeepSeek R1 reasoning fallback model" assert_file_contains "$workflow_file" "github-models/deepseek/deepseek-v3-0324" "opencode review has a reachable DeepSeek V3 fallback model" assert_file_contains "$workflow_file" "github-models/openai/gpt-5" "opencode review still has a bounded GPT-5 fallback model" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 6e86a065..3c8ade2d 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -21,8 +21,9 @@ def test_code_reviewer_subagent_contract_is_configured(): assert reviewer["color"] == "#7c3aed" # Reasoning effort is model-level only (see the model configs below and the # ci-autofix agent). An agent-level reasoningEffort is applied to every - # candidate the agent runs, including non-reasoning candidates whose - # provider backends reject the reasoning_effort request argument outright. + # candidate the agent runs, including non-reasoning models like + # github-models/openai/gpt-4.1, whose OpenAI backend rejects the + # reasoning_effort request argument outright. assert "reasoningEffort" not in reviewer assert "model" not in reviewer assert "Reviews only; never edits code" in reviewer["description"] @@ -41,7 +42,7 @@ def test_code_reviewer_subagent_contract_is_configured(): for primary_agent in ("ci-review", "ci-review-fallback"): # Reasoning effort must NOT be set at the agent level: it would be sent - # to every pool candidate, and non-reasoning candidates reject the + # to every pool candidate, and non-reasoning models (gpt-4.1) reject the # reasoning_effort argument. Reasoning models carry it per-model instead. assert "reasoningEffort" not in agents[primary_agent] permission = agents[primary_agent]["permission"] @@ -102,6 +103,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert candidate_pairs assert candidate_pairs == [ + ["github-models", "openai/gpt-4.1"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], ["github-models", "openai/gpt-5"], @@ -113,6 +115,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): assert direct_openai_models == ["gpt-5.6-luna"] assert set(github_candidate_models).issubset(set(github_models)) assert github_candidate_models == [ + "openai/gpt-4.1", "deepseek/deepseek-v3-0324", "openai/gpt-5", "openai/gpt-5-chat", @@ -122,7 +125,6 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ] banned_review_candidates = { "gpt-5-nano", - "openai/gpt-4.1", "openai/gpt-5-nano", "openai/o3-mini", } @@ -1088,7 +1090,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' + 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' + "github-models/deepseek/deepseek-v3-0324 " "openai/gpt-5.6-luna " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " @@ -1210,7 +1213,8 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-5-nano"' not in workflow ) assert ( - 'OPENCODE_MODEL_CANDIDATES: "github-models/deepseek/deepseek-v3-0324 ' + 'OPENCODE_MODEL_CANDIDATES: "github-models/openai/gpt-4.1 ' + "github-models/deepseek/deepseek-v3-0324 " "openai/gpt-5.6-luna " "github-models/openai/gpt-5 " "github-models/openai/gpt-5-chat " From 54538d1d7767b0c2a71050d1c41391c27aed607a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 10:36:03 +0900 Subject: [PATCH 05/10] fix(review): close trusted log and symlink boundaries --- .github/workflows/opencode-review.yml | 21 ++++ tests/test_opencode_agent_contract.py | 169 ++++++++++++++++++++++++-- 2 files changed, 183 insertions(+), 7 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index f5417312..7b6dd9c8 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -1626,7 +1626,13 @@ jobs: printf 'Published compact coverage decision output after sanitization (%s bytes); full command logs remain in the job log and step summary.\n' \ "$(wc -c <"$summary_output_file" | tr -d ' ')" + coverage_log_stop_token="$(python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + while grep -Fq "$coverage_log_stop_token" "$summary_file"; do + coverage_log_stop_token="$(python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + done + printf '::stop-commands::%s\n' "$coverage_log_stop_token" cat "$summary_file" + printf '\n::%s::\n' "$coverage_log_stop_token" # No process running pull-request code may survive into the trusted # publication phase. The result is copied from a root-only tmpfs only # after every low-privilege process has been terminated. @@ -1857,6 +1863,20 @@ jobs: git cat-file -e "${PR_HEAD_SHA}^{commit}" rm -rf "$OPENCODE_SOURCE_WORKDIR" git worktree add --detach "$OPENCODE_SOURCE_WORKDIR" "$PR_HEAD_SHA" + while IFS= read -r -d '' indexed_entry; do + indexed_mode="${indexed_entry%% *}" + case "$indexed_mode" in + 100644 | 100755) ;; + *) + printf '::error::PR worktree contains non-regular tracked entry mode %s; refusing trusted review processing.\n' "$indexed_mode" + exit 1 + ;; + esac + done < <(git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z) + if find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l -print -quit | grep -q .; then + echo "::error::PR worktree contains a symbolic link; refusing trusted review processing." + exit 1 + fi git -C "$OPENCODE_SOURCE_WORKDIR" status --short - name: Configure git identity for OpenCode action @@ -2090,6 +2110,7 @@ jobs: CODEGRAPH_BIN="${CODEGRAPH_TRUSTED_ROOT}/node_modules/.bin/codegraph" test -x "$CODEGRAPH_BIN" printf 'Using trusted CodeGraph CLI version %s.\n' "$("$CODEGRAPH_BIN" --version)" + rm -rf -- "$OPENCODE_SOURCE_WORKDIR/.codegraph" cd "$OPENCODE_SOURCE_WORKDIR" "$CODEGRAPH_BIN" init -i codegraph_status="$(mktemp)" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 3c8ade2d..04c0124f 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -329,7 +329,10 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "docker.io/library/ubuntu@sha256:" in measure_step assert "apt-get install --no-install-recommends -y" in measure_step assert "--require-hashes" in measure_step - assert 'coverage_tool_image="opencode-coverage-tools:${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"' in measure_step + 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 "docker build --pull --no-cache --network=default" in measure_step @@ -406,9 +409,9 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "uv run --no-project" not in measure_step assert "uv run --no-build" not in measure_step assert "python3 -m coverage run -m pytest tests" in measure_step - trusted_requirements = Path( - "requirements-opencode-review-ci-hashes.txt" - ).read_text(encoding="utf-8") + trusted_requirements = Path("requirements-opencode-review-ci-hashes.txt").read_text( + encoding="utf-8" + ) assert "pytest-cov==7.1.0" in trusted_requirements assert ( "a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678" @@ -496,9 +499,9 @@ def test_opencode_model_exhaustion_retry_stays_owned_by_central_scheduler(): def test_opencode_python_coverage_never_resolves_pr_dependency_manifests(): """Use only the trusted image toolchain during networkless PR execution.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") - measure = workflow.split( - " - name: Measure test and docstring evidence\n", 1 - )[1].split("\n - name:", 1)[0] + measure = workflow.split(" - name: Measure test and docstring evidence\n", 1)[ + 1 + ].split("\n - name:", 1)[0] assert "verify_trusted_python_test_toolchain()" in measure assert "PR-selected dependency manifests are never resolved" in measure @@ -1566,6 +1569,18 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) assert 'cat "$summary_output_file"' in coverage_job assert "Published compact coverage decision output" in coverage_job + assert 'coverage_log_stop_token="$(python3 -I -c' in coverage_job + assert 'grep -Fq "$coverage_log_stop_token" "$summary_file"' in coverage_job + assert ( + "printf '::stop-commands::%s\\n' \"$coverage_log_stop_token\"" in coverage_job + ) + assert 'cat "$summary_file"' in coverage_job + assert "printf '\\n::%s::\\n' \"$coverage_log_stop_token\"" in coverage_job + assert ( + coverage_job.index("printf '::stop-commands::%s\\n'") + < coverage_job.index('cat "$summary_file"') + < coverage_job.index("printf '\\n::%s::\\n'") + ) assert "actions: read" in coverage_job assert "contents: read" not in coverage_job assert 'GITHUB_TOKEN: ""' in coverage_job @@ -1617,6 +1632,13 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) < target_job.index( "Exchange OpenCode app token for target repository review reads" ) + materialize_step = target_job.split( + " - name: Materialize pull request head for OpenCode review data", 1 + )[1].split("\n - name:", 1)[0] + assert 'git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z' in materialize_step + assert "100644 | 100755" in materialize_step + assert 'find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l' in materialize_step + assert "refusing trusted review processing" in materialize_step codegraph_step = target_job.split( " - name: Initialize CodeGraph index for OpenCode", 1 )[1].split("\n - name:", 1)[0] @@ -1639,6 +1661,7 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): assert '"$CODEGRAPH_BIN" --version' in codegraph_step assert 'cat "$codegraph_status" >&2' in codegraph_step assert 'cat "$codegraph_raw" >&2' in codegraph_step + assert 'rm -rf -- "$OPENCODE_SOURCE_WORKDIR/.codegraph"' in codegraph_step assert "CodeGraph status failed; approval evidence is incomplete." in codegraph_step assert ( "CodeGraph changed-scope exploration failed; approval evidence is incomplete." @@ -1669,6 +1692,138 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) +def test_coverage_log_replay_disables_runner_commands_and_retries_token_collision( + tmp_path, +): + """Untrusted coverage bytes stay inside a collision-free stop-command envelope.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + replay_start = workflow.index(' coverage_log_stop_token="$(python3 -I -c') + replay_end = workflow.index( + " # No process running pull-request code", replay_start + ) + replay = textwrap.dedent(workflow[replay_start:replay_end]) + + summary = tmp_path / "coverage-evidence.md" + summary.write_text( + "coverage-log-collision\n" + "::set-output name=coverage_summary::ATTACKER\n" + "::add-path::/tmp/attacker\n", + encoding="utf-8", + ) + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + counter = tmp_path / "counter" + fake_python = fake_bin / "python3" + fake_python.write_text( + "#!/usr/bin/env bash\n" + 'count="$(cat "$FAKE_COUNTER" 2>/dev/null || printf 0)"\n' + 'printf "%s" "$((count + 1))" >"$FAKE_COUNTER"\n' + 'if [ "$count" -eq 0 ]; then\n' + " printf '%s\\n' coverage-log-collision\n" + "else\n" + " printf '%s\\n' coverage-log-safe\n" + "fi\n", + encoding="utf-8", + ) + fake_python.chmod(0o755) + + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + replay], + env={ + **os.environ, + "PATH": f"{fake_bin}:{os.environ['PATH']}", + "FAKE_COUNTER": str(counter), + "summary_file": str(summary), + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + lines = result.stdout.splitlines() + assert lines[0] == "::stop-commands::coverage-log-safe" + assert "::set-output name=coverage_summary::ATTACKER" in lines[1:-1] + assert "::add-path::/tmp/attacker" in lines[1:-1] + assert lines[-1] == "::coverage-log-safe::" + assert counter.read_text(encoding="utf-8") == "2" + + +def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_path): + """A tracked symlink cannot escape the PR worktree into runner credentials.""" + if not hasattr(os, "symlink"): + pytest.skip("symlinks are unavailable") + + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + validation_start = workflow.index( + " while IFS= read -r -d '' indexed_entry; do" + ) + validation_end = workflow.index( + ' git -C "$OPENCODE_SOURCE_WORKDIR" status --short', + validation_start, + ) + validation = textwrap.dedent(workflow[validation_start:validation_end]) + + repo = tmp_path / "repo" + repo.mkdir() + subprocess.run(["git", "init", "-q"], cwd=repo, check=True) + subprocess.run( + ["git", "config", "user.name", "Trust Boundary Test"], cwd=repo, check=True + ) + subprocess.run( + ["git", "config", "user.email", "trust@example.invalid"], + cwd=repo, + check=True, + ) + (repo / "safe.txt").write_text("safe\n", encoding="utf-8") + subprocess.run(["git", "add", "safe.txt"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "base"], cwd=repo, check=True) + base_sha = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=repo, text=True + ).strip() + + outside = tmp_path / "runner-credential" + outside.write_text("synthetic-secret\n", encoding="utf-8") + (repo / "credential-link").symlink_to(outside) + subprocess.run(["git", "add", "credential-link"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "symlink head"], cwd=repo, check=True) + + clean_worktree = tmp_path / "clean-worktree" + subprocess.run( + ["git", "worktree", "add", "--detach", str(clean_worktree), base_sha], + cwd=repo, + capture_output=True, + check=True, + ) + clean = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(clean_worktree)}, + text=True, + capture_output=True, + check=False, + ) + assert clean.returncode == 0, clean.stderr + + malicious_worktree = tmp_path / "malicious-worktree" + subprocess.run( + ["git", "worktree", "add", "--detach", str(malicious_worktree), "HEAD"], + cwd=repo, + capture_output=True, + check=True, + ) + rejected = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(malicious_worktree)}, + text=True, + capture_output=True, + check=False, + ) + + assert rejected.returncode == 1 + assert "refusing trusted review processing" in rejected.stdout + assert outside.read_text(encoding="utf-8") == "synthetic-secret\n" + + def test_opencode_pending_peer_checks_hold_blocks_required_workflow_until_approval(): """Pending peer checks cannot satisfy the required gate without a review.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") From 826a628a6612da400a5786c4fe3dde999c38d106 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 12:42:23 +0900 Subject: [PATCH 06/10] fix(review): strip git history from coverage artifacts --- .github/workflows/opencode-review.yml | 17 +++++++++ tests/test_opencode_agent_contract.py | 51 +++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7b6dd9c8..7c6608f2 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -293,7 +293,24 @@ jobs: mkdir -p "$(dirname "$COVERAGE_SOURCE_WORKDIR")" mv "$fetch_dir" "$COVERAGE_SOURCE_WORKDIR" git -C "$COVERAGE_SOURCE_WORKDIR" status --short + # Only the current merged source tree may cross the repository boundary. + # The fetch repository contains target history, including deleted files, + # so remove its metadata before producing the same-run artifact. + rm -rf "$COVERAGE_SOURCE_WORKDIR/.git" tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$COVERAGE_SOURCE_WORKDIR" . + if tar -tf "$COVERAGE_SOURCE_ARCHIVE" | awk ' + { + member = $0 + sub(/^\.\//, "", member) + if (member == ".git" || member ~ /(^|\/)\.git(\/|$)/) { + found = 1 + } + } + END { exit(found ? 0 : 1) } + '; then + echo "::error::Coverage source archive unexpectedly contains Git metadata." + exit 1 + fi - name: Upload materialized pull request merge tree uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 04c0124f..041b6f32 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -3,6 +3,7 @@ import re import shutil import subprocess +import tarfile import textwrap from pathlib import Path @@ -425,6 +426,56 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "github.event_name == 'pull_request_target'" not in target_condition +def test_coverage_source_artifact_excludes_git_history(tmp_path): + """The cross-job source archive must not carry target repository history.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + package_start = workflow.index( + ' git -C "$COVERAGE_SOURCE_WORKDIR" status --short\n' + ) + package_end = workflow.index("\n\n - name:", package_start) + package_script = textwrap.dedent(workflow[package_start:package_end]) + + repo = tmp_path / "source" + repo.mkdir() + subprocess.run(["git", "init", "-q"], cwd=repo, check=True) + subprocess.run( + ["git", "config", "user.name", "Artifact Boundary Test"], + cwd=repo, + check=True, + ) + subprocess.run( + ["git", "config", "user.email", "artifact@example.invalid"], + cwd=repo, + check=True, + ) + (repo / ".env").write_text("PRIVATE_TOKEN=deleted-history\n", encoding="utf-8") + subprocess.run(["git", "add", ".env"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "historical secret"], cwd=repo, check=True) + (repo / ".env").unlink() + (repo / "safe.txt").write_text("current source\n", encoding="utf-8") + subprocess.run(["git", "add", "-A"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "current source"], cwd=repo, check=True) + + archive = tmp_path / "source.tar" + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + package_script], + env={ + **os.environ, + "COVERAGE_SOURCE_WORKDIR": str(repo), + "COVERAGE_SOURCE_ARCHIVE": str(archive), + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + with tarfile.open(archive) as bundle: + names = [name.removeprefix("./") for name in bundle.getnames()] + assert "safe.txt" in names + assert not any(name == ".git" or name.startswith(".git/") for name in names) + + def test_opencode_repository_dispatch_authorization_is_fail_closed(): """Reject an untrusted dispatcher or a target outside the exact allowlist.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") From 039c3c1bc844ce3913fc1408d18a6a3da65d5a79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 13:01:00 +0900 Subject: [PATCH 07/10] fix(review): keep Git history out of coverage artifacts --- .github/workflows/opencode-review.yml | 187 +++++++++++++++++++++----- tests/test_opencode_agent_contract.py | 127 +++++++++++++---- 2 files changed, 249 insertions(+), 65 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 7c6608f2..5171766c 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -248,12 +248,13 @@ jobs: PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }} PR_BASE_SHA: ${{ needs.validate-pr-metadata.outputs.base_sha }} PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} + COVERAGE_BASE_WORKDIR: ${{ runner.temp }}/opencode-coverage-base COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-coverage-source COVERAGE_SOURCE_ARCHIVE: ${{ runner.temp }}/opencode-coverage-source.tar run: | set -euo pipefail fetch_dir="${RUNNER_TEMP}/opencode-coverage-fetch" - rm -rf "$fetch_dir" "$COVERAGE_SOURCE_WORKDIR" "$COVERAGE_SOURCE_ARCHIVE" + rm -rf "$fetch_dir" "$COVERAGE_BASE_WORKDIR" "$COVERAGE_SOURCE_WORKDIR" "$COVERAGE_SOURCE_ARCHIVE" if [ -z "${GH_TOKEN:-}" ]; then echo "::error::Coverage merge tree materialization requires a GitHub token." exit 1 @@ -290,27 +291,72 @@ jobs: echo "::error::Coverage merge tree could not be materialized for base ${PR_BASE_SHA} and head ${PR_HEAD_SHA}; resolve merge conflicts or rerun after GitHub can synthesize the PR merge commit." exit 1 fi - mkdir -p "$(dirname "$COVERAGE_SOURCE_WORKDIR")" + + validate_coverage_tree_modes() { + local treeish="$1" + local indexed_entry indexed_mode + while IFS= read -r -d '' indexed_entry; do + indexed_mode="${indexed_entry%% *}" + case "$indexed_mode" in + 100644 | 100755) ;; + *) + printf '::error::Coverage source tree %s contains non-regular tracked entry mode %s; refusing cross-job artifact materialization.\n' "$treeish" "$indexed_mode" + exit 1 + ;; + esac + done < <(git -C "$fetch_dir" ls-tree -r -z --full-tree "$treeish") + } + validate_coverage_tree_modes "$PR_BASE_SHA" + validate_coverage_tree_modes HEAD + + # BEGIN_COVERAGE_SOURCE_EXPORT + # Export both snapshots without transferring the target repository's + # Git object database into the central repository artifact namespace. + git -C "$fetch_dir" worktree add --detach "$COVERAGE_BASE_WORKDIR" "$PR_BASE_SHA" + rm -f -- "$COVERAGE_BASE_WORKDIR/.git" + rm -rf -- "$fetch_dir/.git" mv "$fetch_dir" "$COVERAGE_SOURCE_WORKDIR" - git -C "$COVERAGE_SOURCE_WORKDIR" status --short - # Only the current merged source tree may cross the repository boundary. - # The fetch repository contains target history, including deleted files, - # so remove its metadata before producing the same-run artifact. - rm -rf "$COVERAGE_SOURCE_WORKDIR/.git" - tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$COVERAGE_SOURCE_WORKDIR" . - if tar -tf "$COVERAGE_SOURCE_ARCHIVE" | awk ' - { - member = $0 - sub(/^\.\//, "", member) - if (member == ".git" || member ~ /(^|\/)\.git(\/|$)/) { - found = 1 - } - } - END { exit(found ? 0 : 1) } - '; then - echo "::error::Coverage source archive unexpectedly contains Git metadata." - exit 1 - fi + for source_tree in "$COVERAGE_BASE_WORKDIR" "$COVERAGE_SOURCE_WORKDIR"; do + if [ -e "$source_tree/.git" ] || [ -L "$source_tree/.git" ]; then + echo "::error::Coverage source export retained forbidden Git metadata at ${source_tree}/.git." + exit 1 + fi + if find -P "$source_tree" -mindepth 1 -type l -print -quit | grep -q .; then + echo "::error::Coverage source export contains a symbolic link; refusing cross-job artifact materialization." + exit 1 + fi + done + tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$RUNNER_TEMP" \ + "$(basename "$COVERAGE_BASE_WORKDIR")" \ + "$(basename "$COVERAGE_SOURCE_WORKDIR")" + python3 -I - "$COVERAGE_SOURCE_ARCHIVE" <<'PY' + from pathlib import Path, PurePosixPath + import sys + import tarfile + + archive = Path(sys.argv[1]) + if not archive.is_file() or archive.is_symlink(): + raise SystemExit( + f"Coverage source archive is not a regular non-symlink file: {archive}" + ) + with tarfile.open(archive, mode="r:") as bundle: + for member in bundle.getmembers(): + path = PurePosixPath(member.name) + if path.is_absolute() or ".." in path.parts: + raise SystemExit( + f"Coverage source archive contains an unsafe path: {member.name!r}" + ) + if ".git" in path.parts: + raise SystemExit( + f"Coverage source archive contains forbidden Git metadata: {member.name!r}" + ) + if not (member.isfile() or member.isdir()): + raise SystemExit( + "Coverage source archive contains a forbidden non-regular " + f"member: {member.name!r}" + ) + PY + # END_COVERAGE_SOURCE_EXPORT - name: Upload materialized pull request merge tree uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -408,12 +454,14 @@ jobs: COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head run: | set -euo pipefail + artifact_extract_dir="${RUNNER_TEMP}/opencode-coverage-extracted" + rm -rf "$artifact_extract_dir" + mkdir -p "$artifact_extract_dir" rm -rf "$COVERAGE_SOURCE_WORKDIR" - mkdir -p "$COVERAGE_SOURCE_WORKDIR" # The archive contains pull-request-controlled paths. Validate every # member before extraction so a symlink, hardlink, device, FIFO, or # traversal path cannot redirect a later trusted host-side parser. - python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$COVERAGE_SOURCE_WORKDIR" <<'PY' + python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir" <<'PY' import os from pathlib import Path, PurePosixPath import sys @@ -429,6 +477,10 @@ jobs: with tarfile.open(archive, mode="r:*") as bundle: members = bundle.getmembers() seen: set[str] = set() + allowed_roots = { + "opencode-coverage-base", + "opencode-coverage-source", + } for member in members: path = PurePosixPath(member.name) normalized = path.as_posix() @@ -436,6 +488,14 @@ jobs: raise SystemExit( f"Coverage source archive contains an unsafe path: {member.name!r}" ) + if not path.parts or path.parts[0] not in allowed_roots: + raise SystemExit( + f"Coverage source archive contains an unexpected root: {member.name!r}" + ) + if ".git" in path.parts: + raise SystemExit( + f"Coverage source archive contains forbidden Git metadata: {member.name!r}" + ) if normalized in seen: raise SystemExit( f"Coverage source archive contains a duplicate path: {member.name!r}" @@ -453,6 +513,39 @@ jobs: ) bundle.extractall(destination, members=members, filter="data") PY + coverage_base_tree="${artifact_extract_dir}/opencode-coverage-base" + coverage_head_tree="${artifact_extract_dir}/opencode-coverage-source" + for source_tree in "$coverage_base_tree" "$coverage_head_tree"; do + if [ ! -d "$source_tree" ] || [ -L "$source_tree" ]; then + echo "::error::Coverage source artifact did not contain both regular base/head snapshot directories." + exit 1 + fi + done + + # Reconstruct only the two snapshots needed for diff-aware coverage. + # Synthetic Git metadata is created locally in this job and is never + # uploaded to the central repository artifact namespace. + git init "$COVERAGE_SOURCE_WORKDIR" + git -C "$COVERAGE_SOURCE_WORKDIR" config user.name "github-actions[bot]" + git -C "$COVERAGE_SOURCE_WORKDIR" config user.email \ + "41898282+github-actions[bot]@users.noreply.github.com" + cp -a -- "$coverage_base_tree/." "$COVERAGE_SOURCE_WORKDIR/" + git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force + git -C "$COVERAGE_SOURCE_WORKDIR" commit --allow-empty --no-gpg-sign \ + -m "coverage base snapshot" + coverage_base_sha="$(git -C "$COVERAGE_SOURCE_WORKDIR" rev-parse HEAD)" + find "$COVERAGE_SOURCE_WORKDIR" -mindepth 1 -maxdepth 1 ! -name .git \ + -exec rm -rf -- {} + + cp -a -- "$coverage_head_tree/." "$COVERAGE_SOURCE_WORKDIR/" + git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force + git -C "$COVERAGE_SOURCE_WORKDIR" commit --allow-empty --no-gpg-sign \ + -m "coverage head snapshot" + coverage_head_sha="$(git -C "$COVERAGE_SOURCE_WORKDIR" rev-parse HEAD)" + { + printf 'COVERAGE_BASE_SHA=%s\n' "$coverage_base_sha" + printf 'COVERAGE_HEAD_SHA=%s\n' "$coverage_head_sha" + } >>"$GITHUB_ENV" + rm -rf "$artifact_extract_dir" git -C "$COVERAGE_SOURCE_WORKDIR" status --short - name: Enforce post-merge stale agent replay guard @@ -473,8 +566,8 @@ jobs: replay_status=0 python3 "$GITHUB_WORKSPACE/scripts/ci/pr_head_replay_guard.py" \ --repo-root "$COVERAGE_SOURCE_WORKDIR" \ - --base-sha "$PR_BASE_SHA" \ - --head-sha "$PR_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? + --base-sha "$COVERAGE_BASE_SHA" \ + --head-sha "$COVERAGE_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? cat "$replay_report" if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then { @@ -502,7 +595,7 @@ jobs: # files, so a syntax error in a changed file that no test imports (or # in a language with no wired-in runner) could otherwise be approved. changed_files_file="${RUNNER_TEMP}/opencode-syntax-changed-files.txt" - if ! git -C "$COVERAGE_SOURCE_WORKDIR" diff --name-only "$PR_BASE_SHA" HEAD >"$changed_files_file" 2>/dev/null; then + if ! git -C "$COVERAGE_SOURCE_WORKDIR" diff --name-only "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA" >"$changed_files_file" 2>/dev/null; then : >"$changed_files_file" fi syntax_report="${RUNNER_TEMP}/opencode-syntax-report.txt" @@ -656,6 +749,8 @@ jobs: --env COVERAGE_SOURCE_WORKDIR=/work \ --env PR_BASE_SHA="$PR_BASE_SHA" \ --env PR_HEAD_SHA="$PR_HEAD_SHA" \ + --env COVERAGE_BASE_SHA="$COVERAGE_BASE_SHA" \ + --env COVERAGE_HEAD_SHA="$COVERAGE_HEAD_SHA" \ --env RUNNER_TEMP=/secure-output \ --env GITHUB_OUTPUT=/secure-output/github-output \ --env GITHUB_STEP_SUMMARY=/secure-output/step-summary \ @@ -846,10 +941,10 @@ jobs: } changed_files_for_coverage() { - if [ -n "${PR_BASE_SHA:-}" ] && [ -n "${PR_HEAD_SHA:-}" ] \ - && trusted_git rev-parse --verify --quiet "$PR_BASE_SHA^{commit}" >/dev/null \ - && trusted_git rev-parse --verify --quiet "$PR_HEAD_SHA^{commit}" >/dev/null; then - trusted_git diff --name-only --find-renames "$PR_BASE_SHA" "$PR_HEAD_SHA" + if [ -n "${COVERAGE_BASE_SHA:-}" ] && [ -n "${COVERAGE_HEAD_SHA:-}" ] \ + && trusted_git rev-parse --verify --quiet "$COVERAGE_BASE_SHA^{commit}" >/dev/null \ + && trusted_git rev-parse --verify --quiet "$COVERAGE_HEAD_SHA^{commit}" >/dev/null; then + trusted_git diff --name-only --find-renames "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA" else trusted_git ls-files fi @@ -1217,8 +1312,8 @@ jobs: run_and_capture "JavaScript/TypeScript coverage threshold" \ python3 "$GITHUB_WORKSPACE/scripts/ci/javascript_coverage_gate.py" \ --repo-root . \ - --base-sha "$PR_BASE_SHA" \ - --head-sha "$PR_HEAD_SHA" \ + --base-sha "$COVERAGE_BASE_SHA" \ + --head-sha "$COVERAGE_HEAD_SHA" \ --summary-list "$summary_list" } @@ -1631,9 +1726,17 @@ jobs: python3 -I "$GITHUB_WORKSPACE/scripts/ci/sanitize_github_output_summary.py" \ "$coverage_output_file" "$summary_output_file" - coverage_output_delimiter="$(python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + coverage_output_delimiter="$(/usr/bin/python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + if ! [[ "$coverage_output_delimiter" =~ ^coverage_[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage output delimiter generation returned an unsafe value." + exit 1 + fi while grep -Fqx "$coverage_output_delimiter" "$summary_output_file"; do - coverage_output_delimiter="$(python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + coverage_output_delimiter="$(/usr/bin/python3 -I -c 'import os; print("coverage_" + os.urandom(24).hex())')" + if ! [[ "$coverage_output_delimiter" =~ ^coverage_[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage output delimiter generation returned an unsafe value." + exit 1 + fi done { printf 'coverage_summary<<%s\n' "$coverage_output_delimiter" @@ -1643,13 +1746,23 @@ jobs: printf 'Published compact coverage decision output after sanitization (%s bytes); full command logs remain in the job log and step summary.\n' \ "$(wc -c <"$summary_output_file" | tr -d ' ')" - coverage_log_stop_token="$(python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + # BEGIN_COVERAGE_LOG_REPLAY + coverage_log_stop_token="$(/usr/bin/python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + if ! [[ "$coverage_log_stop_token" =~ ^coverage-log-[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage log stop-token generation returned an unsafe value." + exit 1 + fi while grep -Fq "$coverage_log_stop_token" "$summary_file"; do - coverage_log_stop_token="$(python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + coverage_log_stop_token="$(/usr/bin/python3 -I -c 'import os; print("coverage-log-" + os.urandom(24).hex())')" + if ! [[ "$coverage_log_stop_token" =~ ^coverage-log-[0-9a-f]{48}$ ]]; then + echo "::error::Trusted coverage log stop-token generation returned an unsafe value." + exit 1 + fi done printf '::stop-commands::%s\n' "$coverage_log_stop_token" cat "$summary_file" printf '\n::%s::\n' "$coverage_log_stop_token" + # END_COVERAGE_LOG_REPLAY # No process running pull-request code may survive into the trusted # publication phase. The result is copied from a root-only tmpfs only # after every low-privilege process has been terminated. @@ -1880,6 +1993,7 @@ jobs: git cat-file -e "${PR_HEAD_SHA}^{commit}" rm -rf "$OPENCODE_SOURCE_WORKDIR" git worktree add --detach "$OPENCODE_SOURCE_WORKDIR" "$PR_HEAD_SHA" + # BEGIN_PR_WORKTREE_ENTRY_VALIDATION while IFS= read -r -d '' indexed_entry; do indexed_mode="${indexed_entry%% *}" case "$indexed_mode" in @@ -1894,6 +2008,7 @@ jobs: echo "::error::PR worktree contains a symbolic link; refusing trusted review processing." exit 1 fi + # END_PR_WORKTREE_ENTRY_VALIDATION git -C "$OPENCODE_SOURCE_WORKDIR" status --short - name: Configure git identity for OpenCode action diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 041b6f32..ac3cc409 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -1,6 +1,7 @@ import json import os import re +import shlex import shutil import subprocess import tarfile @@ -311,6 +312,19 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'merge --no-ff --no-edit "$PR_HEAD_SHA"' in step assert "Coverage merge tree could not be materialized" in step assert "PR_HEAD_SHA:" in step + assert 'validate_coverage_tree_modes "$PR_BASE_SHA"' in step + assert "validate_coverage_tree_modes HEAD" in step + assert 'ls-tree -r -z --full-tree "$treeish"' in step + assert "100644 | 100755" in step + assert 'rm -f -- "$COVERAGE_BASE_WORKDIR/.git"' in step + assert 'rm -rf -- "$fetch_dir/.git"' in step + assert "Coverage source export retained forbidden Git metadata" in step + assert "Coverage source archive contains forbidden Git metadata" in step + assert step.index('rm -rf -- "$fetch_dir/.git"') < step.index( + 'tar -cf "$COVERAGE_SOURCE_ARCHIVE"' + ) + assert '"$(basename "$COVERAGE_BASE_WORKDIR")"' in step + assert '"$(basename "$COVERAGE_SOURCE_WORKDIR")"' in step measure_start = workflow.index( " - name: Measure test and docstring evidence\n" @@ -322,9 +336,23 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "secrets." not in measure_step assert "COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head" in workflow assert ( - 'python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$COVERAGE_SOURCE_WORKDIR"' in workflow + 'python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir"' + in coverage_job ) + assert '"opencode-coverage-base"' in coverage_job + assert '"opencode-coverage-source"' in coverage_job assert "member.isfile() or member.isdir()" in workflow + assert "Coverage source archive contains an unexpected root" in coverage_job + assert "Coverage source archive contains forbidden Git metadata" in coverage_job + assert 'git init "$COVERAGE_SOURCE_WORKDIR"' in coverage_job + assert 'git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force' in coverage_job + assert "coverage base snapshot" in coverage_job + assert "coverage head snapshot" in coverage_job + assert "COVERAGE_BASE_SHA=%s" in coverage_job + assert "COVERAGE_HEAD_SHA=%s" in coverage_job + assert 'diff --name-only "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA"' in coverage_job + assert '--base-sha "$COVERAGE_BASE_SHA"' in coverage_job + assert '--head-sha "$COVERAGE_HEAD_SHA"' in coverage_job assert 'bundle.extractall(destination, members=members, filter="data")' in workflow assert 'tar -xf "$COVERAGE_SOURCE_ARCHIVE"' not in workflow assert "docker.io/library/ubuntu@sha256:" in measure_step @@ -429,13 +457,12 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): def test_coverage_source_artifact_excludes_git_history(tmp_path): """The cross-job source archive must not carry target repository history.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") - package_start = workflow.index( - ' git -C "$COVERAGE_SOURCE_WORKDIR" status --short\n' - ) - package_end = workflow.index("\n\n - name:", package_start) - package_script = textwrap.dedent(workflow[package_start:package_end]) + export_start = workflow.index(" # BEGIN_COVERAGE_SOURCE_EXPORT\n") + export_start = workflow.index("\n", export_start) + 1 + export_end = workflow.index(" # END_COVERAGE_SOURCE_EXPORT", export_start) + export_script = textwrap.dedent(workflow[export_start:export_end]) - repo = tmp_path / "source" + repo = tmp_path / "fetch" repo.mkdir() subprocess.run(["git", "init", "-q"], cwd=repo, check=True) subprocess.run( @@ -454,15 +481,27 @@ def test_coverage_source_artifact_excludes_git_history(tmp_path): (repo / ".env").unlink() (repo / "safe.txt").write_text("current source\n", encoding="utf-8") subprocess.run(["git", "add", "-A"], cwd=repo, check=True) - subprocess.run(["git", "commit", "-qm", "current source"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "safe base"], cwd=repo, check=True) + base_sha = subprocess.check_output( + ["git", "rev-parse", "HEAD"], cwd=repo, text=True + ).strip() + (repo / "safe.txt").write_text("current head source\n", encoding="utf-8") + subprocess.run(["git", "add", "safe.txt"], cwd=repo, check=True) + subprocess.run(["git", "commit", "-qm", "current head"], cwd=repo, check=True) - archive = tmp_path / "source.tar" + base_worktree = tmp_path / "opencode-coverage-base" + source_worktree = tmp_path / "opencode-coverage-source" + archive = tmp_path / "opencode-coverage-source.tar" result = subprocess.run( - ["bash", "-c", "set -euo pipefail\n" + package_script], + ["bash", "-c", "set -euo pipefail\n" + export_script], env={ **os.environ, - "COVERAGE_SOURCE_WORKDIR": str(repo), + "COVERAGE_BASE_WORKDIR": str(base_worktree), + "COVERAGE_SOURCE_WORKDIR": str(source_worktree), "COVERAGE_SOURCE_ARCHIVE": str(archive), + "PR_BASE_SHA": base_sha, + "RUNNER_TEMP": str(tmp_path), + "fetch_dir": str(repo), }, text=True, capture_output=True, @@ -471,9 +510,11 @@ def test_coverage_source_artifact_excludes_git_history(tmp_path): assert result.returncode == 0, result.stderr with tarfile.open(archive) as bundle: - names = [name.removeprefix("./") for name in bundle.getnames()] - assert "safe.txt" in names - assert not any(name == ".git" or name.startswith(".git/") for name in names) + names = [name.removeprefix("./").rstrip("/") for name in bundle.getnames()] + assert "opencode-coverage-base/safe.txt" in names + assert "opencode-coverage-source/safe.txt" in names + assert not any(".git" in name.split("/") for name in names) + assert b"deleted-history" not in archive.read_bytes() def test_opencode_repository_dispatch_authorization_is_fail_closed(): @@ -1620,7 +1661,16 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): ) assert 'cat "$summary_output_file"' in coverage_job assert "Published compact coverage decision output" in coverage_job - assert 'coverage_log_stop_token="$(python3 -I -c' in coverage_job + assert 'coverage_log_stop_token="$(/usr/bin/python3 -I -c' in coverage_job + assert ( + '[[ "$coverage_log_stop_token" =~ ^coverage-log-[0-9a-f]{48}$ ]]' + in coverage_job + ) + assert ( + '[[ "$coverage_output_delimiter" =~ ^coverage_[0-9a-f]{48}$ ]]' in coverage_job + ) + assert "# BEGIN_COVERAGE_LOG_REPLAY" in coverage_job + assert "# END_COVERAGE_LOG_REPLAY" in coverage_job assert 'grep -Fq "$coverage_log_stop_token" "$summary_file"' in coverage_job assert ( "printf '::stop-commands::%s\\n' \"$coverage_log_stop_token\"" in coverage_job @@ -1748,15 +1798,16 @@ def test_coverage_log_replay_disables_runner_commands_and_retries_token_collisio ): """Untrusted coverage bytes stay inside a collision-free stop-command envelope.""" workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") - replay_start = workflow.index(' coverage_log_stop_token="$(python3 -I -c') - replay_end = workflow.index( - " # No process running pull-request code", replay_start - ) + replay_start = workflow.index(" # BEGIN_COVERAGE_LOG_REPLAY\n") + replay_start = workflow.index("\n", replay_start) + 1 + replay_end = workflow.index(" # END_COVERAGE_LOG_REPLAY", replay_start) replay = textwrap.dedent(workflow[replay_start:replay_end]) + collision_token = "coverage-log-" + "a" * 48 + safe_token = "coverage-log-" + "b" * 48 summary = tmp_path / "coverage-evidence.md" summary.write_text( - "coverage-log-collision\n" + f"{collision_token}\n" "::set-output name=coverage_summary::ATTACKER\n" "::add-path::/tmp/attacker\n", encoding="utf-8", @@ -1770,16 +1821,22 @@ def test_coverage_log_replay_disables_runner_commands_and_retries_token_collisio 'count="$(cat "$FAKE_COUNTER" 2>/dev/null || printf 0)"\n' 'printf "%s" "$((count + 1))" >"$FAKE_COUNTER"\n' 'if [ "$count" -eq 0 ]; then\n' - " printf '%s\\n' coverage-log-collision\n" + f" printf '%s\\n' {collision_token}\n" "else\n" - " printf '%s\\n' coverage-log-safe\n" + f" printf '%s\\n' {safe_token}\n" "fi\n", encoding="utf-8", ) fake_python.chmod(0o755) + # Replace the absolute trusted interpreter only inside this test harness so + # the collision-retry branch can be deterministic. The production contract + # itself must remain immune to PATH-prepended executables. + replay_with_fixture = replay.replace( + "/usr/bin/python3", shlex.quote(str(fake_python)) + ) result = subprocess.run( - ["bash", "-c", "set -euo pipefail\n" + replay], + ["bash", "-c", "set -euo pipefail\n" + replay_with_fixture], env={ **os.environ, "PATH": f"{fake_bin}:{os.environ['PATH']}", @@ -1793,25 +1850,25 @@ def test_coverage_log_replay_disables_runner_commands_and_retries_token_collisio assert result.returncode == 0, result.stderr lines = result.stdout.splitlines() - assert lines[0] == "::stop-commands::coverage-log-safe" + assert lines[0] == f"::stop-commands::{safe_token}" assert "::set-output name=coverage_summary::ATTACKER" in lines[1:-1] assert "::add-path::/tmp/attacker" in lines[1:-1] - assert lines[-1] == "::coverage-log-safe::" + assert lines[-1] == f"::{safe_token}::" assert counter.read_text(encoding="utf-8") == "2" def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_path): - """A tracked symlink cannot escape the PR worktree into runner credentials.""" + """Tracked and untracked symlinks cannot escape into runner credentials.""" if not hasattr(os, "symlink"): pytest.skip("symlinks are unavailable") workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") validation_start = workflow.index( - " while IFS= read -r -d '' indexed_entry; do" + " # BEGIN_PR_WORKTREE_ENTRY_VALIDATION\n" ) + validation_start = workflow.index("\n", validation_start) + 1 validation_end = workflow.index( - ' git -C "$OPENCODE_SOURCE_WORKDIR" status --short', - validation_start, + " # END_PR_WORKTREE_ENTRY_VALIDATION", validation_start ) validation = textwrap.dedent(workflow[validation_start:validation_end]) @@ -1855,6 +1912,18 @@ def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_pa ) assert clean.returncode == 0, clean.stderr + (clean_worktree / "untracked-credential-link").symlink_to(outside) + untracked_rejected = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + validation], + env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(clean_worktree)}, + text=True, + capture_output=True, + check=False, + ) + assert untracked_rejected.returncode == 1 + assert "PR worktree contains a symbolic link" in untracked_rejected.stdout + assert outside.read_text(encoding="utf-8") == "synthetic-secret\n" + malicious_worktree = tmp_path / "malicious-worktree" subprocess.run( ["git", "worktree", "add", "--detach", str(malicious_worktree), "HEAD"], From 6b129f8456351247b4e0dd4038d59858604a30d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 13:24:48 +0900 Subject: [PATCH 08/10] fix(review): fail closed on artifact validation --- .github/workflows/opencode-review.yml | 46 ++++++++++++++------------- tests/test_opencode_agent_contract.py | 7 +++- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 5171766c..5c2676bd 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -295,16 +295,17 @@ jobs: validate_coverage_tree_modes() { local treeish="$1" local indexed_entry indexed_mode - while IFS= read -r -d '' indexed_entry; do - indexed_mode="${indexed_entry%% *}" - case "$indexed_mode" in - 100644 | 100755) ;; - *) - printf '::error::Coverage source tree %s contains non-regular tracked entry mode %s; refusing cross-job artifact materialization.\n' "$treeish" "$indexed_mode" - exit 1 - ;; - esac - done < <(git -C "$fetch_dir" ls-tree -r -z --full-tree "$treeish") + git -C "$fetch_dir" ls-tree -r -z --full-tree "$treeish" | + while IFS= read -r -d '' indexed_entry; do + indexed_mode="${indexed_entry%% *}" + case "$indexed_mode" in + 100644 | 100755) ;; + *) + printf '::error::Coverage source tree %s contains non-regular tracked entry mode %s; refusing cross-job artifact materialization.\n' "$treeish" "$indexed_mode" + exit 1 + ;; + esac + done } validate_coverage_tree_modes "$PR_BASE_SHA" validate_coverage_tree_modes HEAD @@ -329,7 +330,7 @@ jobs: tar -cf "$COVERAGE_SOURCE_ARCHIVE" -C "$RUNNER_TEMP" \ "$(basename "$COVERAGE_BASE_WORKDIR")" \ "$(basename "$COVERAGE_SOURCE_WORKDIR")" - python3 -I - "$COVERAGE_SOURCE_ARCHIVE" <<'PY' + /usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" <<'PY' from pathlib import Path, PurePosixPath import sys import tarfile @@ -461,7 +462,7 @@ jobs: # The archive contains pull-request-controlled paths. Validate every # member before extraction so a symlink, hardlink, device, FIFO, or # traversal path cannot redirect a later trusted host-side parser. - python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir" <<'PY' + /usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir" <<'PY' import os from pathlib import Path, PurePosixPath import sys @@ -1994,16 +1995,17 @@ jobs: rm -rf "$OPENCODE_SOURCE_WORKDIR" git worktree add --detach "$OPENCODE_SOURCE_WORKDIR" "$PR_HEAD_SHA" # BEGIN_PR_WORKTREE_ENTRY_VALIDATION - while IFS= read -r -d '' indexed_entry; do - indexed_mode="${indexed_entry%% *}" - case "$indexed_mode" in - 100644 | 100755) ;; - *) - printf '::error::PR worktree contains non-regular tracked entry mode %s; refusing trusted review processing.\n' "$indexed_mode" - exit 1 - ;; - esac - done < <(git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z) + git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z | + while IFS= read -r -d '' indexed_entry; do + indexed_mode="${indexed_entry%% *}" + case "$indexed_mode" in + 100644 | 100755) ;; + *) + printf '::error::PR worktree contains non-regular tracked entry mode %s; refusing trusted review processing.\n' "$indexed_mode" + exit 1 + ;; + esac + done if find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l -print -quit | grep -q .; then echo "::error::PR worktree contains a symbolic link; refusing trusted review processing." exit 1 diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index ac3cc409..f188fe92 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -315,6 +315,8 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'validate_coverage_tree_modes "$PR_BASE_SHA"' in step assert "validate_coverage_tree_modes HEAD" in step assert 'ls-tree -r -z --full-tree "$treeish"' in step + assert 'ls-tree -r -z --full-tree "$treeish" |' in step + assert 'done < <(git -C "$fetch_dir" ls-tree' not in step assert "100644 | 100755" in step assert 'rm -f -- "$COVERAGE_BASE_WORKDIR/.git"' in step assert 'rm -rf -- "$fetch_dir/.git"' in step @@ -336,9 +338,10 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "secrets." not in measure_step assert "COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head" in workflow assert ( - 'python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir"' + '/usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" "$artifact_extract_dir"' in coverage_job ) + assert '/usr/bin/python3 -I - "$COVERAGE_SOURCE_ARCHIVE" <<' in source_job assert '"opencode-coverage-base"' in coverage_job assert '"opencode-coverage-source"' in coverage_job assert "member.isfile() or member.isdir()" in workflow @@ -1737,6 +1740,8 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): " - name: Materialize pull request head for OpenCode review data", 1 )[1].split("\n - name:", 1)[0] assert 'git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z' in materialize_step + assert 'git -C "$OPENCODE_SOURCE_WORKDIR" ls-files -s -z |' in materialize_step + assert 'done < <(git -C "$OPENCODE_SOURCE_WORKDIR" ls-files' not in materialize_step assert "100644 | 100755" in materialize_step assert 'find -P "$OPENCODE_SOURCE_WORKDIR" -mindepth 1 -type l' in materialize_step assert "refusing trusted review processing" in materialize_step From 2fbfdf6fc9eb8b2059ac4d78d7b05e7c3307da29 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 13:43:40 +0900 Subject: [PATCH 09/10] fix(review): preserve replay guard history --- .github/workflows/opencode-review.yml | 110 ++++++++++++++++++-------- tests/test_opencode_agent_contract.py | 24 ++++-- 2 files changed, 94 insertions(+), 40 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 5c2676bd..786980e4 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -171,6 +171,55 @@ jobs: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true steps: + - name: Resolve trusted replay-guard source ref + id: coverage_source_trusted + env: + JOB_CONTEXT_JSON: ${{ toJSON(job) }} + GITHUB_CONTEXT_JSON: ${{ toJSON(github) }} + run: | + set -euo pipefail + /usr/bin/python3 -I <<'PY' >>"$GITHUB_OUTPUT" + import json + import os + import re + import sys + + try: + job_context = json.loads(os.environ.get("JOB_CONTEXT_JSON") or "{}") + github_context = json.loads(os.environ.get("GITHUB_CONTEXT_JSON") or "{}") + except json.JSONDecodeError as exc: + print(f"::error::Could not parse GitHub workflow context JSON: {exc}", file=sys.stderr) + raise SystemExit(1) + + trusted_ref = str( + job_context.get("workflow_sha") or github_context.get("workflow_sha") or "" + ).strip() + workflow_ref = str( + job_context.get("workflow_ref") or github_context.get("workflow_ref") or "" + ).strip() + + if not trusted_ref: + trusted_ref = "main" + prefix = "ContextualWisdomLab/.github/.github/workflows/opencode-review.yml@" + if workflow_ref.startswith(prefix): + trusted_ref = workflow_ref.split("@", 1)[1] + + if not re.fullmatch(r"[0-9a-fA-F]{40}|refs/[^\s]+|[A-Za-z0-9._/-]+", trusted_ref): + print("::error::Trusted OpenCode workflow ref resolved to an invalid value.", file=sys.stderr) + raise SystemExit(1) + + print(f"ref={trusted_ref}") + PY + + - name: Checkout trusted replay guard + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + repository: ContextualWisdomLab/.github + fetch-depth: 1 + persist-credentials: false + ref: ${{ steps.coverage_source_trusted.outputs.ref }} + path: trusted-replay-guard + - name: Exchange OpenCode app token for target repository coverage reads id: coverage_read_app_token if: >- @@ -251,6 +300,7 @@ jobs: COVERAGE_BASE_WORKDIR: ${{ runner.temp }}/opencode-coverage-base COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-coverage-source COVERAGE_SOURCE_ARCHIVE: ${{ runner.temp }}/opencode-coverage-source.tar + TRUSTED_REPLAY_GUARD: ${{ github.workspace }}/trusted-replay-guard/scripts/ci/pr_head_replay_guard.py run: | set -euo pipefail fetch_dir="${RUNNER_TEMP}/opencode-coverage-fetch" @@ -310,6 +360,32 @@ jobs: validate_coverage_tree_modes "$PR_BASE_SHA" validate_coverage_tree_modes HEAD + # Evaluate replay/unmerge evidence while the real fetched commit + # graph still exists. Snapshot-only artifacts intentionally omit + # target-repository Git history and cannot reproduce this decision. + if [ ! -f "$TRUSTED_REPLAY_GUARD" ] || [ -L "$TRUSTED_REPLAY_GUARD" ]; then + echo "::error::Trusted PR head replay guard is missing or not a regular file." + exit 1 + fi + replay_report="${RUNNER_TEMP}/pr-head-replay-guard.txt" + replay_status=0 + /usr/bin/python3 -I "$TRUSTED_REPLAY_GUARD" \ + --repo-root "$fetch_dir" \ + --base-sha "$PR_BASE_SHA" \ + --head-sha "$PR_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? + cat "$replay_report" + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { + printf '## PR head replay guard\n\n```text\n' + cat "$replay_report" + printf '\n```\n' + } >>"$GITHUB_STEP_SUMMARY" + fi + if [ "$replay_status" -ne 0 ]; then + echo "::error::Current HEAD discarded a prior base merge or replay evidence could not be evaluated; see the exact SHAs and deletion counts above." + exit "$replay_status" + fi + # BEGIN_COVERAGE_SOURCE_EXPORT # Export both snapshots without transferring the target repository's # Git object database into the central repository artifact namespace. @@ -384,6 +460,7 @@ jobs: coverage_summary: ${{ steps.measure.outputs.coverage_summary }} env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + GITHUB_TOKEN: "" steps: - name: Resolve trusted OpenCode source ref id: trusted_source @@ -549,39 +626,6 @@ jobs: rm -rf "$artifact_extract_dir" git -C "$COVERAGE_SOURCE_WORKDIR" status --short - - name: Enforce post-merge stale agent replay guard - env: - PR_BASE_SHA: ${{ needs.validate-pr-metadata.outputs.base_sha }} - PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }} - COVERAGE_SOURCE_WORKDIR: ${{ runner.temp }}/pr-head - # Dependency resolution may consume wheels/packages, but PR-defined - # install/build hooks are never executed implicitly. - UV_NO_BUILD: "1" - NPM_CONFIG_IGNORE_SCRIPTS: "true" - PNPM_CONFIG_IGNORE_SCRIPTS: "true" - YARN_ENABLE_SCRIPTS: "false" - GITHUB_TOKEN: "" - run: | - set -euo pipefail - replay_report="${RUNNER_TEMP}/pr-head-replay-guard.txt" - replay_status=0 - python3 "$GITHUB_WORKSPACE/scripts/ci/pr_head_replay_guard.py" \ - --repo-root "$COVERAGE_SOURCE_WORKDIR" \ - --base-sha "$COVERAGE_BASE_SHA" \ - --head-sha "$COVERAGE_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? - cat "$replay_report" - if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then - { - printf '## PR head replay guard\n\n```text\n' - cat "$replay_report" - printf '\n```\n' - } >>"$GITHUB_STEP_SUMMARY" - fi - if [ "$replay_status" -ne 0 ]; then - echo "::error::Current HEAD discarded a prior base merge or replay evidence could not be evaluated; see the exact SHAs and deletion counts above." - exit "$replay_status" - fi - # Run every trusted follow-up before executing pull-request code. Even a # credential-free test can write runner command files, so no trusted shell # step may consume state after coverage measurement begins. diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index f188fe92..10427c26 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -185,18 +185,18 @@ def test_opencode_trusted_source_ref_is_not_controlled_by_workflow_inputs(): assert workflow.count("ref: ${{ steps.trusted_source.outputs.ref }}") == 1 assert "TRUSTED_SOURCE_REF: ${{ steps.trusted_source.outputs.ref }}" in workflow assert "ref: ${{ github.workflow_sha }}" not in workflow - assert workflow.count("JOB_CONTEXT_JSON: ${{ toJSON(job) }}") == 2 - assert workflow.count("GITHUB_CONTEXT_JSON: ${{ toJSON(github) }}") == 2 + assert workflow.count("JOB_CONTEXT_JSON: ${{ toJSON(job) }}") == 3 + assert workflow.count("GITHUB_CONTEXT_JSON: ${{ toJSON(github) }}") == 3 assert ( workflow.count( 'job_context.get("workflow_sha") or github_context.get("workflow_sha")' ) - == 2 + == 3 ) - assert workflow.count('workflow_ref.split("@", 1)[1]') == 2 + assert workflow.count('workflow_ref.split("@", 1)[1]') == 3 assert ( workflow.count("Trusted OpenCode workflow ref resolved to an invalid value.") - == 2 + == 3 ) @@ -273,6 +273,9 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert ( "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" in source_job ) + assert "Checkout trusted replay guard" in source_job + assert "persist-credentials: false" in source_job + assert "ref: ${{ steps.coverage_source_trusted.outputs.ref }}" in source_job coverage_start = workflow.index(" coverage-evidence:\n") coverage_end = workflow.index("\n opencode-review-target:", coverage_start) @@ -318,6 +321,14 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert 'ls-tree -r -z --full-tree "$treeish" |' in step assert 'done < <(git -C "$fetch_dir" ls-tree' not in step assert "100644 | 100755" in step + assert "TRUSTED_REPLAY_GUARD: ${{ github.workspace }}/trusted-replay-guard/" in step + assert '/usr/bin/python3 -I "$TRUSTED_REPLAY_GUARD"' in step + assert '--repo-root "$fetch_dir"' in step + assert '--base-sha "$PR_BASE_SHA"' in step + assert '--head-sha "$PR_HEAD_SHA"' in step + assert step.index('/usr/bin/python3 -I "$TRUSTED_REPLAY_GUARD"') < step.index( + 'rm -rf -- "$fetch_dir/.git"' + ) assert 'rm -f -- "$COVERAGE_BASE_WORKDIR/.git"' in step assert 'rm -rf -- "$fetch_dir/.git"' in step assert "Coverage source export retained forbidden Git metadata" in step @@ -354,8 +365,7 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "COVERAGE_BASE_SHA=%s" in coverage_job assert "COVERAGE_HEAD_SHA=%s" in coverage_job assert 'diff --name-only "$COVERAGE_BASE_SHA" "$COVERAGE_HEAD_SHA"' in coverage_job - assert '--base-sha "$COVERAGE_BASE_SHA"' in coverage_job - assert '--head-sha "$COVERAGE_HEAD_SHA"' in coverage_job + assert "pr_head_replay_guard.py" not in coverage_job assert 'bundle.extractall(destination, members=members, filter="data")' in workflow assert 'tar -xf "$COVERAGE_SOURCE_ARCHIVE"' not in workflow assert "docker.io/library/ubuntu@sha256:" in measure_step From 71528f67447e7cc98388d711a8dce59a0fd17435 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 21 Jul 2026 13:44:51 +0900 Subject: [PATCH 10/10] fix(review): close cross-job source and log boundaries --- .github/workflows/opencode-review.yml | 20 ++++-- scripts/ci/run_opencode_review_model_pool.sh | 9 ++- scripts/ci/test_strix_quick_gate.sh | 18 ++++- tests/test_opencode_agent_contract.py | 73 +++++++++++++++++++- tests/test_opencode_model_pool_runner.py | 11 +++ 5 files changed, 117 insertions(+), 14 deletions(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 786980e4..a72d0027 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -324,6 +324,11 @@ jobs: "${PR_HEAD_SHA:-}" exit 1 fi + target_visibility="$(gh api "repos/${TARGET_REPOSITORY}" --jq .visibility)" + if [ "$target_visibility" != "public" ]; then + echo "::error::Cross-repository coverage artifacts require a public target repository; ${TARGET_REPOSITORY} reported visibility=${target_visibility:-unknown}." + exit 1 + fi auth_header="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 | tr -d '\n')" echo "::add-mask::$auth_header" git init "$fetch_dir" @@ -341,7 +346,6 @@ jobs: echo "::error::Coverage merge tree could not be materialized for base ${PR_BASE_SHA} and head ${PR_HEAD_SHA}; resolve merge conflicts or rerun after GitHub can synthesize the PR merge commit." exit 1 fi - validate_coverage_tree_modes() { local treeish="$1" local indexed_entry indexed_mode @@ -373,7 +377,8 @@ jobs: --repo-root "$fetch_dir" \ --base-sha "$PR_BASE_SHA" \ --head-sha "$PR_HEAD_SHA" >"$replay_report" 2>&1 || replay_status=$? - cat "$replay_report" + printf 'Replay guard completed with status %s and captured %s bytes of bounded evidence.\n' \ + "$replay_status" "$(wc -c <"$replay_report" | tr -d ' ')" if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then { printf '## PR head replay guard\n\n```text\n' @@ -2295,15 +2300,17 @@ jobs: codegraph_raw="$(mktemp)" changed_scope="$(git diff --name-only "$PR_BASE_SHA" "$PR_HEAD_SHA" | sed -n '1,80p' | tr '\n' ' ')" if ! "$CODEGRAPH_BIN" status >"$codegraph_status" 2>&1; then - cat "$codegraph_status" >&2 + printf 'CodeGraph status command failed; captured %s bytes without replaying PR-derived log content.\n' \ + "$(wc -c <"$codegraph_status" | tr -d ' ')" >&2 echo "::error::CodeGraph status failed; approval evidence is incomplete." rm -f "$codegraph_status" "$codegraph_raw" exit 1 fi if ! timeout 120s "$CODEGRAPH_BIN" explore \ "Review the blast radius, call paths, security boundaries, and focused tests for these current-head changed files: ${changed_scope}" \ - >"$codegraph_raw" 2>&1; then - cat "$codegraph_raw" >&2 + >"$codegraph_raw" 2>&1; then + printf 'CodeGraph exploration command failed; captured %s bytes without replaying PR-derived log content.\n' \ + "$(wc -c <"$codegraph_raw" | tr -d ' ')" >&2 echo "::error::CodeGraph changed-scope exploration failed; approval evidence is incomplete." rm -f "$codegraph_status" "$codegraph_raw" exit 1 @@ -2316,7 +2323,8 @@ jobs: } >"$CODEGRAPH_EVIDENCE_FILE" rm -f "$codegraph_status" "$codegraph_raw" test -s "$CODEGRAPH_EVIDENCE_FILE" - cat "$CODEGRAPH_EVIDENCE_FILE" + printf 'Captured bounded CodeGraph evidence (%s bytes) without replaying PR-derived log content.\n' \ + "$(wc -c <"$CODEGRAPH_EVIDENCE_FILE" | tr -d ' ')" - name: Prepare bounded OpenCode review evidence timeout-minutes: 12 diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 85e122ab..fe0a4b44 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -498,9 +498,6 @@ main() { fi fi deadline=0 - if [ "$budget_seconds" -gt 0 ]; then - deadline=$((SECONDS + budget_seconds)) - fi : >"$OPENCODE_OUTPUT_FILE" cd "$OPENCODE_REVIEW_WORKDIR" read -r -a model_candidates <<<"${OPENCODE_MODEL_CANDIDATES:-}" @@ -528,6 +525,12 @@ main() { opencode_json_file="${candidate_output_file}.jsonl" opencode_export_file="${candidate_output_file}.session.json" write_prompt "$model_candidate" "$prompt_file" + # The retry budget measures provider attempts. Trusted local prompt + # preparation can be slower on a busy runner and must not exhaust the + # budget before the first provider is invoked. + if [ "$deadline" -eq 0 ] && [ "$budget_seconds" -gt 0 ]; then + deadline=$((SECONDS + budget_seconds)) + fi for attempt in $(seq 1 "$attempts"); do now="$SECONDS" if [ "$deadline" -gt 0 ] && [ "$now" -ge "$deadline" ]; then diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b51edd55..131734bb 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -87,6 +87,16 @@ assert_file_not_contains() { fi } +assert_file_not_matches() { + local file_path="$1" + local pattern="$2" + local message="$3" + + if [ -f "$file_path" ] && grep -Eq -- "$pattern" "$file_path"; then + record_failure "$message (unexpected pattern '$pattern')" + fi +} + seal_opencode_test_artifacts() { local runner_temp="$1" local head_sha="$2" @@ -593,8 +603,12 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'locked_version" != "4.0.4"' "opencode review verifies both nested installed and locked picomatch evidence" assert_file_contains "$workflow_file" '"$CODEGRAPH_BIN" explore' "opencode review precomputes structural evidence outside the model process" assert_file_contains "$workflow_file" '"$CODEGRAPH_BIN" --version' "opencode review logs the exact trusted CodeGraph version" - assert_file_contains "$workflow_file" 'cat "$codegraph_status" >&2' "opencode review exposes CodeGraph status failures in the job log" - assert_file_contains "$workflow_file" 'cat "$codegraph_raw" >&2' "opencode review exposes CodeGraph exploration failures in the job log" + assert_file_not_contains "$workflow_file" 'cat "$codegraph_status" >&2' "opencode review does not replay PR-derived CodeGraph status bytes as workflow commands" + assert_file_not_contains "$workflow_file" 'cat "$codegraph_raw" >&2' "opencode review does not replay PR-derived CodeGraph exploration bytes as workflow commands" + assert_file_not_matches "$workflow_file" '^[[:space:]]{10}cat "\$CODEGRAPH_EVIDENCE_FILE"[[:space:]]*$' "opencode review does not replay assembled PR-derived CodeGraph evidence into the command channel" + assert_file_contains "$workflow_file" 'CodeGraph status command failed; captured %s bytes without replaying PR-derived log content.' "opencode review reports bounded CodeGraph status failure metadata" + assert_file_contains "$workflow_file" 'CodeGraph exploration command failed; captured %s bytes without replaying PR-derived log content.' "opencode review reports bounded CodeGraph exploration failure metadata" + assert_file_contains "$workflow_file" 'Captured bounded CodeGraph evidence (%s bytes) without replaying PR-derived log content.' "opencode review reports bounded CodeGraph success metadata" assert_file_not_contains "$workflow_file" "serve --mcp" "opencode review must not fetch or launch CodeGraph again for MCP" assert_file_not_contains "$workflow_file" "https://mcp.deepwiki.com/mcp" "opencode review does not expose remote MCP to the model" assert_file_not_contains "$workflow_file" "@upstash/context7-mcp@3.1.0" "opencode review does not install Context7 at runtime" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index 10427c26..ab0098c8 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -311,6 +311,13 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): 'fetch --no-tags --prune --no-recurse-submodules origin "$PR_BASE_SHA" "$PR_HEAD_SHA"' in step ) + assert ( + 'target_visibility="$(gh api "repos/${TARGET_REPOSITORY}" --jq .visibility)"' + in step + ) + assert ( + "Cross-repository coverage artifacts require a public target repository" in step + ) assert "Coverage fetch could not authenticate" in step assert 'merge --no-ff --no-edit "$PR_HEAD_SHA"' in step assert "Coverage merge tree could not be materialized" in step @@ -338,6 +345,10 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): ) assert '"$(basename "$COVERAGE_BASE_WORKDIR")"' in step assert '"$(basename "$COVERAGE_SOURCE_WORKDIR")"' in step + assert 'cat "$replay_report"' not in step.split( + 'if [ -n "${GITHUB_STEP_SUMMARY:-}" ]', 1 + )[0] + assert "Replay guard completed with status" in step measure_start = workflow.index( " - name: Measure test and docstring evidence\n" @@ -368,6 +379,17 @@ def test_opencode_target_coverage_materializes_only_after_authorized_dispatch(): assert "pr_head_replay_guard.py" not in coverage_job assert 'bundle.extractall(destination, members=members, filter="data")' in workflow assert 'tar -xf "$COVERAGE_SOURCE_ARCHIVE"' not in workflow + prepare_start = workflow.index( + " - name: Prepare pull request merge tree for coverage measurement\n" + ) + prepare_end = workflow.index("\n - name:", prepare_start + 1) + prepare_step = workflow[prepare_start:prepare_end] + assert 'git init "$COVERAGE_SOURCE_WORKDIR"' in prepare_step + assert 'git -C "$COVERAGE_SOURCE_WORKDIR" add --all --force' in prepare_step + assert 'git -C "$COVERAGE_SOURCE_WORKDIR" commit --allow-empty --no-gpg-sign' in prepare_step + assert prepare_step.index("bundle.extractall") < prepare_step.index( + 'git init "$COVERAGE_SOURCE_WORKDIR"' + ) assert "docker.io/library/ubuntu@sha256:" in measure_step assert "apt-get install --no-install-recommends -y" in measure_step assert "--require-hashes" in measure_step @@ -1775,8 +1797,12 @@ def test_opencode_privileged_review_security_boundaries_are_fail_closed(): assert '"$CODEGRAPH_BIN" init -i' in codegraph_step assert '"$CODEGRAPH_BIN" status' in codegraph_step assert '"$CODEGRAPH_BIN" --version' in codegraph_step - assert 'cat "$codegraph_status" >&2' in codegraph_step - assert 'cat "$codegraph_raw" >&2' in codegraph_step + assert 'cat "$codegraph_status" >&2' not in codegraph_step + assert 'cat "$codegraph_raw" >&2' not in codegraph_step + assert 'cat "$CODEGRAPH_EVIDENCE_FILE"' not in codegraph_step + assert "CodeGraph status command failed; captured" in codegraph_step + assert "CodeGraph exploration command failed; captured" in codegraph_step + assert "Captured bounded CodeGraph evidence" in codegraph_step assert 'rm -rf -- "$OPENCODE_SOURCE_WORKDIR/.codegraph"' in codegraph_step assert "CodeGraph status failed; approval evidence is incomplete." in codegraph_step assert ( @@ -1872,6 +1898,45 @@ def test_coverage_log_replay_disables_runner_commands_and_retries_token_collisio assert counter.read_text(encoding="utf-8") == "2" +def test_coverage_log_replay_rejects_unsafe_stop_token(tmp_path): + """A compromised PATH generator cannot inject a workflow command token.""" + workflow = Path(".github/workflows/opencode-review.yml").read_text(encoding="utf-8") + replay_start = workflow.index(" # BEGIN_COVERAGE_LOG_REPLAY\n") + replay_end = workflow.index(" # END_COVERAGE_LOG_REPLAY\n", replay_start) + replay = textwrap.dedent(workflow[replay_start:replay_end]) + + summary = tmp_path / "coverage-evidence.md" + summary.write_text("safe log\n", encoding="utf-8") + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + fake_python = fake_bin / "python3" + fake_python.write_text( + "#!/usr/bin/env bash\n" + "printf 'coverage-log-safe\\n::set-output name=pwned::yes\\n'\n", + encoding="utf-8", + ) + fake_python.chmod(0o755) + + replay_with_fixture = replay.replace( + "/usr/bin/python3", shlex.quote(str(fake_python)) + ) + result = subprocess.run( + ["bash", "-c", "set -euo pipefail\n" + replay_with_fixture], + env={ + **os.environ, + "PATH": f"{fake_bin}:{os.environ['PATH']}", + "summary_file": str(summary), + }, + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode != 0 + assert "stop-token generation returned an unsafe value" in result.stdout + assert "::stop-commands::" not in result.stdout + + def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_path): """Tracked and untracked symlinks cannot escape into runner credentials.""" if not hasattr(os, "symlink"): @@ -1927,7 +1992,8 @@ def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_pa ) assert clean.returncode == 0, clean.stderr - (clean_worktree / "untracked-credential-link").symlink_to(outside) + untracked_link = clean_worktree / "untracked-credential-link" + untracked_link.symlink_to(outside) untracked_rejected = subprocess.run( ["bash", "-c", "set -euo pipefail\n" + validation], env={**os.environ, "OPENCODE_SOURCE_WORKDIR": str(clean_worktree)}, @@ -1938,6 +2004,7 @@ def test_materialized_pr_worktree_rejects_symlinks_before_trusted_readers(tmp_pa assert untracked_rejected.returncode == 1 assert "PR worktree contains a symbolic link" in untracked_rejected.stdout assert outside.read_text(encoding="utf-8") == "synthetic-secret\n" + untracked_link.unlink() malicious_worktree = tmp_path / "malicious-worktree" subprocess.run( diff --git a/tests/test_opencode_model_pool_runner.py b/tests/test_opencode_model_pool_runner.py index 71e96add..fdb59273 100644 --- a/tests/test_opencode_model_pool_runner.py +++ b/tests/test_opencode_model_pool_runner.py @@ -608,6 +608,17 @@ def test_dynamic_review_cadence_uses_small_change_timeout(tmp_path: Path) -> Non assert "retry budget remaining." in result.stdout +def test_review_retry_budget_starts_after_trusted_prompt_preparation() -> None: + """Trusted preflight work cannot consume the provider-attempt retry budget.""" + runner = RUNNER.read_text(encoding="utf-8") + main = runner[runner.index("main() {") :] + prompt = main.index('write_prompt "$model_candidate" "$prompt_file"') + deadline = main.index("deadline=$((SECONDS + budget_seconds))") + first_budget_check = main.index('if [ "$deadline" -gt 0 ]', prompt) + + assert prompt < deadline < first_budget_check + + def test_dynamic_review_cadence_caps_large_change_queue_budget(tmp_path: Path) -> None: """Large PR cadence caps queue time without converting unlimited cycles to one cycle.""" changed_files = [f"backend/changed_{index}.py" for index in range(21)]