From c263cf326696e3e6fdf9a6bf25b26d4b63cc567c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:28:15 +0900 Subject: [PATCH 1/5] test(noema): require standard OIDC exchange envelope --- tests/test_noema_oidc_exchange_contract.py | 52 ++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/test_noema_oidc_exchange_contract.py diff --git a/tests/test_noema_oidc_exchange_contract.py b/tests/test_noema_oidc_exchange_contract.py new file mode 100644 index 000000000..1370438a3 --- /dev/null +++ b/tests/test_noema_oidc_exchange_contract.py @@ -0,0 +1,52 @@ +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +WORKFLOW_PATH = REPO_ROOT / ".github" / "workflows" / "noema-review.yml" + + +def workflow_step(workflow: str, name: str) -> str: + """Return one named workflow step without parsing untrusted YAML tags.""" + marker = f" - name: {name}\n" + start = workflow.index(marker) + try: + end = workflow.index("\n - name:", start + len(marker)) + except ValueError: + end = len(workflow) + return workflow[start:end] + + +def test_oidc_exchange_consumes_noema_standard_success_envelope() -> None: + """The central reviewer must read the repository token from ``data``. + + Noema's stable success contract is ``{ok: true, data: {token, ...}}``. A + top-level ``.token`` lookup silently turns a successful exchange into an + empty credential and makes every OIDC-backed central review fail after the + token was already minted. The consumer must authenticate the complete + envelope and bind it to the requested repository before extracting the + short-lived token. + """ + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + exchange = workflow_step(workflow, "Exchange Noema app token through OIDC") + + assert 'jq -r ".token // empty"' not in exchange + assert "Noema app token exchange unavailable: response envelope was invalid." in exchange + assert 'jq -e --arg target_repository "$TARGET_REPOSITORY"' in exchange + assert ".ok == true" in exchange + assert "(.data | type == \"object\")" in exchange + assert "(.data.token | type == \"string\" and length > 0)" in exchange + assert ".data.repository == $target_repository" in exchange + assert "(.data.workflow_ref | type == \"string\" and length > 0)" in exchange + assert "(.data.token_expires_at | type == \"string\" and length > 0)" in exchange + assert 'app_token="$(jq -r \'.data.token\' <<<"$token_response")"' in exchange + + +def test_oidc_exchange_keeps_token_out_of_diagnostics() -> None: + """Envelope failures may identify fields but must never reflect credentials.""" + workflow = WORKFLOW_PATH.read_text(encoding="utf-8") + exchange = workflow_step(workflow, "Exchange Noema app token through OIDC") + + assert 'echo "$token_response"' not in exchange + assert 'printf "%s" "$token_response"' not in exchange + assert 'echo "::add-mask::$app_token"' in exchange + assert 'echo "token=$app_token" >>"$GITHUB_OUTPUT"' in exchange From c8f8e452de999583b8e9c1002201d570cd14b755 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:33:38 +0900 Subject: [PATCH 2/5] fix(noema): consume the standard token envelope --- .github/workflows/noema-review.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 59b25e343..07b6fa5a1 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -242,11 +242,18 @@ jobs: fail_unavailable "Noema app token exchange unavailable: app token request did not complete." fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" - if [ -z "$app_token" ]; then - fail_unavailable "Noema app token exchange unavailable: app token response was empty." + if ! jq -e --arg target_repository "$TARGET_REPOSITORY" ' + .ok == true + and (.data | type == "object") + and (.data.token | type == "string" and length > 0) + and .data.repository == $target_repository + and (.data.workflow_ref | type == "string" and length > 0) + and (.data.token_expires_at | type == "string" and length > 0) + ' >/dev/null <<<"$token_response"; then + fail_unavailable "Noema app token exchange unavailable: response envelope was invalid." fi + app_token="$(jq -r '.data.token' <<<"$token_response")" echo "::add-mask::$app_token" echo "token=$app_token" >>"$GITHUB_OUTPUT" From d708b55391a57c188cbda0b5eb3675ba3cd92b8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:36:15 +0900 Subject: [PATCH 3/5] fix(noema): preserve empty-token fail-closed contract --- .github/workflows/noema-review.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 07b6fa5a1..b85e53c49 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -254,6 +254,9 @@ jobs: fi app_token="$(jq -r '.data.token' <<<"$token_response")" + if [ -z "$app_token" ]; then + fail_unavailable "Noema app token exchange unavailable: app token response was empty." + fi echo "::add-mask::$app_token" echo "token=$app_token" >>"$GITHUB_OUTPUT" From 6504eee5f200c03428bfa7a388ae4be7ca7dd012 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:38:49 +0900 Subject: [PATCH 4/5] docs(doctoring): record Noema envelope interoperability basis --- .../doctoring/noema-oidc-exchange-envelope.md | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/doctoring/noema-oidc-exchange-envelope.md diff --git a/docs/doctoring/noema-oidc-exchange-envelope.md b/docs/doctoring/noema-oidc-exchange-envelope.md new file mode 100644 index 000000000..1ed2400d4 --- /dev/null +++ b/docs/doctoring/noema-oidc-exchange-envelope.md @@ -0,0 +1,60 @@ +# Noema OIDC exchange response-envelope contract + +검토 기준일: **2026-08-07** + +## 문제 + +중앙 `noema-review.yml`의 OIDC credential 경로는 Noema `/exchange` 성공 응답에서 top-level `.token`을 읽고 있었습니다. 그러나 Noema의 공개 API 안정성 계약은 성공 값을 다음과 같이 `data` object 아래에 둡니다. + +```json +{ + "ok": true, + "data": { + "token": "ghs_...", + "repository": "ContextualWisdomLab/example", + "workflow_ref": "ContextualWisdomLab/.github/.github/workflows/noema-review.yml@refs/heads/main", + "token_expires_at": "2026-08-07T12:00:00Z" + }, + "trace_id": "..." +} +``` + +따라서 provider가 token을 정상 발급해도 consumer가 `.token`을 조회하면 빈 값이 되어 중앙 reviewer가 항상 실패했습니다. 이 결함은 credential이 없는 것처럼 보이지만 실제 원인은 provider/consumer schema 불일치입니다. + +## 결정 + +OIDC consumer는 token field 하나만 permissive하게 조회하지 않고 다음 전체 contract를 fail closed로 검증합니다. + +1. top-level `ok`가 정확히 `true`여야 합니다. +2. `data`가 JSON object여야 합니다. +3. `data.token`이 비어 있지 않은 string이어야 합니다. +4. `data.repository`가 요청한 `TARGET_REPOSITORY`와 정확히 같아야 합니다. +5. `data.workflow_ref`가 비어 있지 않은 string이어야 합니다. +6. `data.token_expires_at`가 비어 있지 않은 string이어야 합니다. +7. 검증된 뒤에만 `data.token`을 추출하고 즉시 GitHub Actions mask를 적용합니다. +8. malformed response를 진단할 때 raw response나 token 값을 출력하지 않습니다. + +이 변경은 Noema의 reviewer App, PAT fallback, LLM provider, `NVIDIA_NIM_API_KEY`, repository permission 또는 merge authority를 변경하지 않습니다. OIDC path가 이미 발행된 stable response envelope를 정확히 소비하도록 고치는 interoperability repair입니다. + +## 표준 근거 + +RFC 8259는 JSON object를 name/value member의 집합으로 정의하고, member name이 고유할 때 구현 간 mapping agreement가 가능하다고 설명합니다. 또한 networked JSON text는 UTF-8을 사용해야 하며 parser가 size·depth·string length 제한을 둘 수 있음을 명시합니다. 이 변경은 shell의 loose field lookup 대신 object shape와 typed member를 명시적으로 검사하여 producer/consumer가 같은 mapping을 사용하도록 합니다. + +NIST SP 800-218 SSDF Version 1.1은 소프트웨어 생산자가 vulnerability의 근본 원인을 줄이고 소비자·구매자와 공통 보안 언어로 소통할 수 있도록 secure-development practices를 SDLC에 통합할 것을 권고합니다. 현재 finalized baseline은 v1.1이며, Rev. 1 / SSDF Version 1.2는 2025년 12월 공개된 initial public draft입니다. 이 변경은 실제 integration failure를 회귀 계약으로 고정하고 permissive fallback 대신 명시적 failure evidence를 남긴다는 점에서 해당 원칙을 적용합니다. + +## 회귀 계약 + +- workflow가 `.token // empty`를 사용하지 않습니다. +- `jq -e`가 stable envelope와 target repository를 검증합니다. +- 추출 경로는 `.data.token`입니다. +- malformed envelope는 `response envelope was invalid`로 실패합니다. +- raw response는 diagnostic output으로 반사하지 않습니다. +- token은 output 기록 전에 `::add-mask::` 처리됩니다. + +## References (APA 7th) + +Bray, T. (2017). *The JavaScript Object Notation (JSON) data interchange format* (RFC 8259). Internet Engineering Task Force. https://doi.org/10.17487/RFC8259 + +Souppaya, M., Scarfone, K., & Dodson, D. (2022). *Secure Software Development Framework (SSDF) version 1.1: Recommendations for mitigating the risk of software vulnerabilities* (NIST Special Publication 800-218). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-218 + +National Institute of Standards and Technology. (2025, December 17). *Secure Software Development Framework (SSDF) version 1.2 is available for public comment*. https://www.nist.gov/news-events/news/2025/12/secure-software-development-framework-ssdf-version-12-available-public From 43c510bc3b9e16ea6aa80816407cb2d8ee163f64 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 7 Aug 2026 20:39:24 +0900 Subject: [PATCH 5/5] docs(changelog): record Noema OIDC envelope repair --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c993bf7cb..fd995ea63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Fixed the central Noema OIDC credential consumer to authenticate the stable `{ok, data, trace_id}` success envelope, bind `data.repository` to the requested repository, require workflow and expiry metadata, and extract `data.token` instead of the nonexistent top-level `.token`; malformed responses fail closed without reflecting token-bearing JSON. - 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. - Bound both trusted-uv quality jobs to `github.event.pull_request.head.sha` and added a permanent two-checkout regression contract so exact-head compatibility, coverage, docstring, and compilation claims cannot silently measure GitHub's generated pull-request merge revision. - Made Strix treat only a single LiteLLM provider-error line containing NVIDIA NIM context and model-catalog 404 evidence as cross-model fallback evidence, rejecting cross-line signal assembly and provider-like target source literals; moved the public default to Nemotron 3 Super 120B and added a second NVIDIA hosted candidate before GitHub Models without neutralizing reported vulnerabilities.