Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions docs/main-governance-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ The integration requirement prevents a similarly named status from an arbitrary

A failed governance audit stops all write actions but still uploads `main-governance-audit` evidence.

## API execution boundary

The GitHub CLI subprocess is shell-free, output-bounded, pinned to `github.com`, and limited to 20 seconds per request. It receives only `PATH`, the scoped `GH_TOKEN`, and the pinned `GH_HOST`; unrelated runner environment variables and proxy overrides are not inherited. Missing credentials, process timeout, malformed pagination, nonzero CLI exit, empty response, or invalid JSON produce a bounded `governance_collection_failed` report and a failing exit code.

## Permissions

GitHub documents the active branch-rules endpoint as requiring only repository `Metadata: read` for a fine-grained or GitHub App installation token. The maintainer App therefore does **not** receive repository administration permission.
Expand Down
306 changes: 306 additions & 0 deletions scripts/lib/maintainer-app-readiness.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
export const REQUIRED_API_PROBES = Object.freeze([
"actions_read",
"checks_read",
"statuses_read",
"pull_requests_read",
"contents_read",
]);

const MAX_DETAIL_CHARS = 800;
const expectedRepository = "ContextualWisdomLab/noema";
const appSlugPattern = /^[a-z0-9](?:[a-z0-9-]{0,98}[a-z0-9])?$/;
const botLoginPattern = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,98}[A-Za-z0-9])?\[bot\]$/;

function normalized(value) {
return String(value ?? "").trim();
}

function objectValue(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}

function safeDetail(value) {
const text = String(value ?? "")
.replace(/[\u0000-\u001f\u007f]/g, "")
.trim();
return text.length <= MAX_DETAIL_CHARS
? text
: `${text.slice(0, MAX_DETAIL_CHARS - 1)}…`;
}

function addCheck(checks, failures, code, pass, detail) {
const retainedDetail = safeDetail(detail);
const check = { code, pass, detail: retainedDetail };
checks.push(check);
if (!pass) failures.push({ code, detail: retainedDetail });
}

function validateIdentity(evidence, checks, failures) {
const repository = normalized(evidence.repository);
const installationId = evidence.installationId;
const appSlug = normalized(evidence.appSlug);
const maintainerAccount = objectValue(evidence.maintainerAccount);
const reviewerAppSlug = normalized(evidence.reviewerAppSlug);
const reviewerInstallationId = evidence.reviewerInstallationId;
const reviewerLogin = normalized(evidence.reviewerLogin);
const reviewerAccount = objectValue(evidence.reviewerAccount);
const expectedMaintainerLogin = appSlug ? `${appSlug}[bot]` : "";
const expectedReviewerLogin = reviewerAppSlug ? `${reviewerAppSlug}[bot]` : "";
const maintainerLogin = normalized(maintainerAccount.login);
const observedReviewerLogin = normalized(reviewerAccount.login);

addCheck(
checks,
failures,
"repository_mismatch",
repository === expectedRepository,
repository === expectedRepository
? `Evidence is bound to ${expectedRepository}.`
: `Evidence repository ${repository || "missing"} does not match ${expectedRepository}.`,
);
addCheck(
checks,
failures,
"maintenance_already_enabled",
evidence.maintenanceEnabled === false,
evidence.maintenanceEnabled === false
? "Automated maintenance remains disabled during pre-activation audit."
: "NOEMA_MAINTENANCE_ENABLED must remain disabled until pre-activation evidence and independent approval pass.",
);
addCheck(
checks,
failures,
"installation_id_invalid",
Number.isSafeInteger(installationId) && installationId > 0,
Number.isSafeInteger(installationId) && installationId > 0
? `Installation id ${installationId} is a positive integer.`
: "Installation id must be a positive safe integer.",
);
addCheck(
checks,
failures,
"app_slug_invalid",
appSlugPattern.test(appSlug),
appSlugPattern.test(appSlug)
? `Maintainer App slug ${appSlug} is valid.`
: "Maintainer App slug is missing or malformed.",
);
addCheck(
checks,
failures,
"maintainer_login_mismatch",
Boolean(expectedMaintainerLogin) && maintainerLogin === expectedMaintainerLogin,
maintainerLogin === expectedMaintainerLogin && expectedMaintainerLogin
? `Maintainer bot login matches ${expectedMaintainerLogin}.`
: `Maintainer bot login ${maintainerLogin || "missing"} does not match ${expectedMaintainerLogin || "the App slug"}.`,
);
addCheck(
checks,
failures,
"maintainer_type_invalid",
normalized(maintainerAccount.type) === "Bot",
normalized(maintainerAccount.type) === "Bot"
? "Maintainer identity is a GitHub Bot account."
: `Maintainer identity type is ${normalized(maintainerAccount.type) || "missing"}, not Bot.`,
);
addCheck(
checks,
failures,
"reviewer_installation_id_invalid",
Number.isSafeInteger(reviewerInstallationId) && reviewerInstallationId > 0,
Number.isSafeInteger(reviewerInstallationId) && reviewerInstallationId > 0
? `Reviewer installation id ${reviewerInstallationId} is a positive integer.`
: "Reviewer installation id must be a positive safe integer.",
);
addCheck(
checks,
failures,
"reviewer_app_slug_invalid",
appSlugPattern.test(reviewerAppSlug),
appSlugPattern.test(reviewerAppSlug)
? `Reviewer App slug ${reviewerAppSlug} is valid.`
: "Reviewer App slug is missing or malformed.",
);
addCheck(
checks,
failures,
"reviewer_app_login_mismatch",
Boolean(expectedReviewerLogin) && reviewerLogin === expectedReviewerLogin,
reviewerLogin === expectedReviewerLogin && expectedReviewerLogin
? `Configured reviewer login is bound to authenticated Reviewer App ${reviewerAppSlug}.`
: `Configured reviewer login ${reviewerLogin || "missing"} does not match ${expectedReviewerLogin || "the authenticated Reviewer App slug"}.`,
);
addCheck(
checks,
failures,
"reviewer_login_invalid",
botLoginPattern.test(reviewerLogin),
botLoginPattern.test(reviewerLogin)
? `Configured reviewer login ${reviewerLogin} is an exact bot login.`
: "Configured reviewer login must end in [bot] and contain only supported GitHub login characters.",
);
addCheck(
checks,
failures,
"reviewer_login_mismatch",
Boolean(reviewerLogin) && observedReviewerLogin === reviewerLogin,
observedReviewerLogin === reviewerLogin && reviewerLogin
? `Reviewer API identity matches ${reviewerLogin}.`
: `Reviewer API identity ${observedReviewerLogin || "missing"} does not match ${reviewerLogin || "the configured reviewer"}.`,
);
addCheck(
checks,
failures,
"reviewer_type_invalid",
normalized(reviewerAccount.type) === "Bot",
normalized(reviewerAccount.type) === "Bot"
? "Reviewer identity is a GitHub Bot account."
: `Reviewer identity type is ${normalized(reviewerAccount.type) || "missing"}, not Bot.`,
);
addCheck(
checks,
failures,
"app_identity_not_separated",
Boolean(maintainerLogin && reviewerLogin) && maintainerLogin !== reviewerLogin,
maintainerLogin && reviewerLogin && maintainerLogin !== reviewerLogin
? "Maintainer and reviewer bot identities are distinct."
: "Maintainer and reviewer bot identities must be distinct.",
);
}

function validateRepositoryScope(evidence, checks, failures) {
const accessibleRepositories = Array.isArray(evidence.accessibleRepositories)
? evidence.accessibleRepositories
: [];
const repositoryNames = accessibleRepositories.map((item) => normalized(item?.full_name));
const exactScope = repositoryNames.length === 1 && repositoryNames[0] === expectedRepository;
addCheck(
checks,
failures,
"repository_scope_invalid",
exactScope,
exactScope
? `Effective token is scoped only to ${expectedRepository}.`
: `Effective token reports ${repositoryNames.length} accessible repositories; expected exactly one repository scoped to ${expectedRepository}.`,
);

const permissions = objectValue(evidence.repositoryPermissions);
addCheck(
checks,
failures,
"repository_pull_missing",
permissions.pull === true,
permissions.pull === true
? "Effective token reports repository read access."
: "Effective token does not report repository read access.",
);
addCheck(
checks,
failures,
"repository_push_missing",
permissions.push === true,
permissions.push === true
? "Effective token reports the scoped write access required by the maintainer loop."
: "Effective token does not report the scoped write access required by the maintainer loop.",
);
const adminStateKnown = typeof permissions.admin === "boolean";
addCheck(
checks,
failures,
"repository_admin_state_invalid",
adminStateKnown,
adminStateKnown
? "Repository administrator permission state is explicitly reported."
: "Repository administrator permission state is missing or non-boolean.",
);
addCheck(
checks,
failures,
"repository_admin_present",
permissions.admin === false,
permissions.admin === false
? "Effective token does not have repository administrator access."
: permissions.admin === true
? "Effective token has repository administrator access."
: "Administrator absence cannot be established from unknown permission evidence.",
);
}

function validateApiProbes(evidence, checks, failures) {
const probes = objectValue(evidence.apiProbes);
for (const probe of REQUIRED_API_PROBES) {
const pass = probes[probe] === true;
addCheck(
checks,
failures,
`api_probe_${probe}`,
pass,
pass
? `Required GitHub API probe ${probe} passed.`
: `Required GitHub API probe ${probe} did not pass.`,
);
}
}

function validateGovernance(evidence, checks, failures) {
const governance = evidence.governanceReport;
const valid = governance && typeof governance === "object" && !Array.isArray(governance);
addCheck(
checks,
failures,
"governance_report_invalid",
Boolean(valid),
valid ? "Main governance audit report is present." : "Main governance audit report is missing or malformed.",
);
if (!valid) return;

addCheck(
checks,
failures,
"governance_repository_mismatch",
normalized(governance.repository) === expectedRepository,
normalized(governance.repository) === expectedRepository
? `Governance evidence is bound to ${expectedRepository}.`
: `Governance evidence repository ${normalized(governance.repository) || "missing"} does not match ${expectedRepository}.`,
);
addCheck(
checks,
failures,
"governance_branch_mismatch",
normalized(governance.branch) === "main",
normalized(governance.branch) === "main"
? "Governance evidence is bound to main."
: `Governance evidence branch is ${normalized(governance.branch) || "missing"}, not main.`,
);
const status = normalized(governance.status).toUpperCase();
addCheck(
checks,
failures,
"governance_status_not_pass",
status === "PASS",
status === "PASS"
? "Live main governance audit passed."
: `Live main governance audit status is ${status || "missing"}, not PASS.`,
);
}

/**
* Evaluate bounded, already-collected evidence for the Maintainer GitHub App.
* The function is pure so tests and buyers can reproduce the decision without
* network, filesystem, environment, or clock dependencies. Public GitHub user
* responses are used only for exact login and account-type identity checks;
* installation suspension is outside that endpoint's documented schema.
*/
export function evaluateMaintainerAppReadiness(evidence = {}) {
const checks = [];
const failures = [];
validateIdentity(evidence, checks, failures);
validateRepositoryScope(evidence, checks, failures);
validateApiProbes(evidence, checks, failures);
validateGovernance(evidence, checks, failures);
return {
status: failures.length === 0 ? "PASS" : "FAIL",
checks,
failures,
};
}
24 changes: 20 additions & 4 deletions scripts/main-governance-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,37 @@ import { evaluateMainGovernanceRules } from "./lib/main-governance-audit.mjs";

const MAX_ERROR_CHARS = 4_000;
const MAX_GH_OUTPUT_BYTES = 4 * 1024 * 1024;
const MAX_GH_REQUEST_MILLISECONDS = 20_000;
const repositoryPattern = /^ContextualWisdomLab\/[A-Za-z0-9_.-]+$/;
const defaultReportPath = "artifacts/governance/main-governance-audit.json";
const githubApiHeaders = [
"-H",
"Accept: application/vnd.github+json",
"-H",
"X-GitHub-Api-Version: 2022-11-28",
];

function bound(value, limit = MAX_ERROR_CHARS) {
const text = String(value ?? "")
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "")
.replace(/[\u0000-\u001f\u007f]/g, "")
.trim();
return text.length <= limit ? text : `${text.slice(0, limit)}…`;
}

function runGh(args) {
const completed = spawnSync("gh", args, {
const completed = spawnSync("gh", ["api", ...githubApiHeaders, ...args], {
encoding: "utf8",
maxBuffer: MAX_GH_OUTPUT_BYTES,
timeout: MAX_GH_REQUEST_MILLISECONDS,
shell: false,
env: {
PATH: process.env.PATH,
GH_TOKEN: process.env.GH_TOKEN,
GH_HOST: "github.com",
},
});
if (completed.error) {
throw new Error(`GitHub CLI could not start: ${bound(completed.error.message)}`);
throw new Error(`GitHub CLI could not complete: ${bound(completed.error.message)}`);
}
if (completed.status !== 0) {
const detail = completed.stderr || completed.stdout || `exit ${completed.status}`;
Expand Down Expand Up @@ -145,8 +158,11 @@ export function main() {
if (!repositoryPattern.test(repository)) {
throw new Error("GITHUB_REPOSITORY must identify a ContextualWisdomLab repository.");
}
if (!process.env.GH_TOKEN) {
throw new Error("GH_TOKEN is required for the governance audit.");
}
const endpoint = `repos/${repository}/rules/branches/main?per_page=100`;
const pages = runGhJson(["api", "--paginate", "--slurp", endpoint]);
const pages = runGhJson(["--paginate", "--slurp", endpoint]);
const rules = flattenRulePages(pages);
report = buildReport(repository, rules, evaluateMainGovernanceRules(rules));
} catch (error) {
Expand Down
Loading
Loading