-
Notifications
You must be signed in to change notification settings - Fork 0
fix(noema): validate stable OIDC exchange envelope #834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c595f07
45c60e7
93d3102
c163fb9
7b64d26
1a202f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -210,6 +210,9 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail_unavailable "Noema app token exchange unavailable: OIDC request environment is missing." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if [ -z "${GITHUB_WORKFLOW_REF:-}" ]; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail_unavailable "Noema app token exchange unavailable: workflow identity is missing." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| separator="&" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -242,11 +245,28 @@ jobs: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail_unavailable "Noema app token exchange unavailable: app token request did not complete." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app_token="$(jq -r '.token // empty' <<<"$token_response")" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ! jq -e \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --arg target_repository "$TARGET_REPOSITORY" \ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| --arg workflow_ref "$GITHUB_WORKFLOW_REF" ' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .ok == true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and (.data | type == "object") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and (.data.token | type == "string" and length > 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and .data.repository == $target_repository | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and .data.workflow_ref == $workflow_ref | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and (.data.token_expires_at | type == "string" and length > 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (try (.data.token_expires_at | fromdateiso8601) catch null) as $expires_at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| | ($expires_at | type == "number") and $expires_at > now | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and (.trace_id | type == "string" and length > 0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ' >/dev/null <<<"$token_response"; then | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+248
to
+262
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: jq envelope validation is correct and short-circuits safely The new envelope validation at noema-review.yml is sound. jq's Was this helpful? React with 👍 or 👎 to provide feedback. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fail_unavailable "Noema app token exchange unavailable: response envelope was invalid." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fi | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| app_token="$(jq -r '.data.token' <<<"$token_response")" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+253
to
+266
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
현재 검사는 비어 있지 않은 문자열만 허용합니다. 응답의 token에 CR 또는 LF가 있으면
수정 예시 app_token="$(jq -r '.data.token' <<<"$token_response")"
+ case "$app_token" in
+ *$'\n'* | *$'\r'*)
+ fail_unavailable "Noema app token exchange unavailable: response envelope was invalid."
+ ;;
+ esac
if [ -z "$app_token" ]; then📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| # Noema OIDC exchange response-envelope contract | ||
|
|
||
| 검토 기준일: **2026-08-24** | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 검토 기준일을 실제 날짜로 수정하세요. 현재 날짜는 2026-08-23입니다. 🤖 Prompt for AI Agents |
||
|
|
||
| ## 문제 | ||
|
|
||
| 중앙 `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. Actions가 제공한 `GITHUB_WORKFLOW_REF`가 존재하고, | ||
| `data.workflow_ref`가 그 실행 workflow ref와 정확히 같아야 합니다. | ||
| 6. `data.token_expires_at`가 RFC 3339 UTC timestamp로 해석 가능하고 현재 | ||
| 시각보다 뒤여야 합니다. | ||
| 7. top-level `trace_id`가 비어 있지 않은 string이어야 합니다. | ||
| 8. 검증된 뒤에만 `data.token`을 추출하고 즉시 GitHub Actions mask를 적용합니다. | ||
| 9. 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입니다. Noema producer는 원래 OIDC assertion의 | ||
| audience, repository, workflow ref 및 source SHA를 검증하고 제한된 GitHub App | ||
| installation token을 발행합니다. 중앙 consumer는 그 assertion을 다시 검증한다고 | ||
| 주장하지 않고, producer가 반환한 repository, workflow ref, expiry 및 trace binding을 | ||
| 검증합니다. | ||
|
|
||
| ## 표준 근거 | ||
|
|
||
| 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를 남긴다는 점에서 해당 원칙을 적용합니다. | ||
|
|
||
| RFC 6749 places an OAuth access token at the top-level `access_token` member | ||
| (Hardt, 2012). Noema's public exchange instead wraps the GitHub App token under | ||
| `data.token` with repository, workflow, expiry, and trace evidence. NIST SP | ||
| 800-63C-4 requires relying parties to validate assertion audience and time | ||
| windows and to preserve replay resistance (Temoshok et al., 2025). The Noema | ||
| producer performs the assertion validation; this consumer accepts the returned | ||
| credential only when its stable envelope is bound to this exact repository and | ||
| executing workflow and remains unexpired. Reading `.token` as if the response | ||
| were RFC 6749 instead treats a schema mismatch as a missing secret and discards | ||
| the binding evidence. | ||
|
|
||
| ## 회귀 계약 | ||
|
|
||
| - workflow가 `.token // empty`를 사용하지 않습니다. | ||
| - `jq -e`가 stable envelope, target repository, exact executing workflow ref, | ||
| future expiry 및 trace identifier를 검증합니다. | ||
| - 추출 경로는 `.data.token`입니다. | ||
| - malformed envelope는 `response envelope was invalid`로 실패합니다. | ||
| - raw response는 diagnostic output으로 반사하지 않습니다. | ||
| - token은 output 기록 전에 `::add-mask::` 처리됩니다. | ||
|
|
||
| ## 롤백과 호환성 | ||
|
|
||
| 롤백은 top-level `.token`으로 되돌리는 것이 아니라, provider의 실제 stable envelope가 변경되었다는 독립적으로 검증된 근거가 있을 때 producer와 consumer 계약을 같은 변경에서 함께 갱신하는 방식으로 수행합니다. 기존 GitHub App 및 PAT credential 경로는 이 OIDC schema repair와 독립적으로 유지되며, standalone product repositories는 중앙 reviewer의 내부 response parsing에 런타임 결합되지 않습니다. | ||
|
|
||
| ## 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 | ||
|
|
||
| ContextualWisdomLab. (2026). *Noema API specification* [Computer software | ||
| documentation]. GitHub. | ||
| https://github.com/ContextualWisdomLab/noema/blob/main/docs/api-spec.md | ||
|
|
||
| Hardt, D. (Ed.). (2012). *The OAuth 2.0 authorization framework* (RFC 6749). | ||
| Internet Engineering Task Force. https://doi.org/10.17487/RFC6749 | ||
|
|
||
| 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 | ||
|
|
||
| Temoshok, D., Richer, J., Choong, Y.-Y., Fenton, J., Lefkovitz, N., | ||
| Regenscheid, A., & Galluzzo, R. (2025). *Digital identity guidelines: | ||
| Federation and assertions* (NIST Special Publication 800-63C-4). National | ||
| Institute of Standards and Technology. | ||
| https://doi.org/10.6028/NIST.SP.800-63c-4 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| """Regression contracts for the Noema OIDC exchange consumer.""" | ||
|
|
||
| import json | ||
| import os | ||
| import subprocess | ||
| from datetime import UTC, datetime, timedelta | ||
| 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 workflow_run_script(workflow: str, name: str) -> str: | ||
| """Return the executable shell body from one named workflow step.""" | ||
| step = workflow_step(workflow, name) | ||
| marker = " run: |\n" | ||
| body = step.split(marker, maxsplit=1)[1] | ||
| return "\n".join(line.removeprefix(" ") for line in body.splitlines()) | ||
|
|
||
|
|
||
| def run_exchange_script( | ||
| tmp_path: Path, token_response: dict[str, object] | ||
| ) -> subprocess.CompletedProcess[str]: | ||
| """Execute the production exchange shell with a deterministic fake transport.""" | ||
| workflow = WORKFLOW_PATH.read_text(encoding="utf-8") | ||
| script = workflow_run_script(workflow, "Exchange Noema app token through OIDC") | ||
| fake_bin = tmp_path / "bin" | ||
| fake_bin.mkdir(exist_ok=True) | ||
| fake_curl = fake_bin / "curl" | ||
| fake_curl.write_text( | ||
| """#!/usr/bin/env python3 | ||
| import os | ||
| import sys | ||
|
|
||
| if "audience=" in sys.argv[-1]: | ||
| print('{"value":"synthetic-oidc-assertion"}') | ||
| else: | ||
| print(os.environ["FAKE_TOKEN_RESPONSE"]) | ||
| """, | ||
| encoding="utf-8", | ||
| ) | ||
| fake_curl.chmod(0o755) | ||
| github_output = tmp_path / "github-output" | ||
| github_output.unlink(missing_ok=True) | ||
| environment = os.environ.copy() | ||
| environment.update( | ||
| { | ||
| "PATH": f"{fake_bin}{os.pathsep}{environment['PATH']}", | ||
| "ACTIONS_ID_TOKEN_REQUEST_TOKEN": "synthetic-request-token", | ||
| "ACTIONS_ID_TOKEN_REQUEST_URL": "https://actions.invalid/id-token", | ||
| "OIDC_AUDIENCE": "synthetic-noema-review", | ||
| "TOKEN_EXCHANGE_URL": "https://noema.invalid/exchange", | ||
| "TARGET_REPOSITORY": "ExampleOrg/example-repository", | ||
| "GITHUB_WORKFLOW_REF": ( | ||
| "ExampleOrg/control-plane/.github/workflows/" | ||
| "noema-review.yml@refs/heads/main" | ||
| ), | ||
| "GITHUB_OUTPUT": str(github_output), | ||
| "FAKE_TOKEN_RESPONSE": json.dumps(token_response), | ||
| } | ||
| ) | ||
| return subprocess.run( | ||
| ["bash", "-c", script], | ||
| check=False, | ||
| capture_output=True, | ||
| env=environment, | ||
| text=True, | ||
| ) | ||
|
|
||
|
|
||
| def test_oidc_exchange_consumes_noema_standard_success_envelope() -> None: | ||
| """Require the central reviewer to consume Noema's stable data envelope.""" | ||
| workflow = WORKFLOW_PATH.read_text(encoding="utf-8") | ||
| exchange = workflow_step(workflow, "Exchange Noema app token through OIDC") | ||
|
|
||
| assert ".token // empty" not in exchange | ||
| assert "Noema app token exchange unavailable: response envelope was invalid." in exchange | ||
| assert 'if [ -z "${GITHUB_WORKFLOW_REF:-}" ]; then' in exchange | ||
| assert '--arg target_repository "$TARGET_REPOSITORY"' in exchange | ||
| assert '--arg workflow_ref "$GITHUB_WORKFLOW_REF"' 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 == $workflow_ref" in exchange | ||
| assert "(.data.token_expires_at | type == \"string\" and length > 0)" in exchange | ||
| assert "fromdateiso8601" in exchange | ||
| assert "$expires_at > now" in exchange | ||
| assert "(.trace_id | 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: | ||
| """Require envelope failures to avoid reflecting raw credential material.""" | ||
| 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 | ||
| mask = 'echo "::add-mask::$app_token"' | ||
| output = 'echo "token=$app_token" >>"$GITHUB_OUTPUT"' | ||
| assert mask in exchange | ||
| assert output in exchange | ||
| assert exchange.index(mask) < exchange.index(output) | ||
|
|
||
|
|
||
| def test_oidc_exchange_accepts_only_exact_live_producer_binding(tmp_path: Path) -> None: | ||
| """Exercise the production shell against realistic valid and invalid envelopes.""" | ||
| repository = "ExampleOrg/example-repository" | ||
| workflow_ref = ( | ||
| "ExampleOrg/control-plane/.github/workflows/" | ||
| "noema-review.yml@refs/heads/main" | ||
| ) | ||
| future_expiry = (datetime.now(UTC) + timedelta(hours=1)).strftime( | ||
| "%Y-%m-%dT%H:%M:%SZ" | ||
| ) | ||
| valid = { | ||
| "ok": True, | ||
| "data": { | ||
| "token": "synthetic-app-token", | ||
| "repository": repository, | ||
| "workflow_ref": workflow_ref, | ||
| "token_expires_at": future_expiry, | ||
| }, | ||
| "trace_id": "synthetic-trace-id", | ||
| } | ||
|
|
||
| accepted = run_exchange_script(tmp_path, valid) | ||
|
|
||
| assert accepted.returncode == 0, accepted.stdout + accepted.stderr | ||
| assert "::add-mask::synthetic-app-token" in accepted.stdout | ||
| assert (tmp_path / "github-output").read_text(encoding="utf-8") == ( | ||
| "token=synthetic-app-token\n" | ||
| ) | ||
|
|
||
| invalid_responses = [ | ||
| {"ok": True, "token": "synthetic-app-token"}, | ||
| {**valid, "data": {**valid["data"], "repository": "ExampleOrg/other"}}, | ||
| { | ||
| **valid, | ||
| "data": {**valid["data"], "workflow_ref": "ExampleOrg/other/workflow"}, | ||
| }, | ||
| { | ||
| **valid, | ||
| "data": { | ||
| **valid["data"], | ||
| "token_expires_at": "2000-01-01T00:00:00Z", | ||
| }, | ||
| }, | ||
| {**valid, "data": {**valid["data"], "token_expires_at": "not-a-time"}}, | ||
| {key: value for key, value in valid.items() if key != "trace_id"}, | ||
| ] | ||
|
|
||
| for invalid in invalid_responses: | ||
| rejected = run_exchange_script(tmp_path, invalid) | ||
| diagnostic = rejected.stdout + rejected.stderr | ||
| assert rejected.returncode != 0 | ||
| assert "response envelope was invalid" in diagnostic | ||
| assert "synthetic-app-token" not in diagnostic | ||
| assert not (tmp_path / "github-output").exists() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 workflow_ref binding requires byte-exact producer echo
The check
.data.workflow_ref == $workflow_ref(noema-review.yml) compares against the runtimeGITHUB_WORKFLOW_REF. If the Noema producer returnsworkflow_refwith any different normalization (branch ref vs SHA, path form), this fail-closed check rejects every otherwise valid token. Confirm the producer emits the byte-identicalGITHUB_WORKFLOW_REFacross the real trigger contexts.Was this helpful? React with 👍 or 👎 to provide feedback.