Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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이 운영·인수 준비 증거를 제조하지 못하게 한다.
Expand All @@ -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는 별도 증거 계층으로 유지한다.
Expand Down
17 changes: 12 additions & 5 deletions scripts/lib/pilot-readiness.mjs
Original file line number Diff line number Diff line change
@@ -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";
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
Comment thread
seonghobae marked this conversation as resolved.
Resolved
}

function metricValue(entry, name) {
Expand Down Expand Up @@ -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");
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Resolved
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");
Expand Down
33 changes: 33 additions & 0 deletions test/pilot-readiness-future-date.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
Loading