-
Notifications
You must be signed in to change notification settings - Fork 0
feat(operations): execute one verified orphan disablement #392
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
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 867a09a
feat(operations): execute one verified orphan disablement
seonghobae 8f72348
test(coverage): measure workflow disable operator
seonghobae 298aa7c
Merge protected main into workflow registry operator
seonghobae 4a35eba
test(coverage): exercise live disable operator boundaries
seonghobae 86bad40
test(coverage): align malformed JSON assertion with fail-closed scanner
seonghobae 5532160
Merge protected main into workflow registry operator
seonghobae ba0797d
test(operations): define deterministic live-disable CLI boundary
seonghobae f9b01e0
fix(operations): isolate live-disable CLI dispatch
seonghobae 087aa75
fix(ops): remove unreachable workflow JSON parse branch
seonghobae 528473c
test(ops): cover live disable executable path
seonghobae d6a8ca3
test(ops): exercise live-disable fail-closed branches
seonghobae 5f787bd
test(ops): cover live-disable default and nullish boundaries
seonghobae 17d250c
test(ops): exercise residual live-disable evidence branches
seonghobae 6107d92
test(coverage): exercise absent post-audit workflow records
seonghobae 73e6674
test(security): reject ambiguous privileged GitHub JSON
seonghobae aa55838
fix(operations): harden disablement JSON boundary
seonghobae da82dc7
fix(operations): preserve malformed JSON diagnostic
seonghobae d4f9756
test(security): reproduce fine-grained token diagnostic leak
seonghobae 99d3f90
fix(security): redact fine-grained tokens from CLI failures
seonghobae bbdb3b3
fix(operations): remove unreachable JSON parse branch
seonghobae 82d5ad8
test(operations): assert missing token capability failure
seonghobae 004315e
merge: integrate protected main after #396
seonghobae 15b642d
test(operations): pin bounded invalid-JSON diagnostic
seonghobae 1a2dee6
fix(operations): normalize malformed GitHub JSON failures
seonghobae d09ac4b
merge: restack workflow disablement on protected main a634066
seonghobae 1ef3fc7
test(operations): reject invalid disablement before credential read
seonghobae 1648e46
fix(operations): validate disablement before credential read
seonghobae d1aff09
test(operations): exercise non-Error CLI failure contract
seonghobae 8956df0
test(operations): prove absent fetch capability
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * 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(); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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,status가PASS/FAIL, 이 ID가active_orphan_workflow에서 빼졌는지, 단일 후보 계획의PASS는 요구하지 않습니다.post_audit_status는FAIL이나undefined여도 그대로 날라가며main()은 exit 0입니다.다음 동작: 사후 감사 봉투를 강제하고 잔여 failure code와 orphan ID를 영수증에 낣기십시오. 수정본은 #398 (
0a15b9e) 입니다. 이 head는 머지 말고 #398을 심사하십시오.