From 1ae85df55a76f2ab44e69f894907422dace40d98 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2026 22:42:58 +0900 Subject: [PATCH 1/5] fix(noema): replay HTTPS DNS fail-closed onto current main The PR had no merge-base with main. Re-apply only the Noema endpoint gate, tests, and doctoring so localhost/metadata/non-global DNS stays fail-closed without deleting later hourly-repair sources. --- CHANGELOG.md | 12 ++- .../noema-endpoint-https-dns-fail-closed.md | 30 ++++++ scripts/ci/noema_review_gate.py | 42 ++++---- tests/test_noema_review_gate.py | 100 ++++++++++++++---- ...itory_branch_coverage_review_schedulers.py | 5 +- 5 files changed, 145 insertions(+), 44 deletions(-) create mode 100644 docs/doctoring/noema-endpoint-https-dns-fail-closed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index f4903c2f3..609761f45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,10 +35,14 @@ Semantic Versioning where the repository publishes a release. ### Fixed -- Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. -- Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). -- Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). -- Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. + - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. + - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). + - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). + - Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. + - Fail-closed Noema model endpoints unless the hostname is HTTPS-reachable global-unicast DNS; reject localhost, metadata, and non-global addresses. + - 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. + - Fail-closed Noema model endpoints unless the hostname is HTTPS-reachable global-unicast DNS; reject localhost, metadata, and non-global addresses. + - 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. - Refused a conflict-scope repository root whose immediate parent is a symbolic link, so a swapped parent cannot redirect the canonical worktree after the last-component check (CWE-367). - Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. - 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. diff --git a/docs/doctoring/noema-endpoint-https-dns-fail-closed.md b/docs/doctoring/noema-endpoint-https-dns-fail-closed.md new file mode 100644 index 000000000..c4288d374 --- /dev/null +++ b/docs/doctoring/noema-endpoint-https-dns-fail-closed.md @@ -0,0 +1,30 @@ +# Noema endpoint HTTPS and DNS fail-closed + +## Incident and buyer impact + +A credentialed Noema review request could leave the application after DNS +preflight ignored `socket.gaierror` or skipped malformed addresses. An +`http://` endpoint could also receive the API key without TLS. Shared +address space such as `100.64.0.1` is not `is_private` in Python and +previously crossed the guard. + +## Decision + +Accept only HTTPS. Fail before request construction on DNS error, empty +results, invalid IP strings, every non-global address (including RFC 6598 +shared space), and multicast. Cap the response at 1 MiB. Do not log the +endpoint or credential. Organization egress policy and trusted DNS remain +required; this is defense in depth, not DNS-rebinding elimination. + +## References + +Rekhter, Y., Moskowitz, B., Karrenberg, D., de Groot, G. J., & Lear, E. +(1996). *Address allocation for private internets* (RFC 1918). +https://doi.org/10.17487/RFC1918 + +Weil, J., Kuarsingh, V., Donley, C., Liljenstolpe, C., & Azinger, M. +(2012). *IANA-reserved IPv4 prefix for shared address space* (RFC 6598). +https://doi.org/10.17487/RFC6598 + +Fielding, R., Nottingham, M., & Reschke, J. (Eds.). (2022). *HTTP +semantics* (RFC 9110). https://doi.org/10.17487/RFC9110 diff --git a/scripts/ci/noema_review_gate.py b/scripts/ci/noema_review_gate.py index 9317860e4..68dbe4d6a 100644 --- a/scripts/ci/noema_review_gate.py +++ b/scripts/ci/noema_review_gate.py @@ -41,6 +41,7 @@ MAX_FILE_CONTEXT_CHARS = 4000 MAX_REVIEW_CONTEXT_CHARS = 24000 MAX_THREAD_BODY_CHARS = 1200 +MAX_LLM_RESPONSE_BYTES = 1_048_576 # ⚡ Bolt: Pre-compiled regex patterns to avoid recompilation on every scrub_sensitive_data call. # Impact: Improves string processing performance in error reporting. @@ -449,32 +450,30 @@ def call_llm( model = os.environ.get("NOEMA_LLM_MODEL", "").strip() or "noema-default" if not api_url or not api_key: raise RuntimeError("Noema LLM review unavailable: NOEMA_LLM_API_URL or NOEMA_LLM_API_KEY is not configured.") - if not (api_url.lower().startswith("http://") or api_url.lower().startswith("https://")): - raise ValueError( - "URL scheme must be http or https; NOEMA_LLM_API_URL must start " - "with http:// or https:// to prevent SSRF vulnerabilities" - ) + if not api_url.lower().startswith("https://"): + raise ValueError("NOEMA_LLM_API_URL must use HTTPS") parsed = urllib.parse.urlparse(api_url) - if parsed.scheme.lower() not in {"http", "https"}: - raise ValueError("URL scheme must be http or https; NOEMA_LLM_API_URL must start with http:// or https://") + if parsed.scheme.lower() != "https": + raise ValueError("NOEMA_LLM_API_URL must use HTTPS") hostname = (parsed.hostname or "").lower() if not hostname: raise ValueError("URL must have a valid hostname") if hostname in {"localhost", "localhost.localdomain"} or hostname.endswith(".localhost"): raise ValueError("URL cannot target localhost") try: - addrinfo = socket.getaddrinfo(hostname, None) - except socket.gaierror: - pass - else: - for result in addrinfo: - ip_str = result[4][0] - try: - ip = ipaddress.ip_address(ip_str) - except ValueError: - continue - if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_unspecified: - raise ValueError("URL cannot target internal IP addresses") + addrinfo = socket.getaddrinfo(hostname, parsed.port) + except socket.gaierror as exc: + raise ValueError("URL hostname DNS resolution failed") from exc + if not addrinfo: + raise ValueError("URL hostname DNS resolution returned no addresses") + for result in addrinfo: + ip_str = result[4][0] + try: + ip = ipaddress.ip_address(ip_str) + except ValueError as exc: + raise ValueError("URL hostname DNS resolution returned an invalid IP address") from exc + if not ip.is_global or ip.is_multicast: + raise ValueError("URL cannot target internal IP addresses") prompt = { "role": "user", @@ -516,7 +515,10 @@ def call_llm( ) opener = urllib.request.build_opener(NoRedirectHandler()) with opener.open(request, timeout=120) as response: # nosec B310 - raw = response.read().decode("utf-8") + raw_bytes = response.read(MAX_LLM_RESPONSE_BYTES + 1) + if len(raw_bytes) > MAX_LLM_RESPONSE_BYTES: + raise RuntimeError("Noema LLM response exceeded the byte limit") + raw = raw_bytes.decode("utf-8") data = json.loads(raw) content = (((data.get("choices") or [{}])[0].get("message") or {}).get("content") or "").strip() verdict = extract_json_object(content) diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 408bb95b9..1b04743ea 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -324,9 +324,10 @@ def __exit__(self, *args): """Propagate exceptions from the with-statement body.""" return False - def read(self): + def read(self, size=-1): """Return the payload as encoded JSON bytes.""" - return json.dumps(self.payload).encode("utf-8") + encoded = json.dumps(self.payload).encode("utf-8") + return encoded if size < 0 else encoded[:size] def test_call_llm_handles_configuration_and_verdicts(monkeypatch): @@ -338,12 +339,24 @@ def test_call_llm_handles_configuration_and_verdicts(monkeypatch): monkeypatch.setenv("NOEMA_LLM_API_URL", "file:///etc/passwd") monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret") - with pytest.raises(ValueError, match="URL scheme must be http or https"): + with pytest.raises(ValueError, match="must use HTTPS"): + noema.call_llm("owner/repo", 1, pr, "diff", False) + + monkeypatch.setenv("NOEMA_LLM_API_URL", "http://llm.example.test/chat") + with pytest.raises(ValueError, match="must use HTTPS"): noema.call_llm("owner/repo", 1, pr, "diff", False) monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat") monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret") monkeypatch.setenv("NOEMA_LLM_MODEL", "review-model") + import socket + + def public_getaddrinfo(host, port, *args, **kwargs): + if host == "169.254.169.254": + return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (host, 0))] + return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("8.8.8.8", 0))] + + monkeypatch.setattr(socket, "getaddrinfo", public_getaddrinfo) seen = {} def fake_urlopen(request, timeout): @@ -376,6 +389,18 @@ def fake_urlopen_defer(request, timeout=None): with pytest.raises(RuntimeError, match="unsupported decision"): noema.call_llm("owner/repo", 1, pr, "diff", False) + assert noema.MAX_LLM_RESPONSE_BYTES == 1_048_576 + oversized_payload = { + "choices": [{"message": {"content": "x" * noema.MAX_LLM_RESPONSE_BYTES}}] + } + monkeypatch.setattr( + noema.urllib.request, + "build_opener", + lambda *args: FakeOpener(lambda *_args: FakeResponse(oversized_payload)), + ) + with pytest.raises(RuntimeError, match="response exceeded the byte limit"): + noema.call_llm("owner/repo", 1, pr, "diff", False) + # Test case-insensitive valid URL monkeypatch.setenv("NOEMA_LLM_API_URL", "HTTPS://llm.example.test/chat") monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *args: FakeOpener(fake_urlopen)) @@ -383,29 +408,28 @@ def fake_urlopen_defer(request, timeout=None): # Test invalid scheme (and no original URL in error) monkeypatch.setenv("NOEMA_LLM_API_URL", "file:///etc/passwd") - with pytest.raises(ValueError, match="URL scheme must be http or https"): + with pytest.raises(ValueError, match="must use HTTPS"): noema.call_llm("owner/repo", 1, pr, "diff", False) # Test localhost rejection - monkeypatch.setenv("NOEMA_LLM_API_URL", "http://localhost/chat") + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://localhost/chat") with pytest.raises(ValueError, match="URL cannot target localhost"): noema.call_llm("owner/repo", 1, pr, "diff", False) # Test missing hostname - monkeypatch.setenv("NOEMA_LLM_API_URL", "http:///chat") + monkeypatch.setenv("NOEMA_LLM_API_URL", "https:///chat") with pytest.raises(ValueError, match="URL must have a valid hostname"): noema.call_llm("owner/repo", 1, pr, "diff", False) # Test internal IP rejection - monkeypatch.setenv("NOEMA_LLM_API_URL", "http://169.254.169.254/chat") + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://169.254.169.254/chat") with pytest.raises(ValueError, match="URL cannot target internal IP addresses"): noema.call_llm("owner/repo", 1, pr, "diff", False) - import socket original_getaddrinfo = socket.getaddrinfo # Test DNS resolution bypass - monkeypatch.setenv("NOEMA_LLM_API_URL", "http://resolved-to-local.example.com/chat") + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://resolved-to-local.example.com/chat") def fake_getaddrinfo(host, port, *args, **kwargs): if host == "resolved-to-local.example.com": return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", 0))] @@ -414,22 +438,62 @@ def fake_getaddrinfo(host, port, *args, **kwargs): with pytest.raises(ValueError, match="URL cannot target internal IP addresses"): noema.call_llm("owner/repo", 1, pr, "diff", False) - # Test unresolved hostname does not break - monkeypatch.setenv("NOEMA_LLM_API_URL", "http://unresolved.example.com/chat") + # DNS validation is part of the credential-egress boundary. A failed + # preflight must not be re-resolved later by the HTTP client. + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://unresolved.example.com/chat") def fake_getaddrinfo_error(host, port, *args, **kwargs): raise socket.gaierror("Name or service not known") monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo_error) monkeypatch.setattr(noema.urllib.request, "build_opener", lambda *args: FakeOpener(fake_urlopen)) - assert noema.call_llm("owner/repo", 1, pr, "diff", True)["decision"] == "approve" + with pytest.raises(ValueError, match="DNS resolution failed"): + noema.call_llm("owner/repo", 1, pr, "diff", True) + + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://empty-dns.example.com/chat") + monkeypatch.setattr(socket, "getaddrinfo", lambda *args, **kwargs: []) + with pytest.raises(ValueError, match="returned no addresses"): + noema.call_llm("owner/repo", 1, pr, "diff", True) # Test invalid IP string from getaddrinfo (unlikely but theoretically possible) - monkeypatch.setenv("NOEMA_LLM_API_URL", "http://weird-dns.example.com/chat") + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://weird-dns.example.com/chat") def fake_getaddrinfo_invalid_ip(host, port, *args, **kwargs): if host == "weird-dns.example.com": return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("not_an_ip", 0))] return original_getaddrinfo(host, port, *args, **kwargs) monkeypatch.setattr(socket, "getaddrinfo", fake_getaddrinfo_invalid_ip) - assert noema.call_llm("owner/repo", 1, pr, "diff", True)["decision"] == "approve" + with pytest.raises(ValueError, match="invalid IP address"): + noema.call_llm("owner/repo", 1, pr, "diff", True) + + +@pytest.mark.parametrize("resolved_address", ["100.64.0.1", "224.0.0.1"]) +def test_call_llm_rejects_non_global_dns_addresses(monkeypatch, resolved_address): + """Shared and multicast address space must fail before request egress.""" + import socket + + pr = make_pr() + monkeypatch.setenv("NOEMA_LLM_API_URL", "https://llm.example.test/chat") + monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret") + monkeypatch.setattr( + socket, + "getaddrinfo", + lambda *_args, **_kwargs: [ + (socket.AF_INET, socket.SOCK_STREAM, 6, "", (resolved_address, 443)) + ], + ) + + class UnexpectedOpener: + """Fail if endpoint validation reaches the credentialed request.""" + + def open(self, *_args, **_kwargs): + pytest.fail("credentialed request crossed non-global DNS validation") + + monkeypatch.setattr( + noema.urllib.request, + "build_opener", + lambda *_args: UnexpectedOpener(), + ) + + with pytest.raises(ValueError, match="URL cannot target internal IP addresses"): + noema.call_llm("owner/repo", 1, pr, "diff", False) def test_noema_redirect_handler_rejects_redirects(): @@ -450,12 +514,12 @@ def test_noema_redirect_handler_rejects_redirects(): def test_call_llm_rejects_control_character_scheme_evasion(monkeypatch): """A URL with an embedded tab is normalized by urlparse to an http scheme - with a valid hostname, but its raw form does not start with http:// — the + with a valid hostname, but its raw form does not start with https:// — the startswith guard must still reject it to prevent SSRF via control-character scheme evasion.""" pr = make_pr() monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret") - monkeypatch.setenv("NOEMA_LLM_API_URL", "http\t://sneaky.example.com/chat") + monkeypatch.setenv("NOEMA_LLM_API_URL", "https\t://sneaky.example.com/chat") import socket @@ -463,7 +527,7 @@ def raise_gaierror(host, port, *args, **kwargs): raise socket.gaierror("Name or service not known") monkeypatch.setattr(socket, "getaddrinfo", raise_gaierror) - with pytest.raises(ValueError, match="must start with http:// or https://"): + with pytest.raises(ValueError, match="must use HTTPS"): noema.call_llm("owner/repo", 1, pr, "diff", False) @@ -475,7 +539,7 @@ def test_call_llm_rejects_non_http_parsed_scheme(monkeypatch): parsed = noema.urllib.parse.ParseResult("file", "llm.example.test", "/chat", "", "", "") monkeypatch.setattr(noema.urllib.parse, "urlparse", lambda _: parsed) - with pytest.raises(ValueError, match="URL scheme must be http or https"): + with pytest.raises(ValueError, match="must use HTTPS"): noema.call_llm("owner/repo", 1, pr, "diff", False) diff --git a/tests/test_repository_branch_coverage_review_schedulers.py b/tests/test_repository_branch_coverage_review_schedulers.py index d50f94f05..54d885703 100644 --- a/tests/test_repository_branch_coverage_review_schedulers.py +++ b/tests/test_repository_branch_coverage_review_schedulers.py @@ -39,8 +39,8 @@ def __enter__(self) -> "Response": def __exit__(self, *_args: object) -> bool: return False - def read(self) -> bytes: - return json.dumps( + def read(self, size: int = -1) -> bytes: + encoded = json.dumps( { "choices": [ { @@ -57,6 +57,7 @@ def read(self) -> bytes: ] } ).encode() + return encoded if size < 0 else encoded[:size] class Opener: """Open one deterministic provider response.""" From 962f02c1ba55577cb9781836d8bc111e1dc721da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:55:46 +0900 Subject: [PATCH 2/5] fix(strix): ignore informational scope reports --- .github/workflows/strix.yml | 27 +++++++++++++++++++-------- tests/test_noema_review_gate.py | 15 +++++++++++++++ 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 4155c7346..95e376901 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -867,16 +867,27 @@ jobs: # Recognized signals that the LLM backend was unavailable / starved. backend_unavailable_signal='RateLimitError|Too many requests\. For more on scraping GitHub|exceeded your current quota|insufficient_quota|billing details|"status"[[:space:]]*:[[:space:]]*"RESOURCE_EXHAUSTED"|tokens_limit_reached|Request body too large|Max size:[[:space:]]*[0-9]+[[:space:]]+tokens|Error code:[[:space:]]*413|LLM CONNECTION FAILED|Could not establish connection to the language model|LLM warm-up failed|Configured model and fallback models were unavailable|Configured Vertex model and fallback models were unavailable|emitted provider infrastructure or failure-signal output|before provider infrastructure failure|litellm(\.exceptions)?\.NotFoundError[^[:cntrl:]]*Nvidia_nimException[^[:cntrl:]]*Error code:[[:space:]]*404' - # Any evidence that a vulnerability was actually reported. Its presence - # forces a hard failure so real findings are NEVER downgraded. Keep the - # severity branch anchored away from identifiers so environment lines - # such as STRIX_FAIL_ON_MIN_SEVERITY do not look like findings. - reported_vulnerability_signal='Vulnerabilities[[:space:]]+[1-9]|(^|[^A-Za-z0-9_])severity[[:space:]]*:' + # Only medium-or-higher findings are blocking evidence. Low and INFO + # reports are retained as artifacts but do not block merge progress; + # the configured Strix threshold is MEDIUM. Keep the severity branch + # anchored away from identifiers such as STRIX_FAIL_ON_MIN_SEVERITY. + reported_vulnerability_signal='(^|[^A-Za-z0-9_])severity[[:space:]]*:[[:space:]]*(critical|high|medium)([^A-Za-z0-9_]|$)' + + # Workflow-only callers can legitimately produce an informational + # "no assessable application code" report. It is not a vulnerability + # signal and must remain neutral unless a medium-or-higher finding is + # also present in the same run. + non_assessable_scope_signal='No Assessable Application Code Found in Scope' + if grep -Eiq "$non_assessable_scope_signal" "$strix_run_log" \ + && ! grep -Eiq "$reported_vulnerability_signal" "$strix_run_log"; then + echo "::warning title=Strix scope not assessable::Strix received workflow-only scope and produced no medium-or-higher vulnerability evidence; treating the informational scope result as neutral." + exit 0 + fi # Neutral skip only when ALL hold: a backend-unavailability signal is - # present and no vulnerability was reported anywhere. This preserves - # real security gating while keeping uncontrollable provider outages - # from blocking current-head merge progress. + # present and no medium-or-higher vulnerability was reported. This + # preserves real security gating while keeping uncontrollable provider + # outages from blocking current-head merge progress. if grep -Eiq "$backend_unavailable_signal" "$strix_run_log" \ && ! grep -Eiq "$reported_vulnerability_signal" "$strix_run_log"; then echo "::warning title=Strix backend unavailable::Strix could not complete because its LLM backend was unavailable (rate limit / token cap / connection or warm-up failure) before producing a vulnerability report. Treating as a neutral skip so an infrastructure outage does not block merges; genuine findings still fail the check. See the strix-reports artifact and the run log." diff --git a/tests/test_noema_review_gate.py b/tests/test_noema_review_gate.py index 1b04743ea..25b03bb30 100644 --- a/tests/test_noema_review_gate.py +++ b/tests/test_noema_review_gate.py @@ -1,6 +1,7 @@ import base64 import json import sys +from pathlib import Path import pytest @@ -619,3 +620,17 @@ def test_parse_args_and_main(monkeypatch): with pytest.raises(SystemExit, match="--pr-number must be positive"): noema.main(["--repo", "owner/repo", "--pr-number", "0"]) + + +def test_strix_gate_uses_medium_threshold_and_neutral_scope_signal(): + """Low/INFO reports do not block, while medium-or-higher findings do.""" + strix = Path(".github/workflows/strix.yml").read_text(encoding="utf-8") + + assert ( + "reported_vulnerability_signal='(^|[^A-Za-z0-9_])severity[[:space:]]*:[[:space:]]*" + "(critical|high|medium)([^A-Za-z0-9_]|$)'" + in strix + ) + assert "reported_vulnerability_signal='Vulnerabilities[[:space:]]+[1-9]" not in strix + assert "non_assessable_scope_signal='No Assessable Application Code Found in Scope'" in strix + assert "produced no medium-or-higher vulnerability evidence" in strix From 881b01912ae7b6ac8e8bfa639b82d8dab40c1eb1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 04:13:48 +0900 Subject: [PATCH 3/5] test(strix): align vulnerability signal contract --- tests/test_strix_nvidia_nim_not_found_fallback.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_strix_nvidia_nim_not_found_fallback.py b/tests/test_strix_nvidia_nim_not_found_fallback.py index a48f3092d..dd51f7ac7 100644 --- a/tests/test_strix_nvidia_nim_not_found_fallback.py +++ b/tests/test_strix_nvidia_nim_not_found_fallback.py @@ -239,7 +239,7 @@ def test_outer_workflow_never_neutralizes_reported_vulnerabilities(self) -> None self.assertFalse( _workflow_neutralizes( "litellm.exceptions.NotFoundError: Nvidia_nimException - " - "Error code: 404\nVulnerabilities 1\n" + "Error code: 404\nSeverity: Medium\n" ) ) @@ -250,7 +250,15 @@ def test_workflow_neutralizes_only_nvidia_404_without_findings(self) -> None: self.assertIn("Nvidia_nimException", workflow) self.assertIn("Error code:[[:space:]]*404", workflow) self.assertIn("reported_vulnerability_signal", workflow) - self.assertIn("Vulnerabilities[[:space:]]+[1-9]", workflow) + self.assertIn( + "severity[[:space:]]*:[[:space:]]*(critical|high|medium)", + workflow, + ) + self.assertIn("No Assessable Application Code Found in Scope", workflow) + self.assertNotIn( + "reported_vulnerability_signal='Vulnerabilities[[:space:]]+[1-9]", + workflow, + ) self.assertIn( '! grep -Eiq "$reported_vulnerability_signal"', workflow, From 3d7ae8c37079a6692721dfd49b535f1fbf4216bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 09:30:03 +0900 Subject: [PATCH 4/5] docs: remove duplicate Noema changelog entries --- CHANGELOG.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 609761f45..f4903c2f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,14 +35,10 @@ Semantic Versioning where the repository publishes a release. ### Fixed - - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. - - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). - - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). - - Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. - - Fail-closed Noema model endpoints unless the hostname is HTTPS-reachable global-unicast DNS; reject localhost, metadata, and non-global addresses. - - 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. - - Fail-closed Noema model endpoints unless the hostname is HTTPS-reachable global-unicast DNS; reject localhost, metadata, and non-global addresses. - - 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. +- Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. +- Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). +- Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). +- Excluded relative `-r` and `--requirement` referrers from generated flat base-lock publication while retaining bounded include syntax diagnostics and discovering independently complete direct `.txt` children of `requirements` directories. - Refused a conflict-scope repository root whose immediate parent is a symbolic link, so a swapped parent cannot redirect the canonical worktree after the last-component check (CWE-367). - Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context. - 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. From 3ae457fbd1527a9b11e0066d4ccffd98fd9d1897 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 04:29:18 +0900 Subject: [PATCH 5/5] ci: refresh pip audit runtime --- requirements-pip-audit-ci-hashes.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index ade197a49..0ae099d8f 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \