Skip to content
36 changes: 34 additions & 2 deletions scripts/hourly-commercial-readiness.mjs
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,50 @@ function bound(value, limit = MAX_REPORT_DETAIL_CHARS) {
return text.length <= limit ? text : `${text.slice(0, limit)}…`;
}

export function redactSensitiveValue(value, sensitiveValues = []) {
let redacted = String(value ?? "");
for (const sensitiveValue of Array.isArray(sensitiveValues) ? sensitiveValues : []) {
if (typeof sensitiveValue !== "string" || sensitiveValue.length === 0) {
continue;
}
redacted = redacted.split(sensitiveValue).join("[REDACTED]");
}
return redacted;
}

export function createGhSubprocessEnvironment(sourceEnvironment) {
const childEnvironment = {
GH_HOST: "github.com",
NO_COLOR: "1",
};
if (typeof sourceEnvironment.PATH === "string" && sourceEnvironment.PATH.length > 0) {
childEnvironment.PATH = sourceEnvironment.PATH;
}
if (typeof sourceEnvironment.GH_TOKEN === "string" && sourceEnvironment.GH_TOKEN.length > 0) {
childEnvironment.GH_TOKEN = sourceEnvironment.GH_TOKEN;
}
return childEnvironment;
}

function runGh(args, { input } = {}) {
const childEnvironment = createGhSubprocessEnvironment({
PATH: process.env.PATH,
GH_TOKEN: process.env.GH_TOKEN,
});
const completed = spawnSync("gh", args, {
encoding: "utf8",
env: childEnvironment,
input,
maxBuffer: MAX_GH_OUTPUT_BYTES,
shell: false,
});
if (completed.error) {
throw new Error(`GitHub CLI could not start: ${bound(completed.error.message, MAX_ERROR_CHARS)}`);
const detail = redactSensitiveValue(completed.error.message, [childEnvironment.GH_TOKEN]);
throw new Error(`GitHub CLI could not start: ${bound(detail, MAX_ERROR_CHARS)}`);
}
if (completed.status !== 0) {
const detail = completed.stderr || completed.stdout || `exit ${completed.status}`;
const rawDetail = completed.stderr || completed.stdout || `exit ${completed.status}`;
const detail = redactSensitiveValue(rawDetail, [childEnvironment.GH_TOKEN]);
throw new Error(`GitHub CLI failed: ${bound(detail, MAX_ERROR_CHARS)}`);
}
return completed.stdout.trim();
Expand Down
15 changes: 15 additions & 0 deletions test/hourly-commercial-readiness-credential-ingress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";

describe("hourly commercial-readiness credential ingress", () => {
it("requires explicit parent credential transport before building the gh child environment", () => {
const script = readFileSync("scripts/hourly-commercial-readiness.mjs", "utf8");
const helperStart = script.indexOf("export function createGhSubprocessEnvironment");
const helperEnd = script.indexOf("\nfunction runGh(", helperStart);

expect(helperStart).toBeGreaterThanOrEqual(0);
expect(helperEnd).toBeGreaterThan(helperStart);
expect(script.slice(helperStart, helperEnd)).not.toContain("process.env");
expect(script).not.toContain("createGhSubprocessEnvironment();");
});
});
54 changes: 52 additions & 2 deletions test/hourly-commercial-readiness-script.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import {
createGhSubprocessEnvironment,
flattenArrayPages,
hasActiveNoemaReviewRun,
latestCheckRunsBySuite,
latestReviewStates,
parseNoemaReviewDecision,
redactSensitiveValue,
} from "../scripts/hourly-commercial-readiness.mjs";

const repository = "ContextualWisdomLab/noema";
Expand Down Expand Up @@ -242,11 +244,59 @@ describe("hourly commercial-readiness GitHub adapter", () => {
).toBe(false);
});

it("passes only explicit GitHub CLI authority into child processes", () => {
expect(createGhSubprocessEnvironment({
PATH: "/trusted/bin",
GH_TOKEN: "read-only-maintainer-token",
GH_HOST: "evil.example",
NO_COLOR: "0",
GITHUB_TOKEN: "ambient-workflow-token",
NVIDIA_NIM_API_KEY: "model-secret",
NOEMA_MAINTAINER_APP_PRIVATE_KEY: "maintainer-private-key",
NOEMA_REVIEWER_APP_PRIVATE_KEY: "reviewer-private-key",
NOEMA_REVIEWER_LOGIN: "reviewer[bot]",
CLOUDFLARE_API_TOKEN: "cloudflare-secret",
HTTPS_PROXY: "http://proxy.invalid",
HTTP_PROXY: "http://proxy.invalid",
ALL_PROXY: "socks5://proxy.invalid",
HOME: "/credential-bearing-home",
NODE_OPTIONS: "--require /tmp/preload.cjs",
NOEMA_MAINTENANCE_ENABLED: "true",
})).toEqual({
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
GH_HOST: "github.com",
NO_COLOR: "1",
PATH: "/trusted/bin",
GH_TOKEN: "read-only-maintainer-token",
});

expect(createGhSubprocessEnvironment({})).toEqual({
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
GH_HOST: "github.com",
NO_COLOR: "1",
});
});

it("redacts an explicit maintainer token before child diagnostics can reach retained outputs", () => {
const token = "read-only-maintainer-token";
const detail = `gh failed with ${token}; retry also exposed ${token}`;

expect(redactSensitiveValue(detail, [token])).toBe(
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
"gh failed with [REDACTED]; retry also exposed [REDACTED]",
);
expect(redactSensitiveValue(detail, ["", null, undefined, token])).not.toContain(token);
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
expect(redactSensitiveValue("safe diagnostic", [])).toBe("safe diagnostic");
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
});

it("uses shell-free complete pagination and exact-head write contracts", () => {
const script = readFileSync("scripts/hourly-commercial-readiness.mjs", "utf8");

expect(script).toContain('spawnSync("gh"');
expect(script).toContain("shell: false");
expect(script).toContain("env: childEnvironment");
expect(script).not.toContain("env: process.env");
expect(script).toContain(
"redactSensitiveValue(completed.error.message, [childEnvironment.GH_TOKEN])",
);
expect(script).toContain("redactSensitiveValue(rawDetail, [childEnvironment.GH_TOKEN])");
expect(script).toContain('"--paginate", "--slurp"');
expect(script).toContain("pulls?state=open&per_page=100");
expect(script).toContain("check-runs?filter=all&per_page=100");
Expand All @@ -265,16 +315,16 @@ describe("hourly commercial-readiness GitHub adapter", () => {
expect(script).toContain("live?.head?.repo?.full_name !== repository");
});

it("writes a bounded report and post-action queue outputs without exposing tokens", () => {
it("writes a bounded report and post-action queue outputs without ambient credential payloads", () => {
const script = readFileSync("scripts/hourly-commercial-readiness.mjs", "utf8");

expect(script).toContain("open_pull_request_count=");
expect(script).toContain("remaining_open_pull_request_count=");
expect(script).toContain("report_path=");
expect(script).toContain("remainingOpenPullRequestCount");
expect(script).toContain("MAX_ERROR_CHARS");
expect(script).not.toContain("GH_TOKEN");
expect(script).not.toContain("GITHUB_TOKEN");
expect(script).not.toContain("read-only-maintainer-token");
});

it("documents the operator contract and buyer-visible governance boundaries", () => {
Expand Down
Loading