Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e91c0ea
feat(operations): add default-branch Maintainer App preflight
seonghobae Aug 4, 2026
0f7fe63
test(operations): retain dry-run execution failures
seonghobae Aug 4, 2026
f1a7d7f
fix(operations): retain dry-run execution failures
seonghobae Aug 4, 2026
63b9c3a
test(operations): require normalized dry-run evidence
seonghobae Aug 4, 2026
6a35034
fix(operations): normalize bounded dry-run evidence
seonghobae Aug 4, 2026
53c9fe6
docs(operations): document dry-run evidence normalization
seonghobae Aug 4, 2026
a218911
test(operations): define bounded evidence normalization
seonghobae Aug 4, 2026
feac2f1
feat(operations): normalize bounded dry-run evidence
seonghobae Aug 4, 2026
5265b51
refactor(test): verify reusable evidence normalizer
seonghobae Aug 4, 2026
98dd215
refactor(operations): reuse tested evidence normalizer
seonghobae Aug 4, 2026
e96333f
fix(operations): atomically replace unsafe evidence
seonghobae Aug 4, 2026
5e38505
test(operations): reject noncanonical and symlinked evidence writes
seonghobae Aug 4, 2026
0896594
fix(operations): reject ambiguous timestamps and temp symlinks
seonghobae Aug 4, 2026
c522e04
docs(operations): document normalized readiness evidence
seonghobae Aug 4, 2026
b57c38e
test(ci): require 100 percent production coverage execution
seonghobae Aug 4, 2026
25e5c4f
docs(changelog): record normalized preflight evidence
seonghobae Aug 4, 2026
df1e375
ci(coverage): execute 100 percent production coverage gate
seonghobae Aug 4, 2026
b6ab625
ci(coverage): include evidence normalizer in production gate
seonghobae Aug 4, 2026
6a98fed
test(ci): require exact-PR workflow concurrency
seonghobae Aug 4, 2026
29f9ee4
test(ci): require immutable pull-request workflow sources
seonghobae Aug 4, 2026
10d10a7
ci(workflows): cancel stale exact-PR verification runs
seonghobae Aug 4, 2026
52214a1
ci(workflows): cancel stale exact-PR reviewer runs
seonghobae Aug 4, 2026
301cc7e
test(operations): cover the complete evidence trust boundary
seonghobae Aug 4, 2026
e6bb86d
fix(operations): close evidence descriptor race and coverage gaps
seonghobae Aug 4, 2026
382310d
docs(operations): document exact evidence and CI trust boundaries
seonghobae Aug 4, 2026
a3b936a
test(operations): require runner-isolated preflight evidence
seonghobae Aug 4, 2026
3ebccfc
fix(operations): isolate preflight evidence from checkout
seonghobae Aug 4, 2026
6b789b3
fix(test): escape runner context literals
seonghobae Aug 4, 2026
14e60ee
docs(operations): document runner-isolated evidence paths
seonghobae Aug 4, 2026
fb69035
test(operations): reject duplicate JSON evidence keys
seonghobae Aug 4, 2026
609ddc0
fix(operations): reject ambiguous duplicate JSON keys
seonghobae Aug 4, 2026
1ef7d76
docs(operations): document duplicate-key evidence rejection
seonghobae Aug 4, 2026
5351cb8
docs(changelog): record bounded JSON ambiguity rejection
seonghobae Aug 4, 2026
fdd5bec
test(security): reject malformed UTF-8 evidence
seonghobae Aug 4, 2026
8d5f418
test(operations): reject malformed UTF-8 evidence
seonghobae Aug 4, 2026
0488187
fix(security): fail closed on malformed UTF-8 evidence
seonghobae Aug 4, 2026
1e117cd
docs(doctoring): record fatal UTF-8 evidence boundary
seonghobae Aug 4, 2026
8ad7410
test(security): consolidate UTF-8 evidence regression
seonghobae Aug 4, 2026
157d51c
test(security): preserve allowlisted international evidence
seonghobae Aug 4, 2026
622e7ab
test(performance): forbid quadratic primitive suffix copies
seonghobae Aug 4, 2026
13adf12
test(operations): require stable failure-report identity
seonghobae Aug 4, 2026
f590c03
fix(performance): scan JSON primitives without suffix copies
seonghobae Aug 4, 2026
39fd987
fix(workflows): preserve bounded failure reason evidence
seonghobae Aug 4, 2026
6decdc9
test(coverage): exercise URL parser fail-closed branches
seonghobae Aug 4, 2026
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
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
branches:
- main

concurrency:
group: noema-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
verify:
name: verify
Expand All @@ -15,10 +19,12 @@ jobs:
contents: read
steps:
- name: checkout
uses: actions/checkout@v4
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

- name: setup node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"
cache: npm
Expand Down
206 changes: 206 additions & 0 deletions .github/workflows/maintainer-app-readiness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
name: maintainer-app-readiness
run-name: Noema Maintainer App pre-activation audit

on:
# repository_dispatch is evaluated only from the default branch. A caller
# cannot select unreviewed workflow code for these privileged token mints.
repository_dispatch:
types: [maintainer-app-readiness]

concurrency:
group: noema-maintainer-app-readiness
cancel-in-progress: true

# GITHUB_TOKEN is used only by the trusted checkout. Audit API calls use the
# repository-scoped Maintainer token; the Reviewer token is never exposed to a
# script and supplies only authenticated App slug/installation identity output.
permissions:
contents: read

jobs:
preflight:
name: maintainer-app-pre-activation-audit
runs-on: ubuntu-latest
timeout-minutes: 30
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
steps:
- name: checkout event-bound default-branch commit
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
# For repository_dispatch, github.sha is the default-branch commit
# bound when the event was created. Do not re-resolve a moving branch.
ref: ${{ github.sha }}
persist-credentials: false

- name: mint repository-scoped Maintainer App token
id: maintainer_app
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.NOEMA_MAINTAINER_APP_CLIENT_ID }}
private-key: ${{ secrets.NOEMA_MAINTAINER_APP_PRIVATE_KEY }}
owner: ContextualWisdomLab
repositories: noema
permission-actions: read
permission-checks: read
permission-contents: write
permission-metadata: read
permission-pull-requests: write
permission-statuses: read

- name: mint repository-scoped Reviewer App identity token
id: reviewer_app
continue-on-error: true
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.NOEMA_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.NOEMA_GITHUB_APP_PRIVATE_KEY }}
owner: ContextualWisdomLab
repositories: noema
permission-metadata: read

- name: setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: "24"

- name: audit active main governance
id: governance
continue-on-error: true
env:
GH_TOKEN: ${{ steps.maintainer_app.outputs.token }}
NOEMA_GOVERNANCE_AUDIT_PATH: ${{ runner.temp }}/noema-maintainer-app-readiness/main-governance-audit.json
run: node scripts/main-governance-audit.mjs

- name: audit effective Maintainer App identity and access
id: readiness
if: always()
continue-on-error: true
env:
GH_TOKEN: ${{ steps.maintainer_app.outputs.token }}
NOEMA_MAINTAINER_APP_SLUG: ${{ steps.maintainer_app.outputs.app-slug }}
NOEMA_MAINTAINER_INSTALLATION_ID: ${{ steps.maintainer_app.outputs.installation-id }}
NOEMA_REVIEWER_APP_SLUG: ${{ steps.reviewer_app.outputs.app-slug }}
NOEMA_REVIEWER_INSTALLATION_ID: ${{ steps.reviewer_app.outputs.installation-id }}
NOEMA_REVIEWER_LOGIN: ${{ vars.NOEMA_REVIEWER_LOGIN }}
NOEMA_MAINTENANCE_ENABLED: ${{ vars.NOEMA_MAINTENANCE_ENABLED }}
NOEMA_GOVERNANCE_AUDIT_PATH: ${{ runner.temp }}/noema-maintainer-app-readiness/main-governance-audit.json
NOEMA_MAINTAINER_READINESS_PATH: ${{ runner.temp }}/noema-maintainer-app-readiness/maintainer-app-readiness.json
run: node scripts/maintainer-app-readiness.mjs

- name: inspect commercial-readiness loop without writes
id: dry_run
if: always()
continue-on-error: true
env:
GH_TOKEN: ${{ steps.maintainer_app.outputs.token }}
NOEMA_REVIEWER_LOGIN: ${{ vars.NOEMA_REVIEWER_LOGIN }}
MAINTAINER_APP_OUTCOME: ${{ steps.maintainer_app.outcome }}
run: |
set -euo pipefail
report_path="$RUNNER_TEMP/noema-maintainer-app-readiness/commercial-readiness-loop-dry-run.json"
write_failure_report() {
local reasonCode="$1"
local reasonDetail="$2"
FAILURE_CODE="$reasonCode" FAILURE_DETAIL="$reasonDetail" REPORT_PATH="$report_path" \
node --input-type=module <<'NODE'
import { mkdirSync, writeFileSync } from "node:fs";
import { dirname } from "node:path";

const reportPath = process.env.REPORT_PATH;
const reasonCode = process.env.FAILURE_CODE;
const reasonDetail = process.env.FAILURE_DETAIL;
const report = {
schemaVersion: 1,
repository: "ContextualWisdomLab/noema",
generatedAt: new Date().toISOString(),
apply: false,
openPullRequestCount: null,
remainingOpenPullRequestCount: null,
results: [
{
number: null,
result: "operational_error",
reasons: [{ code: reasonCode, detail: reasonDetail }],
},
],
};
mkdirSync(dirname(reportPath), { recursive: true });
writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
NODE
}

if [ "$MAINTAINER_APP_OUTCOME" != "success" ]; then
write_failure_report \
"maintainer_token_unavailable" \
"Maintainer App token mint did not succeed; no GitHub API requests were attempted."
exit 1
fi

set +e
node scripts/hourly-commercial-readiness.mjs --report "$report_path"
loop_status=$?
set -e
if [ "$loop_status" -ne 0 ] && [ ! -s "$report_path" ]; then
write_failure_report \
"commercial_loop_failed" \
"Commercial-readiness dry run failed before it could retain its bounded report."
fi
exit "$loop_status"

- name: normalize bounded commercial-loop evidence
id: dry_run_evidence
if: always()
continue-on-error: true
env:
REPORT_PATH: ${{ runner.temp }}/noema-maintainer-app-readiness/commercial-readiness-loop-dry-run.json
run: node scripts/normalize-commercial-readiness-evidence.mjs

- name: upload main governance evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: main-governance-audit
path: ${{ runner.temp }}/noema-maintainer-app-readiness/main-governance-audit.json
if-no-files-found: error
retention-days: 90

- name: upload Maintainer App readiness evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: maintainer-app-readiness
path: ${{ runner.temp }}/noema-maintainer-app-readiness/maintainer-app-readiness.json
if-no-files-found: error
retention-days: 90

- name: upload no-write commercial loop evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: commercial-readiness-loop-dry-run
path: ${{ runner.temp }}/noema-maintainer-app-readiness/commercial-readiness-loop-dry-run.json
if-no-files-found: error
retention-days: 90

- name: enforce pre-activation gates
if: always()
env:
MAINTAINER_APP_OUTCOME: ${{ steps.maintainer_app.outcome }}
REVIEWER_APP_OUTCOME: ${{ steps.reviewer_app.outcome }}
GOVERNANCE_OUTCOME: ${{ steps.governance.outcome }}
READINESS_OUTCOME: ${{ steps.readiness.outcome }}
DRY_RUN_OUTCOME: ${{ steps.dry_run.outcome }}
DRY_RUN_EVIDENCE_OUTCOME: ${{ steps.dry_run_evidence.outcome }}
run: |
set -euo pipefail
failed=0
for gate in MAINTAINER_APP_OUTCOME REVIEWER_APP_OUTCOME GOVERNANCE_OUTCOME READINESS_OUTCOME DRY_RUN_OUTCOME DRY_RUN_EVIDENCE_OUTCOME; do
value="${!gate}"
if [ "$value" != "success" ]; then
printf '::error::%s was %s, not success.\n' "$gate" "$value"
failed=1
fi
done
exit "$failed"
4 changes: 4 additions & 0 deletions .github/workflows/reviewer-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ on:
branches:
- main

concurrency:
group: noema-reviewer-ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

permissions:
contents: read

Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# Changelog

## Unreleased
- 전용 Maintainer GitHub App 활성화 전에 effective token을 기계적으로 감사하는 default-branch-only `maintainer-app-readiness` workflow와 `operations:preflight`를 추가. exact Maintainer/reviewer bot identity 분리, 단일 `ContextualWisdomLab/noema` repository scope, required Actions/checks/statuses/PR/contents read probes, live `main` governance PASS, no-write commercial-loop dry run을 실패-폐쇄 검증하고 bounded JSON evidence를 90일 보존한다. 별도의 Metadata-read Reviewer App token에서 인증된 `app-slug`·`installation-id`를 받아 `NOEMA_REVIEWER_LOGIN`이 실제 Reviewer App의 `<slug>[bot]` identity와 일치하는지 검증하며 Reviewer token 값은 script에 노출하지 않는다. 모든 retained source evidence는 checkout 밖 `${RUNNER_TEMP}`에 격리하고, dry-run evidence는 1 MiB·canonical UTC·exact repository·`apply=false` schema·decoded object-key uniqueness·최대 256단계 nesting으로 재검증한다. malformed/duplicate-key JSON, unknown fields, excessive nesting, symlink·descriptor swap·short read·예측 가능한 temporary-path 공격은 atomically replaced `dry_run_report_invalid` 증빙과 실패 gate로 처리한다. 이 증빙은 해당 run의 scoped token과 reviewer credential binding만 입증하며 complete App registration, key ownership, administrator bypass, break-glass ownership은 #29/#27의 독립 검토 대상으로 명시한다.
- credential-bearing GitHub API와 OIDC discovery/JWKS subrequest에 요청별 10초 deadline을 추가. `Request.signal`과 호출자 `RequestInit.signal`을 `AbortSignal.any()`로 보존하면서 독립 timeout signal을 결합하고, upstream stall은 bodyless `504 blocked-timeout` 정책 응답으로 실패-폐쇄하며 timer는 모든 종료 경로에서 정리한다.
- `/exchange`의 `application/json` request body를 UTF-8 wire bytes 기준 8,192 bytes로 제한. 신뢰 가능한 `Content-Length` 초과는 body read 전에 413으로 차단하고, 길이 헤더가 없거나 잘못된 요청도 stream을 bounded-read하여 chunked 우회를 막는다. 검증된 작은 body만 재구성해 downstream parser로 전달하며 OIDC/JWKS 조회·GitHub App private-key 사용·GitHub API 호출 전에 실패-폐쇄하고 body 원문은 응답·로그에 남기지 않는다.
- credential-bearing GitHub API와 OIDC subrequest에 `redirect: "manual"`을 강제하고, `3xx`/redirected response를 bodyless `502`로 치환하는 fail-closed egress wrapper를 추가. exact `api.github.com` origin과 pinned GitHub Actions discovery/JWKS endpoint 외 destination은 network call 전에 차단하며, wrapper 설치 실패나 runtime 교체 감지는 `/exchange` credential 처리 전에 `503 ERR_GITHUB_API`로 중단한다.
- 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 사용 전에 `503 ERR_GITHUB_API`로 실패-폐쇄하며 허용 값도 canonical origin으로 치환한다. `/health`는 설정 복구 중에도 유지하고 원본 설정값은 응답·로그에 노출하지 않는다.
- 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가 취약 버전에서 실패-폐쇄하도록 유지한다.
Expand Down
25 changes: 25 additions & 0 deletions docs/doctoring/commercial-readiness-evidence-utf8-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Commercial-readiness evidence UTF-8 boundary

## Decision

Noema treats the dry-run commercial-readiness artifact as an exact byte-level protocol input. The normalizer therefore decodes the bounded `Buffer` with a single reusable `TextDecoder("utf-8", { fatal: true })` before duplicate-key scanning or `JSON.parse`. A malformed UTF-8 sequence throws and is converted into the fixed `dry_run_report_invalid` report; replacement-character decoding is not accepted.

This preserves the distinction between the bytes produced by the no-write maintenance run and the canonical evidence retained for due diligence. Without fatal decoding, JavaScript replacement semantics could convert malformed wire bytes to U+FFFD, after which an invalid byte sequence located in an allowlist-dropped field could be silently accepted as valid evidence.

## Standards rationale

RFC 8259 requires JSON exchanged between systems outside a closed ecosystem to use UTF-8. The WHATWG Encoding Living Standard defines a fatal decoder mode that returns an error instead of inserting U+FFFD, and its `TextDecoder` API throws a `TypeError` when fatal decoding encounters an error. Node.js 24 exposes this WHATWG-compatible API globally and documents the same failure behavior.

The implementation does not claim formal conformance certification. It applies the interoperable UTF-8 and fail-closed decoding requirements that are relevant to this evidence boundary.

## Verification contract

The regression suite supplies malformed UTF-8 inside a syntactically valid, unknown JSON member. The expected result is fixed invalid evidence, proving that allowlist dropping cannot hide malformed source bytes. A companion case supplies valid Korean and punctuation text in an allowlisted reason detail and requires exact preservation, proving that the gate rejects malformed encoding rather than non-ASCII content.
Comment thread
seonghobae marked this conversation as resolved.

## APA 7th references

Bray, T. (2017). *The JavaScript Object Notation (JSON) data interchange format* (RFC 8259). Internet Engineering Task Force. https://doi.org/10.17487/RFC8259

Node.js contributors. (2026). *Util: Class TextDecoder* (Node.js v24.11.0 documentation). Retrieved August 4, 2026, from https://nodejs.org/download/release/v24.11.0/docs/api/util.html

WHATWG. (2026, May 21). *Encoding* (Living Standard). Retrieved August 4, 2026, from https://encoding.spec.whatwg.org/
Loading
Loading