Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 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
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
38 changes: 36 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 @@ -235,17 +238,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
331 changes: 331 additions & 0 deletions scripts/workflow-registry-live-disable.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
#!/usr/bin/env node
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { readDelegatedGithubToken } from "./lib/delegated-github-token.mjs";
import { hasDuplicateJsonObjectKeys } from "./normalize-commercial-readiness-evidence.mjs";
import {
buildWorkflowDisablementPlan,
createGithubWorkflowDisablementTransport,
executeWorkflowDisablement,
} from "./workflow-registry-disable-plan.mjs";
import {
collectLiveWorkflowRegistryAudit,
workflowPageFromResponse,
} from "./workflow-registry-live-audit.mjs";

const EXPECTED_REPOSITORY = "ContextualWisdomLab/noema";
const GITHUB_API_ROOT = "https://api.github.com";
const GITHUB_API_VERSION = "2026-03-10";
const PER_PAGE = 100;
const MAX_PAGES = 1_000;
const MAX_RESPONSE_BYTES = 8 * 1024 * 1024;
const REQUEST_TIMEOUT_MS = 10_000;

function validWorkflowId(value) {
return Number.isSafeInteger(value) && value > 0;
}

function boundedError(error) {
const raw = error instanceof Error ? error.message : String(error);
return raw
.replace(/\bbearer\s+\S+/gi, "Bearer [REDACTED]")
.replace(/\bgithub_pat_[A-Za-z0-9_]+\b/g, "[REDACTED]")
.replace(/\bgh[pousr]_[A-Za-z0-9_]+\b/g, "[REDACTED]")
.replace(/[\u0000-\u001f\u007f]/g, "")
.slice(0, 2_048);
}

/**
* Create a bounded, repository-pinned GitHub JSON reader for the live operator.
* The delegated token is closure-private and never copied into process environment.
*
* @param {{token: string, fetchImpl?: typeof fetch}} input delegated token and fetch primitive
* @returns {(endpoint: string) => Promise<unknown>} exact-repository JSON reader
*/
export function createWorkflowRegistryGithubJsonReader(input) {
if (typeof input?.token !== "string" || input.token.length === 0) {
throw new Error("workflow registry GitHub reader requires a delegated token");
}
const fetchImpl = input.fetchImpl ?? globalThis.fetch;
if (typeof fetchImpl !== "function") {
throw new Error("workflow registry GitHub reader requires fetch capability");
}
const token = input.token;

return async (endpoint) => {
if (typeof endpoint !== "string" || endpoint.length === 0 || endpoint.includes("\\")) {
throw new Error("workflow registry GitHub endpoint is invalid");
}
const url = new URL(endpoint, `${GITHUB_API_ROOT}/`);
if (
url.origin !== GITHUB_API_ROOT
|| !url.pathname.startsWith(`/repos/${EXPECTED_REPOSITORY}/`)
|| url.username
|| url.password
|| url.hash
) {
throw new Error("workflow registry GitHub endpoint escapes the Noema repository boundary");
}

let response;
try {
response = await fetchImpl(url, {
method: "GET",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"User-Agent": "ContextualWisdomLab-Noema-workflow-registry-operator",
"X-GitHub-Api-Version": GITHUB_API_VERSION,
},
cache: "no-store",
redirect: "error",
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
});
} catch (error) {
if (error?.name === "TimeoutError") {
throw new Error("workflow registry GitHub request timed out");
}
throw new Error("workflow registry GitHub request failed before receiving an HTTP response");
}
if (!response.ok) {
throw new Error(`workflow registry GitHub request failed with HTTP ${response.status}`);
}

const advertisedLength = Number(response.headers.get("content-length"));
if (Number.isFinite(advertisedLength) && advertisedLength > MAX_RESPONSE_BYTES) {
throw new Error("workflow registry GitHub response exceeds the bounded size limit");
}
const bytes = new Uint8Array(await response.arrayBuffer());
if (bytes.byteLength > MAX_RESPONSE_BYTES) {
throw new Error("workflow registry GitHub response exceeds the bounded size limit");
}

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

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

/**
* Re-read the complete workflow registry immediately before building mutation authority.
* Every page must agree on total count and retained records must exactly match that total.
*
* @param {{repository?: string, ghJson: (endpoint: string) => Promise<unknown>}} input repository and reader
* @returns {Promise<object[]>} complete fresh raw workflow registry
*/
export async function collectLiveWorkflowRecords(input) {
const repository = input?.repository ?? EXPECTED_REPOSITORY;
if (repository !== EXPECTED_REPOSITORY || typeof input?.ghJson !== "function") {
throw new Error("live workflow registry collection is restricted to ContextualWisdomLab/noema");
}

const workflows = [];
let expectedTotal;
for (let page = 1; page <= MAX_PAGES; page += 1) {
const parsed = workflowPageFromResponse(
await input.ghJson(`repos/${repository}/actions/workflows?per_page=${PER_PAGE}&page=${page}`),
page,
PER_PAGE,
);
if (expectedTotal === undefined) expectedTotal = parsed.totalCount;
if (parsed.totalCount !== expectedTotal) {
throw new Error("workflow registry total changed during immediate pre-mutation refresh");
}
workflows.push(...parsed.workflows);
if (!parsed.hasNext) {
if (workflows.length !== expectedTotal) {
throw new Error("workflow registry refresh did not retain the advertised record count");
}
return workflows;
}
}
throw new Error("workflow registry pagination exceeded the bounded page limit");
}

/**
* Execute one and only one requested active-orphan workflow disablement.
* The operator collects a full exact-main audit, immediately refreshes raw registry
* identities, builds process-local mutation authority, revalidates main plus workflow
* state around the mutation, and then performs a second full audit before returning a receipt.
*
* @param {object} input exact repository, workflow id, audit/live collectors, and transport
* @returns {Promise<object>} bounded postcondition receipt
*/
export async function runWorkflowRegistryDisablement(input) {
const repository = input?.repository ?? EXPECTED_REPOSITORY;
const workflowId = input?.workflowId;
if (repository !== EXPECTED_REPOSITORY) {
throw new Error(`workflow disablement is restricted to ${EXPECTED_REPOSITORY}`);
}
if (!validWorkflowId(workflowId)) {
throw new Error("requested workflow id must be a positive safe integer");
}
if (typeof input?.collectAudit !== "function" || typeof input?.collectLiveWorkflows !== "function") {
throw new Error("workflow disablement operator is missing fresh evidence collectors");
}
const transport = input?.transport;
if (
typeof transport?.revalidateDefaultBranch !== "function"
|| typeof transport?.revalidateWorkflow !== "function"
|| typeof transport?.disableWorkflow !== "function"
) {
throw new Error("workflow disablement operator is missing authorized transport");
}

const audit = await input.collectAudit();
const exactMain = audit?.default_branch_sha;
const liveWorkflows = await input.collectLiveWorkflows();
const plan = buildWorkflowDisablementPlan({
audit,
expectedRepository: repository,
expectedDefaultBranchSha: exactMain,
liveWorkflows,
});
if (plan.status !== "PASS") {
const firstFailure = plan.failures?.[0]?.code ?? "unknown";
throw new Error(`fresh workflow disablement plan is non-authorizing: ${firstFailure}`);
}

const candidate = plan.disablements.find((item) => item.workflow_id === workflowId);
if (!candidate) {
throw new Error("requested workflow is not an exact active-orphan candidate");
}

const mutation = await executeWorkflowDisablement({
plan,
candidate,
revalidateDefaultBranch: transport.revalidateDefaultBranch,
revalidateWorkflow: transport.revalidateWorkflow,
disableWorkflow: transport.disableWorkflow,
});

const postAudit = await input.collectAudit();
if (postAudit?.repository_full_name !== repository) {
throw new Error("repository identity changed during post-disablement verification");
}
if (postAudit?.default_branch_sha !== plan.default_branch_sha) {
throw new Error("protected main changed during post-disablement verification");
}
const postWorkflow = Array.isArray(postAudit?.workflows)
? postAudit.workflows.find((item) => item?.workflow_id === workflowId)
: undefined;
if (
postWorkflow?.workflow_path !== candidate.workflow_path
|| postWorkflow?.workflow_state !== "disabled_manually"
|| postWorkflow?.classification !== "disabled_registry_record"
) {
throw new Error("full post-disablement audit did not retain the exact disabled workflow identity");
}

return Object.freeze({
schema_version: 1,
repository_full_name: repository,
protected_main_sha: plan.default_branch_sha,
workflow_id: mutation.workflow_id,
workflow_path: mutation.workflow_path,
prior_state: mutation.prior_state,
final_state: mutation.final_state,
mutation: mutation.mutation,
post_audit_status: postAudit.status,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 영수증은 저장소 이름, 보호된 main SHA, 그리고 해당 워크플로 행이 disabled_manually / disabled_registry_record인지만 확인합니다. schema_version === 1, statusPASS/FAIL, 이 ID가 active_orphan_workflow에서 빼졌는지, 단일 후보 계획의 PASS는 요구하지 않습니다. post_audit_statusFAIL이나 undefined여도 그대로 날라가며 main()은 exit 0입니다.

다음 동작: 사후 감사 봉투를 강제하고 잔여 failure code와 orphan ID를 영수증에 낣기십시오. 수정본은 #398 (0a15b9e) 입니다. 이 head는 머지 말고 #398을 심사하십시오.

});
}

/**
* Operator entrypoint. The target workflow ID is an explicit argument and the
* short-lived delegated GitHub capability comes only from the reviewed token file.
* No batch mode exists: each invocation can mutate at most one exact audited orphan.
*
* @returns {Promise<object>} verified disablement receipt
*/
export async function main() {
const repository = String(process.env.GITHUB_REPOSITORY ?? EXPECTED_REPOSITORY).trim();
const tokenPath = String(process.env.NOEMA_MAINTAINER_TOKEN_PATH ?? "").trim();
const workflowId = Number(process.argv[2] ?? "");
if (repository !== EXPECTED_REPOSITORY) {
throw new Error(`workflow disablement is restricted to ${EXPECTED_REPOSITORY}`);
}
if (!validWorkflowId(workflowId)) {
throw new Error("requested workflow id must be a positive safe integer");
}
const token = readDelegatedGithubToken(tokenPath);
const ghJson = createWorkflowRegistryGithubJsonReader({ token });
const transport = createGithubWorkflowDisablementTransport({
token,
fetchImpl: globalThis.fetch,
});
const collectAudit = () => collectLiveWorkflowRegistryAudit({
repository,
defaultBranch: "main",
ghJson,
});
const receipt = await runWorkflowRegistryDisablement({
repository,
workflowId,
collectAudit,
collectLiveWorkflows: () => collectLiveWorkflowRecords({ repository, ghJson }),
transport,
});
console.log(JSON.stringify(receipt, null, 2));
return receipt;
}

/**
* Execute the live-disable CLI with isolated error and exit-code boundaries.
*
* @param {{mainFn?: () => Promise<unknown>, stderr?: (value: unknown) => void, setExitCode?: (code: number) => void}} [options]
* @returns {Promise<unknown>} operation result or undefined after a bounded failure
*/
export async function startCli({
mainFn = main,
stderr = console.error,
setExitCode = (code) => { process.exitCode = code; },
} = {}) {
try {
return await mainFn();
} catch (error) {
stderr(`workflow-registry-disable failed: ${boundedError(error)}`);
setExitCode(1);
return undefined;
}
}

/**
* Dispatch the CLI only when this module is the process entrypoint.
*
* @param {{scriptUrl?: string, argv?: string[], pathToFileUrlFn?: (path: string) => {href: string}, starter?: () => unknown}} [options]
* @returns {boolean} whether direct-entry execution was selected
*/
export function runIfDirect({
scriptUrl = import.meta.url,
argv = process.argv,
pathToFileUrlFn = (value) => pathToFileURL(resolve(value)),
starter = startCli,
} = {}) {
const invokedAsScript = (
typeof argv[1] === "string"
&& argv[1].length > 0
&& pathToFileUrlFn(argv[1]).href === scriptUrl
);
if (invokedAsScript) void starter();
return invokedAsScript;
}

runIfDirect();
Loading
Loading