Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9084727
test(operations): specify single-orphan live disablement
seonghobae Aug 16, 2026
867a09a
feat(operations): execute one verified orphan disablement
seonghobae Aug 16, 2026
8f72348
test(coverage): measure workflow disable operator
seonghobae Aug 16, 2026
298aa7c
Merge protected main into workflow registry operator
seonghobae Aug 16, 2026
4a35eba
test(coverage): exercise live disable operator boundaries
seonghobae Aug 16, 2026
86bad40
test(coverage): align malformed JSON assertion with fail-closed scanner
seonghobae Aug 16, 2026
5532160
Merge protected main into workflow registry operator
seonghobae Aug 16, 2026
ba0797d
test(operations): define deterministic live-disable CLI boundary
seonghobae Aug 16, 2026
f9b01e0
fix(operations): isolate live-disable CLI dispatch
seonghobae Aug 16, 2026
087aa75
fix(ops): remove unreachable workflow JSON parse branch
seonghobae Aug 16, 2026
528473c
test(ops): cover live disable executable path
seonghobae Aug 16, 2026
d6a8ca3
test(ops): exercise live-disable fail-closed branches
seonghobae Aug 16, 2026
5f787bd
test(ops): cover live-disable default and nullish boundaries
seonghobae Aug 16, 2026
17d250c
test(ops): exercise residual live-disable evidence branches
seonghobae Aug 16, 2026
6107d92
test(coverage): exercise absent post-audit workflow records
seonghobae Aug 16, 2026
73e6674
test(security): reject ambiguous privileged GitHub JSON
seonghobae Aug 16, 2026
aa55838
fix(operations): harden disablement JSON boundary
seonghobae Aug 16, 2026
da82dc7
fix(operations): preserve malformed JSON diagnostic
seonghobae Aug 16, 2026
d4f9756
test(security): reproduce fine-grained token diagnostic leak
seonghobae Aug 16, 2026
99d3f90
fix(security): redact fine-grained tokens from CLI failures
seonghobae Aug 16, 2026
bbdb3b3
fix(operations): remove unreachable JSON parse branch
seonghobae Aug 16, 2026
82d5ad8
test(operations): assert missing token capability failure
seonghobae Aug 16, 2026
004315e
merge: integrate protected main after #396
seonghobae Aug 16, 2026
15b642d
test(operations): pin bounded invalid-JSON diagnostic
seonghobae Aug 16, 2026
1a2dee6
fix(operations): normalize malformed GitHub JSON failures
seonghobae Aug 16, 2026
d09ac4b
merge: restack workflow disablement on protected main a634066
seonghobae Aug 16, 2026
1ef3fc7
test(operations): reject invalid disablement before credential read
seonghobae Aug 16, 2026
1648e46
fix(operations): validate disablement before credential read
seonghobae Aug 16, 2026
d1aff09
test(operations): exercise non-Error CLI failure contract
seonghobae Aug 16, 2026
8956df0
test(operations): prove absent fetch capability
seonghobae Aug 16, 2026
1e40cee
fix(operations): require honest post-disablement audit receipts
cursoragent Aug 16, 2026
0a15b9e
test(operations): close live-disable coverage at the receipt boundary
cursoragent Aug 16, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased
- 검증된 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를 성공으로 승격하지 않는다.
- 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에서 재검증되도록 유지한다.
Expand Down
32 changes: 32 additions & 0 deletions docs/doctoring/workflow-registry-disablement.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,36 @@ The transport is hard-bound to `ContextualWisdomLab/noema`, validates positive i

The delegated token is captured only in a closure used to construct the Authorization header. The returned transport object contains functions, not the token value, and diagnostics do not echo response bodies, raw transport exceptions, or credentials. Credential scope and provisioning remain operator responsibilities; this code does not invent, broaden, or fall back to another secret.

### Operator-callable single-candidate disablement

`scripts/workflow-registry-live-disable.mjs` is the only operator-callable mutation entrypoint. It does not create a repair workflow, does not accept a batch of workflow IDs, and does not disable anything during pull-request CI.

Invoke one exact audited orphan after provisioning a reviewed, owner-only delegated token file:

```bash
NOEMA_MAINTAINER_TOKEN_PATH=/secure/noema-maintainer.token \
GITHUB_REPOSITORY=ContextualWisdomLab/noema \
npm run operations:workflow-registry-disable -- 101
```

Equivalent direct invocation is `node scripts/workflow-registry-live-disable.mjs 101`. The numeric workflow ID is a required argv. `main()` validates `GITHUB_REPOSITORY` and that workflow ID before `readDelegatedGithubToken()` opens the token file. Ambient `GITHUB_TOKEN`, `COPILOT_GITHUB_TOKEN`, and process-environment secret materialization are refused; the delegated Actions-write capability stays in the reviewed file and a closure-private Authorization header.

Each invocation collects a full exact-main audit, immediately refreshes the raw registry, builds a process-local plan, revalidates protected `main` plus the exact workflow around one mutation, and then requires a second full audit before it prints a receipt and exits 0. The retained receipt is:

- `schema_version` (always `1`)
- `repository_full_name`
- `protected_main_sha`
- `workflow_id`
- `workflow_path`
- `prior_state`
- `final_state`
- `mutation`
- `post_audit_status` (`PASS` or `FAIL`)
- `remaining_failure_codes`
- `remaining_active_orphan_ids`

Exit 0 with `post_audit_status: "FAIL"` means this exact ID is now `disabled_manually` and classified `disabled_registry_record`; the registry may still contain other audited orphans. The next operator action is to take the next ID from `remaining_active_orphan_ids` and invoke this command again. Do not disable from a stale list: every invocation re-audits. Exit 0 with `PASS` and empty residual arrays means this was the last authorizing orphan and the second full audit is clean. A missing schema-v1 envelope, a status other than `PASS`/`FAIL`, a residual `active_orphan_workflow` for the ID just disabled, or a dirty audit after a single-candidate plan refuses the receipt even if GitHub already accepted the `204`.

### No self-repair workflow

Noema intentionally does not create a repository Actions workflow to repair the Actions workflow registry. Such a writer would become another workflow identity that could itself be orphaned, stale, or competing. The bounded transport is an operator primitive consumed by the existing fail-closed plan/executor boundary.
Expand All @@ -56,6 +86,8 @@ Noema intentionally does not create a repository Actions workflow to repair the
| HTTP/API failure | Non-2xx and endpoint-inconsistent 2xx statuses fail closed; disable accepts exactly HTTP 204 |
| Malformed successful response | Strict JSON and identity validation fails closed |
| Disable acknowledged but not effective | Executor requires a fresh `disabled_manually` postcondition |
| Operator receipt hides residual orphans | Second full audit must be schema-v1 `PASS`/`FAIL`; this ID cannot remain `active_orphan_workflow`; single-candidate plans require `PASS`; residual codes and orphan IDs are printed on the receipt |
| Credential read before authority checks | CLI validates repository and workflow ID before opening `NOEMA_MAINTAINER_TOKEN_PATH` |
| Credential disclosure | Closure-private token; response bodies and raw transport exceptions excluded from errors |
| API contract drift | Tests pin the current `2026-03-10` version header and documented disable endpoint |
| Competing repair writer | No new repository workflow or self-modifying control plane |
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"typecheck": "tsc --noEmit",
"governance:audit": "node scripts/main-governance-audit.mjs",
"operations:preflight": "node scripts/maintainer-app-readiness.mjs",
"operations:workflow-registry-disable": "node scripts/workflow-registry-live-disable.mjs",
"operations:runner-assignment": "node scripts/actions-runner-assignment-audit.mjs",
"operations:external-scheduler-evidence": "node scripts/external-scheduler-evidence-audit.mjs",
"production:governance": "node scripts/production-environment-governance-audit.mjs",
Expand Down
39 changes: 37 additions & 2 deletions scripts/workflow-registry-disable-plan.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { hasDuplicateJsonObjectKeys } from "./normalize-commercial-readiness-evidence.mjs";

const EXPECTED_REPOSITORY = "ContextualWisdomLab/noema";
const WORKFLOW_PATH_PREFIX = ".github/workflows/";
const LOWERCASE_SHA_40 = /^[0-9a-f]{40}$/;
const ISO_UTC_MILLISECOND = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const GITHUB_API_ROOT = "https://api.github.com";
const GITHUB_API_VERSION = "2026-03-10";
const GITHUB_REQUEST_TIMEOUT_MS = 10_000;
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
const AUTHENTIC_PLANS = new WeakSet();

/**
Expand Down Expand Up @@ -162,6 +165,7 @@ export function createGithubWorkflowDisablementTransport(input) {
const headers = Object.freeze({
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"User-Agent": "ContextualWisdomLab-Noema-workflow-registry-operator",
"X-GitHub-Api-Version": GITHUB_API_VERSION,
});

Expand Down Expand Up @@ -235,17 +239,48 @@ export function createGithubWorkflowDisablementTransport(input) {
}

/**
* Parse a successful GitHub JSON response while hiding invalid remote bytes.
* Parse a successful GitHub JSON response through the same bounded byte-level
* boundary used by the live registry collector. Response size, UTF-8 validity,
* duplicate decoded object keys, and JSON syntax are all fail-closed before
* any remote field can become protected-main or workflow mutation evidence.
*
* @param {Response} response successful GitHub response
* @returns {Promise<unknown>} parsed JSON value
*/
async function parseResponseJson(response) {
const advertisedLength = Number(response.headers.get("content-length"));
if (Number.isFinite(advertisedLength) && advertisedLength > MAX_RESPONSE_BYTES) {
throw new Error(
"GitHub workflow disablement transport response exceeds the bounded size limit",
);
}

const bytes = new Uint8Array(await response.arrayBuffer());
if (bytes.byteLength > MAX_RESPONSE_BYTES) {
throw new Error(
"GitHub workflow disablement transport response exceeds the bounded size limit",
);
}

let text;
try {
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
} catch {
throw new Error("GitHub workflow disablement transport response contains invalid UTF-8");
}

let duplicateKeys;
try {
return JSON.parse(await response.text());
duplicateKeys = hasDuplicateJsonObjectKeys(text);
} catch {
throw new Error("GitHub workflow disablement transport returned invalid JSON");
}
if (duplicateKeys) {
throw new Error(
"GitHub workflow disablement transport response contains duplicate decoded JSON keys",
);
}
return JSON.parse(text);
}

/**
Expand Down
Loading
Loading