Skip to content
Open
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
48 changes: 45 additions & 3 deletions .github/workflows/noema-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,41 @@ jobs:
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.client_payload.target_repository || github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.client_payload.pr_number || '' }}
EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha || '' }}
EXPECTED_BASE_REF: ${{ github.event.pull_request.base.ref || github.event.client_payload.pr_base_ref || '' }}
LEGACY_BASE_REF: ${{ github.event.client_payload.base_branch || '' }}
EXPECTED_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.client_payload.pr_base_sha || '' }}
steps:
- name: Admit only the exact live Noema head
id: live_head
run: |
set -euo pipefail
echo "admitted=false" >>"$GITHUB_OUTPUT"
resolved_base_ref="${EXPECTED_BASE_REF:-${LEGACY_BASE_REF:-}}"
if [ -n "${EXPECTED_BASE_REF:-}" ] && [ -n "${LEGACY_BASE_REF:-}" ] &&
[ "$EXPECTED_BASE_REF" != "$LEGACY_BASE_REF" ]; then
echo "::error::Noema admission rejected conflicting base references."
exit 1
fi
if ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] ||
! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] ||
! [[ "$EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then
! [[ "$EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]] ||
! [[ "${EXPECTED_BASE_SHA:-}" =~ ^[0-9a-f]{40}$ ]] ||
[ -z "$resolved_base_ref" ] ||
! git check-ref-format "refs/heads/$resolved_base_ref" >/dev/null; then
echo "::error::Noema admission rejected malformed pull request metadata."
exit 1
fi
live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
live_head="$(jq -r '.head.sha // empty' <<<"$live_pr")"
live_state="$(jq -r '.state // empty' <<<"$live_pr")"
if ! jq -e --arg target "$TARGET_REPOSITORY" --argjson number "$PR_NUMBER" \
--arg ref "$resolved_base_ref" --arg sha "$EXPECTED_BASE_SHA" '
.number == $number and .base.repo.full_name == $target
and .base.ref == $ref and .base.sha == $sha
' <<<"$live_pr" >/dev/null; then
echo "::error::Noema admission rejected missing or mismatched live base identity."
exit 1
fi
if [ "${live_head,,}" != "${EXPECTED_HEAD_SHA,,}" ] || [ "$live_state" != "open" ]; then
echo "::notice::Noema admission retired a stale trigger before review queue entry."
exit 0
Expand Down Expand Up @@ -296,6 +316,9 @@ jobs:
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.client_payload.target_repository || github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.client_payload.pr_number || '' }}
EXPECTED_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.client_payload.pr_head_sha || '' }}
EXPECTED_BASE_REF: ${{ github.event.pull_request.base.ref || github.event.client_payload.pr_base_ref || '' }}
LEGACY_BASE_REF: ${{ github.event.client_payload.base_branch || '' }}
EXPECTED_BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.client_payload.pr_base_sha || '' }}
steps:
- name: Skip events without pull request context
if: env.PR_NUMBER == ''
Expand Down Expand Up @@ -586,11 +609,30 @@ jobs:
GH_TOKEN: ${{ secrets.NOEMA_REVIEW_TOKEN || steps.noema_github_app_token.outputs.token || steps.noema_oidc_token.outputs.token }}
run: |
set -euo pipefail
if ! [[ "$EXPECTED_HEAD_SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "::error::Noema expected head must be a full commit SHA."
resolved_base_ref="${EXPECTED_BASE_REF:-${LEGACY_BASE_REF:-}}"
if [ -n "${EXPECTED_BASE_REF:-}" ] && [ -n "${LEGACY_BASE_REF:-}" ] &&
[ "$EXPECTED_BASE_REF" != "$LEGACY_BASE_REF" ]; then
echo "::error::Noema review rejected conflicting base references."
exit 1
fi
if ! [[ "$TARGET_REPOSITORY" =~ ^ContextualWisdomLab/[A-Za-z0-9_.-]+$ ]] ||
! [[ "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] ||
! [[ "$EXPECTED_HEAD_SHA" =~ ^[0-9a-f]{40}$ ]] ||
! [[ "${EXPECTED_BASE_SHA:-}" =~ ^[0-9a-f]{40}$ ]] ||
[ -z "$resolved_base_ref" ] ||
! git check-ref-format "refs/heads/$resolved_base_ref" >/dev/null; then
echo "::error::Noema review rejected malformed pull request metadata."
exit 1
fi
pull_request_json="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
if ! jq -e --arg target "$TARGET_REPOSITORY" --argjson number "$PR_NUMBER" \
--arg ref "$resolved_base_ref" --arg sha "$EXPECTED_BASE_SHA" '
.number == $number and .base.repo.full_name == $target
and .base.ref == $ref and .base.sha == $sha
' <<<"$pull_request_json" >/dev/null; then
echo "::error::Noema review rejected missing or mismatched live base identity."
exit 1
fi
live_state="$(jq -r '.state // empty' <<<"$pull_request_json")"
live_head_sha="$(jq -r '.head.sha // empty' <<<"$pull_request_json")"
if [ "$live_state" != "open" ] || [ "${live_head_sha,,}" != "${EXPECTED_HEAD_SHA,,}" ]; then
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/opencode-review-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7718,20 +7718,24 @@ jobs:
GH_REPOSITORY: ${{ needs.validate-pr-metadata.outputs.target_repository }}
PR_NUMBER: ${{ needs.validate-pr-metadata.outputs.pr_number }}
PR_HEAD_SHA: ${{ needs.validate-pr-metadata.outputs.head_sha }}
PR_BASE_REF: ${{ needs.validate-pr-metadata.outputs.base_ref }}
PR_BASE_SHA: ${{ needs.validate-pr-metadata.outputs.base_sha }}
OPENCODE_CHANGED_FILES_FILE: ${{ runner.temp }}/opencode-changed-files.txt
OPENCODE_ARTIFACT_MANIFEST_SHA256: ${{ steps.seal_artifacts.outputs.manifest_sha256 }}
OPENCODE_SOURCE_WORKDIR: ${{ runner.temp }}/opencode-pr-head
OPENCODE_REQUIRE_ADVERSARIAL_VALIDATION: "true"
run: |
set -euo pipefail
if [ -z "${GH_TOKEN:-}" ]; then
echo "::warning::Noema handoff skipped because no target-repository dispatch credential was available."
echo "::warning::Noema handoff skipped because no central dispatch credential was available."
exit 1
fi
python3 scripts/ci/noema_review_handoff.py \
--repo "$GH_REPOSITORY" \
--pr-number "$PR_NUMBER" \
--head-sha "$PR_HEAD_SHA" \
--base-ref "$PR_BASE_REF" \
--base-sha "$PR_BASE_SHA" \
--attempts 90 \
--interval-seconds 10

Expand Down
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ The materialization contract is also covered by [`docs/doctoring/exact-artifact-

## Verification discipline

- Noema handoff는 중앙 `repository_dispatch` 수신 위치와 target PR의 base ref/SHA·head를 함께 검증한다. `pr_base_ref`와 기존 `base_branch`가 함께 있으면 일치해야 하며, admission 이후 모델 실행 직전에도 live base를 다시 확인한다. HTTP 204는 리뷰 완료 증거가 아니다. 재현과 한계는 [handoff runbook](docs/doctoring/noema-central-handoff-base-binding.md)을 따른다.

- producer가 안전한 로그 필드를 추가하면 exact revision 쌍으로 consumer sanitizer를 통과시켜 allowlist의 누락을 확인한다. producer 단위 테스트 성공만으로 CI artifact 보존을 주장하지 않으며, 연결 검증에서도 raw 본문 비출력을 유지한다.

Many agent sessions work this organization concurrently under the same standing
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ repeatable compile command.

## Conventions and gotchas specific to this repo

- Noema handoff의 중앙 수신 위치와 target PR base/head를 함께 확인한다. 기존 `base_branch` 호환성을 유지하되 `pr_base_ref`와 충돌하면 거절한다. Dispatch 접수는 완료가 아니며, [handoff runbook](docs/doctoring/noema-central-handoff-base-binding.md)의 실제 shell 회귀와 exact-head 리뷰 검증을 유지한다.

- **Contract tests pin workflows AND prose.** `tests/` asserts exact strings and structure of
`PR_GOVERNANCE_AUDIT.md`, `docs/org-required-workflow-rollout.md`, `opencode.jsonc`, and several
workflow files (e.g. `test_pr_governance_audit_contract.py`, `test_codeql_pr_workflow_contract.py`,
Expand Down
65 changes: 65 additions & 0 deletions docs/doctoring/noema-central-handoff-base-binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Noema 중앙 handoff와 live base 검증

## 원인과 수정 범위

기준 소스는 `fb17ef556f94f673234aa557254ae52779e9a7b0`이다.
`scripts/ci/noema_review_handoff.py`는 소비자 저장소의 dispatch endpoint에
`noema-review`를 보냈지만, 이 이벤트의 수신 workflow는 중앙 `.github`에 있다.
소비자의 HTTP 204 응답만으로 중앙 리뷰가 시작됐다고 볼 수 없다.

송신 위치를 `repos/ContextualWisdomLab/.github/dispatches`로 맞추고,
OpenCode의 검증된 metadata 출력에서 target repository, PR number,
base ref/SHA, head SHA를 전달한다. 중앙 수신기는 최초 admission과 기존
모델 직전 live PR 조회에서 같은 입력을 확인한다. 대기 중 base가 바뀌어도
예전 입력으로 모델 실행을 허용하지 않는다.

정식 필드는 `pr_base_ref`이고, 기존 agent-mention 송신자의 `base_branch`도
허용한다. 두 값이 모두 있으면 같아야 한다. 누락·잘못된 형식·live PR의
repository/number/base/head 불일치는 fail closed이며 기본 base를 지어내지 않는다.
기존 `pull_request_target` 경로는 event의 PR base/head를 사용한다.
이 수신 workflow에 `workflow_call` 또는 `pull_request` trigger를 새로 추가하지 않는다.

## 재현과 검증

`tests/test_noema_review_handoff.py`는 실제 송신 함수를 주입 runner로 실행하고
중앙 endpoint와 전체 payload를 확인한다. 수신기 검증은 기존 workflow shell
추출기를 재사용해 실제 두 `run:` 블록을 실행한다. fake `gh`는 지정된 PR GET만
허용하며 외부 API를 호출하지 않는다. canonical/legacy/both 양성과 충돌·누락·
malformed·base 변경·다른 repository/PR/head 음성을 함께 검증한다.

최초 endpoint/admission RED는 11 failed, 4 passed였다. 모델 직전 검사에도
같은 회귀를 연결한 RED는 10 failed, 18 passed였다. 초기 fixture 목록의
SyntaxError와 shell 추출 오류는 별도 테스트 작성 오류였으며 생산 결함의 RED에
포함하지 않는다. 정확한 최종 실행 명령과 결과는 PR receipt에 기록한다.

최종 영향 범위는 18개 기존 테스트 파일이다. normal과 `GITHUB_ACTIONS=true`에서
각각 `-W error`로 788 passed, 1 skipped를 확인했다(8파일 308 + 인접 7파일
135/1 skipped + scheduler 3파일 345). 전체 저장소 suite 결과로 확대하지 않는다.
handoff 모듈만 별도 측정한 coverage는 154 statements, 50 branches 모두 100%다.

로컬 actionlint 1.7.12의 두 파일 동시 검사는 출력 없이 condition wait에 머물렀다.
자식 process가 없음을 확인하고 본인 실행 PID 46672만 종료했다(exit 143).
`GOMAXPROCS=2` 재시도 PID 53592도 같은 증상으로 종료했다(exit 143).
다른 세션의 PID 89987은 변경하지 않았다. 이후 `gtimeout 30` 단일 파일 검사에서
Noema는 exit 0, OpenCode는 exit 124였고, **기준 fb17ef 원본 OpenCode도**
`gtimeout 20`에서 exit 124였다. 정지 원인 자체는 미확정이다.

따라서 OpenCode 전체 외부 검사 완료를 주장하지 않는다. 두 파일의 actionlint
native YAML/expression 검사(`-shellcheck= -pyflakes=`)는 exit 0이고, 기존
추출기로 읽은 변경된 세 shell 블록의 ShellCheck는 각각 exit 0이다. 기존 shell
syntax/queue 계약과 위 normal/CI 회귀도 통과했다. 재발 시 기준·변경본의 동일
검사를 비교하고, 제한 시간 종료나 출력 부재를 통과로 세지 않는다.

## 남은 경계와 운영 확인

HTTP 204 뒤에도 기존 trusted publisher의 exact-head terminal review를 확인해야
하며, 접수·queued·모델 시작을 승인으로 바꾸지 않는다. 이 변경은 리뷰 자체의
base/workflow/run provenance를 새로 증명하지 않는다. webhook 설정 소유권,
Strix→OpenCode→Noema 순차 admission, provider runtime 502 복구도 별도 작업이다.
ContextualWisdomLab/.github#2051의 remote terminal model-run binding도 이 변경에
포함하지 않는다.
토큰 선택, 권한, concurrency, 재시도/대기 시간, free-only/ZDR 정책은 바꾸지 않는다.

배포 후 운영자는 중앙 receiver run의 target PR metadata와 실제 terminal review를
각각 확인해야 한다. 로컬 mock 테스트는 배포·credential 접근·실제 모델 완료
증거가 아니다. 실패 시 원래 진단을 보존하고 무조건 재dispatch하지 않는다.
6 changes: 6 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ flowchart LR

## 3. Gap register

2026-09-12 G-02 후속: `fb17ef556f94f673234aa557254ae52779e9a7b0`에서
OpenCode→Noema 송신 endpoint가 중앙 수신기와 달랐고 base identity 전달·검증이
빠져 있었다. [중앙 handoff 수리](doctoring/noema-central-handoff-base-binding.md)는
송수신 계약과 live base 검증에 한정한다. 로컬 검증은 hosted 리뷰 완료나 provider
복구가 아니며, 기존 리뷰의 base/workflow/run provenance는 여전히 별도 검증 대상이다.

우선순위는 구매자 체감, 보안/증거 위험, 선행 의존성 순서다.

| Gap ID | 현재 관측 | 구매자 영향 | 우선 구현/검증 |
Expand Down
32 changes: 29 additions & 3 deletions scripts/ci/noema_review_handoff.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,19 +193,35 @@ def dispatch_noema(
number: int,
head_sha: str,
*,
base_ref: str,
base_sha: str,
runner: GhRunner = run_gh,
) -> None:
"""Dispatch the target repository's default-branch Noema workflow."""
"""Dispatch the central default-branch receiver with exact target inputs."""
if (
not REPOSITORY_RE.fullmatch(repo)
or number < 1
or not SHA_RE.fullmatch(head_sha)
or not SHA_RE.fullmatch(base_sha)
or not base_ref
or subprocess.run(
["git", "check-ref-format", f"refs/heads/{base_ref}"],
check=False, capture_output=True,
).returncode != 0
):
raise ValueError("Noema dispatch requires valid exact pull request identity")
payload = {
"event_type": "noema-review",
"client_payload": {
"target_repository": repo,
"pr_number": number,
"pr_head_sha": head_sha,
"pr_base_ref": base_ref,
"pr_base_sha": base_sha,
},
}
runner(
["api", "-X", "POST", f"repos/{repo}/dispatches", "--input", "-"],
["api", "-X", "POST", "repos/ContextualWisdomLab/.github/dispatches", "--input", "-"],
json.dumps(payload),
)

Expand All @@ -222,6 +238,8 @@ def run_handoff(
number: int,
head_sha: str,
*,
base_ref: str,
base_sha: str,
attempts: int,
interval_seconds: float,
runner: GhRunner = run_gh,
Expand Down Expand Up @@ -292,7 +310,7 @@ def run_handoff(

if not dispatched:
try:
dispatch_noema(repo, number, head_sha, runner=runner)
dispatch_noema(repo, number, head_sha, base_ref=base_ref, base_sha=base_sha, runner=runner)
except RuntimeError as exc:
consecutive_failures += 1
detail = redact_text(str(exc)).strip() or "GitHub API call failed"
Expand Down Expand Up @@ -347,6 +365,8 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser.add_argument("--repo", required=True)
parser.add_argument("--pr-number", required=True, type=int)
parser.add_argument("--head-sha", required=True)
parser.add_argument("--base-ref", required=True)
parser.add_argument("--base-sha", required=True)
parser.add_argument("--attempts", type=int, default=90)
parser.add_argument("--interval-seconds", type=float, default=10.0)
args = parser.parse_args(argv)
Expand All @@ -356,6 +376,10 @@ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser.error("--pr-number must be positive")
if not SHA_RE.fullmatch(args.head_sha):
parser.error("--head-sha must be a 40-character Git SHA")
if not SHA_RE.fullmatch(args.base_sha):
parser.error("--base-sha must be a 40-character Git SHA")
if not args.base_ref:
parser.error("--base-ref must not be empty")
if args.attempts < 1:
parser.error("--attempts must be positive")
if args.interval_seconds < 0:
Expand All @@ -370,6 +394,8 @@ def main(argv: Sequence[str] | None = None) -> int:
args.repo,
args.pr_number,
args.head_sha,
base_ref=args.base_ref,
base_sha=args.base_sha,
attempts=args.attempts,
interval_seconds=args.interval_seconds,
)
Expand Down
5 changes: 4 additions & 1 deletion tests/test_noema_orchestrator_workflow_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ def test_noema_admission_retires_out_of_order_dispatch_before_concurrency(
)
fake_gh = tmp_path / "gh"
fake_gh.write_text(
"#!/usr/bin/env bash\nprintf '%s' '{\"head\":{\"sha\":\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"},\"state\":\"open\"}'\n",
"#!/usr/bin/env bash\nprintf '%s' '{\"number\":7,\"head\":{\"sha\":\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"},\"state\":\"open\",\"base\":{\"ref\":\"develop\",\"sha\":\"cccccccccccccccccccccccccccccccccccccccc\",\"repo\":{\"full_name\":\"ContextualWisdomLab/example\"}}}'\n",
encoding="utf-8",
)
fake_gh.chmod(0o755)
Expand All @@ -316,6 +316,9 @@ def test_noema_admission_retires_out_of_order_dispatch_before_concurrency(
"TARGET_REPOSITORY": "ContextualWisdomLab/example",
"PR_NUMBER": "7",
"EXPECTED_HEAD_SHA": "a" * 40,
"EXPECTED_BASE_REF": "develop",
"EXPECTED_BASE_SHA": "c" * 40,
"LEGACY_BASE_REF": "",
},
capture_output=True,
text=True,
Expand Down
Loading
Loading