From d26994bf06f3d7329e13f48df037eda18a37b2aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:33:23 +0900 Subject: [PATCH 01/20] fix(sidecar): advance contextual-orchestrator pin after admin merge --- scripts/ci/contextual_orchestrator_review_sidecar.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ci/contextual_orchestrator_review_sidecar.sh b/scripts/ci/contextual_orchestrator_review_sidecar.sh index 8aad862bef..3773e103c0 100755 --- a/scripts/ci/contextual_orchestrator_review_sidecar.sh +++ b/scripts/ci/contextual_orchestrator_review_sidecar.sh @@ -14,7 +14,7 @@ # (fail-closed zero-cost) pool. set -euo pipefail -ORCHESTRATOR_PIN_SHA="${ORCHESTRATOR_PIN_SHA:-464da4715b495b5eaaa593eba3796e2d976ee0c9}" +ORCHESTRATOR_PIN_SHA="${ORCHESTRATOR_PIN_SHA:-212ff437dc297613289dba2e6064ade9942e07d8}" ORCHESTRATOR_GIT_URL="${ORCHESTRATOR_GIT_URL:-https://github.com/ContextualWisdomLab/contextual-orchestrator.git}" # The Strix gate and Noema SSRF guard accept this one process-local origin. # Keep it fixed so an environment override cannot create an unvalidated sidecar. From 8bbedc079d6c4944065a3131af45a0283f0e6b77 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:33:39 +0900 Subject: [PATCH 02/20] test(sidecar): follow current orchestrator revision --- tests/test_contextual_orchestrator_review_sidecar_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_contextual_orchestrator_review_sidecar_contract.py b/tests/test_contextual_orchestrator_review_sidecar_contract.py index 19d419a375..590ff05c9d 100644 --- a/tests/test_contextual_orchestrator_review_sidecar_contract.py +++ b/tests/test_contextual_orchestrator_review_sidecar_contract.py @@ -40,7 +40,7 @@ ) GATEWAY_MODEL = "contextual-orchestrator/orchestrator/free" -ORCH_PIN_SHA = "464da4715b495b5eaaa593eba3796e2d976ee0c9" +ORCH_PIN_SHA = "212ff437dc297613289dba2e6064ade9942e07d8" def _read(path: Path) -> str: From 40f2f98751f16e7eb380c88a57a92ff4e75c0173 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:33:42 +0900 Subject: [PATCH 03/20] docs(adr): record current orchestrator revision --- docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md b/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md index 56619409bb..36c8918c76 100644 --- a/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md +++ b/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md @@ -24,7 +24,7 @@ all five, and auto-optimize routing by cost. 1. **Vendoring, pinned**: `scripts/ci/contextual_orchestrator_review_sidecar.sh` clones `ContextualWisdomLab/contextual-orchestrator` at an exact SHA - (`464da4715b495b5eaaa593eba3796e2d976ee0c9` today) into `RUNNER_TEMP`. The + (`212ff437dc297613289dba2e6064ade9942e07d8` today) into `RUNNER_TEMP`. The source's `requirements.lock` is installed with `--require-hashes` and `--no-deps`, so dependency resolution cannot silently move the reviewed runtime. @@ -113,7 +113,7 @@ all five, and auto-optimize routing by cost. - **2026-09-02 amendment: advance the governed runtime pin to current CO main.** The single sidecar default now advances from `045d17da5e2aea56a97e241ee158ab1628d78660` to the exact - `contextual-orchestrator` main revision `464da4715b495b5eaaa593eba3796e2d976ee0c9`, which contains the + `contextual-orchestrator` main revision `212ff437dc297613289dba2e6064ade9942e07d8`, which contains the current provider-discovery and gateway contracts. The SHA remains immutable; this is a reviewed dependency refresh, not a floating branch reference. From 5f24cdee48eea89b5449c58ac19d96eb4d8210aa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:33:45 +0900 Subject: [PATCH 04/20] docs(changelog): record orchestrator pin drift repair --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 033dee9ff6..3eae3b0091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ### Contextual-orchestrator pin refresh -- Advanced the central sidecar's default immutable CO revision from `045d17da5e2aea56a97e241ee158ab1628d78660` to `464da4715b495b5eaaa593eba3796e2d976ee0c9` and updated its contract test/ADR. All callers still consume an exact SHA; no branch or tag is introduced. +- Advanced the central sidecar's default immutable CO revision from `045d17da5e2aea56a97e241ee158ab1628d78660` to `212ff437dc297613289dba2e6064ade9942e07d8` and updated its contract test/ADR. All callers still consume an exact SHA; no branch or tag is introduced. ### Scheduler target admission From 23a402b880d926911bd1fe60b9f10682e122b6b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:34:49 +0900 Subject: [PATCH 05/20] docs(baseline): append exact-head execution checkpoint --- docs/product-technical-gap-baseline.md | 62 ++++++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 7888a5e04a..254d29e72d 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1,12 +1,68 @@ # Product and Technical Gap Baseline -작성 기준일: **2026-08-26 10:35 KST** +작성 기준일: **2026-09-02 (execution checkpoint appended)** 대상: **ContextualWisdomLab/.github** 중앙 거버넌스·자동화 레포지터리와 이를 소비하는 naruon 생태계 -현재 보호된 `main`: `826b92394c63deb6981c3a8d16a724d71f85a0d7` -현재 열린 PR 수: **107** (아래 표에 이 스냅샷의 전체 목록 포함; live API 재수집) +현재 보호된 `main`: `86ef3e71305daebce2d825c667f7f0619e1f55a5` +이전 스냅샷의 열린 PR 수: **107** (아래 표는 2026-08-26 관측값이며, 아래 checkpoint가 최신 실행 상태다) 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. +## 2026-09-02 실행 checkpoint + +이 섹션은 기존 SHA-bound inventory를 보존하면서, 이번 실행에서 다시 수집한 +exact head·merge 결과·Checks 관측·미완료 경계를 기록한다. 이 섹션의 상태도 +merge authorization이 아니며, 병합 직전에는 각 PR의 live API를 다시 확인한다. + +### 이번 실행에서 보호 브랜치에 도착한 변경 + +| 소유 저장소 | PR / 결과 commit | 확인된 효과 | +|---|---|---| +| `.github` | [#1672](https://github.com/ContextualWisdomLab/.github/pull/1672) `a28fc2f4e185df7847e2f2f5f6ec561d1e84805d` | Noema caller의 고정 900초 repair deadline과 중복 repair model call 제거; CO가 structured-output repair/failover를 소유하고 caller는 한 번 요청 | +| `.github` | [#1683](https://github.com/ContextualWisdomLab/.github/pull/1683) `c2bb59e7e58779aa6b9b41dcf4433632ba70e81e` | exact-head coverage quality gate 추가; 실제 중복인 두 quality caller만 thin caller로 전환 | +| `.github` | [#1736](https://github.com/ContextualWisdomLab/.github/pull/1736) `bbe65f08b1ae663c467be343e8fd5a98881eb686` | provider credential을 중앙 sidecar에 격리하고 `orchestrator/free`를 제공하는 immutable composite action 추가 | +| `.github` | [#1739](https://github.com/ContextualWisdomLab/.github/pull/1739) `7bf98d013c0c08e17fb3c88f4c37a0bba6eeea10` | scheduler allowlist에서 누락된 `ContextualWisdomLab/governance-risk-compliance`를 좁은 bootstrap bridge로 보정 | +| `.github` | [#1740](https://github.com/ContextualWisdomLab/.github/pull/1740) `73b250f568d8892ead48bff85de06a4e3eb34e93` | central sidecar의 구 pin을 CO `464da4715b495b5eaaa593eba3796e2d976ee0c9`로 갱신 | +| `.github` | [#1738](https://github.com/ContextualWisdomLab/.github/pull/1738) `86ef3e71305daebce2d825c667f7f0619e1f55a5` | 과거 Noema 900초 오류와 현재 sandbox shell `timeout 900`을 분리 기록하고 telemetry 계약을 명시 | +| `contextual-orchestrator` | [#1026](https://github.com/ContextualWisdomLab/contextual-orchestrator/pull/1026) `212ff437dc297613289dba2e6064ade9942e07d8` | admin model_groups save/delete 뒤 `/admin/state` 재조회, audit/model-group stale-state 회귀 테스트 추가 | +| `html4tree` | [#601](https://github.com/ContextualWisdomLab/html4tree/pull/601) `ce55268a7bd9494d67cdb419ac5b7fb7bf96a7b0` | PR synchronize 실행에 top-level concurrency와 stale run cancellation 추가 | + +### 현재 열린 successor/repair PR의 exact head와 Checks + +| 저장소 / PR | exact head | 현재 관측 | 판단 | +|---|---|---|---| +| [appguardrail#1092](https://github.com/ContextualWisdomLab/appguardrail/pull/1092) | `4486fe27ac3e81f32c447bcaa08bf423febda2a9` | CodeQL은 `startup_failure`(job 0개), 나머지 required Checks는 queued | central sidecar 소비 전환은 대기 중; merge-ready 아님 | +| [contextual-orchestrator#1008](https://github.com/ContextualWisdomLab/contextual-orchestrator/pull/1008) | `a03405553e1fe33e77b2838506f32d8a904e2647` | base `main@8839081659df587b19642be17b9114f9dee8b666`; CodeQL startup failure, 나머지 queued | OpenCode Go의 mixed-protocol catalog에서 chat subset만 fail-closed로 허용; Responses/Messages adapter는 후속 | +| [naruon#1543](https://github.com/ContextualWisdomLab/naruon/pull/1543) | `3d29ce2a1b6b47cc22d90958100f7a9954b4cbad` | synchronize 전용 group 분리 후 cancellation; checks queued, CodeQL startup failure | COMMENTED review conversation과 Checks를 재검증해야 함 | +| [wardnet#159](https://github.com/ContextualWisdomLab/wardnet/pull/159) | `89176e2cc57088e4d772de9b1686ab89a3e69aeb` | required `rust` check queued; runner/toolchain pin 선행 PR #153/#77과 결합 필요 | delta 보존 후 prerequisite 착지 뒤 restack | +| [keyverse#132](https://github.com/ContextualWisdomLab/keyverse/pull/132) | `cfb2fe23d0b6487f2815f21c1dbfcefd23ccfb6e` | 큰 수동-vendored CO 변경, checks queued, CodeQL startup failure | central action 소비로 재구성 필요 | +| [EgressWeave#235](https://github.com/ContextualWisdomLab/EgressWeave/pull/235) | `210b5c1a0e21461c3530589b714022f48876e056` | 큰 수동-vendored CO 변경, checks queued, CodeQL startup failure | central action 소비로 재구성 필요 | + +### 정합성·timeout·telemetry RCA + +- newsdom-api job `99864028341`은 gateway route discovery 중 HTTP 413 + `request_too_large`, Bytez 500, NVIDIA timeout/429/404가 겹친 뒤 malformed + Noema JSON으로 종료했다. +- html4tree job `100033086428`의 과거 `NoemaRepairDeadlineExceeded`는 + repository caller가 두 번째 repair 요청을 900초에서 끊던 결함이다. #1672로 + 이 caller deadline은 제거됐다. +- 현재 central OpenCode dispatch의 `timeout --kill-after=20 900`은 모델 호출 + timeout이 아니라 sandboxed test command containment다. 따라서 900초를 3시간 + model budget으로 오해하면 안 된다. 긴 substantive review를 허용하려면 + provider completion과 별도로 shell-test/job budget을 명시한 후 다시 검증해야 한다. +- Noema는 이제 phase(`connecting|reading|decoding|validating`), duration, + caller attempt count, scrubbed serving-model identifier, HTTP/error class를 + 기록한다. raw model response는 로그에 남기지 않는다. + +### 미완료 경계 + +1. QSR default branch는 현재 executable runtime가 아니라 README 수준이므로 Noema/OpenCode의 격리 코드 실행은 아직 배포되지 않았다. +2. General MCP/A2A gateway, SearXNG meta-search, Camoufox session isolation, EgressWeave/Wardnet governed outbound path는 설계·부분 구현 수준이며 보호된 main에서 Noema/Strix에 대한 end-to-end 계약이 아니다. +3. CO의 full OpenAI SDK parity, chat↔responses conversion, provider API-version negotiation, Files/S3, batch-only endpoint policy, OpenCode Go Responses/Anthropic Messages 지원은 모두 보호된 main의 완료 조건이 아니다. +4. Keyverse as KV + product ABAC/RBAC + login credential store, Naruon REST login/signup, OpenAI json_schema, SCIM/OIDC/SAML flexible hierarchy and multi-membership는 현재 cross-repository executable evidence가 없다. +5. 103 workflow 감사에서 확인한 P0 direct-provider caller(`keyverse`, `EgressWeave`, `appguardrail`)와 repository별 concurrency 누락은 남아 있다. #1736은 canonical boundary를 제공하지만 모든 caller가 thin reusable caller로 바뀐 것은 아니다. +6. GitHub App token parser에는 고정 최대 길이 검사가 발견되지 않았고 organization loop는 non-empty string을 검증하지만, legacy workflow의 `.token // empty` 추출을 typed shared contract로 통일하고 약 520자 `ghs_` fixture를 추가하는 후속 조치가 필요하다. +7. 새 repository의 CodeQL 자동 PR은 org ruleset/installation 권한의 live proof가 없어 완료로 판정하지 않는다. + ## 1. 근거와 범위 ### 1.1 우선순위가 높은 근거 From 1a39d229712ede6b58197446bc0934455ef28800 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:35:36 +0900 Subject: [PATCH 06/20] fix(auth): accept long stateless GitHub App tokens in agent-mention-router.yml --- .github/workflows/agent-mention-router.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/agent-mention-router.yml b/.github/workflows/agent-mention-router.yml index a109c8a97c..c53f4093ff 100644 --- a/.github/workflows/agent-mention-router.yml +++ b/.github/workflows/agent-mention-router.yml @@ -140,7 +140,7 @@ jobs: mark_unavailable exit 0 fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" if [ -z "$app_token" ]; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable From 9383c75f7dc63c3331d189ca275ec0e6c470c0ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:35:38 +0900 Subject: [PATCH 07/20] fix(auth): accept long stateless GitHub App tokens in noema-review.yml --- .github/workflows/noema-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 30c9e9a517..4c57df3eb6 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -497,7 +497,7 @@ jobs: fail_unavailable "Noema app token exchange unavailable: app token request did not complete." fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" if [ -z "$app_token" ]; then fail_unavailable "Noema app token exchange unavailable: app token response was empty." fi From d92fa15b630962627d5f3e3e14d1c23bb856616f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:35:42 +0900 Subject: [PATCH 08/20] fix(auth): accept long stateless GitHub App tokens in opencode-review.yml --- .github/workflows/opencode-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 9c2ff1711e..082c16054a 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -360,7 +360,7 @@ jobs: echo "::error::OpenCode review dispatch could not obtain its OIDC token." exit 1 fi - app_token="$(curl -fsS -X POST -H "Authorization: Bearer ${oidc_token}" "${OPENCODE_API_BASE_URL}/exchange_github_app_token" | jq -r '.token // empty')" + app_token="$(curl -fsS -X POST -H "Authorization: Bearer ${oidc_token}" "${OPENCODE_API_BASE_URL}/exchange_github_app_token" | jq -er '.token | select(type == "string" and length > 0)')" if [ -z "$app_token" ]; then echo "::error::OpenCode review dispatch could not obtain its repository-scoped app token." exit 1 From 03008ee2c950f0fa8b9912fc5db4e4a9f9f3a018 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:35:44 +0900 Subject: [PATCH 09/20] fix(auth): accept long stateless GitHub App tokens in pr-auto-rebase.yml --- .github/workflows/pr-auto-rebase.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-auto-rebase.yml b/.github/workflows/pr-auto-rebase.yml index d25bb9c9f9..97e6fe7abf 100644 --- a/.github/workflows/pr-auto-rebase.yml +++ b/.github/workflows/pr-auto-rebase.yml @@ -137,7 +137,7 @@ jobs: exit 0 fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" if [ -z "$app_token" ]; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable From ea45e21f87df0dafa559ba3f36a337abfa88907e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:35:47 +0900 Subject: [PATCH 10/20] fix(auth): accept long stateless GitHub App tokens in pr-review-autofix.yml --- .github/workflows/pr-review-autofix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index 505384ccfd..61f0bd3f4d 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -117,7 +117,7 @@ jobs: exit 0 fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" if [ -z "$app_token" ]; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable From e7d16ad2ead2dffa8aaf89f06bddcdebe45d7597 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:35:50 +0900 Subject: [PATCH 11/20] fix(auth): accept long stateless GitHub App tokens in pr-review-fix-scheduler.yml --- .github/workflows/pr-review-fix-scheduler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-review-fix-scheduler.yml b/.github/workflows/pr-review-fix-scheduler.yml index cb1b53a028..f1750ee0ce 100644 --- a/.github/workflows/pr-review-fix-scheduler.yml +++ b/.github/workflows/pr-review-fix-scheduler.yml @@ -210,7 +210,7 @@ jobs: exit 0 fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" if [ -z "$app_token" ]; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable From b81811dfd3df59df7be9a3c167e3109efb09cf1f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:35:52 +0900 Subject: [PATCH 12/20] fix(auth): accept long stateless GitHub App tokens in pr-review-merge-scheduler.yml --- .github/workflows/pr-review-merge-scheduler.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index f896dbb3db..c640ba1770 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -235,7 +235,7 @@ jobs: exit 0 fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" if [ -z "$app_token" ]; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable @@ -723,7 +723,7 @@ jobs: exit 0 fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" if [ -z "$app_token" ]; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable From fd32fe50b380ba1f9eb423478f30fbbdea32ef07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:35:55 +0900 Subject: [PATCH 13/20] fix(auth): accept long stateless GitHub App tokens in sbom-inventory-scheduler.yml --- .github/workflows/sbom-inventory-scheduler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sbom-inventory-scheduler.yml b/.github/workflows/sbom-inventory-scheduler.yml index 8810c702fd..5658ad7d08 100644 --- a/.github/workflows/sbom-inventory-scheduler.yml +++ b/.github/workflows/sbom-inventory-scheduler.yml @@ -91,7 +91,7 @@ jobs: exit 0 fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" if [ -z "$app_token" ]; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable From df70009e4d2c2d081036f2f002ef341c01837d03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:35:57 +0900 Subject: [PATCH 14/20] fix(auth): accept long stateless GitHub App tokens in strix.yml --- .github/workflows/strix.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index d7e3f5b05a..eb885ac26a 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -344,7 +344,7 @@ jobs: exit 0 fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" if [ -z "$app_token" ]; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable @@ -1090,7 +1090,7 @@ jobs: exit 0 fi - app_token="$(jq -r '.token // empty' <<<"$token_response")" + app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" if [ -z "$app_token" ]; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable From 3688b0c74b0e5737716534c6c3a8a8463704a8a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:36:11 +0900 Subject: [PATCH 15/20] test(auth): cover long stateless GitHub App token shape --- ...ation_commercial_readiness_loop_secret_scope.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_organization_commercial_readiness_loop_secret_scope.py b/tests/test_organization_commercial_readiness_loop_secret_scope.py index aa50efe00c..a9e668ddf5 100644 --- a/tests/test_organization_commercial_readiness_loop_secret_scope.py +++ b/tests/test_organization_commercial_readiness_loop_secret_scope.py @@ -42,3 +42,17 @@ def test_missing_maintainer_secret_uses_bounded_job_oidc_exchange() -> None: assert "::add-mask::$app_token" in dispatch_step assert "${{ github.token }}" not in dispatch_step assert "GITHUB_TOKEN:" not in dispatch_step + + +def test_installation_token_contract_accepts_the_new_long_stateless_shape() -> None: + """Token validation must not assume the legacy GitHub App token length.""" + source = WORKFLOW_PATH.read_text(encoding="utf-8") + token_extract = 'jq -er \' .token | select(type == "string" and length > 0) \'' + # Keep the assertion tied to the actual jq expression, not to a comment or + # an unrelated length check elsewhere in the workflow. + assert 'jq -er \' .token | select(type == "string" and length > 0) \'' + assert "length == 40" not in source + assert "length == 45" not in source + assert "cut -c1-40" not in source + long_stateless_token = "ghs_" + "x" * 516 + assert len(long_stateless_token) == 520 From b09eb81af0f7e817d48812719de0fa62e6bde28c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:36:32 +0900 Subject: [PATCH 16/20] fix(test): assert actual long-token extraction contract --- ...est_organization_commercial_readiness_loop_secret_scope.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_organization_commercial_readiness_loop_secret_scope.py b/tests/test_organization_commercial_readiness_loop_secret_scope.py index a9e668ddf5..198bdd1a84 100644 --- a/tests/test_organization_commercial_readiness_loop_secret_scope.py +++ b/tests/test_organization_commercial_readiness_loop_secret_scope.py @@ -47,10 +47,10 @@ def test_missing_maintainer_secret_uses_bounded_job_oidc_exchange() -> None: def test_installation_token_contract_accepts_the_new_long_stateless_shape() -> None: """Token validation must not assume the legacy GitHub App token length.""" source = WORKFLOW_PATH.read_text(encoding="utf-8") - token_extract = 'jq -er \' .token | select(type == "string" and length > 0) \'' + expected = """jq -er '.token | select(type == "string" and length > 0)'""" # Keep the assertion tied to the actual jq expression, not to a comment or # an unrelated length check elsewhere in the workflow. - assert 'jq -er \' .token | select(type == "string" and length > 0) \'' + assert expected in source assert "length == 40" not in source assert "length == 45" not in source assert "cut -c1-40" not in source From 5cdc77fc37741b8e5915f0cc20111ac1f95e7a20 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:36:44 +0900 Subject: [PATCH 17/20] docs(auth): record stateless GitHub App token compatibility --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eae3b0091..3f8725322d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -### Contextual-orchestrator pin refresh +- **Accept the stateless GitHub App installation-token format.** Central review, scheduler, rebase, SBOM, and mention workflows now extract only a non-empty JSON string with `jq -e`; they impose no legacy 40/45-character limit, so a longer `ghs_...` token remains intact and malformed/object responses fail closed.\n### Contextual-orchestrator pin refresh - Advanced the central sidecar's default immutable CO revision from `045d17da5e2aea56a97e241ee158ab1628d78660` to `212ff437dc297613289dba2e6064ade9942e07d8` and updated its contract test/ADR. All callers still consume an exact SHA; no branch or tag is introduced. From 44aef1aac6a4f21ff8f165ce6eadf05d37ad7d90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:36:57 +0900 Subject: [PATCH 18/20] docs(baseline): record long-token contract coverage --- docs/product-technical-gap-baseline.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 254d29e72d..04d370d77b 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -60,7 +60,7 @@ merge authorization이 아니며, 병합 직전에는 각 PR의 live API를 다 3. CO의 full OpenAI SDK parity, chat↔responses conversion, provider API-version negotiation, Files/S3, batch-only endpoint policy, OpenCode Go Responses/Anthropic Messages 지원은 모두 보호된 main의 완료 조건이 아니다. 4. Keyverse as KV + product ABAC/RBAC + login credential store, Naruon REST login/signup, OpenAI json_schema, SCIM/OIDC/SAML flexible hierarchy and multi-membership는 현재 cross-repository executable evidence가 없다. 5. 103 workflow 감사에서 확인한 P0 direct-provider caller(`keyverse`, `EgressWeave`, `appguardrail`)와 repository별 concurrency 누락은 남아 있다. #1736은 canonical boundary를 제공하지만 모든 caller가 thin reusable caller로 바뀐 것은 아니다. -6. GitHub App token parser에는 고정 최대 길이 검사가 발견되지 않았고 organization loop는 non-empty string을 검증하지만, legacy workflow의 `.token // empty` 추출을 typed shared contract로 통일하고 약 520자 `ghs_` fixture를 추가하는 후속 조치가 필요하다. +6. GitHub App token parser에는 고정 최대 길이 검사가 발견되지 않았다. 이번 checkpoint PR은 central review/scheduler/rebase/SBOM/mention workflow의 `.token` 추출을 non-empty string 계약으로 통일하고 약 520자 `ghs_` fixture 계약을 추가한다. 외부 consumer repository가 별도 token parser를 소유하는 경우에는 해당 owner에서 같은 계약을 검증해야 한다. 7. 새 repository의 CodeQL 자동 PR은 org ruleset/installation 권한의 live proof가 없어 완료로 판정하지 않는다. ## 1. 근거와 범위 From 48ed7092fbec64b711433f1b25d32de6d50ffbe8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 23:38:41 +0000 Subject: [PATCH 19/20] fix(auth): restore mark_unavailable fallback path on malformed app-token responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The jq -er '.token | select(...)' extraction added in this PR exits non-zero whenever the token field is null, empty, or non-string. Assigned via plain command substitution under `set -euo pipefail`, that failure aborted the script immediately -- before the subsequent `if [ -z "$app_token" ]` block could call mark_unavailable/fail_unavailable and fall back to a configured PAT or continue gracefully. Confirmed by reproducing both the bug (hard exit, fallback never reached) and the fix (fallback reached) with representative malformed/empty/valid token payloads. Wrap each extraction as `if ! app_token="$(...)"; then ; fi` across agent-mention-router.yml, noema-review.yml, opencode-review.yml, pr-auto-rebase.yml, pr-review-autofix.yml, pr-review-fix-scheduler.yml, pr-review-merge-scheduler.yml (both exchanges), sbom-inventory-scheduler.yml, and strix.yml (both exchanges) -- matching the pattern organization-commercial-readiness-loop.yml already used for the same extraction. Kept every diagnostic message text byte-for-byte identical to what this PR already introduced, since noema-review.yml's exact wording is pinned by test_required_workflow_queue_contract.py. Also, three docs fixes surfaced by re-running the full suite locally: - docs/product-technical-gap-baseline.md: this PR's own header edit dropped the literal phrase "현재 열린 PR 수:" that test_baseline_inventory_contains_sha_bound_open_pr_rows regex-matches, failing that test. Restored the phrase while keeping the clarifying snapshot-date note. - CHANGELOG.md: the sidecar-pin bullet had a literal `\n` instead of a real newline (visibly broken rendering), and both it and the matching ADR-0003 amendment overwrote the already-merged 045d17da->464da4715 record in place with 045d17da->212ff437, silently skipping the real immediate predecessor. Fixed the newline and split into two entries (045d17da->464da4715, then 464da4715->212ff437) so history stays traceable, consistent with this PR's own "preserve the historical inventory instead of rewriting it" convention applied to the gap-baseline table. Verified: coverage run -m pytest tests (2645 passed, 1 skipped, 100% coverage) and interrogate (100% docstrings), both green on this branch after the fix. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4 --- .github/workflows/agent-mention-router.yml | 3 +-- .github/workflows/noema-review.yml | 3 +-- .github/workflows/opencode-review.yml | 3 +-- .github/workflows/pr-auto-rebase.yml | 3 +-- .github/workflows/pr-review-autofix.yml | 3 +-- .github/workflows/pr-review-fix-scheduler.yml | 3 +-- .github/workflows/pr-review-merge-scheduler.yml | 6 ++---- .github/workflows/sbom-inventory-scheduler.yml | 3 +-- .github/workflows/strix.yml | 6 ++---- CHANGELOG.md | 9 +++++++-- .../0003-contextual-orchestrator-vendored-free-zdr.md | 9 ++++++++- docs/product-technical-gap-baseline.md | 2 +- 12 files changed, 27 insertions(+), 26 deletions(-) diff --git a/.github/workflows/agent-mention-router.yml b/.github/workflows/agent-mention-router.yml index c53f4093ff..6fcd8a9e73 100644 --- a/.github/workflows/agent-mention-router.yml +++ b/.github/workflows/agent-mention-router.yml @@ -140,8 +140,7 @@ jobs: mark_unavailable exit 0 fi - app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" - if [ -z "$app_token" ]; then + if ! app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")"; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable exit 0 diff --git a/.github/workflows/noema-review.yml b/.github/workflows/noema-review.yml index 4c57df3eb6..5132d81722 100644 --- a/.github/workflows/noema-review.yml +++ b/.github/workflows/noema-review.yml @@ -497,8 +497,7 @@ jobs: fail_unavailable "Noema app token exchange unavailable: app token request did not complete." fi - app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" - if [ -z "$app_token" ]; then + if ! app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")"; then fail_unavailable "Noema app token exchange unavailable: app token response was empty." fi diff --git a/.github/workflows/opencode-review.yml b/.github/workflows/opencode-review.yml index 082c16054a..0d2d38e4ac 100644 --- a/.github/workflows/opencode-review.yml +++ b/.github/workflows/opencode-review.yml @@ -360,8 +360,7 @@ jobs: echo "::error::OpenCode review dispatch could not obtain its OIDC token." exit 1 fi - app_token="$(curl -fsS -X POST -H "Authorization: Bearer ${oidc_token}" "${OPENCODE_API_BASE_URL}/exchange_github_app_token" | jq -er '.token | select(type == "string" and length > 0)')" - if [ -z "$app_token" ]; then + if ! app_token="$(curl -fsS -X POST -H "Authorization: Bearer ${oidc_token}" "${OPENCODE_API_BASE_URL}/exchange_github_app_token" | jq -er '.token | select(type == "string" and length > 0)')"; then echo "::error::OpenCode review dispatch could not obtain its repository-scoped app token." exit 1 fi diff --git a/.github/workflows/pr-auto-rebase.yml b/.github/workflows/pr-auto-rebase.yml index 97e6fe7abf..48cb469323 100644 --- a/.github/workflows/pr-auto-rebase.yml +++ b/.github/workflows/pr-auto-rebase.yml @@ -137,8 +137,7 @@ jobs: exit 0 fi - app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" - if [ -z "$app_token" ]; then + if ! app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")"; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable exit 0 diff --git a/.github/workflows/pr-review-autofix.yml b/.github/workflows/pr-review-autofix.yml index 61f0bd3f4d..d8d144b3c1 100644 --- a/.github/workflows/pr-review-autofix.yml +++ b/.github/workflows/pr-review-autofix.yml @@ -117,8 +117,7 @@ jobs: exit 0 fi - app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" - if [ -z "$app_token" ]; then + if ! app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")"; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable exit 0 diff --git a/.github/workflows/pr-review-fix-scheduler.yml b/.github/workflows/pr-review-fix-scheduler.yml index f1750ee0ce..e5777d4af7 100644 --- a/.github/workflows/pr-review-fix-scheduler.yml +++ b/.github/workflows/pr-review-fix-scheduler.yml @@ -210,8 +210,7 @@ jobs: exit 0 fi - app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" - if [ -z "$app_token" ]; then + if ! app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")"; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable exit 0 diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index c640ba1770..27b91cd75a 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -235,8 +235,7 @@ jobs: exit 0 fi - app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" - if [ -z "$app_token" ]; then + if ! app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")"; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable exit 0 @@ -723,8 +722,7 @@ jobs: exit 0 fi - app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" - if [ -z "$app_token" ]; then + if ! app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")"; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable exit 0 diff --git a/.github/workflows/sbom-inventory-scheduler.yml b/.github/workflows/sbom-inventory-scheduler.yml index 5658ad7d08..a68f806c91 100644 --- a/.github/workflows/sbom-inventory-scheduler.yml +++ b/.github/workflows/sbom-inventory-scheduler.yml @@ -91,8 +91,7 @@ jobs: exit 0 fi - app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" - if [ -z "$app_token" ]; then + if ! app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")"; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable exit 0 diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index eb885ac26a..8993bc6876 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -344,8 +344,7 @@ jobs: exit 0 fi - app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" - if [ -z "$app_token" ]; then + if ! app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")"; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable exit 0 @@ -1090,8 +1089,7 @@ jobs: exit 0 fi - app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")" - if [ -z "$app_token" ]; then + if ! app_token="$(jq -er '.token | select(type == "string" and length > 0)' <<<"$token_response")"; then echo "OpenCode app token exchange unavailable: app token response was empty." mark_unavailable exit 0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f8725322d..e8dc6fdee8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ -- **Accept the stateless GitHub App installation-token format.** Central review, scheduler, rebase, SBOM, and mention workflows now extract only a non-empty JSON string with `jq -e`; they impose no legacy 40/45-character limit, so a longer `ghs_...` token remains intact and malformed/object responses fail closed.\n### Contextual-orchestrator pin refresh +### GitHub App token validation -- Advanced the central sidecar's default immutable CO revision from `045d17da5e2aea56a97e241ee158ab1628d78660` to `212ff437dc297613289dba2e6064ade9942e07d8` and updated its contract test/ADR. All callers still consume an exact SHA; no branch or tag is introduced. +- **Accept the stateless GitHub App installation-token format.** Central review, scheduler, rebase, SBOM, and mention workflows now extract only a non-empty JSON string with `jq -e`; they impose no legacy 40/45-character limit, so a longer `ghs_...` token remains intact and malformed/object responses fail closed. + +### Contextual-orchestrator pin refresh + +- Advanced the central sidecar's default immutable CO revision from `045d17da5e2aea56a97e241ee158ab1628d78660` to `464da4715b495b5eaaa593eba3796e2d976ee0c9` and updated its contract test/ADR. All callers still consume an exact SHA; no branch or tag is introduced. +- Advanced the central sidecar's pin a second time the same day, from `464da4715b495b5eaaa593eba3796e2d976ee0c9` to `212ff437dc297613289dba2e6064ade9942e07d8` (contextual-orchestrator#1026, the admin model-group audit-refresh fix), and updated its contract test/ADR again. Recorded as a separate entry rather than rewritten in place so the immediate-predecessor history stays traceable. ### Scheduler target admission diff --git a/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md b/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md index 36c8918c76..a2ec181024 100644 --- a/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md +++ b/docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md @@ -113,9 +113,16 @@ all five, and auto-optimize routing by cost. - **2026-09-02 amendment: advance the governed runtime pin to current CO main.** The single sidecar default now advances from `045d17da5e2aea56a97e241ee158ab1628d78660` to the exact - `contextual-orchestrator` main revision `212ff437dc297613289dba2e6064ade9942e07d8`, which contains the + `contextual-orchestrator` main revision `464da4715b495b5eaaa593eba3796e2d976ee0c9`, which contains the current provider-discovery and gateway contracts. The SHA remains immutable; this is a reviewed dependency refresh, not a floating branch reference. +- **2026-09-02 amendment (second refresh, same day): advance the pin again after + contextual-orchestrator#1026.** The sidecar default advances a second time, + from `464da4715b495b5eaaa593eba3796e2d976ee0c9` to the exact + `contextual-orchestrator` main revision `212ff437dc297613289dba2e6064ade9942e07d8`, + which contains the admin model-group audit-refresh fix. Recorded as a + separate amendment rather than rewritten in place so the immediate-predecessor + history stays traceable. ## Consequences diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 04d370d77b..ddb1418fa5 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -3,7 +3,7 @@ 작성 기준일: **2026-09-02 (execution checkpoint appended)** 대상: **ContextualWisdomLab/.github** 중앙 거버넌스·자동화 레포지터리와 이를 소비하는 naruon 생태계 현재 보호된 `main`: `86ef3e71305daebce2d825c667f7f0619e1f55a5` -이전 스냅샷의 열린 PR 수: **107** (아래 표는 2026-08-26 관측값이며, 아래 checkpoint가 최신 실행 상태다) +현재 열린 PR 수: **107** (아래 표는 2026-08-26 관측값이며, 아래 checkpoint가 최신 실행 상태다) 이 문서는 제품·기술·운영 Gap을 현재 문서와 현재 GitHub 상태에 묶어 두는 기준선이다. 새 작업은 먼저 이 문서의 Gap ID를 PR 설명과 테스트 증거에 연결하고, PR의 정확한 exact HEAD·Checks·리뷰를 다시 수집한 뒤 구현한다. 표의 상태는 작성 시점의 관측값이므로, 병합 판단에는 재사용하지 않는다. 이 인벤토리는 스냅샷이며 merge authorization이 아니다. From ddd60ac0e0aeb93ced70a78125601383602af7b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 01:11:05 +0900 Subject: [PATCH 20/20] fix(ci): skip unused SBOM app token exchange Signed-off-by: Seongho Bae --- .../workflows/sbom-inventory-scheduler.yml | 2 + .../test_sbom_inventory_scheduler_contract.py | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/.github/workflows/sbom-inventory-scheduler.yml b/.github/workflows/sbom-inventory-scheduler.yml index a68f806c91..9eee940d2c 100644 --- a/.github/workflows/sbom-inventory-scheduler.yml +++ b/.github/workflows/sbom-inventory-scheduler.yml @@ -36,10 +36,12 @@ jobs: id-token: write pull-requests: write env: + SBOM_TOKEN_CONFIGURED: ${{ secrets.SBOM_INVENTORY_TOKEN != '' }} ORG_LOGIN: ${{ github.event.client_payload.org || vars.SBOM_INVENTORY_ORG || 'ContextualWisdomLab' }} steps: - name: Exchange OpenCode app token for cross-repo reads id: aggregator_app_token + if: env.SBOM_TOKEN_CONFIGURED != 'true' env: OIDC_AUDIENCE: opencode-github-action OPENCODE_API_BASE_URL: https://api.opencode.ai diff --git a/tests/test_sbom_inventory_scheduler_contract.py b/tests/test_sbom_inventory_scheduler_contract.py index f181dd0891..0daa1c5f05 100644 --- a/tests/test_sbom_inventory_scheduler_contract.py +++ b/tests/test_sbom_inventory_scheduler_contract.py @@ -1,6 +1,12 @@ """Executable contract for the central SBOM inventory scheduler.""" from pathlib import Path +import os +import re +import subprocess +import textwrap + +import pytest WORKFLOW = Path(".github/workflows/sbom-inventory-scheduler.yml") @@ -70,3 +76,62 @@ def test_sbom_inventory_scheduler_does_not_force_push() -> None: publication_step = _step_body("Open or update inventory PR") assert "--force" not in publication_step assert "--force-with-lease" not in publication_step + + +@pytest.mark.parametrize("dedicated,app_token,expected_calls,expected_exit", [ + ("fixture-dedicated", "fixture-app", 0, 0), + ("", "fixture-app", 2, 0), + ("", "", 2, 1), +]) +def test_sbom_token_exchange_selection(tmp_path, dedicated, app_token, expected_calls, expected_exit): + """Run real exchange and credential shells with local-only request fixtures.""" + workflow = _workflow_text() + exchange = _step_body("Exchange OpenCode app token for cross-repo reads") + credential = _step_body("Require organization-wide SBOM credential") + # Pin the real job-env binding and parse only the supported step condition. + job_env = workflow.split(" env:\n", 1)[1].split(" steps:\n", 1)[0] + condition = re.search(r"^ if: (.+)$", exchange, re.MULTILINE) + should_exchange = True + if condition: + assert "SBOM_TOKEN_CONFIGURED: ${{ secrets.SBOM_INVENTORY_TOKEN != '' }}" in job_env + assert condition[1] == "env.SBOM_TOKEN_CONFIGURED != 'true'" + should_exchange = not bool(dedicated) + assert "GH_TOKEN: ${{ secrets.SBOM_INVENTORY_TOKEN || steps.aggregator_app_token.outputs.token }}" in credential + output = tmp_path / "output" + calls = tmp_path / "calls" + env = { + "PATH": os.environ["PATH"], "GITHUB_OUTPUT": str(output), + "MOCK_CALLS": str(calls), "MOCK_APP_TOKEN": app_token, + "ACTIONS_ID_TOKEN_REQUEST_TOKEN": "fixture-request", + "ACTIONS_ID_TOKEN_REQUEST_URL": "https://fixture.invalid/oidc", + "OIDC_AUDIENCE": "opencode-github-action", + "OPENCODE_API_BASE_URL": "https://fixture.invalid", + } + mock = ''' +curl() { + printf 'request\\n' >> "$MOCK_CALLS" + case "${@: -1}" in + 'https://fixture.invalid/oidc?audience=opencode-github-action') + printf '%s' '{"value":"fixture-oidc"}' ;; + 'https://fixture.invalid/exchange_github_app_token') + printf '{"token":"%s"}' "$MOCK_APP_TOKEN" ;; + *) return 97 ;; + esac +} +''' + if should_exchange: + result = subprocess.run( + ["bash", "-c", textwrap.dedent(mock) + textwrap.dedent(exchange.split(" run: |\n", 1)[1])], + env=env, capture_output=True, text=True, check=False, + ) + assert result.returncode == 0, result.stderr + outputs = dict(line.split("=", 1) for line in output.read_text().splitlines()) if output.exists() else {} + env["GH_TOKEN"] = dedicated or outputs.get("token", "") + result = subprocess.run( + ["bash", "-c", textwrap.dedent(credential.split(" run: |\n", 1)[1])], + env=env, capture_output=True, text=True, check=False, + ) + assert result.returncode == expected_exit + assert (len(calls.read_text().splitlines()) if calls.exists() else 0) == expected_calls + if expected_exit: + assert "refusing partial inventory" in result.stderr