From aa631bf91ae4b6a1d2e65bcbab11ef3393a786c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:01:16 -0700 Subject: [PATCH 1/4] test(pilot): reject future completion evidence --- test/pilot-readiness-future-date.test.ts | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/pilot-readiness-future-date.test.ts diff --git a/test/pilot-readiness-future-date.test.ts b/test/pilot-readiness-future-date.test.ts new file mode 100644 index 000000000..c7abb026f --- /dev/null +++ b/test/pilot-readiness-future-date.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import { evaluatePilotReadinessText } from "../scripts/lib/pilot-readiness.mjs"; + +function completedPilotWithDates(handoverDate: string, onboardingDate: string) { + return `# 파일럿 온보딩 진행 기록 + +## 항목 1 +- 고객명: Acme Security +- NOEMA URL: https://noema.acme-security.com/exchange +- 지원 채널 합의: Slack acme-noema-ops +- 증빙 출처: production +- 계약/매출 증빙 경로: contracts/acme-paid-pilot.pdf +- 분석 데이터 경로: artifacts/saleable-readiness/noema-kpi-evidence.json +- exchange_failure_rate: 0 +- exchange_p95_latency_ms: 157 +- [x] 실패율 <= 0.02 +- [x] p95 < 300 +- [x] 운영 이관 승인 +- 운영 전환 승인일: ${handoverDate} +- 온보딩 완료일: ${onboardingDate} +- trace_id 샘플: trace-2f4c9a77-1e8a-4f3b-9b9a-a8c1e6f0b5d1 +`; +} + +describe("pilot readiness completion chronology", () => { + it("rejects future completion dates instead of granting saleable-readiness authority", () => { + const result = evaluatePilotReadinessText(completedPilotWithDates("9999-12-30", "9999-12-31")); + + expect(result.passed).toBe(false); + expect(result.entries[0].failures).toContain("운영 전환 승인일 must not be in the future"); + expect(result.entries[0].failures).toContain("온보딩 완료일 must not be in the future"); + }); +}); From 0d9c387e5c86743d1e27d8738a6724330c8032a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:01:39 -0700 Subject: [PATCH 2/4] fix(pilot): reject future completion evidence --- scripts/lib/pilot-readiness.mjs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/lib/pilot-readiness.mjs b/scripts/lib/pilot-readiness.mjs index 35b3f2a18..0b23cf7b6 100644 --- a/scripts/lib/pilot-readiness.mjs +++ b/scripts/lib/pilot-readiness.mjs @@ -1,10 +1,13 @@ const dateOnlyRegex = /^\d{4}-\d{2}-\d{2}$/; -function hasValidDate(value) { +function dateStatus(value) { const normalized = String(value ?? "").trim(); - if (!dateOnlyRegex.test(normalized)) return false; + if (!dateOnlyRegex.test(normalized)) return "invalid"; const parsed = new Date(`${normalized}T00:00:00.000Z`); - return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === normalized; + if (Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== normalized) { + return "invalid"; + } + return parsed.getTime() > Date.now() ? "future" : "valid"; } function metricValue(entry, name) { @@ -60,13 +63,17 @@ function evaluatePilotEntry(entry) { const traceId = fieldValue(entry, "trace_id 샘플"); const failureRate = metricValue(entry, "exchange_failure_rate"); const p95 = metricValue(entry, "exchange_p95_latency_ms"); + const onboardingDateStatus = dateStatus(onboardingDate); + const handoverDateStatus = dateStatus(handoverDate); const failures = []; if (!customerName) failures.push("고객명 required"); if (!isUsableProductionUrl(noemaUrl)) failures.push("NOEMA URL must be a non-example HTTPS production URL"); if (!isUsableSupportChannel(supportChannel)) failures.push("지원 채널 합의 must be a real non-local channel"); - if (!hasValidDate(onboardingDate)) failures.push("온보딩 완료일 required"); - if (!hasValidDate(handoverDate)) failures.push("운영 전환 승인일 required"); + if (onboardingDateStatus === "invalid") failures.push("온보딩 완료일 required"); + if (onboardingDateStatus === "future") failures.push("온보딩 완료일 must not be in the future"); + if (handoverDateStatus === "invalid") failures.push("운영 전환 승인일 required"); + if (handoverDateStatus === "future") failures.push("운영 전환 승인일 must not be in the future"); if (!hasCheckedLine(entry, "운영 이관 승인")) failures.push("운영 이관 승인 required"); if (!hasCheckedLine(entry, "(?:p95 <= 300|p95 < 300)")) failures.push("p95 threshold checkbox required"); if (!hasCheckedLine(entry, "실패율 <= 0\\.02")) failures.push("failure-rate threshold checkbox required"); From 8841e6f48927092cc790b080329c4d0d61a2557d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:06:50 -0700 Subject: [PATCH 3/4] docs(changelog): record pilot future-date rejection --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecea4f7d6..4000c70d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Unreleased +- 판매 가능성 파일럿 완료 증거의 시간 권한을 fail-closed로 강화한다. `운영 전환 승인일`과 `온보딩 완료일`은 실제 존재하는 달력 날짜이면서 검증 시점보다 미래가 아니어야 하며, 아직 발생하지 않은 완료·이관 날짜가 saleable-readiness 증거를 제조하지 못하게 한다. - Actions runner-assignment 운영 증거를 fail-closed로 강화한다. `observed_at`은 exact canonical UTC instant만 허용하고 `Date.parse()`가 정규화하는 비정규·존재하지 않는 시각은 거부하며, audit report는 심볼릭 링크가 포함된 parent 경로를 거부한다. 테스트용 filesystem seam도 parent 검증과 atomic write가 동일한 I/O authority를 사용하도록 결합해 검증 경계와 쓰기 경계가 서로 다른 파일시스템을 보지 않게 한다. - 중앙 `.github` reviewer workflow의 immutable OIDC source commit 이동을 Noema runtime trust에 즉시 반영한다. reviewed `noema-review.yml` blob이 동일하더라도 GitHub `job_workflow_sha`는 source commit에 결합되므로 `ALLOWED_WORKFLOW_SHA`와 executable regression을 현재 중앙 protected source에 정확히 맞춰 stale trust를 실패-폐쇄한다. - 외부 스케줄러 운영 증거의 시간 권한을 fail-closed로 강화한다. canonical UTC `scheduled_at`/`started_at`이 검증 시점보다 미래인 retained evidence는 `scheduler_timestamp_future`로 거부해 아직 실행되지 않은 hourly run이 운영·인수 준비 증거를 제조하지 못하게 한다. @@ -10,7 +11,7 @@ - 비리뷰 LLM 작업인 `hourly-product-development`를 리뷰와 동일한 `contextual-orchestrator` 게이트웨이 계약(`NOEMA_LLM_API_URL` `/v1`, 모델 별칭 `contextual-orchestrator`, 전용 `NOEMA_LLM_API_KEY`)으로 전환한다. Llama Nemotron → Nemotron Super → DeepSeek 순차 NIM 후보 폴백과 `NVIDIA_NIM_API_KEY` 직접 호출을 제거하고, 공유 `scripts/verify-orchestrator-gateway.mjs`가 `/healthz` 신원과 직접 공급자 호스트를 실패-폐쇄한다. 리뷰어의 `NOEMA_FALLBACK_*` / PydanticAI `FallbackModel` 순차 폴백도 제거해 남은 설정은 실패-폐쇄한다. 동일 계약을 `contracts/orchestrator-gateway.json`으로 공개해 `ContextualWisdomLab/naruon` 판단·결정 에이전트가 1급 소비자로 재사용할 수 있게 한다. naruon 배선은 별도 저장소 PR이다. 상위 공급자 키는 오케스트레이터 KV에 남기며 OIDC 토큰 중개·App 신원·3-runner 샌드박스 경계는 유지한다. - 검증된 active-orphan 워크플로 하나를 운영자가 호출할 수 있는 `operations:workflow-registry-disable` 경로를 추가한다. 저장소와 워크플로 ID를 `NOEMA_MAINTAINER_TOKEN_PATH` 위임 토큰 파일 읽기 전에 검사하고, 신선한 전체 레지스트리 감사·즉시 live refresh·프로세스 로컬 plan·보호된 main/워크플로 재검증·사후 전체 감사 봉투(`schema_version` 1, `PASS`/`FAIL`, `remaining_failure_codes`, `remaining_active_orphan_ids`)를 통과한 뒤에만 영수증을 유지한다. 성공 종료와 `post_audit_status: FAIL`은 해당 ID만 `disabled_manually`가 되었고 레지스트리는 아직 더럽을 수 있음을 뜻하므로, 운영자는 영수증의 `remaining_active_orphan_ids`로 다음 단일 호출을 이어간다. 배치 비활성화·자가 수리 워크플로·거버넌스 완화는 추가하지 않으며 호출 계약은 doctoring에 기록한다. - 읽기 전용 `operations:runner-assignment` audit를 추가해 exact workflow run/source head에 대한 runner assignment를 완전 pagination으로 진단하고, 신선한 unassigned queue는 bounded grace 이후 실패-폐쇄한다. 이 증빙은 runner assignment와 required Check/CI, formal review, merge, release, deployment authority를 분리하며 assigned runner 이후 workflow failure를 성공으로 승격하지 않는다. -- production `operations:runner-assignment` audit는 `NOEMA_MAINTAINER_TOKEN_PATH`의 owner-only capability file만 읽고, ambient `GH_TOKEN`만 있으면 실패-폐쇄한다. `gh` spawn/stderr 진단은 활성 토큰을 exact-match로 `[REDACTED]` 치환하며, 빈 secret에 대해서는 원문 진단을 보존한다. assignment authority는 양의 `runner_id` 또는 비어 있지 않은 `runner_name`만 인정하며 queued `started_at`은 assignment evidence가 아니다. 운영자는 `printf '%s'`로 capability file을 만들고(`echo`/`printf '%s\\n'`는 trailing newline 때문에 실패-폐쇄), Actions workflow-run/job read만 가진 짧은 토큰을 준비한 뒤 PASS를 required Check·formal review·merge 권한으로 해석하지 마십시오. +- production `operations:runner-assignment` audit는 `NOEMA_MAINTAINER_TOKEN_PATH`의 owner-only capability file만 읽고, ambient `GH_TOKEN`만 있으면 실패-폐쇄한다. `gh` spawn/stderr 진단은 활성 토큰을 exact-match로 `[REDACTED]` 치환하며, 빈 secret에 대해서는 원문 진단을 보존한다. assignment authority는 양의 `runner_id` 또는 비어 있지 않은 `runner_name`만 인정하며 queued `started_at`은 assignment evidence가 아니다. 운영자는 `printf '%s'`로 capability file을 만들고(`echo`/`printf '%s\n'`는 trailing newline 때문에 실패-폐쇄), Actions workflow-run/job read만 가진 짧은 토큰을 준비한 뒤 PASS를 required Check·formal review·merge 권한으로 해석하지 마십시오. - coordinated vulnerability disclosure 정책과 evidence-preserving vulnerability handling lifecycle, read-only private-vulnerability-reporting setting audit를 추가한다. 이 source 변경은 live private reporting 활성화·notification staffing·end-to-end advisory exercise·release/deployment authority를 증명하지 않는다. - 개발 의존성 체인의 transitive `nanoid` lockfile resolution을 `3.3.17`에서 `3.3.18`로 최소 갱신하여 GHSA-2v37-7h3g-55p8 / CVE-2026-67213 보안 게이트를 복구한다. PostCSS의 선언 범위 `^3.3.16`과 다른 package metadata는 변경하지 않으며 audit waiver·ignore·severity 완화 없이 `npm ci`/`npm audit --audit-level=high`가 exact head에서 재검증되도록 유지한다. - lockfile 재생성 도구 체인을 Node.js 24.19.0/npm 11.17.0으로 정확히 고정하고, `strict-allow-scripts=true` 아래 승인된 install-script identity만 실행하며 schema v3 exact-base lockfile change control로 package metadata drift를 실패-폐쇄한다. exact package before/after digest에 더해 top-level metadata digest와 대규모 package-set bulk evidence를 결합하며, 선행 `nanoid@3.3.18` 보안 수정과 explicit `npm ci --legacy-peer-deps=false --install-links=false` 계약을 보존한다. package-manager/toolchain·install-script authority·vulnerability audit·review/merge authority는 별도 증거 계층으로 유지한다. From 20068ecacae2be4197e73db6e9b090c7ffd778f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 22:07:52 -0700 Subject: [PATCH 4/4] docs(changelog): preserve runner capability example --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4000c70d1..dfad0a6a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - 비리뷰 LLM 작업인 `hourly-product-development`를 리뷰와 동일한 `contextual-orchestrator` 게이트웨이 계약(`NOEMA_LLM_API_URL` `/v1`, 모델 별칭 `contextual-orchestrator`, 전용 `NOEMA_LLM_API_KEY`)으로 전환한다. Llama Nemotron → Nemotron Super → DeepSeek 순차 NIM 후보 폴백과 `NVIDIA_NIM_API_KEY` 직접 호출을 제거하고, 공유 `scripts/verify-orchestrator-gateway.mjs`가 `/healthz` 신원과 직접 공급자 호스트를 실패-폐쇄한다. 리뷰어의 `NOEMA_FALLBACK_*` / PydanticAI `FallbackModel` 순차 폴백도 제거해 남은 설정은 실패-폐쇄한다. 동일 계약을 `contracts/orchestrator-gateway.json`으로 공개해 `ContextualWisdomLab/naruon` 판단·결정 에이전트가 1급 소비자로 재사용할 수 있게 한다. naruon 배선은 별도 저장소 PR이다. 상위 공급자 키는 오케스트레이터 KV에 남기며 OIDC 토큰 중개·App 신원·3-runner 샌드박스 경계는 유지한다. - 검증된 active-orphan 워크플로 하나를 운영자가 호출할 수 있는 `operations:workflow-registry-disable` 경로를 추가한다. 저장소와 워크플로 ID를 `NOEMA_MAINTAINER_TOKEN_PATH` 위임 토큰 파일 읽기 전에 검사하고, 신선한 전체 레지스트리 감사·즉시 live refresh·프로세스 로컬 plan·보호된 main/워크플로 재검증·사후 전체 감사 봉투(`schema_version` 1, `PASS`/`FAIL`, `remaining_failure_codes`, `remaining_active_orphan_ids`)를 통과한 뒤에만 영수증을 유지한다. 성공 종료와 `post_audit_status: FAIL`은 해당 ID만 `disabled_manually`가 되었고 레지스트리는 아직 더럽을 수 있음을 뜻하므로, 운영자는 영수증의 `remaining_active_orphan_ids`로 다음 단일 호출을 이어간다. 배치 비활성화·자가 수리 워크플로·거버넌스 완화는 추가하지 않으며 호출 계약은 doctoring에 기록한다. - 읽기 전용 `operations:runner-assignment` audit를 추가해 exact workflow run/source head에 대한 runner assignment를 완전 pagination으로 진단하고, 신선한 unassigned queue는 bounded grace 이후 실패-폐쇄한다. 이 증빙은 runner assignment와 required Check/CI, formal review, merge, release, deployment authority를 분리하며 assigned runner 이후 workflow failure를 성공으로 승격하지 않는다. -- production `operations:runner-assignment` audit는 `NOEMA_MAINTAINER_TOKEN_PATH`의 owner-only capability file만 읽고, ambient `GH_TOKEN`만 있으면 실패-폐쇄한다. `gh` spawn/stderr 진단은 활성 토큰을 exact-match로 `[REDACTED]` 치환하며, 빈 secret에 대해서는 원문 진단을 보존한다. assignment authority는 양의 `runner_id` 또는 비어 있지 않은 `runner_name`만 인정하며 queued `started_at`은 assignment evidence가 아니다. 운영자는 `printf '%s'`로 capability file을 만들고(`echo`/`printf '%s\n'`는 trailing newline 때문에 실패-폐쇄), Actions workflow-run/job read만 가진 짧은 토큰을 준비한 뒤 PASS를 required Check·formal review·merge 권한으로 해석하지 마십시오. +- production `operations:runner-assignment` audit는 `NOEMA_MAINTAINER_TOKEN_PATH`의 owner-only capability file만 읽고, ambient `GH_TOKEN`만 있으면 실패-폐쇄한다. `gh` spawn/stderr 진단은 활성 토큰을 exact-match로 `[REDACTED]` 치환하며, 빈 secret에 대해서는 원문 진단을 보존한다. assignment authority는 양의 `runner_id` 또는 비어 있지 않은 `runner_name`만 인정하며 queued `started_at`은 assignment evidence가 아니다. 운영자는 `printf '%s'`로 capability file을 만들고(`echo`/`printf '%s\\n'`는 trailing newline 때문에 실패-폐쇄), Actions workflow-run/job read만 가진 짧은 토큰을 준비한 뒤 PASS를 required Check·formal review·merge 권한으로 해석하지 마십시오. - coordinated vulnerability disclosure 정책과 evidence-preserving vulnerability handling lifecycle, read-only private-vulnerability-reporting setting audit를 추가한다. 이 source 변경은 live private reporting 활성화·notification staffing·end-to-end advisory exercise·release/deployment authority를 증명하지 않는다. - 개발 의존성 체인의 transitive `nanoid` lockfile resolution을 `3.3.17`에서 `3.3.18`로 최소 갱신하여 GHSA-2v37-7h3g-55p8 / CVE-2026-67213 보안 게이트를 복구한다. PostCSS의 선언 범위 `^3.3.16`과 다른 package metadata는 변경하지 않으며 audit waiver·ignore·severity 완화 없이 `npm ci`/`npm audit --audit-level=high`가 exact head에서 재검증되도록 유지한다. - lockfile 재생성 도구 체인을 Node.js 24.19.0/npm 11.17.0으로 정확히 고정하고, `strict-allow-scripts=true` 아래 승인된 install-script identity만 실행하며 schema v3 exact-base lockfile change control로 package metadata drift를 실패-폐쇄한다. exact package before/after digest에 더해 top-level metadata digest와 대규모 package-set bulk evidence를 결합하며, 선행 `nanoid@3.3.18` 보안 수정과 explicit `npm ci --legacy-peer-deps=false --install-links=false` 계약을 보존한다. package-manager/toolchain·install-script authority·vulnerability audit·review/merge authority는 별도 증거 계층으로 유지한다.