Skip to content
Closed
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
199 changes: 199 additions & 0 deletions .github/workflows/maintainer-app-readiness.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
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: artifacts/governance/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: artifacts/governance/main-governance-audit.json
NOEMA_MAINTAINER_READINESS_PATH: artifacts/operations/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="artifacts/operations/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: process.env.GITHUB_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
reasonCode="maintainer_token_unavailable"
write_failure_report \
"$reasonCode" \
"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
reasonCode="commercial_loop_failed"
write_failure_report \
"$reasonCode" \
"Commercial-readiness dry run failed before it could retain its bounded report."
fi
exit "$loop_status"

- name: upload main governance evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: main-governance-audit
path: artifacts/governance/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: artifacts/operations/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: artifacts/operations/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 }}
run: |
set -euo pipefail
failed=0
for gate in MAINTAINER_APP_OUTCOME REVIEWER_APP_OUTCOME GOVERNANCE_OUTCOME READINESS_OUTCOME DRY_RUN_OUTCOME; do
value="${!gate}"
if [ "$value" != "success" ]; then
printf '::error::%s was %s, not success.\n' "$gate" "$value"
failed=1
fi
done
exit "$failed"
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 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에 노출하지 않는다. 이 증빙은 해당 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`로 중단한다.
Expand Down
90 changes: 90 additions & 0 deletions docs/maintainer-app-readiness-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Maintainer App pre-activation audit

## Purpose

Noema's hourly commercial-readiness loop can dispatch exact-head reviews and perform SHA-bound merges only through a dedicated Maintainer GitHub App. The write path remains intentionally disabled until operators can prove that the effective Maintainer token, authenticated Reviewer App identity, and live `main` governance satisfy the repository's fail-closed policy. In this document, **effective installation token** means the repository-scoped token actually minted and exercised during the audited workflow run.

The default-branch-only `.github/workflows/maintainer-app-readiness.yml` workflow produces that machine-readable preflight evidence without dispatching a review, merging a pull request, changing repository configuration, or enabling `NOEMA_MAINTENANCE_ENABLED`.

## Trigger

The workflow accepts only the `maintainer-app-readiness` `repository_dispatch` event. GitHub evaluates `repository_dispatch` from the default branch, so a caller cannot select workflow code from an unreviewed branch. Checkout is pinned to `github.sha`, the event-bound default-branch commit, rather than re-resolving the moving branch name after the run starts.

```bash
gh api repos/ContextualWisdomLab/noema/dispatches \
--method POST \
--input - <<'JSON'
{"event_type":"maintainer-app-readiness"}
JSON
```

## Required configuration

Configure these repository values before dispatching the preflight:

- variable `NOEMA_MAINTAINER_APP_CLIENT_ID`;
- secret `NOEMA_MAINTAINER_APP_PRIVATE_KEY`;
- variable `NOEMA_GITHUB_APP_CLIENT_ID` for the existing Reviewer App;
- secret `NOEMA_GITHUB_APP_PRIVATE_KEY` for the existing Reviewer App;
- variable `NOEMA_REVIEWER_LOGIN`, including the exact `[bot]` suffix.

Leave `NOEMA_MAINTENANCE_ENABLED` unset or different from `true` until the preflight, independently reviewed App registration evidence, and an approved activation run all pass. The preflight verifies this state and fails with `maintenance_already_enabled` if the write path has been enabled prematurely.

The workflow creates two separately scoped installation tokens:

1. The **Maintainer App token** is scoped to `ContextualWisdomLab/noema` with only the effective permissions needed by the commercial loop: Actions read, Checks read, Contents write, Metadata read, Pull requests write, and Commit statuses read. It is the only token exposed to governance, API-probe, and no-write dry-run scripts as `GH_TOKEN`.
2. The **Reviewer App identity token** is scoped to the same repository with Metadata read only. Its token value is never passed to a script. Only the pinned action's authenticated `app-slug` and `installation-id` outputs are supplied to the readiness evaluator so the configured reviewer login can be bound to the actual Reviewer App credentials.

The job-level `GITHUB_TOKEN` remains Contents read and is not a write fallback. The audited commands use only Node.js built-ins and repository scripts, so the privileged job does not run `npm ci`, install repository dependencies, execute lifecycle scripts, or perform a package-manager audit.

Both token-mint steps are allowed to continue only so the workflow can generate bounded failure evidence. Their exact outcomes remain mandatory inputs to the final gate. A failed Maintainer mint produces governance and readiness failure reports plus a fixed, no-network `maintainer_token_unavailable` dry-run artifact; it can never be interpreted as a successful preflight.

## Enforced checks

`npm run operations:preflight` fails closed unless all of the following are true:

1. automated maintenance remains disabled during the pre-activation audit;
2. the Maintainer token action reports a positive installation identifier and a valid Maintainer App slug;
3. the observed Maintainer bot login exactly equals `<maintainer-app-slug>[bot]` and is a GitHub `Bot`;
4. the Reviewer token action reports a positive installation identifier and valid Reviewer App slug;
5. `NOEMA_REVIEWER_LOGIN` exactly equals `<reviewer-app-slug>[bot]`, and the public user lookup returns that same GitHub `Bot` login;
6. the Maintainer and reviewer bot identities differ;
7. the effective Maintainer token can enumerate exactly one repository, `ContextualWisdomLab/noema`;
8. repository metadata reports read and scoped write access but not administrator access;
9. Actions, checks, commit statuses, pull requests, and contents read probes all succeed;
10. the retained live governance report is bound to `ContextualWisdomLab/noema`, branch `main`, and status `PASS`;
11. the existing commercial-readiness loop completes without `--apply`;
12. both pinned App token actions themselves complete successfully.

A configured bot account by itself is not sufficient reviewer authentication. A mismatch between `NOEMA_REVIEWER_LOGIN` and the authenticated Reviewer App slug fails with `reviewer_app_login_mismatch`; a missing or malformed Reviewer App installation identity fails with `reviewer_installation_id_invalid` or `reviewer_app_slug_invalid`.

The Maintainer installation repository endpoint is fully paginated at 100 records per page. The collector retains only bounded identity, scope, permission, probe, governance, activation-state, App-binding, and commit-binding fields. If scope is broader than the expected repository, the report retains only the effective repository count and does not persist unexpected repository names. It does not retain API response bodies, access tokens, private keys, authorization headers, or secret-derived fingerprints. GitHub CLI subprocesses are shell-free, output-bounded, pinned to `github.com`, and terminated after 20 seconds so a stalled upstream cannot consume the entire preflight window.

The `permissions.admin` value must be explicitly present as `false`; a missing or non-boolean value remains unknown and fails closed rather than being coerced into evidence of least privilege.

The public `GET /users/{username}` response is used only to confirm the bounded login and GitHub `Bot` account type after the configured login has already been bound to the authenticated Reviewer App slug. GitHub does not document installation `suspended_at` state in that user-profile schema, so the preflight does not infer an App installation's suspension state from a missing user field. Successful token minting and the required API probes establish that the scoped tokens are operational for the audited run; the complete installation records and suspension states remain separate administrator evidence.

## Evidence artifacts

Every run attempts to retain these artifacts for 90 days, including token-mint and policy failures after checkout:

- `main-governance-audit`;
- `maintainer-app-readiness`;
- `commercial-readiness-loop-dry-run`.

The primary JSON report is `artifacts/operations/maintainer-app-readiness.json`. It records the Maintainer App slug and installation identifier, Reviewer App slug and installation identifier, configured reviewer login, effective Maintainer repository count and exact expected scope when valid, coarse permissions, API probes, governance binding, and stable pass/failure codes. Missing artifact files are themselves workflow errors; a green preflight cannot omit its machine-readable evidence.

## Evidence boundary

A passing report proves the effective Maintainer installation token minted for that workflow run, the live APIs exercised with it, and that the configured reviewer bot login matches the authenticated Reviewer App slug and positive installation identifier produced from the configured Reviewer App credentials. It does **not** prove the complete underlying GitHub App registration for either App, all repositories available to either installation before token scoping, installation suspension state, private-key ownership and rotation, administrator bypass policy, or break-glass ownership. Those facts remain independently reviewed operational evidence under issue #29 and issue #27.

A failed report must never be converted into an activation approval by weakening permissions, removing probes, substituting `GITHUB_TOKEN`, trusting an arbitrary bot login, exposing the Reviewer token to scripts, enabling maintenance before approval, or bypassing governance. Correct the external configuration and rerun the default-branch workflow.

## Authoritative references

- GitHub Actions `repository_dispatch`: <https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#repository_dispatch>
- GitHub App token action: <https://github.com/actions/create-github-app-token>
- Public user lookup schema: <https://docs.github.com/en/rest/users/users#get-a-user>
- Installation repository enumeration: <https://docs.github.com/en/rest/apps/installations#list-repositories-accessible-to-the-app-installation>
- Installation token permissions and repository scoping: <https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app>
- Installation record and suspension fields: <https://docs.github.com/en/rest/apps/apps#get-an-installation-for-the-authenticated-app>
Loading
Loading