From 211d976e0bd5025ea14b454c029c1ac1c152fece Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 12:00:59 +0900 Subject: [PATCH 1/5] fix(opencode): bound free-model failover pool --- .../workflows/opencode-review-dispatch.yml | 94 +------------------ scripts/ci/test_strix_quick_gate.sh | 2 +- tests/test_opencode_agent_contract.py | 29 +----- 3 files changed, 9 insertions(+), 116 deletions(-) diff --git a/.github/workflows/opencode-review-dispatch.yml b/.github/workflows/opencode-review-dispatch.yml index 83f6830d5..9e4701265 100644 --- a/.github/workflows/opencode-review-dispatch.yml +++ b/.github/workflows/opencode-review-dispatch.yml @@ -3767,91 +3767,6 @@ jobs: "context": 200000, "output": 32000 } - }, - "hy3-free": { - "name": "Hy3 Free", - "tool_call": true, - "reasoning": true, - "options": { - "reasoningEffort": "high" - }, - "variants": { - "high": { - "reasoningEffort": "high" - } - }, - "limit": { - "context": 190000, - "output": 64000 - } - }, - "minimax-m3-free": { - "name": "MiniMax-M3 Free", - "tool_call": true, - "reasoning": true, - "options": { - "reasoningEffort": "high" - }, - "variants": { - "high": { - "reasoningEffort": "high" - } - }, - "limit": { - "context": 200000, - "output": 32000 - } - }, - "glm-5-free": { - "name": "GLM-5 Free", - "tool_call": true, - "reasoning": true, - "options": { - "reasoningEffort": "high" - }, - "variants": { - "high": { - "reasoningEffort": "high" - } - }, - "limit": { - "context": 204800, - "output": 131072 - } - }, - "kimi-k2.5-free": { - "name": "Kimi K2.5 Free", - "tool_call": true, - "reasoning": true, - "options": { - "reasoningEffort": "high" - }, - "variants": { - "high": { - "reasoningEffort": "high" - } - }, - "limit": { - "context": 262144, - "output": 262144 - } - }, - "qwen3.6-plus-free": { - "name": "Qwen3.6 Plus Free", - "tool_call": true, - "reasoning": true, - "options": { - "reasoningEffort": "high" - }, - "variants": { - "high": { - "reasoningEffort": "high" - } - }, - "limit": { - "context": 262144, - "output": 65536 - } } } }, @@ -4305,15 +4220,16 @@ jobs: # existing keyed fallback list: OpenCode Zen GPT-5.6 Terra, DeepSeek # V3, the direct GPT-5.6 Luna slot, and pinned PAID # OpenRouter coder models (free-tier candidates hit the shared - # free-models-per-day cap and hung for the full candidate timeout, - # so the OpenRouter slots use cheap paid models billed against the + # free-models-per-day cap and can otherwise hold the queue for hours, + # so free-tier candidates use a bounded five-minute timeout and the + # OpenRouter slots use cheap paid models billed against the # org's OpenRouter credits), then the full-size GPT-4.1 long-context # endpoint 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: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 nvidia-nim/nvidia/llama-3.1-nemotron-ultra-253b-v1 nvidia-nim/nvidia/nemotron-3-super-120b-a12b nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b nvidia-nim/meta/llama-3.3-70b-instruct nvidia-nim/deepseek-ai/deepseek-v4-pro nvidia-nim/mistralai/codestral-22b-instruct-v0.1 opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free opencode-free/hy3-free opencode-free/minimax-m3-free opencode-free/glm-5-free opencode-free/kimi-k2.5-free opencode-free/qwen3.6-plus-free ' || '' }}opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder 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: "${{ needs.validate-pr-metadata.outputs.is_private == 'false' && 'nvidia-nim/nvidia/llama-3.3-nemotron-super-49b-v1.5 nvidia-nim/nvidia/llama-3.1-nemotron-ultra-253b-v1 nvidia-nim/nvidia/nemotron-3-super-120b-a12b nvidia-nim/nvidia/nemotron-3-ultra-550b-a55b nvidia-nim/meta/llama-3.3-70b-instruct nvidia-nim/deepseek-ai/deepseek-v4-pro nvidia-nim/mistralai/codestral-22b-instruct-v0.1 opencode-free/nemotron-3-ultra-free opencode-free/deepseek-v4-flash-free opencode-free/north-mini-code-free opencode-free/laguna-s-2.1-free opencode-free/ling-3.0-flash-free opencode-free/big-pickle opencode-free/mimo-v2.5-free ' || '' }}opencode/gpt-5.6-terra github-models/deepseek/deepseek-v3-0324 openai/gpt-5.6-luna openrouter/deepseek/deepseek-v3.2 openrouter/qwen/qwen3-coder 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" # 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. @@ -4347,7 +4263,7 @@ jobs: OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "1" OPENCODE_NVIDIA_NIM_RUN_TIMEOUT_SECONDS: "180" OPENCODE_NVIDIA_NIM_TOTAL_BUDGET_SECONDS: "900" - OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "3600" + OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "300" # This installation currently reports a 4k request-body limit for # GitHub Models GPT-5 endpoints even though the public catalog is # larger. Keep the exact runtime failure visible without spending a diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index 7343c06ac..fd2a11670 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -733,7 +733,7 @@ assert_opencode_review_uses_codegraph_and_gpt5_fallback() { assert_file_contains "$workflow_file" 'timeout-minutes: 34' "opencode fast approval publication is bounded around the dynamic image and package/GPU check wait" 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: "5400"' "opencode primary review preserves legitimate full-hour provider sessions" -assert_file_contains "$workflow_file" 'OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "3600"' "opencode free-tier failover timeout is hour-class (~3600s)" +assert_file_contains "$workflow_file" 'OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "300"' "opencode free-tier failover timeout is bounded to five minutes" assert_file_contains "$workflow_file" 'OPENCODE_NVIDIA_NIM_RUN_TIMEOUT_SECONDS: "180"' "opencode NVIDIA NIM candidates have a short per-candidate failover timeout" assert_file_contains "$workflow_file" 'OPENCODE_NVIDIA_NIM_TOTAL_BUDGET_SECONDS: "900"' "opencode NVIDIA NIM candidates share a bounded combined runtime budget" assert_file_contains "$REPO_ROOT/scripts/ci/run_opencode_review_model_pool.sh" 'OPENCODE_RUN_TIMEOUT_SECONDS:-3600' "opencode pool defaults primary run timeout to hour-class (~3600s) for large repos" diff --git a/tests/test_opencode_agent_contract.py b/tests/test_opencode_agent_contract.py index daeaa37a2..1f7337868 100644 --- a/tests/test_opencode_agent_contract.py +++ b/tests/test_opencode_agent_contract.py @@ -106,11 +106,7 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): "opencode-free/ling-3.0-flash-free " "opencode-free/big-pickle " "opencode-free/mimo-v2.5-free " - "opencode-free/hy3-free " - "opencode-free/minimax-m3-free " - "opencode-free/glm-5-free " - "opencode-free/kimi-k2.5-free " - "opencode-free/qwen3.6-plus-free ' || '' }}" + "' || '' }}" ) candidates_text = candidates_match.group(1) assert candidates_text.startswith(conditional_public_candidate) @@ -129,11 +125,6 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): "opencode-free/ling-3.0-flash-free", "opencode-free/big-pickle", "opencode-free/mimo-v2.5-free", - "opencode-free/hy3-free", - "opencode-free/minimax-m3-free", - "opencode-free/glm-5-free", - "opencode-free/kimi-k2.5-free", - "opencode-free/qwen3.6-plus-free", *candidates_text.removeprefix(conditional_public_candidate).split(), ] candidate_pairs = [candidate.split("/", 1) for candidate in candidates] @@ -172,11 +163,6 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): ["opencode-free", "ling-3.0-flash-free"], ["opencode-free", "big-pickle"], ["opencode-free", "mimo-v2.5-free"], - ["opencode-free", "hy3-free"], - ["opencode-free", "minimax-m3-free"], - ["opencode-free", "glm-5-free"], - ["opencode-free", "kimi-k2.5-free"], - ["opencode-free", "qwen3.6-plus-free"], ["opencode", "gpt-5.6-terra"], ["github-models", "deepseek/deepseek-v3-0324"], ["openai", "gpt-5.6-luna"], @@ -255,11 +241,6 @@ def test_opencode_model_pool_sets_high_effort_for_capable_candidates(): "ling-3.0-flash-free", "big-pickle", "mimo-v2.5-free", - "hy3-free", - "minimax-m3-free", - "glm-5-free", - "kimi-k2.5-free", - "qwen3.6-plus-free", } assert set(paid_zen_models) == {"gpt-5.6-terra"} terra_model = paid_zen_models["gpt-5.6-terra"] @@ -1526,11 +1507,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): "opencode-free/ling-3.0-flash-free " "opencode-free/big-pickle " "opencode-free/mimo-v2.5-free " - "opencode-free/hy3-free " - "opencode-free/minimax-m3-free " - "opencode-free/glm-5-free " - "opencode-free/kimi-k2.5-free " - "opencode-free/qwen3.6-plus-free ' || ''" + "' || ''" ) in workflow assert ( "opencode/gpt-5.6-terra " @@ -1569,7 +1546,7 @@ def test_workflow_provisions_sandbox_tool_and_reviewer_agent(): assert 'OPENCODE_DYNAMIC_MAX_CYCLES_CAP: "1"' in workflow assert 'OPENCODE_NVIDIA_NIM_RUN_TIMEOUT_SECONDS: "180"' in workflow assert 'OPENCODE_NVIDIA_NIM_TOTAL_BUDGET_SECONDS: "900"' in workflow - assert 'OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "3600"' in workflow + assert 'OPENCODE_FREE_RUN_TIMEOUT_SECONDS: "300"' in workflow assert 'OPENCODE_GITHUB_GPT5_RUN_TIMEOUT_SECONDS: "45"' in workflow assert 'OPENCODE_DYNAMIC_MAX_CYCLES: "1"' in workflow assert 'OPENCODE_BACKOFF_MAX_SECONDS: "30"' in workflow From 18e8177b4473b3b7e720c391888c4409955c513b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 12:04:12 +0900 Subject: [PATCH 2/5] docs(opencode): record free-tier failover budget and citations Document the 300s free-candidate cap so hung aliases cannot consume the paid/NIM review budget. Cite NIST SP 800-53 Rev. 5. Isolate Darwin installer tests on the linux x86_64 path. --- CHANGELOG.md | 1 + .../opencode-free-failover-budget.md | 23 +++++++++++++++++++ ...st_materialize_base_python_requirements.py | 10 ++++++++ 3 files changed, 34 insertions(+) create mode 100644 docs/doctoring/opencode-free-failover-budget.md diff --git a/CHANGELOG.md b/CHANGELOG.md index bf30091dd..68acf1b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Removed five stale OpenCode free-model aliases and bounded remaining free-tier failover to 300 seconds so a hung free candidate cannot consume the hour-class paid/NIM review budget; a timeout still only advances the pool and cannot approve. - Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics. - Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed. - Bound each review-agent invocation key to the wrapper's complete canonical payload, including the base branch and requesting actor; altered fields with a valid-format key now fail before durable-leader election or forwarding, and wrapper write permission is job-scoped. diff --git a/docs/doctoring/opencode-free-failover-budget.md b/docs/doctoring/opencode-free-failover-budget.md new file mode 100644 index 000000000..288da4377 --- /dev/null +++ b/docs/doctoring/opencode-free-failover-budget.md @@ -0,0 +1,23 @@ +# OpenCode free-tier failover budget + +## Incident and buyer impact + +A stale free alias (`ling-3.0-flash-free` and five ungoverned catalog names) +could occupy the central review pool for 3600 seconds and exhaust the step +without a verdict. Paid NVIDIA NIM and GitHub Models candidates never ran. +Review evidence then looked as if the lab had no LLM review. + +## Decision + +Keep the seven governed free candidates. Bound +`OPENCODE_FREE_RUN_TIMEOUT_SECONDS` to 300. Leave the 5400-second paid/large +review budget and the 180/900-second NVIDIA NIM caps unchanged. A free-tier +timeout only advances the pool; it cannot approve. This allocates remaining +test-time compute to the deep paid/NIM path rather than a dead free worker. +Speed is not the success metric; completing a real review is. + +## References + +National Institute of Standards and Technology. (2020). *Security and privacy +controls for information systems and organizations* (NIST Special Publication +800-53 Rev. 5). https://doi.org/10.6028/NIST.SP.800-53r5 diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 8a383f0c2..10f682b3e 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -30,6 +30,13 @@ def _created_tool_directory(path: Path) -> str: return str(path) +def _force_linux_x86_64_installer(monkeypatch: pytest.MonkeyPatch) -> None: + """Exercise the installer path that GitHub-hosted linux x86_64 runners use.""" + monkeypatch.setattr(materializer.sys, "platform", "linux") + monkeypatch.setattr(materializer.platform, "machine", lambda: "x86_64") + materializer._install_trusted_uv.cache_clear() + + def test_materializes_only_regular_hash_locks_from_exact_base(tmp_path: Path) -> None: """A PR-modified lock cannot enter the networked coverage image build context.""" repo = tmp_path / "repo" @@ -644,6 +651,7 @@ def test_install_trusted_uv_verifies_version_and_caches_path( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """The installer writes one executable, verifies its version, and caches it.""" + _force_linux_x86_64_installer(monkeypatch) tool_dir = tmp_path / "uv" monkeypatch.setattr( materializer.tempfile, @@ -690,6 +698,7 @@ def test_install_trusted_uv_rejects_version_process_failures( failure: OSError | subprocess.TimeoutExpired, ) -> None: """A missing or hung downloaded executable is removed and rejected.""" + _force_linux_x86_64_installer(monkeypatch) tool_dir = tmp_path / "uv" monkeypatch.setattr( materializer.tempfile, @@ -721,6 +730,7 @@ def test_install_trusted_uv_rejects_wrong_version_or_exit_status( completed: subprocess.CompletedProcess[bytes], ) -> None: """Unexpected version output or a nonzero status cannot satisfy the pin.""" + _force_linux_x86_64_installer(monkeypatch) tool_dir = tmp_path / f"uv-{completed.returncode}-{len(completed.stdout)}" monkeypatch.setattr( materializer.tempfile, From fb24aecffa887975d4b34a8c914337c2d95fc378 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 00:33:26 +0900 Subject: [PATCH 3/5] fix(coverage): accept only bounded relative requirement includes Materialize a base Python lock only when every package line is an exact SHA-256 pin or a two-token relative -r/--requirement include of a candidate lock path. A lone --require-hashes directive, ./dotted paths, and -r other-hashes.txt no longer enter the trusted build context. --- AGENTS.md | 2 + CHANGELOG.md | 1 + .../opencode-free-failover-budget.md | 3 + .../materialize_base_python_requirements.py | 82 +++++++++++++++---- ...st_materialize_base_python_requirements.py | 19 ++++- 5 files changed, 91 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 688b33035..e1b2df1b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,3 +2,5 @@ > **Agents: read the master context FIRST.** Before any work, read [`docs/CWL-MASTER-CONTEXT.md`](docs/CWL-MASTER-CONTEXT.md) (mission · naruon-as-platform + inter-component UML · cross-cutting disciplines · conventions · roadmap · current state), the live **GitHub Project #1** (work/roadmap source of truth), the full spec **ContextualWisdomLab/naruon#974**, and operate the Project per [`docs/agent-github-project-protocol.md`](docs/agent-github-project-protocol.md). The repo/Project — not any private agent memory — is the source of truth. + +Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`); a lone `--require-hashes` directive is not trust evidence. See [`docs/doctoring/opencode-free-failover-budget.md`](docs/doctoring/opencode-free-failover-budget.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index 68acf1b53..9490d2489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. - Removed five stale OpenCode free-model aliases and bounded remaining free-tier failover to 300 seconds so a hung free candidate cannot consume the hour-class paid/NIM review budget; a timeout still only advances the pool and cannot approve. - Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics. - Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed. diff --git a/docs/doctoring/opencode-free-failover-budget.md b/docs/doctoring/opencode-free-failover-budget.md index 288da4377..c6770fa67 100644 --- a/docs/doctoring/opencode-free-failover-budget.md +++ b/docs/doctoring/opencode-free-failover-budget.md @@ -2,6 +2,9 @@ ## Incident and buyer impact +Materialize accepts only exact SHA-256 pins or a bounded relative `-r` +include; a lone `--require-hashes` line is not lock evidence. + A stale free alias (`ling-3.0-flash-free` and five ungoverned catalog names) could occupy the central review pool for 3600 seconds and exhaust the step without a verdict. Paid NVIDIA NIM and GitHub Models candidates never ran. diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 98cdad459..9848c3ff6 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -87,6 +87,57 @@ def _is_candidate_lock_name(name: str) -> bool: ) +def _is_candidate_lock_path(path: pathlib.PurePosixPath) -> bool: + """Return whether one safe tracked path can name a pip requirements lock. + + In addition to conventional ``requirements*.txt`` names, repositories often + keep concrete environment closures as direct children such as + ``requirements/ci.txt`` or ``service/requirements/package.txt``. Only direct + ``.txt`` children of a directory named ``requirements`` gain this path-based + eligibility; content must still pass the independent complete hash-pin + validation before it reaches the trusted image build context. + """ + return _is_candidate_lock_name(path.name) or ( + path.suffix == ".txt" and path.parent.name == "requirements" + ) + + +def _is_bounded_requirement_include(line: str) -> bool: + """Return whether one requirements include names a bounded relative file. + + Includes are accepted only as a two-token ``-r``/``--requirement`` form + whose target is itself a candidate lock path written as a normalized + relative POSIX path. Absolute paths, ``.`` or ``..`` components, double + slashes, URLs, option-like targets, shell/Windows path separators, + fragments, queries, extra inline options or hashes, and includes of + non-lock files are rejected before a base-owned file can enter the + trusted build context. + The downstream installer still proves that the candidate is an independently + complete hash closure; this predicate grants syntax eligibility only. + """ + fields = line.split() + if len(fields) != 2 or fields[0] not in {"-r", "--requirement"}: + return False + target = fields[1] + if ( + target.startswith(("-", "~")) + or "\\" in target + or ":" in target + or "?" in target + or "#" in target + ): + return False + include_path = pathlib.PurePosixPath(target) + return ( + bool(include_path.parts) + and target == include_path.as_posix() + and not include_path.is_absolute() + and "." not in include_path.parts + and ".." not in include_path.parts + and _is_candidate_lock_path(include_path) + ) + + def _requirement_lines(content: bytes) -> list[str]: """Return logical requirement lines, joining backslash line-continuations. @@ -107,23 +158,26 @@ def _requirement_lines(content: bytes) -> list[str]: def _is_hash_pinned(content: bytes) -> bool: - """Return whether content carries hash pins and is safe to preflight. - - Discovery is content-based rather than name-based so hash-pinned locks in any - location (a service subdirectory, ``requirements-dev.txt``, - ``requirements-test.txt``) can be considered for offline coverage, while an - unpinned or PR-mutable requirements file is still excluded from the networked - build context. Hash syntax cannot prove that a file includes every transitive - dependency, so the trusted image installer separately preflights every - candidate as an independent ``--require-hashes`` closure. An empty file - carries no installable dependency and is not materialized. + """Return whether content carries only trusted pins or bounded includes. + + Discovery is content-based rather than name-based so exact hash-pinned locks + in service subdirectories and role-specific requirements files can be + considered for offline coverage. Candidate syntax is deliberately stricter + than a substring search: each package line must be an exact ``==`` pin with + one or more complete SHA-256 hashes, or a bounded relative requirements + include. A global ``--require-hashes`` directive is not trust evidence by + itself. The downstream installer separately preflights every candidate as an + independent ``pip --require-hashes`` closure, so syntax eligibility never + substitutes for dependency-closure proof. """ lines = _requirement_lines(content) - if not lines: + requirement_lines = [line for line in lines if line != "--require-hashes"] + if not requirement_lines: return False - return any(line == "--require-hashes" for line in lines) or all( - "--hash=" in line or line.startswith(("-r ", "--requirement ")) - for line in lines + return all( + _is_fully_hash_pinned_requirement(line) + or _is_bounded_requirement_include(line) + for line in requirement_lines ) diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 10f682b3e..317ab5f5c 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -157,9 +157,24 @@ def test_lock_name_candidates_are_pip_requirements_files() -> None: def test_hash_pin_detection_includes_pinned_and_excludes_unpinned_or_empty() -> None: """Only fully hash-pinned, non-empty lock content is materialized.""" assert not materializer._is_hash_pinned(b"# comment only\n\n") - assert materializer._is_hash_pinned(b"--require-hashes\ndemo==1\n") + assert not materializer._is_hash_pinned(b"--require-hashes\ndemo==1\n") assert materializer._is_hash_pinned(b"demo==1 --hash=sha256:" + b"a" * 64 + b"\n") - assert materializer._is_hash_pinned(b"-r other-hashes.txt\n") + assert materializer._is_hash_pinned(b"-r requirements-other.txt\n") + assert not materializer._is_hash_pinned(b"-r other-hashes.txt\n") + assert not materializer._is_hash_pinned(b"-r ./requirements-other.txt\n") + assert not materializer._is_hash_pinned(b"-r ../escape.txt\n") + assert materializer._is_bounded_requirement_include( + "--requirement requirements-other.txt" + ) + assert not materializer._is_bounded_requirement_include("-r .") + assert not materializer._is_bounded_requirement_include("-r -evil.txt") + assert not materializer._is_bounded_requirement_include("-r ~evil.txt") + assert not materializer._is_bounded_requirement_include("-r C:foo.txt") + assert not materializer._is_bounded_requirement_include("-r foo?bar.txt") + assert not materializer._is_bounded_requirement_include("-r foo#bar.txt") + assert not materializer._is_bounded_requirement_include(r"-r foo\\bar.txt") + assert not materializer._is_bounded_requirement_include("-r") + assert not materializer._is_bounded_requirement_include("-r /abs/requirements.txt") assert not materializer._is_hash_pinned(b"untrusted==1\n") # uv export / pip-compile multi-line continuation format (spec, then --hash= lines). assert materializer._is_hash_pinned( From 56e1e7a6804037c9752728b712e4bb1f54f94430 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:57:27 +0900 Subject: [PATCH 4/5] chore(opencode): drop unrelated materializer delta --- AGENTS.md | 2 - CHANGELOG.md | 1 - .../opencode-free-failover-budget.md | 3 - .../materialize_base_python_requirements.py | 82 ++++--------------- ...st_materialize_base_python_requirements.py | 19 +---- 5 files changed, 16 insertions(+), 91 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e1b2df1b4..688b33035 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,5 +2,3 @@ > **Agents: read the master context FIRST.** Before any work, read [`docs/CWL-MASTER-CONTEXT.md`](docs/CWL-MASTER-CONTEXT.md) (mission · naruon-as-platform + inter-component UML · cross-cutting disciplines · conventions · roadmap · current state), the live **GitHub Project #1** (work/roadmap source of truth), the full spec **ContextualWisdomLab/naruon#974**, and operate the Project per [`docs/agent-github-project-protocol.md`](docs/agent-github-project-protocol.md). The repo/Project — not any private agent memory — is the source of truth. - -Materialize accepts only exact SHA-256 pins or a bounded relative `-r` include (no `.`/`..`); a lone `--require-hashes` directive is not trust evidence. See [`docs/doctoring/opencode-free-failover-budget.md`](docs/doctoring/opencode-free-failover-budget.md). diff --git a/CHANGELOG.md b/CHANGELOG.md index 9490d2489..68acf1b53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,6 @@ Semantic Versioning where the repository publishes a release. ### Fixed -- Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. - Removed five stale OpenCode free-model aliases and bounded remaining free-tier failover to 300 seconds so a hung free candidate cannot consume the hour-class paid/NIM review budget; a timeout still only advances the pool and cannot approve. - Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics. - Allowed commas and ASCII parentheses in the bounded Strix changed-file path policy so legal tracked Packrat fixtures can receive exact-head security analysis, while rejecting raw `..` components before normalization and keeping controls, backslashes, whitespace ambiguity, and shell punctuation fail-closed. diff --git a/docs/doctoring/opencode-free-failover-budget.md b/docs/doctoring/opencode-free-failover-budget.md index c6770fa67..288da4377 100644 --- a/docs/doctoring/opencode-free-failover-budget.md +++ b/docs/doctoring/opencode-free-failover-budget.md @@ -2,9 +2,6 @@ ## Incident and buyer impact -Materialize accepts only exact SHA-256 pins or a bounded relative `-r` -include; a lone `--require-hashes` line is not lock evidence. - A stale free alias (`ling-3.0-flash-free` and five ungoverned catalog names) could occupy the central review pool for 3600 seconds and exhaust the step without a verdict. Paid NVIDIA NIM and GitHub Models candidates never ran. diff --git a/scripts/ci/materialize_base_python_requirements.py b/scripts/ci/materialize_base_python_requirements.py index 9848c3ff6..98cdad459 100755 --- a/scripts/ci/materialize_base_python_requirements.py +++ b/scripts/ci/materialize_base_python_requirements.py @@ -87,57 +87,6 @@ def _is_candidate_lock_name(name: str) -> bool: ) -def _is_candidate_lock_path(path: pathlib.PurePosixPath) -> bool: - """Return whether one safe tracked path can name a pip requirements lock. - - In addition to conventional ``requirements*.txt`` names, repositories often - keep concrete environment closures as direct children such as - ``requirements/ci.txt`` or ``service/requirements/package.txt``. Only direct - ``.txt`` children of a directory named ``requirements`` gain this path-based - eligibility; content must still pass the independent complete hash-pin - validation before it reaches the trusted image build context. - """ - return _is_candidate_lock_name(path.name) or ( - path.suffix == ".txt" and path.parent.name == "requirements" - ) - - -def _is_bounded_requirement_include(line: str) -> bool: - """Return whether one requirements include names a bounded relative file. - - Includes are accepted only as a two-token ``-r``/``--requirement`` form - whose target is itself a candidate lock path written as a normalized - relative POSIX path. Absolute paths, ``.`` or ``..`` components, double - slashes, URLs, option-like targets, shell/Windows path separators, - fragments, queries, extra inline options or hashes, and includes of - non-lock files are rejected before a base-owned file can enter the - trusted build context. - The downstream installer still proves that the candidate is an independently - complete hash closure; this predicate grants syntax eligibility only. - """ - fields = line.split() - if len(fields) != 2 or fields[0] not in {"-r", "--requirement"}: - return False - target = fields[1] - if ( - target.startswith(("-", "~")) - or "\\" in target - or ":" in target - or "?" in target - or "#" in target - ): - return False - include_path = pathlib.PurePosixPath(target) - return ( - bool(include_path.parts) - and target == include_path.as_posix() - and not include_path.is_absolute() - and "." not in include_path.parts - and ".." not in include_path.parts - and _is_candidate_lock_path(include_path) - ) - - def _requirement_lines(content: bytes) -> list[str]: """Return logical requirement lines, joining backslash line-continuations. @@ -158,26 +107,23 @@ def _requirement_lines(content: bytes) -> list[str]: def _is_hash_pinned(content: bytes) -> bool: - """Return whether content carries only trusted pins or bounded includes. - - Discovery is content-based rather than name-based so exact hash-pinned locks - in service subdirectories and role-specific requirements files can be - considered for offline coverage. Candidate syntax is deliberately stricter - than a substring search: each package line must be an exact ``==`` pin with - one or more complete SHA-256 hashes, or a bounded relative requirements - include. A global ``--require-hashes`` directive is not trust evidence by - itself. The downstream installer separately preflights every candidate as an - independent ``pip --require-hashes`` closure, so syntax eligibility never - substitutes for dependency-closure proof. + """Return whether content carries hash pins and is safe to preflight. + + Discovery is content-based rather than name-based so hash-pinned locks in any + location (a service subdirectory, ``requirements-dev.txt``, + ``requirements-test.txt``) can be considered for offline coverage, while an + unpinned or PR-mutable requirements file is still excluded from the networked + build context. Hash syntax cannot prove that a file includes every transitive + dependency, so the trusted image installer separately preflights every + candidate as an independent ``--require-hashes`` closure. An empty file + carries no installable dependency and is not materialized. """ lines = _requirement_lines(content) - requirement_lines = [line for line in lines if line != "--require-hashes"] - if not requirement_lines: + if not lines: return False - return all( - _is_fully_hash_pinned_requirement(line) - or _is_bounded_requirement_include(line) - for line in requirement_lines + return any(line == "--require-hashes" for line in lines) or all( + "--hash=" in line or line.startswith(("-r ", "--requirement ")) + for line in lines ) diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 317ab5f5c..10f682b3e 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -157,24 +157,9 @@ def test_lock_name_candidates_are_pip_requirements_files() -> None: def test_hash_pin_detection_includes_pinned_and_excludes_unpinned_or_empty() -> None: """Only fully hash-pinned, non-empty lock content is materialized.""" assert not materializer._is_hash_pinned(b"# comment only\n\n") - assert not materializer._is_hash_pinned(b"--require-hashes\ndemo==1\n") + assert materializer._is_hash_pinned(b"--require-hashes\ndemo==1\n") assert materializer._is_hash_pinned(b"demo==1 --hash=sha256:" + b"a" * 64 + b"\n") - assert materializer._is_hash_pinned(b"-r requirements-other.txt\n") - assert not materializer._is_hash_pinned(b"-r other-hashes.txt\n") - assert not materializer._is_hash_pinned(b"-r ./requirements-other.txt\n") - assert not materializer._is_hash_pinned(b"-r ../escape.txt\n") - assert materializer._is_bounded_requirement_include( - "--requirement requirements-other.txt" - ) - assert not materializer._is_bounded_requirement_include("-r .") - assert not materializer._is_bounded_requirement_include("-r -evil.txt") - assert not materializer._is_bounded_requirement_include("-r ~evil.txt") - assert not materializer._is_bounded_requirement_include("-r C:foo.txt") - assert not materializer._is_bounded_requirement_include("-r foo?bar.txt") - assert not materializer._is_bounded_requirement_include("-r foo#bar.txt") - assert not materializer._is_bounded_requirement_include(r"-r foo\\bar.txt") - assert not materializer._is_bounded_requirement_include("-r") - assert not materializer._is_bounded_requirement_include("-r /abs/requirements.txt") + assert materializer._is_hash_pinned(b"-r other-hashes.txt\n") assert not materializer._is_hash_pinned(b"untrusted==1\n") # uv export / pip-compile multi-line continuation format (spec, then --hash= lines). assert materializer._is_hash_pinned( From 44a302d283e2032f8b220d8f70a0acaae557bfa1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 22:15:42 +0900 Subject: [PATCH 5/5] chore(opencode): remove unrelated installer-test drift --- tests/test_materialize_base_python_requirements.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tests/test_materialize_base_python_requirements.py b/tests/test_materialize_base_python_requirements.py index 10f682b3e..8a383f0c2 100644 --- a/tests/test_materialize_base_python_requirements.py +++ b/tests/test_materialize_base_python_requirements.py @@ -30,13 +30,6 @@ def _created_tool_directory(path: Path) -> str: return str(path) -def _force_linux_x86_64_installer(monkeypatch: pytest.MonkeyPatch) -> None: - """Exercise the installer path that GitHub-hosted linux x86_64 runners use.""" - monkeypatch.setattr(materializer.sys, "platform", "linux") - monkeypatch.setattr(materializer.platform, "machine", lambda: "x86_64") - materializer._install_trusted_uv.cache_clear() - - def test_materializes_only_regular_hash_locks_from_exact_base(tmp_path: Path) -> None: """A PR-modified lock cannot enter the networked coverage image build context.""" repo = tmp_path / "repo" @@ -651,7 +644,6 @@ def test_install_trusted_uv_verifies_version_and_caches_path( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """The installer writes one executable, verifies its version, and caches it.""" - _force_linux_x86_64_installer(monkeypatch) tool_dir = tmp_path / "uv" monkeypatch.setattr( materializer.tempfile, @@ -698,7 +690,6 @@ def test_install_trusted_uv_rejects_version_process_failures( failure: OSError | subprocess.TimeoutExpired, ) -> None: """A missing or hung downloaded executable is removed and rejected.""" - _force_linux_x86_64_installer(monkeypatch) tool_dir = tmp_path / "uv" monkeypatch.setattr( materializer.tempfile, @@ -730,7 +721,6 @@ def test_install_trusted_uv_rejects_wrong_version_or_exit_status( completed: subprocess.CompletedProcess[bytes], ) -> None: """Unexpected version output or a nonzero status cannot satisfy the pin.""" - _force_linux_x86_64_installer(monkeypatch) tool_dir = tmp_path / f"uv-{completed.returncode}-{len(completed.stdout)}" monkeypatch.setattr( materializer.tempfile,