mirror of
https://github.com/Heretek-AI/openrig.git
synced 2026-11-07 11:06:04 -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.
338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
const roots: string[] = [];
|
|
const REPO_ROOT = resolve(import.meta.dirname, "../../..");
|
|
|
|
afterEach(() => {
|
|
for (const root of roots.splice(0)) {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe("control-plane JSON generator", () => {
|
|
it("exposes the daemon-workspace generation command without a root YAML dependency", () => {
|
|
const daemonPackage = JSON.parse(
|
|
readFileSync(join(REPO_ROOT, "packages/daemon/package.json"), "utf8"),
|
|
);
|
|
const rootPackage = JSON.parse(
|
|
readFileSync(join(REPO_ROOT, "package.json"), "utf8"),
|
|
);
|
|
|
|
expect(daemonPackage.scripts["gen:control-plane-json"]).toBe(
|
|
"node scripts/gen-control-plane-json.mjs",
|
|
);
|
|
expect(daemonPackage.dependencies.yaml).toBeTruthy();
|
|
expect(rootPackage.dependencies?.yaml).toBeUndefined();
|
|
expect(rootPackage.devDependencies?.yaml).toBeUndefined();
|
|
});
|
|
|
|
it("reads product JSON and preserves authority bytes while refreshing digests", async () => {
|
|
const generator = await loadGenerator();
|
|
const root = tempRoot();
|
|
const input = seedGeneratorInput(root);
|
|
seedEdges(root);
|
|
const sourcePaths = [input.membershipPath, input.layoutPath];
|
|
const sourceBytes = sourcePaths.map((path) => readFileSync(path));
|
|
|
|
await generator.generateControlPlaneJson(input);
|
|
const first = readGenerated(input.outputDir);
|
|
await generator.generateControlPlaneJson(input);
|
|
expect(readGenerated(input.outputDir)).toEqual(first);
|
|
expect(sourcePaths.map((path) => readFileSync(path))).toEqual(sourceBytes);
|
|
expect(first.membership.product_public.clean).toEqual(["alpha"]);
|
|
expect(first.layout.skills.alpha).toEqual({ edges: ["canonical", "plugin", "spec"], category: "core" });
|
|
expect(first.digests.edges.plugin["alpha/SKILL.md"]).toBe(sha256("# Plugin alpha\n"));
|
|
});
|
|
|
|
it("exact-tree extraction ignores illustrative layout and applies only forward_overrides", async () => {
|
|
const generator = await loadGenerator();
|
|
const root = tempRoot();
|
|
seedEdges(root);
|
|
|
|
const config = {
|
|
version: 0,
|
|
owner: "skills-architect@example",
|
|
edges: {
|
|
spec: {
|
|
path: "packages/daemon/specs/agents/shared/skills",
|
|
layout: "categorized",
|
|
},
|
|
canonical: {
|
|
path: "skills/_canonical",
|
|
layout: "mirror-of-spec",
|
|
},
|
|
plugin: {
|
|
path: "packages/daemon/assets/plugins/openrig-core/skills",
|
|
layout: "flat",
|
|
},
|
|
},
|
|
extract_from_committed_trees: true,
|
|
forward_overrides: {
|
|
future: { edges: ["spec", "plugin"], category: "process" },
|
|
},
|
|
reference_layout_da101b29: {
|
|
spec_categorized: { pm: ["alpha"], core: ["stale-only"] },
|
|
plugin_flat: ["stale-only"],
|
|
},
|
|
};
|
|
|
|
const layout = await generator.extractSkillEdgeLayout({
|
|
repoRoot: root,
|
|
config,
|
|
});
|
|
|
|
expect(layout.skills.alpha).toEqual({
|
|
edges: ["canonical", "plugin", "spec"],
|
|
category: "core",
|
|
});
|
|
expect(layout.skills["stale-only"]).toBeUndefined();
|
|
expect(layout.skills.future).toEqual({
|
|
edges: ["canonical", "plugin", "spec"],
|
|
category: "process",
|
|
});
|
|
});
|
|
|
|
it("projects spec overrides onto the canonical mirror edge", async () => {
|
|
const generator = await loadGenerator();
|
|
const root = tempRoot();
|
|
seedEdges(root);
|
|
|
|
const layout = await generator.extractSkillEdgeLayout({
|
|
repoRoot: root,
|
|
config: {
|
|
version: 0,
|
|
owner: "skills-architect@example",
|
|
edges: edgeConfig(),
|
|
extract_from_committed_trees: true,
|
|
forward_overrides: {
|
|
future: { edges: ["spec"], category: "process" },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(layout.skills.future).toEqual({
|
|
edges: ["canonical", "spec"],
|
|
category: "process",
|
|
});
|
|
});
|
|
|
|
it("removes an empty-edge override from the generated product layout", async () => {
|
|
const generator = await loadGenerator();
|
|
const root = tempRoot();
|
|
seedEdges(root);
|
|
|
|
const layout = await generator.extractSkillEdgeLayout({
|
|
repoRoot: root,
|
|
config: {
|
|
version: 0,
|
|
owner: "skills-architect@example",
|
|
edges: edgeConfig(),
|
|
extract_from_committed_trees: true,
|
|
forward_overrides: {
|
|
alpha: { edges: [], category: null },
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(layout.skills.alpha).toBeUndefined();
|
|
});
|
|
|
|
it("rejects malformed forward overrides with the source path and reason", async () => {
|
|
const generator = await loadGenerator();
|
|
const root = tempRoot();
|
|
seedEdges(root);
|
|
|
|
await expect(
|
|
generator.extractSkillEdgeLayout({
|
|
repoRoot: root,
|
|
sourcePath: "/canon/conventions/skill-edge-layout.yaml",
|
|
config: {
|
|
version: 0,
|
|
edges: edgeConfig(),
|
|
extract_from_committed_trees: true,
|
|
forward_overrides: {
|
|
broken: { edges: ["spec"], category: "not-a-category" },
|
|
},
|
|
},
|
|
}),
|
|
).rejects.toThrow(
|
|
/skill-edge-layout\.yaml.*broken.*category|broken.*category.*skill-edge-layout\.yaml/i,
|
|
);
|
|
});
|
|
|
|
it("reports malformed product policy with its product path", async () => {
|
|
const generator = await loadGenerator();
|
|
const input = seedGeneratorInput(tempRoot());
|
|
write(input.membershipPath, JSON.stringify({ version: 0 }));
|
|
await expect(generator.generateControlPlaneJson(input)).rejects.toThrow(/product-public-skills\.generated\.json.*product_public/);
|
|
});
|
|
|
|
it("refuses a denylist input: the leak guard's term lists come from a local file", async () => {
|
|
const generator = await loadGenerator();
|
|
const input = seedGeneratorInput(tempRoot());
|
|
await expect(generator.generateControlPlaneJson({ ...input, denylistPath: join(input.repoRoot, "denylist.json") }))
|
|
.rejects.toThrow(/OPENRIG_LEAK_GUARD_RULES/);
|
|
});
|
|
|
|
it("rejects edge file symlinks before digesting outside-root bytes", async () => {
|
|
const generator = await loadGenerator();
|
|
const root = tempRoot();
|
|
const input = seedGeneratorInput(root);
|
|
seedEdges(root);
|
|
const outside = join(root, "outside-file.txt");
|
|
write(outside, "outside edge bytes\n");
|
|
const link = join(
|
|
root,
|
|
"packages/daemon/assets/plugins/openrig-core/skills/alpha/references/linked.txt",
|
|
);
|
|
mkdirSync(dirname(link), { recursive: true });
|
|
symlinkSync(outside, link, "file");
|
|
|
|
await expect(
|
|
generator.generateControlPlaneJson(input),
|
|
).rejects.toThrow(/symlink.*linked\.txt|linked\.txt.*symlink/i);
|
|
expect(
|
|
existsSync(join(input.outputDir, "skill-edge-digests.generated.json")),
|
|
).toBe(false);
|
|
});
|
|
|
|
it("rejects edge directory symlinks before traversal or digesting outside-root bytes", async () => {
|
|
const generator = await loadGenerator();
|
|
const root = tempRoot();
|
|
const input = seedGeneratorInput(root);
|
|
seedEdges(root);
|
|
const outside = join(root, "outside-directory");
|
|
write(join(outside, "secret.txt"), "outside directory bytes\n");
|
|
const link = join(
|
|
root,
|
|
"skills/_canonical/core/alpha/references/linked-directory",
|
|
);
|
|
mkdirSync(dirname(link), { recursive: true });
|
|
symlinkSync(outside, link, "dir");
|
|
|
|
await expect(
|
|
generator.generateControlPlaneJson(input),
|
|
).rejects.toThrow(/symlink.*linked-directory|linked-directory.*symlink/i);
|
|
expect(
|
|
existsSync(join(input.outputDir, "skill-edge-digests.generated.json")),
|
|
).toBe(false);
|
|
});
|
|
});
|
|
|
|
async function loadGenerator(): Promise<Record<string, any>> {
|
|
const url = pathToFileURL(
|
|
join(REPO_ROOT, "packages/daemon/scripts/gen-control-plane-json.mjs"),
|
|
).href;
|
|
const loaded = await import(url).catch(() => null);
|
|
expect(loaded, "daemon control-plane JSON generator must exist").not.toBeNull();
|
|
expect(typeof loaded?.generateControlPlaneJson).toBe("function");
|
|
expect(typeof loaded?.extractSkillEdgeLayout).toBe("function");
|
|
return loaded as Record<string, any>;
|
|
}
|
|
|
|
function tempRoot(): string {
|
|
const root = mkdtempSync(join(tmpdir(), "openrig-control-json-red-"));
|
|
roots.push(root);
|
|
return root;
|
|
}
|
|
|
|
function seedEdges(root: string): void {
|
|
write(
|
|
join(
|
|
root,
|
|
"packages/daemon/specs/agents/shared/skills/core/alpha/SKILL.md",
|
|
),
|
|
"# Spec alpha\n",
|
|
);
|
|
write(
|
|
join(root, "skills/_canonical/core/alpha/SKILL.md"),
|
|
"# Spec alpha\n",
|
|
);
|
|
write(
|
|
join(
|
|
root,
|
|
"packages/daemon/assets/plugins/openrig-core/skills/alpha/SKILL.md",
|
|
),
|
|
"# Plugin alpha\n",
|
|
);
|
|
write(
|
|
join(
|
|
root,
|
|
"packages/daemon/assets/plugins/openrig-core/skills/pluginOnly/SKILL.md",
|
|
),
|
|
"# Plugin only\n",
|
|
);
|
|
}
|
|
|
|
function seedGeneratorInput(root: string): {
|
|
repoRoot: string;
|
|
membershipPath: string;
|
|
layoutPath: string;
|
|
outputDir: string;
|
|
} {
|
|
const scripts = join(root, "scripts");
|
|
const membershipPath = join(scripts, "product-public-skills.generated.json");
|
|
const layoutPath = join(scripts, "skill-edge-layout.generated.json");
|
|
write(membershipPath, JSON.stringify({
|
|
version: 0,
|
|
product_public: { clean: ["alpha"], ship_after_fix: [], ship_misses_add: [], sanitize_borderlines_ship: [] },
|
|
vendored_ship_with_provenance: [], not_public: {}, pending_author_public: [],
|
|
}));
|
|
write(layoutPath, JSON.stringify({
|
|
version: 0, edges: edgeConfig(),
|
|
skills: { alpha: { edges: ["canonical", "plugin", "spec"], category: "core" } },
|
|
}));
|
|
return { repoRoot: root, membershipPath, layoutPath, outputDir: join(root, "export") };
|
|
}
|
|
|
|
function edgeConfig(): Record<string, unknown> {
|
|
return {
|
|
spec: {
|
|
path: "packages/daemon/specs/agents/shared/skills",
|
|
layout: "categorized",
|
|
},
|
|
canonical: {
|
|
path: "skills/_canonical",
|
|
layout: "mirror-of-spec",
|
|
},
|
|
plugin: {
|
|
path: "packages/daemon/assets/plugins/openrig-core/skills",
|
|
layout: "flat",
|
|
},
|
|
};
|
|
}
|
|
|
|
function readGenerated(output: string): Record<string, any> {
|
|
return {
|
|
membership: readJson(join(output, "product-public-skills.generated.json")),
|
|
layout: readJson(join(output, "skill-edge-layout.generated.json")),
|
|
digests: readJson(join(output, "skill-edge-digests.generated.json")),
|
|
};
|
|
}
|
|
|
|
function readJson(path: string): any {
|
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
}
|
|
|
|
function write(path: string, content: string): void {
|
|
mkdirSync(dirname(path), { recursive: true });
|
|
writeFileSync(path, content);
|
|
}
|
|
|
|
function sha256(value: string): string {
|
|
return createHash("sha256").update(value).digest("hex");
|
|
}
|