mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
The leak guard's term lists now come from an optional local rules file instead of a file tracked in the repository. Set `OPENRIG_LEAK_GUARD_RULES` (or pass `--rules`) to an absolute path, and every reader uses it through one loader: the leak guard, `build-package.sh`, the context packs, the skill mirror and the control-plane generator. The publishing rules, the frontmatter keys and the internal-section fence stay in the repository unchanged, so mirrored skills are the same. Without the file, a contributor's `npm run build:package` still works and prints that the term checks were skipped. The release substance gate refuses to run without a rules file, so a release can't pass with the checks silently off. The tracked rules file is removed, and the tests write their own fixture rules. 16 files, +227 -244.
353 lines
12 KiB
JavaScript
353 lines
12 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
|
|
const GUARD = resolve("scripts/check-internal-leak-guard.mjs");
|
|
|
|
test("full mode scans tracked files and fails loudly on a planted leak", () => {
|
|
requireGuard();
|
|
withRepo((repo, rules) => {
|
|
write(join(repo, "README.md"), "founder-only detail\n");
|
|
git(repo, "add", "README.md");
|
|
git(repo, "commit", "-m", "tracked leak");
|
|
|
|
const result = runGuard(repo, rules, "--mode", "full");
|
|
assert.notEqual(result.status, 0);
|
|
assert.match(result.stderr, /README\.md/);
|
|
assert.match(result.stderr, /founder/);
|
|
assert.match(result.stderr, /line 1/i);
|
|
assert.match(result.stderr, /sidecar|fence|genericize|host-only/i);
|
|
});
|
|
});
|
|
|
|
test("full mode passes a clean tracked tree and ignores untracked files", () => {
|
|
requireGuard();
|
|
withRepo((repo, rules) => {
|
|
write(join(repo, "README.md"), "public detail\n");
|
|
git(repo, "add", "README.md");
|
|
git(repo, "commit", "-m", "clean");
|
|
write(join(repo, "untracked.md"), "founder\n");
|
|
|
|
const result = runGuard(repo, rules, "--mode", "full");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
});
|
|
});
|
|
|
|
test("without a rules file the term checks are skipped with a note; the file the environment names turns them on", () => {
|
|
requireGuard();
|
|
withRepo((repo, rules) => {
|
|
writeFileSync(rules, JSON.stringify({ ...fixtureRules(), charged_terms: ["private-term"] }));
|
|
write(join(repo, "README.md"), "a private-term detail\n");
|
|
git(repo, "add", "README.md");
|
|
git(repo, "commit", "-m", "tracked term");
|
|
const env = { ...process.env };
|
|
delete env.OPENRIG_LEAK_GUARD_RULES;
|
|
const guard = (extra) => spawnSync(process.execPath, [GUARD, "--repo", repo, "--mode", "full"],
|
|
{ cwd: repo, encoding: "utf8", env: { ...env, ...extra } });
|
|
|
|
const skipped = guard({});
|
|
assert.equal(skipped.status, 0, skipped.stderr);
|
|
assert.match(skipped.stderr, /No leak-guard rules file .*the term checks are skipped/);
|
|
|
|
const named = guard({ OPENRIG_LEAK_GUARD_RULES: rules });
|
|
assert.notEqual(named.status, 0);
|
|
assert.match(named.stderr, /README\.md/);
|
|
assert.match(named.stderr, /private-term/);
|
|
|
|
const missing = guard({ OPENRIG_LEAK_GUARD_RULES: join(repo, "absent.json") });
|
|
assert.notEqual(missing.status, 0);
|
|
assert.match(missing.stderr, /Rules file not found/);
|
|
});
|
|
});
|
|
|
|
test("optional exact-line rules reach the full guard and malformed values are refused", () => {
|
|
withRepo((repo, rulesPath) => {
|
|
const line = "Generic operator-agent@<seat> example";
|
|
const rules = { ...fixtureRules(), allowed_context_lines: [line] };
|
|
writeFileSync(rulesPath, JSON.stringify(rules));
|
|
write(join(repo, "README.md"), `${line}\n`);
|
|
git(repo, "add", ".");
|
|
git(repo, "commit", "-m", "generic example");
|
|
const result = runGuard(repo, rulesPath, "--mode", "full");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
|
|
for (const invalid of [null, "text", [42], [""], ["two\nlines"], ["two\rlines"]]) {
|
|
writeFileSync(rulesPath, JSON.stringify({ ...rules, allowed_context_lines: invalid }));
|
|
const rejected = runGuard(repo, rulesPath, "--mode", "full");
|
|
assert.notEqual(rejected.status, 0);
|
|
assert.match(rejected.stderr, /allowed_context_lines/);
|
|
}
|
|
});
|
|
});
|
|
|
|
test("staged mode reads index bytes rather than the worktree", () => {
|
|
requireGuard();
|
|
withRepo((repo, rules) => {
|
|
write(join(repo, "README.md"), "clean\n");
|
|
git(repo, "add", "README.md");
|
|
git(repo, "commit", "-m", "base");
|
|
|
|
write(join(repo, "README.md"), "operator-agent@kernel\n");
|
|
git(repo, "add", "README.md");
|
|
write(join(repo, "README.md"), "clean worktree replacement\n");
|
|
|
|
const result = runGuard(repo, rules, "--mode", "staged");
|
|
assert.notEqual(result.status, 0);
|
|
assert.match(result.stderr, /operator-agent@/);
|
|
assert.doesNotMatch(result.stderr, /clean worktree replacement/);
|
|
});
|
|
});
|
|
|
|
test("staged mode handles rename and deletion without reading missing worktree paths", () => {
|
|
requireGuard();
|
|
withRepo((repo, rules) => {
|
|
write(join(repo, "old.md"), "clean\n");
|
|
write(join(repo, "delete.md"), "clean\n");
|
|
git(repo, "add", ".");
|
|
git(repo, "commit", "-m", "base");
|
|
|
|
git(repo, "mv", "old.md", "renamed.md");
|
|
git(repo, "rm", "delete.md");
|
|
|
|
const result = runGuard(repo, rules, "--mode", "staged");
|
|
assert.equal(result.status, 0, result.stderr);
|
|
});
|
|
});
|
|
|
|
test("range mode scans exact committed bytes between from and to", () => {
|
|
requireGuard();
|
|
withRepo((repo, rules) => {
|
|
write(join(repo, "README.md"), "clean\n");
|
|
git(repo, "add", "README.md");
|
|
git(repo, "commit", "-m", "base");
|
|
const from = git(repo, "rev-parse", "HEAD").trim();
|
|
|
|
write(join(repo, "skills", "public", "SKILL.md"), "mm2-secret\n");
|
|
git(repo, "add", ".");
|
|
git(repo, "commit", "-m", "leak");
|
|
const to = git(repo, "rev-parse", "HEAD").trim();
|
|
write(join(repo, "README.md"), "clean uncommitted replacement\n");
|
|
|
|
const result = runGuard(
|
|
repo,
|
|
rules,
|
|
"--mode",
|
|
"range",
|
|
"--from",
|
|
from,
|
|
"--to",
|
|
to,
|
|
);
|
|
assert.notEqual(result.status, 0);
|
|
assert.match(result.stderr, /skills\/public\/SKILL\.md/);
|
|
assert.match(result.stderr, /mm2-/);
|
|
});
|
|
});
|
|
|
|
test("range mode rejects a leak from an intermediate pushed commit even when cleaned at to", () => {
|
|
requireGuard();
|
|
withRepo((repo, rules) => {
|
|
write(join(repo, "README.md"), "clean\n");
|
|
git(repo, "add", "README.md");
|
|
git(repo, "commit", "-m", "base");
|
|
const from = git(repo, "rev-parse", "HEAD").trim();
|
|
|
|
write(join(repo, "README.md"), "founder-only intermediate\n");
|
|
git(repo, "add", "README.md");
|
|
git(repo, "commit", "-m", "intermediate leak");
|
|
|
|
write(join(repo, "README.md"), "clean again\n");
|
|
git(repo, "add", "README.md");
|
|
git(repo, "commit", "-m", "clean tip");
|
|
const to = git(repo, "rev-parse", "HEAD").trim();
|
|
|
|
const result = runGuard(
|
|
repo,
|
|
rules,
|
|
"--mode",
|
|
"range",
|
|
"--from",
|
|
from,
|
|
"--to",
|
|
to,
|
|
);
|
|
assert.notEqual(result.status, 0);
|
|
assert.match(result.stderr, /README\.md/);
|
|
assert.match(result.stderr, /founder/);
|
|
});
|
|
});
|
|
|
|
test("each explicit mode refuses a missing generated rules file", () => {
|
|
requireGuard();
|
|
withRepo((repo) => {
|
|
write(join(repo, "README.md"), "clean\n");
|
|
git(repo, "add", "README.md");
|
|
git(repo, "commit", "-m", "base");
|
|
const head = git(repo, "rev-parse", "HEAD").trim();
|
|
const missing = join(repo, "missing-rules.json");
|
|
|
|
for (const args of [
|
|
["--mode", "full"],
|
|
["--mode", "staged"],
|
|
["--mode", "range", "--from", head, "--to", head],
|
|
["--mode", "tree", "--tree", repo],
|
|
]) {
|
|
const result = runGuard(repo, missing, ...args);
|
|
assert.notEqual(result.status, 0);
|
|
assert.match(result.stderr, /missing-rules\.json|rules/i);
|
|
}
|
|
});
|
|
});
|
|
|
|
test("each explicit mode refuses an empty generated rules object", () => {
|
|
requireGuard();
|
|
withRepo((repo) => {
|
|
write(join(repo, "README.md"), "clean\n");
|
|
git(repo, "add", "README.md");
|
|
git(repo, "commit", "-m", "base");
|
|
const head = git(repo, "rev-parse", "HEAD").trim();
|
|
const empty = join(repo, "empty-rules.json");
|
|
writeFileSync(empty, "{}");
|
|
|
|
for (const args of [
|
|
["--mode", "full"],
|
|
["--mode", "staged"],
|
|
["--mode", "range", "--from", head, "--to", head],
|
|
["--mode", "tree", "--tree", repo],
|
|
]) {
|
|
const result = runGuard(repo, empty, ...args);
|
|
assert.notEqual(result.status, 0);
|
|
assert.match(result.stderr, /rules|path_prefixes|invalid/i);
|
|
}
|
|
});
|
|
});
|
|
|
|
test("tree mode scans the actual packaging subtree, including an untracked seeded fixture", () => {
|
|
requireGuard();
|
|
const root = mkdtempSync(join(tmpdir(), "openrig-guard-tree-"));
|
|
const tree = join(root, "specs");
|
|
const rules = join(root, "rules.json");
|
|
try {
|
|
writeFileSync(rules, JSON.stringify(fixtureRules()));
|
|
write(join(tree, "agents/private/notes.md"), "operator-agent@kernel\n");
|
|
|
|
const refused = runGuard(root, rules, "--mode", "tree", "--tree", tree);
|
|
assert.notEqual(refused.status, 0);
|
|
assert.match(refused.stderr, /agents\/private\/notes\.md/);
|
|
assert.match(refused.stderr, /operator-agent@/);
|
|
|
|
rmSync(join(tree, "agents/private/notes.md"));
|
|
write(join(tree, "agents/private/notes.md"), "public fixture\n");
|
|
const admitted = runGuard(root, rules, "--mode", "tree", "--tree", tree);
|
|
assert.equal(admitted.status, 0, admitted.stderr);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("full artifact mode scans exactly the packager manifest and reports its file set", () => {
|
|
requireGuard();
|
|
const root = mkdtempSync(join(tmpdir(), "openrig-guard-artifact-"));
|
|
const tree = join(root, "package");
|
|
const rules = join(root, "rules.json");
|
|
const filesManifest = join(root, "artifact-files.json");
|
|
const report = join(root, "scan-report.json");
|
|
try {
|
|
writeFileSync(rules, JSON.stringify(fixtureRules()));
|
|
write(join(tree, "daemon/assets/guide.md"), "public guide\n");
|
|
write(join(tree, "package.json"), "{}\n");
|
|
write(join(tree, "not-packed.md"), "founder-only ignored staging residue\n");
|
|
writeFileSync(filesManifest, JSON.stringify({
|
|
files: ["package.json", "daemon/assets/guide.md"],
|
|
}));
|
|
|
|
const result = runGuard(
|
|
root,
|
|
rules,
|
|
"--mode", "full",
|
|
"--tree", tree,
|
|
"--files-manifest", filesManifest,
|
|
"--report", report,
|
|
);
|
|
assert.equal(result.status, 0, result.stderr);
|
|
assert.deepEqual(JSON.parse(readFileSync(report, "utf8")), {
|
|
mode: "full",
|
|
scannedFiles: ["daemon/assets/guide.md", "package.json"],
|
|
findingCount: 0,
|
|
});
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("Plain B keeps guard enforcement out of root test:repo while fixture tests remain discovered", () => {
|
|
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
|
|
const command = pkg.scripts["test:repo"];
|
|
|
|
// Packaging tests rebuild daemon/dist while other script tests import it.
|
|
assert.match(command, /node --test --test-concurrency=1 scripts\/\*\.test\.mjs/);
|
|
assert.doesNotMatch(command, /node scripts\/check-internal-leak-guard\.mjs/);
|
|
});
|
|
|
|
function withRepo(run) {
|
|
const root = mkdtempSync(join(tmpdir(), "openrig-guard-red-"));
|
|
const repo = join(root, "repo");
|
|
const rules = join(root, "rules.json");
|
|
mkdirSync(repo, { recursive: true });
|
|
writeFileSync(rules, JSON.stringify(fixtureRules()));
|
|
git(repo, "init");
|
|
git(repo, "config", "user.email", "[email protected]");
|
|
git(repo, "config", "user.name", "QA");
|
|
try {
|
|
run(repo, rules);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
function requireGuard() {
|
|
assert.equal(
|
|
existsSync(GUARD),
|
|
true,
|
|
"scripts/check-internal-leak-guard.mjs must exist",
|
|
);
|
|
}
|
|
|
|
function runGuard(repo, rules, ...args) {
|
|
return spawnSync(
|
|
process.execPath,
|
|
[GUARD, "--repo", repo, "--rules", rules, ...args],
|
|
{ cwd: repo, encoding: "utf8" },
|
|
);
|
|
}
|
|
|
|
function git(repo, ...args) {
|
|
return execFileSync("git", args, { cwd: repo, encoding: "utf8" });
|
|
}
|
|
|
|
function write(path, content) {
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, content);
|
|
}
|
|
|
|
function fixtureRules() {
|
|
return {
|
|
path_prefixes: ["openrig-work/"],
|
|
seat_and_rig_patterns: ["operator-agent@"],
|
|
host_patterns: ["mm2-"],
|
|
charged_terms: ["founder"],
|
|
internal_path_globs: ["*.internal.*", "**/internal/**", "*-internal/**"],
|
|
allowed_context_substrings: ["do not ship"],
|
|
};
|
|
}
|