Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 24 additions & 20 deletions .github/workflows/central-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,8 @@ jobs:
EXPECTED_HEAD_SHA: ${{ github.event.client_payload.pr_head_sha || '' }}
NOEMA_CODEGRAPH_SANDBOX_SOURCE_IMAGE: gcr.io/distroless/nodejs24-debian13:nonroot
steps:
- name: Bind dispatch to live organization PR head
- name: Validate target repository identifier
id: target
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] ||
Expand All @@ -48,24 +46,8 @@ jobs:
echo "::error::Noema dispatch requires a ContextualWisdomLab repository, positive PR number, and full expected head SHA."
exit 1
fi
pr_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
state="$(jq -r '.state // empty' <<<"$pr_json")"
live_head="$(jq -r '.head.sha // empty' <<<"$pr_json")"
head_repo="$(jq -r '.head.repo.full_name // empty' <<<"$pr_json")"
base_repo="$(jq -r '.base.repo.full_name // empty' <<<"$pr_json")"
if [ "$state" != "open" ] ||
[ "$live_head" != "$EXPECTED_HEAD_SHA" ] ||
[ "$head_repo" != "$TARGET_REPOSITORY" ] ||
[ "$base_repo" != "$TARGET_REPOSITORY" ]; then
printf '::error::Noema dispatch metadata is stale or untrusted: state=%s expected_head=%s live_head=%s head_repo=%s base_repo=%s.\n' \
"${state:-missing}" "$EXPECTED_HEAD_SHA" "${live_head:-missing}" \
"${head_repo:-missing}" "${base_repo:-missing}"
exit 1
fi
repository_name="${TARGET_REPOSITORY#*/}"
printf 'repository_name=%s\nhead_sha=%s\n' "$repository_name" "$live_head" >>"$GITHUB_OUTPUT"
printf 'Bound Noema evidence collection to %s#%s current head %s.\n' \
"$TARGET_REPOSITORY" "$PR_NUMBER" "$live_head"
printf 'repository_name=%s\nhead_sha=%s\n' "$repository_name" "$EXPECTED_HEAD_SHA" >>"$GITHUB_OUTPUT"

- name: Mint read-only repository-scoped Noema App token
id: noema_read_app
Expand All @@ -84,6 +66,28 @@ jobs:
permission-statuses: read
permission-vulnerability-alerts: read

- name: Bind dispatch to live organization PR head
env:
GH_TOKEN: ${{ steps.noema_read_app.outputs.token }}
run: |
set -euo pipefail
pr_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
state="$(jq -r '.state // empty' <<<"$pr_json")"
live_head="$(jq -r '.head.sha // empty' <<<"$pr_json")"
head_repo="$(jq -r '.head.repo.full_name // empty' <<<"$pr_json")"
base_repo="$(jq -r '.base.repo.full_name // empty' <<<"$pr_json")"
if [ "$state" != "open" ] ||
[ "$live_head" != "$EXPECTED_HEAD_SHA" ] ||
[ "$head_repo" != "$TARGET_REPOSITORY" ] ||
[ "$base_repo" != "$TARGET_REPOSITORY" ]; then
printf '::error::Noema dispatch metadata is stale or untrusted: state=%s expected_head=%s live_head=%s head_repo=%s base_repo=%s.\n' \
"${state:-missing}" "$EXPECTED_HEAD_SHA" "${live_head:-missing}" \
"${head_repo:-missing}" "$base_repo"
exit 1
fi
printf 'Bound Noema evidence collection to %s#%s current head %s.\n' \
"$TARGET_REPOSITORY" "$PR_NUMBER" "$live_head"

- name: Checkout trusted Noema reviewer
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased
- 중앙 `central-review` evidence 수집의 첫 cross-repository PR 조회를 workflow-repository `GITHUB_TOKEN`에서 exact target repository로 범위가 제한된 read-only Noema GitHub App token 뒤로 이동한다. dispatch payload는 먼저 조직-qualified repository·positive PR number·40-hex expected head 형식만 검증하고, App token 발급 뒤 live PR이 open인지, exact dispatched head와 일치하는지, head/base repository가 동일 target인지 실패-폐쇄 재검증한 후에만 checkout/evidence collection을 진행한다. public target이 workflow token scope 결함을 가리는 문제를 제거하면서 App 권한·merge/release/deployment authority는 넓히지 않으며, private-target protected-main acceptance와 APA 7 doctoring을 별도 증거로 요구한다.
- `hourly-product-development`가 `NVIDIA_NIM_API_KEY`뿐 아니라 `NOEMA_MAINTAINER_APP_CLIENT_ID`와 `NOEMA_MAINTAINER_APP_PRIVATE_KEY` 존재를 checkout·OpenCode 설치·NVIDIA 호출 전에 검증한다. 게시 경로가 준비되지 않았으면 `maintainer_app_unavailable`로 실패 폐쇄하여 알려진 실패에 추론 비용을 쓰지 않으며, `dry_run`은 credential 없이 queue와 task contract를 검토하는 경로로 유지한다. 기존 reviewer App 및 `NOEMA_LLM_API_KEY`·`contextual-orchestrator` reviewer credential 경계는 변경하지 않는다.
- zero open pull requests일 때만 `NVIDIA_NIM_API_KEY` 전용 OpenCode 1.17.13 세션을 실행하는 proposal-only `hourly-product-development` 루프를 추가. minute-47 schedule·non-cancelling single flight·OpenCode binary SHA-256 pin·NVIDIA NIM model fallback·후보 실패 시 clean reset·GitHub/OIDC credential 제거·reviewer key 비참조·full release verification·40-file/500,000-byte proposal budget·trusted one-PR packaging을 강제한다. 각 후보 실행은 900초와 30초 kill grace로 제한하고, 실패 후 `npm ci --ignore-scripts` 재설치는 별도 60초와 10초 kill grace로 제한한다. 재설치가 실패하거나 시간 초과되면 불완전한 dependency tree로 다음 후보를 실행하지 않고 실패 폐쇄한다. 세 후보의 실행·종료 2,790초, 두 번의 후보 간 재설치 140초, 300초 setup/diagnostic reserve를 합친 3,230초가 55분(3,300초) job budget에 들어가며 70초 여유를 남긴다. 마지막 후보가 실패하면 불필요한 reset·clean·재설치를 생략하고 안정적인 전체 후보 실패 진단으로 곧바로 종료한다. 모델 실행, 제안 코드 검증, publication credential을 각각 별도의 GitHub-hosted runner로 분리하고, immutable artifact의 exact ID·workflow-run ID·archive digest와 patch SHA-256·base SHA·file/byte count를 교차 검증하며 symlink(`120000`)와 gitlink(`160000`)를 세 경계 모두에서 차단한다. 제안 코드를 실행한 runner에는 Maintainer App secret/token을 절대 제공하지 않고, 세 번째 non-executing publisher에서만 late-bound repository-scoped App token을 발급한다. merge/release/deploy authority는 기존 `hourly-commercial-readiness` exact-head governance에 유지하며, 운영 Runbook과 OpenCode/NVIDIA/GitHub Actions/NIST SP 800-218 근거를 APA 7th doctoring에 기록했다. package version은 release·deployment·production KPI evidence를 발행하지 않으므로 유지한다.
- `/health` liveness와 분리된 unauthenticated `GET`/`HEAD /ready` runtime readiness endpoint를 추가. GitHub Actions OIDC issuer·audience·organization/workflow binding·exact workflow ref·GitHub Cloud API origin·GitHub App identifiers·PKCS#8 private key를 외부 호출 없이 검증하며, 불완전한 설정은 secret/config value를 반사하지 않는 deterministic failure codes와 `503 ERR_SERVICE_NOT_READY`, `Retry-After`, no-store/nosniff/trace/latency headers로 실패-폐쇄한다. exact workflow named ref는 Git `check-ref-format`의 모호성·유효성 경계(`..`, `//`, dot-leading/`.lock` component, revision-expression 문자, trailing dot/slash 등)를 만족해야 하므로 GitHub가 실제로 표현할 수 없는 ref에서 false-ready가 발생하지 않는다. 배포 smoke contract가 liveness·runtime readiness·unauthenticated exchange challenge를 모두 요구하도록 확장하고 Kubernetes probe separation, RFC 9110, NIST SSDF, Git ref-format 근거를 APA 7th doctoring에 기록했다.
Expand Down
61 changes: 61 additions & 0 deletions docs/doctoring/private-target-review-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Private target review authentication boundary

## Status

Proposed security and interoperability repair for the trusted `central-review` workflow. This document describes PR-scoped behavior until the change is protected-integrated and proven on a private target repository.

## Problem

The central review workflow runs in `ContextualWisdomLab/noema` but can review a different repository. GitHub documents the automatically generated `GITHUB_TOKEN` as an installation token whose permissions are limited to the repository that contains the workflow. Therefore the workflow-repository token is not a valid general authority for reading a private target repository.

The previous ordering attempted to read `repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}` with `${{ github.token }}` before creating the existing Noema GitHub App installation token scoped to the target repository. Public targets can mask this defect because their pull-request metadata is publicly readable. A private target cannot rely on that path.

## Decision

Use two separate boundaries:

1. **Syntactic target validation without target-state access.** Validate the organization-qualified repository name, positive pull-request number, and full expected head SHA. Derive only the safe repository-name output required to request a scoped App token.
2. **Repository-scoped authorization before target-state access.** Mint the existing read-only Noema App token for exactly that target repository, then use that token for the first live pull-request read and every later evidence-collection read.

The live binding remains fail closed. Before checkout, the authenticated target read must prove that the pull request is open, its live head equals the dispatched exact head, and both head and base repositories equal the requested target repository.

The App token remains explicitly scoped to read-only evidence permissions for the target repository. This repair does not grant merge, release, deployment, or additional publication authority.

## Why this is the narrowest viable repair

GitHub explicitly recommends a GitHub App when a workflow needs access to additional resources such as another repository. The `actions/create-github-app-token` action supports an `owner` plus a bounded `repositories` list, and recommends explicit `permission-*` inputs rather than inheriting the full installation permission set.

The workflow already had this repository-scoped read token for checkout, checks, statuses, security evidence, and pull-request reads later in the job. Moving the first live target read behind the same token avoids a second credential family and preserves least privilege.

## Failure and recovery

- Invalid repository, PR number, or head syntax fails before the App credential is minted.
- A repository outside the App installation or without required read permission fails during token creation or the authenticated PR lookup.
- A closed PR, changed head, forked head, or mismatched base repository fails before checkout.
- A stale dispatched head is never silently replaced with the newly observed head.
- App-token creation failure does not fall back to `${{ github.token }}` or another ambient credential.

A later retry must start from a fresh dispatch bound to a freshly read exact target head. No predecessor-head review evidence transfers.

## Test contract

`reviewer/tests/test_central_review_workflow.py` requires the order:

```text
validate target identifier
→ mint repository-scoped read-only Noema App token
→ bind dispatch to live PR head using that App token
→ checkout trusted reviewer and exact target source
```

The contract also rejects `${{ github.token }}` in the live target-binding step.

Protected-main operational acceptance requires a real review dispatch against a private repository on which the Noema App is installed, proving that evidence collection reaches the exact target head without widening the App permissions.

## References

GitHub. (2026). *GITHUB_TOKEN*. GitHub Docs. https://docs.github.com/en/actions/concepts/security/github_token

GitHub. (2026). *Making authenticated API requests with a GitHub App in a GitHub Actions workflow*. GitHub Docs. https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow

GitHub. (2026). *actions/create-github-app-token* [Computer software]. GitHub. https://github.com/actions/create-github-app-token
36 changes: 36 additions & 0 deletions reviewer/tests/test_central_review_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,42 @@ def _workflow() -> str:
)


def test_target_pr_binding_uses_repository_scoped_app_token() -> None:
"""Private target PR identity must be read only after target-scoped App auth."""
workflow = _workflow()
validate_index = workflow.index("Validate target repository identifier")
mint_index = workflow.index("Mint read-only repository-scoped Noema App token")
bind_index = workflow.index("Bind dispatch to live organization PR head")
checkout_index = workflow.index("Checkout trusted Noema reviewer")

assert validate_index < mint_index < bind_index < checkout_index

mint_end = workflow.index(" - name:", mint_index + 1)
mint_step = workflow[mint_index:mint_end]
assert "owner: ContextualWisdomLab" in mint_step
assert "repositories: ${{ steps.target.outputs.repository_name }}" in mint_step
for permission in (
"permission-actions: read",
"permission-checks: read",
"permission-contents: read",
"permission-metadata: read",
"permission-pull-requests: read",
"permission-security-events: read",
"permission-statuses: read",
"permission-vulnerability-alerts: read",
):
assert permission in mint_step
assert "permission-contents: write" not in mint_step
assert "permission-pull-requests: write" not in mint_step
assert "repositories: ${{ env.TARGET_REPOSITORY }}" not in mint_step

bind_end = workflow.index(" - name:", bind_index + 1)
bind_step = workflow[bind_index:bind_end]
assert "GH_TOKEN: ${{ steps.noema_read_app.outputs.token }}" in bind_step
assert "GH_TOKEN: ${{ github.token }}" not in bind_step
assert 'gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}"' in bind_step


def test_review_wait_excludes_only_exact_review_dependent_checks() -> None:
"""The independent reviewer must not wait on checks that consume its verdict."""
workflow = _workflow()
Expand Down
Loading