From f6361246975872f348484e445d02a00cf805bcf1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 21:04:29 +0900 Subject: [PATCH 1/5] test(agent-runtime): bind rejection outcome into evaluator envelope --- test/procedural-evaluation-authority.test.mjs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/procedural-evaluation-authority.test.mjs b/test/procedural-evaluation-authority.test.mjs index 1d05b2640..79bd0d9f9 100644 --- a/test/procedural-evaluation-authority.test.mjs +++ b/test/procedural-evaluation-authority.test.mjs @@ -10,7 +10,7 @@ import { const digest = (character) => character.repeat(64); -async function screenedDecision() { +async function screenedDecision(rejectedKeys = []) { const baseline = await createProceduralGraph({ schemaVersion: "noema.procedural-graph/v1", tenantId: "tenant-a", @@ -71,7 +71,7 @@ async function screenedDecision() { { caseId: "case-2", score: 0.8, safetyViolations: 0 }, ], }, - rejectedKeys: [], + rejectedKeys, }); } @@ -106,6 +106,28 @@ test("admits only an exact evaluation envelope digest from the trusted handoff", assert.equal(admitted.activationAuthorized, false); }); +test("binds rejection-history outcome into the authenticated evaluation envelope identity", async () => { + const eligible = await screenedDecision(); + const rejected = await screenedDecision([eligible.rejectionKey]); + const input = authorityInput(); + + assert.equal(eligible.eligibleForApproval, true); + assert.equal(eligible.reason, "validation_non_regression"); + assert.equal(rejected.eligibleForApproval, false); + assert.equal(rejected.reason, "previously_rejected"); + assert.equal(rejected.rejectionKey, eligible.rejectionKey); + assert.equal(rejected.candidateDigest, eligible.candidateDigest); + assert.equal(rejected.candidateReceiptDigest, eligible.candidateReceiptDigest); + + const eligibleDigest = await proceduralEvaluationEvidenceDigest(eligible, input); + const rejectedDigest = await proceduralEvaluationEvidenceDigest(rejected, input); + assert.notEqual(rejectedDigest, eligibleDigest); + await assert.rejects( + admitProceduralEvaluationEvidence(rejected, input, eligibleDigest), + /evaluation_evidence_digest_mismatch/, + ); +}); + test("rejects caller-created structural lookalikes", () => { assert.throws( () => assertProceduralEvaluationEvidence(Object.freeze({ From 6e9c18a654d170cb1faaabbdf510c70c34d45e6f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 21:07:09 +0900 Subject: [PATCH 2/5] fix(agent-runtime): bind rejection disposition into evaluation envelope --- .../procedural-evaluation-authority.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/agent-runtime/procedural-evaluation-authority.ts b/src/agent-runtime/procedural-evaluation-authority.ts index e65e8325f..4f790c2fd 100644 --- a/src/agent-runtime/procedural-evaluation-authority.ts +++ b/src/agent-runtime/procedural-evaluation-authority.ts @@ -14,7 +14,8 @@ import { /** * Process-local evidence authority created only after a locally admitted screening decision is bound * to an exact digest supplied through an already authenticated evaluator handoff. It carries opaque - * evaluator/profile identities for later State / Checkpoint retention without granting activation. + * evaluator/profile identities plus the exact rejection/disposition identity for later State / + * Checkpoint retention without granting activation. */ export interface ProceduralEvaluationEvidence { readonly schemaVersion: "noema.procedural-evaluation-authority/v1"; @@ -32,6 +33,8 @@ export interface ProceduralEvaluationEvidence { readonly contextDigest: string; readonly baselineReceiptDigest: string; readonly candidateReceiptDigest: string; + readonly rejectionKey: string; + readonly decisionReason: ProceduralCandidateDecision["reason"]; readonly envelopeDigest: string; readonly eligibleForApproval: boolean; readonly activationAuthorized: false; @@ -93,6 +96,9 @@ async function evidenceDigest( decision.contextDigest, decision.baselineReceiptDigest, decision.candidateReceiptDigest, + decision.rejectionKey, + decision.reason, + decision.eligibleForApproval, metadata.evaluatorId, metadata.evaluatorVersion, metadata.policyVersion, @@ -109,7 +115,9 @@ async function evidenceDigest( * Computes the exact local envelope identity that a trusted evaluator producer can authenticate out * of band. This digest is deliberately not an authentication result: callers must not treat knowing * or recomputing it as evaluator identity, approval, persistence, publication, or activation authority. - * @param decision Locally admitted screening result whose exact graph and paired receipt identities bind the envelope. + * The identity includes deterministic rejection history and screening disposition so the same graph + * and receipt tuple cannot be authenticated once and replayed with a different approval eligibility. + * @param decision Locally admitted screening result whose exact graph, receipts, rejection key, and disposition bind the envelope. * @param input Exact-key evaluator/profile metadata whose opaque digests identify the registered evaluation conditions. * @returns Lowercase SHA-256 identity of the complete local procedural evaluation evidence envelope. */ @@ -152,6 +160,8 @@ export async function admitProceduralEvaluationEvidence( contextDigest: decision.contextDigest, baselineReceiptDigest: decision.baselineReceiptDigest, candidateReceiptDigest: decision.candidateReceiptDigest, + rejectionKey: decision.rejectionKey, + decisionReason: decision.reason, envelopeDigest: observed, eligibleForApproval: decision.eligibleForApproval, activationAuthorized: false as const, From a047df0634470c113664bde10f7b66a19606eab4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 21:13:22 +0900 Subject: [PATCH 3/5] docs(changelog): record procedural disposition binding repair --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1d8125a0..b155a97e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Add a Noema-owned exact-claim evidence receipt contract whose execution and research producers serialize one canonical artifact that binds every receipt semantic field, including command/result/isolation/network or source revision/excerpt/retrieval policy. Admission accepts only a receipt ID from untrusted model output. The owner API first verifies the exact authenticated OpenCode-handoff manifest digest, canonical envelope bytes, reviewed producer-to-kind policy, and repository/head/workflow/run/attempt identity before it can construct an immutable typed index; admission then reconstructs each canonical artifact and verifies time/claim/artifact identity. The version-2 manifest now binds a separate producer-authenticated `ClaimEvidenceRequirement` containing the exact claim, independently required evidence kind, and `context` or `finding` publication authority. Raw current-head source lines are context only: they are withheld from finding-reference prompts and cannot publish a finding or `request_changes`; an explicitly producer-authorized source finding remains usable and retains exact path/line checks. Finding-free model `request_changes` and `blocked` verdicts cannot bypass receipt admission to publish a vacuous blocking review. Requirement/receipt kind mismatch, fixed-artifact semantic substitution, caller-supplied receipt dictionaries, model self-classification, stale identities, cross-kind receipts, marker-only sandbox output, noncanonical artifact bytes, and expired receipts fail closed before the GitHub publisher. This remains the owner prerequisite for ContextualWisdomLab/.github#1641 and issue #555. The reviewed `sandboxed_verify` adapter exists in owner source, but its actual central stdout/stderr/marker-to-manifest wiring and the trusted research producer are not yet integrated; exact-head hosted GREEN, immutable release, and the verified central consumer bump remain required. ## Unreleased +- Agent Runtime의 procedural evaluation envelope이 baseline/candidate/context/paired receipt identity뿐 아니라 deterministic `rejectionKey`, screening reason, `eligibleForApproval`까지 함께 해시한다. 동일 graph/evaluation evidence라도 durable rejection history에 따라 `validation_non_regression`과 `previously_rejected` 처분이 달라지면 envelope identity도 달라져, 하나의 signed evaluator handoff를 다른 approval-eligibility 결과에 재사용할 수 없다. 반환 evidence에도 rejection/disposition identity를 보존하며 `activationAuthorized:false`, Keyverse key custody, State / Checkpoint persistence, Policy / Approval, publication 경계는 그대로 유지한다. issue #584, PR #596. - Agent Runtime이 separately authenticated evaluator handoff의 P-256 ECDSA 서명을 composition root가 공급한 trusted public key/key id로 검증하고 exact evaluation envelope digest와 최대 300초 validity window를 결합한다. 검증된 evidence도 signed expiry 이후에는 process-local authority로 재사용할 수 없으며 structural copy·wrong key/signer/envelope·expired/future handoff는 실패-폐쇄한다. Keyverse/owner가 signer identity·key custody를 유지하고 Noema는 private key·credential discovery·provider routing·durable State / Checkpoint·Policy / Approval·publication·activation authority를 소유하지 않는다. issue #584, PR #594. - Agent Runtime의 screened procedural decision과 evaluator/profile 조건을 하나의 canonical envelope digest에 결합하고, separately authenticated evaluator handoff에서 받은 exact digest와 일치할 때만 process-local evaluation evidence로 admit한다. 이 adapter는 binding만 검증하며 digest 재계산 자체를 evaluator authentication으로 취급하지 않고, Keyverse/owner signer trust·credential·provider routing·durable State / Checkpoint·Policy / Approval·publication·activation authority를 소유하지 않는다. 모든 evidence는 `activationAuthorized:false`를 유지한다. issue #584, PR #593. - Agent Runtime의 procedural candidate screening decision을 process-local provenance로 제한한다. `assessProceduralCandidate()`가 기존 lineage·held-out·safety·score 검증을 마친 결과만 locally admitted decision으로 발행하고, State / Checkpoint나 Policy / Approval 경계가 구조만 복사·직렬화·프록시·직접 생성한 lookalike를 `assertProceduralCandidateDecision()`으로 실패-폐쇄한다. 이 brand는 evaluator authentication·persistence·approval·activation authority가 아니며 모든 decision의 `activationAuthorized:false`는 유지된다. issue #584, PR #591. @@ -65,7 +66,7 @@ - credential-bearing GitHub App REST 요청의 egress를 exact `https://api.github.com` origin으로 고정. 새 Worker entrypoint가 `/exchange` 전에 `GITHUB_API_BASE`의 scheme·origin·userinfo·port·path·query·fragment를 검증하고, lookalike/malformed 설정은 rate-limit·OIDC parsing·private-key 사용·GitHub API 호출 전에 `503 ERR_GITHUB_API`로 실패-폐쇄하며 허용 값도 canonical origin으로 치환한다. `/health`는 설정 복구 중에도 유지하고 원본 설정값은 응답·로그에 노출하지 않는다. - `src/**/*.ts` 전체에 statements·branches·functions·lines 100% coverage threshold를 강제하고, `/exchange` wrapper·OIDC replay guard·distributed limiter의 fail-closed 및 malformed-decision 경계를 회귀 테스트로 고정했다. 새 source branch가 coverage를 낮추면 CI가 즉시 실패한다. - `/exchange` distributed rate-limit identity가 없는 요청을 shared `unknown` bucket으로 합치지 않고 `503`으로 실패-폐쇄하도록 강화. Cloudflare의 `CF-Connecting-IP`가 정확히 하나의 유효한 IPv4/IPv6가 아니면 Durable Object lookup과 bearer parsing 전에 중단하고, 유효한 IPv6는 canonical form으로 정규화하여 동일 주소의 표기 차이가 rate-limit bucket을 분할하지 않도록 한다. -- CI 검증 중 공개된 `undici` 취약점 묶음(GHSA-4cwx-7wf7-3272 포함)을 제거하기 위해 Wrangler→Miniflare 경유 transitive dependency를 patched `7.29.0`으로 override하고 lockfile을 재생성했다. `npm audit --audit-level=high`가 0건으로 복구하고 release gate가 취약 버전에서 실패-폐쇄하도록 유지한다. +- CI 검증 중 공개된 `undici` 취약점 묶음(GHSA-4cwx-7wf7-3272 포함)을 제거하기 위해 Wrangler→Miniflare 경유 transitive dependency를 patched `7.29.0`으로 override하고 lockfile을 재생성했다. `npm audit --audit-level=high`가 0건으로 복구하여 매일 실패하던 `readiness-audit` 스케줄 및 `release:verify` 게이트를 복구. - EOL 상태인 Node.js 20을 배포 계약에서 제거하고 `engines.node >=22` 및 배포 가이드의 지원 중 LTS 요구사항을 일치시켰다. - SQLite-backed OIDC replay guard의 alarm cleanup을 current-claim-aware 방식으로 강화. Cloudflare alarm의 at-least-once·지연·재시도 실행이 만료 후 교체된 활성 `jti` claim을 삭제하지 않도록 저장된 현재 expiry를 transactionally 재검증하고, 활성 claim이면 해당 만료 시각과 grace period로 reschedule하며 expired/empty storage만 삭제한다. - SQLite-backed `/exchange` rate limiter의 alarm cleanup을 current-window-aware 방식으로 강화. Cloudflare alarm의 지연·재시도 실행이 새 60초 window의 활성 bucket을 삭제해 요청 예산을 조기 재개하지 않도록 저장된 window deadline을 transactionally 재검증하고, 아직 활성인 경우 실제 reset 시각으로 reschedule하며 expired/empty storage만 삭제한다. From 609ffec78d270cf84a244e97ebdb314aff891f72 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 21:16:12 +0900 Subject: [PATCH 4/5] fix(changelog): preserve existing undici release-gate wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b155a97e9..796f84cb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -66,7 +66,7 @@ - credential-bearing GitHub App REST 요청의 egress를 exact `https://api.github.com` origin으로 고정. 새 Worker entrypoint가 `/exchange` 전에 `GITHUB_API_BASE`의 scheme·origin·userinfo·port·path·query·fragment를 검증하고, lookalike/malformed 설정은 rate-limit·OIDC parsing·private-key 사용·GitHub API 호출 전에 `503 ERR_GITHUB_API`로 실패-폐쇄하며 허용 값도 canonical origin으로 치환한다. `/health`는 설정 복구 중에도 유지하고 원본 설정값은 응답·로그에 노출하지 않는다. - `src/**/*.ts` 전체에 statements·branches·functions·lines 100% coverage threshold를 강제하고, `/exchange` wrapper·OIDC replay guard·distributed limiter의 fail-closed 및 malformed-decision 경계를 회귀 테스트로 고정했다. 새 source branch가 coverage를 낮추면 CI가 즉시 실패한다. - `/exchange` distributed rate-limit identity가 없는 요청을 shared `unknown` bucket으로 합치지 않고 `503`으로 실패-폐쇄하도록 강화. Cloudflare의 `CF-Connecting-IP`가 정확히 하나의 유효한 IPv4/IPv6가 아니면 Durable Object lookup과 bearer parsing 전에 중단하고, 유효한 IPv6는 canonical form으로 정규화하여 동일 주소의 표기 차이가 rate-limit bucket을 분할하지 않도록 한다. -- CI 검증 중 공개된 `undici` 취약점 묶음(GHSA-4cwx-7wf7-3272 포함)을 제거하기 위해 Wrangler→Miniflare 경유 transitive dependency를 patched `7.29.0`으로 override하고 lockfile을 재생성했다. `npm audit --audit-level=high`가 0건으로 복구하여 매일 실패하던 `readiness-audit` 스케줄 및 `release:verify` 게이트를 복구. +- CI 검증 중 공개된 `undici` 취약점 묶음(GHSA-4cwx-7wf7-3272 포함)을 제거하기 위해 Wrangler→Miniflare 경유 transitive dependency를 patched `7.29.0`으로 override하고 lockfile을 재생성했다. `npm audit --audit-level=high`가 0건으로 복구하고 release gate가 취약 버전에서 실패-폐쇄하도록 유지한다. - EOL 상태인 Node.js 20을 배포 계약에서 제거하고 `engines.node >=22` 및 배포 가이드의 지원 중 LTS 요구사항을 일치시켰다. - SQLite-backed OIDC replay guard의 alarm cleanup을 current-claim-aware 방식으로 강화. Cloudflare alarm의 at-least-once·지연·재시도 실행이 만료 후 교체된 활성 `jti` claim을 삭제하지 않도록 저장된 현재 expiry를 transactionally 재검증하고, 활성 claim이면 해당 만료 시각과 grace period로 reschedule하며 expired/empty storage만 삭제한다. - SQLite-backed `/exchange` rate limiter의 alarm cleanup을 current-window-aware 방식으로 강화. Cloudflare alarm의 지연·재시도 실행이 새 60초 window의 활성 bucket을 삭제해 요청 예산을 조기 재개하지 않도록 저장된 window deadline을 transactionally 재검증하고, 아직 활성인 경우 실제 reset 시각으로 reschedule하며 expired/empty storage만 삭제한다. From 99f4505a1f93f90badc61c953dcaee2266a2ced5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 10 Sep 2026 21:16:44 +0900 Subject: [PATCH 5/5] test(agent-runtime): assert retained rejection disposition identity --- test/procedural-evaluation-authority.test.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/procedural-evaluation-authority.test.mjs b/test/procedural-evaluation-authority.test.mjs index 79bd0d9f9..f878a30e2 100644 --- a/test/procedural-evaluation-authority.test.mjs +++ b/test/procedural-evaluation-authority.test.mjs @@ -103,6 +103,9 @@ test("admits only an exact evaluation envelope digest from the trusted handoff", assert.equal(admitted.candidateDigest, decision.candidateDigest); assert.equal(admitted.baselineReceiptDigest, decision.baselineReceiptDigest); assert.equal(admitted.candidateReceiptDigest, decision.candidateReceiptDigest); + assert.equal(admitted.rejectionKey, decision.rejectionKey); + assert.equal(admitted.decisionReason, decision.reason); + assert.equal(admitted.eligibleForApproval, decision.eligibleForApproval); assert.equal(admitted.activationAuthorized, false); });