diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 96c91e020..b0a6bf12d 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -3223,10 +3223,11 @@ jobs: OPENCODE_EXPORT_TIMEOUT_SECONDS: "120" OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600" OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "3600" - # Visit the high-sensitivity candidate catalog once. Per-provider - # failures and timeouts are logged before the publish gate evaluates - # the current-head result and fails closed when no verdict exists. - OPENCODE_POOL_MAX_CYCLES: "1" + # Keep cycling through the high-sensitivity candidate catalog until + # the retry budget or step timeout is exhausted; a single invalid + # cycle can be all provider formatting noise rather than review + # evidence. + OPENCODE_POOL_MAX_CYCLES: "0" OPENCODE_DYNAMIC_REVIEW_CADENCE: "true" OPENCODE_SMALL_CHANGE_FILE_THRESHOLD: "3" OPENCODE_MEDIUM_CHANGE_FILE_THRESHOLD: "20" @@ -3243,7 +3244,7 @@ jobs: # larger. Keep the exact runtime failure visible without spending a # full medium/large cadence slot after the long-context candidate. OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45" - OPENCODE_DYNAMIC_MAX_CYCLES: "1" + OPENCODE_DYNAMIC_MAX_CYCLES: "0" CENTRAL_REVIEW_PROCESS_FALLBACK_ELIGIBLE: ${{ steps.central_review_process_fallback_scope.outputs.eligible || 'false' }} CENTRAL_REVIEW_PROCESS_FALLBACK_SCOPE_LABEL: ${{ steps.central_review_process_fallback_scope.outputs.scope_label || 'unsupported' }} OPENCODE_CENTRAL_REVIEW_PROCESS_FALLBACK_RUN_TIMEOUT_SECONDS: "600" diff --git a/scripts/ci/run_opencode_review_model_pool.sh b/scripts/ci/run_opencode_review_model_pool.sh index 291ef5766..0e5122748 100644 --- a/scripts/ci/run_opencode_review_model_pool.sh +++ b/scripts/ci/run_opencode_review_model_pool.sh @@ -645,13 +645,13 @@ main() { original_run_timeout="$(env_integer_or_default OPENCODE_LARGE_CHANGE_RUN_TIMEOUT_SECONDS 3600)" budget_seconds="$(env_integer_or_default OPENCODE_LARGE_CHANGE_TOTAL_BUDGET_SECONDS 7200)" fi - max_cycles="$(env_integer_or_default OPENCODE_DYNAMIC_MAX_CYCLES 1)" + max_cycles="$(env_integer_or_default OPENCODE_DYNAMIC_MAX_CYCLES 0)" printf 'OpenCode dynamic review cadence selected %ss per attempt and %ss total budget for %s changed file(s); max-cycles=%s.\n' \ "$original_run_timeout" "$budget_seconds" "$changed_file_count" "$max_cycles" else original_run_timeout="$(env_integer_or_default OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS 1800)" budget_seconds="$(env_integer_or_default OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS 3900)" - max_cycles="$(env_integer_or_default OPENCODE_DYNAMIC_MAX_CYCLES 1)" + max_cycles="$(env_integer_or_default OPENCODE_DYNAMIC_MAX_CYCLES 0)" printf 'OpenCode dynamic review cadence could not read OPENCODE_CHANGED_FILES_FILE; using %ss per attempt and %ss total budget; max-cycles=%s.\n' \ "$original_run_timeout" "$budget_seconds" "$max_cycles" fi diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index c3db54c09..5bb5fc7c1 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -586,7 +586,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'continue-on-error: true' "opencode approval gate still runs after model-pool failure to publish a reason" assert_file_contains "$workflow_file" 'OPENCODE_RUN_TIMEOUT_SECONDS: "600"' "opencode primary review advances after a bounded stalled provider attempt" assert_file_contains "$workflow_file" 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600"' "opencode model pool exits before the step timeout so the approval gate can publish a reason" - assert_file_contains "$workflow_file" 'OPENCODE_POOL_MAX_CYCLES: "1"' "opencode model pool visits the candidate catalog once without reoccupying the queue" + assert_file_contains "$workflow_file" 'OPENCODE_POOL_MAX_CYCLES: "0"' "opencode model pool keeps cycling until the bounded retry budget or step timeout is exhausted" assert_file_contains "$workflow_file" "needs.coverage-evidence.result == 'success'" "opencode model pool only runs after coverage evidence passed" assert_file_contains "$workflow_file" "id: opencode_review_model_pool" "opencode DeepSeek V3 fallback still runs after a primary model timeout or step failure when coverage evidence passed" assert_file_contains "$workflow_file" "always()" "opencode fallback chain uses always() so failed model steps cannot skip every fallback" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index c1f84af32..92bf9f4f5 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -725,7 +725,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "3600"' in workflow assert 'timeout --kill-after=30s "${OPENCODE_POOL_STEP_TIMEOUT_SECONDS:-3600}s"' in workflow assert "OpenCode model pool exceeded the outer" in workflow - assert 'OPENCODE_POOL_MAX_CYCLES: "1"' in workflow + assert 'OPENCODE_POOL_MAX_CYCLES: "0"' in workflow assert re.search(r"Run OpenCode PR Review model pool[\s\S]{0,280}continue-on-error: true", workflow) assert re.search(r"Publish central OpenCode fast approval[\s\S]{0,900}timeout-minutes: 8", workflow) assert re.search(r"Publish OpenCode review outcome[\s\S]{0,900}timeout-minutes: 10", workflow) @@ -749,7 +749,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_EXPORT_TIMEOUT_SECONDS: "120"' in workflow assert 'OPENCODE_TOTAL_RETRY_BUDGET_SECONDS: "3600"' in workflow assert 'OPENCODE_POOL_STEP_TIMEOUT_SECONDS: "3600"' in workflow - assert 'OPENCODE_POOL_MAX_CYCLES: "1"' in workflow + assert 'OPENCODE_POOL_MAX_CYCLES: "0"' in workflow assert 'OPENCODE_DYNAMIC_REVIEW_CADENCE: "true"' in workflow assert 'OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt' in workflow assert 'OPENCODE_SMALL_CHANGE_RUN_TIMEOUT_SECONDS: "600"' in workflow @@ -761,7 +761,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_UNKNOWN_CHANGE_RUN_TIMEOUT_SECONDS: "600"' in workflow assert 'OPENCODE_UNKNOWN_CHANGE_TOTAL_BUDGET_SECONDS: "3600"' in workflow assert 'OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45"' in workflow - assert 'OPENCODE_DYNAMIC_MAX_CYCLES: "1"' in workflow + assert 'OPENCODE_DYNAMIC_MAX_CYCLES: "0"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow publish_step = workflow.split(" - name: Publish OpenCode review outcome", 1)[1].split( " - name: Run merge scheduler after approval", 1 diff --git a/tests/test_opencode_security_boundaries.py b/tests/test_opencode_security_boundaries.py index 8253373f6..13d6f019a 100644 --- a/tests/test_opencode_security_boundaries.py +++ b/tests/test_opencode_security_boundaries.py @@ -71,6 +71,9 @@ def test_sensitive_log_redaction_assignment_parser_edges_remain_auditable() -> N "token visible": "token visible", "token=": "token=", "token=,": "token=,", + "9safe=value": "9safe=value", + '"token: value': f'"token: {redactor.REDACTED}', + "token: ": "token: ", } for source, expected in cases.items(): @@ -81,6 +84,27 @@ def test_sensitive_log_redaction_assignment_parser_edges_remain_auditable() -> N ) +def test_sensitive_log_redaction_scrubs_provider_token_shapes() -> None: + """Provider-shaped tokens are removed even when they are not key/value assignments.""" + source = "\n".join( + [ + "classic ghp_" + ("A" * 24), + "fine github_pat_" + ("B" * 24), + "openai sk-" + ("C" * 24), + "slack xoxb-" + ("D" * 24), + "aws AKIA" + ("E" * 16), + ] + ) + cleaned = redactor.redact_text(source) + + assert "ghp_" not in cleaned + assert "github_pat_" not in cleaned + assert "sk-" not in cleaned + assert "xoxb-" not in cleaned + assert "AKIA" not in cleaned + assert cleaned.count(redactor.REDACTED) == 5 + + def test_sensitive_log_redaction_handles_lists_empty_input_and_cli(monkeypatch: pytest.MonkeyPatch) -> None: """Recursive lists, empty input, and the streaming CLI share the same scrubber.""" source = '{"values":[{"ok":1,"api_key":"secret-value"},2]}\n'