From 774a54636a82220a0a144b8b5c566a81cd94674d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 17:04:16 +0900 Subject: [PATCH 1/2] test(operations): reject ambiguous runner API bytes --- test/actions-runner-assignment-cli.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/actions-runner-assignment-cli.test.ts b/test/actions-runner-assignment-cli.test.ts index 1b0b51ea4..153c5a431 100644 --- a/test/actions-runner-assignment-cli.test.ts +++ b/test/actions-runner-assignment-cli.test.ts @@ -3,6 +3,7 @@ import { createGhReadAdapters, createGhSubprocessEnvironment, ghApi, + parseGhJsonEvidence, runActionsRunnerAssignmentAudit, } from "../scripts/actions-runner-assignment-audit.mjs"; @@ -32,6 +33,23 @@ describe("runner-assignment operator audit", () => { expect(() => ghApi(path)).toThrow(message); }); + it("rejects malformed UTF-8 and duplicate decoded keys in GitHub API evidence", () => { + expect(() => parseGhJsonEvidence(Buffer.concat([ + Buffer.from('{"id":100,"name":"', "utf8"), + Buffer.from([0xff]), + Buffer.from('"}', "utf8"), + ]))).toThrow("invalid UTF-8"); + + expect(() => parseGhJsonEvidence(Buffer.from('{"id":100,"i\\u0064":101}', "utf8"))).toThrow( + "duplicate decoded object keys", + ); + + expect(parseGhJsonEvidence(Buffer.from('{"id":100,"head_sha":"0123456789abcdef0123456789abcdef01234567"}', "utf8"))).toEqual({ + id: 100, + head_sha: expectedHead, + }); + }); + it("isolates the gh subprocess from unrelated repository, model, and proxy credentials", () => { expect(createGhSubprocessEnvironment({ PATH: "/usr/bin:/bin", From 56802e206ffb7dcc5336c62f87df54823004cc5e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 17:05:04 +0900 Subject: [PATCH 2/2] fix(operations): preserve runner API JSON byte integrity --- scripts/actions-runner-assignment-audit.mjs | 48 ++++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/scripts/actions-runner-assignment-audit.mjs b/scripts/actions-runner-assignment-audit.mjs index 77457a543..933df70a6 100644 --- a/scripts/actions-runner-assignment-audit.mjs +++ b/scripts/actions-runner-assignment-audit.mjs @@ -21,6 +21,7 @@ import { collectRunnerAssignmentEvidence, parseSelectedRunIds, } from "./lib/actions-runner-assignment-source.mjs"; +import { hasDuplicateJsonObjectKeys } from "./normalize-commercial-readiness-evidence.mjs"; const AUDITED_REPOSITORY = "ContextualWisdomLab/noema"; const GITHUB_API_VERSION = "2026-03-10"; @@ -28,6 +29,7 @@ const GH_API_TIMEOUT_MILLISECONDS = 20_000; const GH_API_MAX_BUFFER_BYTES = 2 * 1024 * 1024; const REPORT_PATH = "artifacts/operations/actions-runner-assignment-audit.json"; const canonicalShaPattern = /^[0-9a-f]{40}$/; +const fatalUtf8Decoder = new TextDecoder("utf-8", { fatal: true }); function boundedErrorText(value) { const text = typeof value === "string" ? value : String(value ?? ""); @@ -66,6 +68,45 @@ export function createGhSubprocessEnvironment(environment) { }; } +/** + * Decode and parse bounded GitHub API bytes without normalizing ambiguous input. + * + * Malformed UTF-8 and duplicate decoded object keys fail before `JSON.parse`, so + * runner-assignment evidence cannot inherit replacement-character or + * last-key-wins semantics from the JavaScript runtime. + * + * @param {Uint8Array} bytes Raw stdout bytes returned by the GitHub CLI. + * @returns {unknown} Parsed JSON evidence. + */ +export function parseGhJsonEvidence(bytes) { + if (!(bytes instanceof Uint8Array)) { + throw new TypeError("GitHub Actions evidence must be supplied as raw bytes."); + } + + let text; + try { + text = fatalUtf8Decoder.decode(bytes); + } catch { + throw new Error("GitHub Actions evidence read returned invalid UTF-8."); + } + + let duplicateKeys; + try { + duplicateKeys = hasDuplicateJsonObjectKeys(text); + } catch { + throw new Error("GitHub Actions evidence read returned malformed JSON."); + } + if (duplicateKeys) { + throw new Error("GitHub Actions evidence read returned duplicate decoded object keys."); + } + + try { + return JSON.parse(text); + } catch { + throw new Error("GitHub Actions evidence read returned malformed JSON."); + } +} + /** * Read one GitHub REST resource through the authenticated `gh` CLI. * @@ -94,7 +135,6 @@ export function ghApi(path, options = {}) { args.push(path); const result = spawnSync("gh", args, { - encoding: "utf8", timeout: GH_API_TIMEOUT_MILLISECONDS, maxBuffer: GH_API_MAX_BUFFER_BYTES, env: createGhSubprocessEnvironment(process.env), @@ -110,11 +150,7 @@ export function ghApi(path, options = {}) { ); } - try { - return JSON.parse(result.stdout); - } catch { - throw new Error("GitHub Actions evidence read returned malformed JSON."); - } + return parseGhJsonEvidence(result.stdout); } /**