feat: add scan-all attestation

This commit is contained in:
artale 2026-06-23 11:05:27 +02:00
parent ebf1c7eddd
commit 77836ac7fe
4 changed files with 82 additions and 19 deletions

View File

@ -57,9 +57,11 @@ fable-agent plinius godmode "improve explanation quality"
- `verify <target>`: verify a target and emit an agentic attestation
- `--out <path>`
- `--run <id>`
- `--scan-all`
- `attest <target>`: alias for `verify`
- `--out <path>`
- `--run <id>`
- `--scan-all`
### `run`
@ -212,9 +214,11 @@ fable-agent plinius godmode "improve explanation quality"
- `factory verify <target>`: verify a target and emit an agentic attestation
- `--out <path>`
- `--run <id>`
- `--scan-all`
- `factory attest <target>`: alias for `factory verify`; emits an agentic attestation
- `--out <path>`
- `--run <id>`
- `--scan-all`
- `factory deploy <command>`
- `--token <token>`
- `--gate-receipt <path>`

View File

@ -7,7 +7,7 @@ import { verifyTarget } from "./attestation.js";
function okRunner(changed: string) {
return (command: string, args: string[]) => {
const full = [command, ...args].join(" ");
if (full === "git status --short") return { status: 0, stdout: ` M ${changed}\n`, stderr: "" };
if (full === "git status --short") return { status: 0, stdout: changed ? ` M ${changed}\n` : "", stderr: "" };
if (full === "npm run -s build") return { status: 0, stdout: "", stderr: "" };
if (full === "npm run -s test") return { status: 0, stdout: "", stderr: "" };
if (full === "npm run -s docs:check") return { status: 0, stdout: "", stderr: "" };
@ -15,16 +15,20 @@ function okRunner(changed: string) {
};
}
function packageJson(dir: string) {
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ scripts: { build: "tsc", test: "vitest", "docs:check": "node x" } }));
}
describe("agentic attestation", () => {
it("emits a typed verified receipt for changed files", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "attest-"));
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ scripts: { build: "tsc", test: "vitest", "docs:check": "node x" } }));
packageJson(dir);
const changed = path.join(dir, "src", "fable5", "safe.ts");
fs.mkdirSync(path.dirname(changed), { recursive: true });
fs.writeFileSync(changed, "export const add = (a: number, b: number) => a + b;\n");
const out = path.join(dir, "receipt.json");
const receipt = verifyTarget({ target: dir, out, runner: okRunner(changed), now: new Date("2026-06-21T00:00:00.000Z") });
const receipt = verifyTarget({ target: dir, out, runner: okRunner(changed), now: new Date("2026-06-21T00:00:00.000Z"), memoryCachePath: path.join(dir, ".cache.json") });
expect(receipt.schema).toBe("fable.verification.attestation.v1");
expect(receipt.deploy_route).toBe("8099/deploy via git-proxy");
@ -34,12 +38,26 @@ describe("agentic attestation", () => {
expect(JSON.parse(fs.readFileSync(out, "utf-8")).schema).toBe("fable.verification.attestation.v1");
});
it("scanAll verifies a clean repo target by scanning all files", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "attest-all-"));
packageJson(dir);
fs.writeFileSync(path.join(dir, "safe.ts"), "export const ok = true;\n");
fs.mkdirSync(path.join(dir, "TEMP"));
fs.writeFileSync(path.join(dir, "TEMP", "glyph-risk.txt"), "\uE000\n");
const receipt = verifyTarget({ target: dir, scanAll: true, runner: okRunner(""), now: new Date("2026-06-21T00:00:00.000Z"), memoryCachePath: path.join(dir, ".cache.json") });
expect(receipt.verification_status).toBe("verified");
expect(receipt.checks.find((c) => c.name === "target_files")?.detail).toContain("scannable total file(s)");
expect(receipt.memory_b_cells.length).toBe(1);
});
it("requires review for risky capabilities without treating review as verification failure", () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "attest-risk-"));
const file = path.join(dir, "index.ts");
fs.writeFileSync(file, "import { execSync } from 'node:child_process'; execSync('echo hi');\n");
const receipt = verifyTarget({ target: file, runner: okRunner(file), now: new Date("2026-06-21T00:00:00.000Z") });
const receipt = verifyTarget({ target: file, runner: okRunner(file), now: new Date("2026-06-21T00:00:00.000Z"), memoryCachePath: path.join(dir, ".cache.json") });
expect(receipt.verification_status).toBe("partial");
expect(receipt.risk_findings.some((f) => f.kind === "shell_exec")).toBe(true);

View File

@ -44,6 +44,7 @@ export interface VerifyTargetOptions {
now?: Date;
requestedBy?: "human" | "agent" | "ci";
memoryCachePath?: string;
scanAll?: boolean;
runner?: (command: string, args: string[], cwd?: string) => { status: number | null; stdout: string; stderr: string };
}
@ -59,8 +60,12 @@ export function verifyTarget(opts: VerifyTargetOptions): VerificationAttestation
const changed = runGitStatus(runner);
checks.push(commandCheck("git_status", "git status --short", 0, changed.status, changed.stderr || `${changed.files.length} changed file(s)`));
const files = collectFiles(target, changed.files);
checks.push({ name: "target_files", status: files.length > 0 ? "passed" : "skipped", detail: `${files.length} scannable changed file(s)` });
const files = collectFiles(target, changed.files, opts.scanAll ?? false);
checks.push({
name: "target_files",
status: files.length > 0 ? "passed" : "skipped",
detail: `${files.length} scannable ${opts.scanAll ? "total" : "changed"} file(s)`,
});
for (const file of files) {
const content = fs.readFileSync(file, "utf-8");
@ -74,8 +79,16 @@ export function verifyTarget(opts: VerifyTargetOptions): VerificationAttestation
}
}
checks.push({ name: "glyph_legibility", status: risk_findings.some((f) => f.kind === "human_legibility") ? "failed" : "passed" });
checks.push({ name: "memory_b_cell_cache", status: "passed", detail: `${memory_b_cells.filter((c) => c.encounter === "cache_hit").length} hit(s), ${memory_b_cells.filter((c) => c.encounter === "first_encounter").length} first encounter(s)` });
checks.push({ name: "ast_capabilities", status: "passed", detail: `${risk_findings.filter((f) => ["shell_exec", "network", "fs_write", "eval_like", "memory_b_cell"].includes(f.kind)).length} capability finding(s)` });
checks.push({
name: "memory_b_cell_cache",
status: "passed",
detail: `${memory_b_cells.filter((c) => c.encounter === "cache_hit").length} hit(s), ${memory_b_cells.filter((c) => c.encounter === "first_encounter").length} first encounter(s)`,
});
checks.push({
name: "ast_capabilities",
status: "passed",
detail: `${risk_findings.filter((f) => ["shell_exec", "network", "fs_write", "eval_like", "memory_b_cell"].includes(f.kind)).length} capability finding(s); review required but not a local verification failure`,
});
const pkg = findPackageJson(target);
if (pkg) {
@ -116,18 +129,35 @@ export function writeAttestation(file: string, attestation: VerificationAttestat
fs.writeFileSync(file, `${JSON.stringify(attestation, null, 2)}\n`);
}
function collectFiles(target: string, changed: string[]): string[] {
function collectFiles(target: string, changed: string[], scanAll: boolean): string[] {
const accept = (file: string) => /\.(ts|tsx|js|jsx|md|txt|json|yaml|yml)$/i.test(file);
if (fs.statSync(target).isFile()) return accept(target) ? [target] : [];
const root = path.resolve(target);
if (scanAll) return walkFiles(root, accept);
const changedFiles = changed
.map((f) => f.trim().replace(/^[A-Z? ]+\s+/, ""))
.map((f) => path.resolve(f))
.filter((f) => f.startsWith(root) && fs.existsSync(f) && fs.statSync(f).isFile() && accept(f));
// ponytail: repo verify scans changed files; use a file target for deep single-file audit.
return [...new Set(changedFiles)].sort();
}
function walkFiles(root: string, accept: (file: string) => boolean): string[] {
const out: string[] = [];
const walk = (file: string) => {
const st = fs.statSync(file);
if (st.isDirectory()) {
for (const name of fs.readdirSync(file)) {
if ([".git", "node_modules", "dist", ".fable", "LOGS", "TEMP"].includes(name)) continue;
walk(path.join(file, name));
}
return;
}
if (accept(file)) out.push(file);
};
walk(root);
return out.sort();
}
function addAstFindings(report: AstSafetyReport, findings: RiskFinding[]): void {
if (report.shell_exec) findings.push({ kind: "shell_exec", severity: "high", file: report.file, detail: "shell execution capability present" });
if (report.network) findings.push({ kind: "network", severity: "medium", file: report.file, detail: "network capability present" });
@ -138,7 +168,14 @@ function addAstFindings(report: AstSafetyReport, findings: RiskFinding[]): void
function addMemoryFindings(cell: MemoryBCellResult, findings: RiskFinding[]): void {
const kinds = ["shell_exec", "network", "fs_write", "eval_like", "private_glyphs"].filter((k) => cell.reason.includes(k));
for (const kind of kinds.length > 0 ? kinds : ["memory_b_cell"]) findings.push({ kind, severity: kind === "network" || kind === "fs_write" ? "medium" : "high", file: cell.file, detail: `memory B cell cache hit: ${cell.reason}` });
for (const kind of kinds.length > 0 ? kinds : ["memory_b_cell"]) {
findings.push({
kind,
severity: kind === "network" || kind === "fs_write" ? "medium" : "high",
file: cell.file,
detail: `memory B cell cache marks this pattern unsafe: ${cell.reason}`,
});
}
}
function reviewReasonsFor(findings: RiskFinding[], changed: string[]): string[] {

View File

@ -28,11 +28,12 @@ program
.description("Verify a target and emit an agentic attestation")
.option("--out <path>", "Attestation output path")
.option("--run <id>", "Emit attestation event to .runs/<id>/channel.jsonl")
.action(async (target: string, opts: { out?: string; run?: string }) => {
.option("--scan-all", "Scan every scannable file under the target, not just changed files")
.action(async (target: string, opts: { out?: string; run?: string; scanAll?: boolean }) => {
const { verifyTarget } = await import("./fable5/attestation.js");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const out = opts.out ?? path.join(".fable", "attestations", stamp + ".json");
const receipt = verifyTarget({ target, out });
const receipt = verifyTarget({ target, out, scanAll: opts.scanAll });
console.log(JSON.stringify(receipt, null, 2));
console.log("\n Attestation: " + out + "\n");
await emitRunEvent(opts.run, { source: "gate", type: receipt.verification_status === "failed" ? "error" : "receipt", data: { kind: "agentic-attestation", receipt, path: out } });
@ -44,11 +45,12 @@ program
.description("Alias for verify: emit an agentic attestation")
.option("--out <path>", "Attestation output path")
.option("--run <id>", "Emit attestation event to .runs/<id>/channel.jsonl")
.action(async (target: string, opts: { out?: string; run?: string }) => {
.option("--scan-all", "Scan every scannable file under the target, not just changed files")
.action(async (target: string, opts: { out?: string; run?: string; scanAll?: boolean }) => {
const { verifyTarget } = await import("./fable5/attestation.js");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const out = opts.out ?? path.join(".fable", "attestations", stamp + ".json");
const receipt = verifyTarget({ target, out });
const receipt = verifyTarget({ target, out, scanAll: opts.scanAll });
console.log(JSON.stringify(receipt, null, 2));
console.log("\n Attestation: " + out + "\n");
await emitRunEvent(opts.run, { source: "gate", type: receipt.verification_status === "failed" ? "error" : "receipt", data: { kind: "agentic-attestation", receipt, path: out } });
@ -1584,11 +1586,12 @@ factory
.description("Verify a target and emit an agentic attestation")
.option("--out <path>", "Attestation output path")
.option("--run <id>", "Emit attestation event to .runs/<id>/channel.jsonl")
.action(async (target: string, opts: { out?: string; run?: string }) => {
.option("--scan-all", "Scan every scannable file under the target, not just changed files")
.action(async (target: string, opts: { out?: string; run?: string; scanAll?: boolean }) => {
const { verifyTarget } = await import("./fable5/attestation.js");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const out = opts.out ?? path.join(".fable", "attestations", stamp + ".json");
const receipt = verifyTarget({ target, out });
const receipt = verifyTarget({ target, out, scanAll: opts.scanAll });
console.log(JSON.stringify(receipt, null, 2));
console.log("\n Attestation: " + out + "\n");
await emitRunEvent(opts.run, { source: "gate", type: receipt.verification_status === "failed" ? "error" : "receipt", data: { kind: "agentic-attestation", receipt, path: out } });
@ -1600,11 +1603,12 @@ factory
.description("Alias for factory verify: emit an agentic attestation")
.option("--out <path>", "Attestation output path")
.option("--run <id>", "Emit attestation event to .runs/<id>/channel.jsonl")
.action(async (target: string, opts: { out?: string; run?: string }) => {
.option("--scan-all", "Scan every scannable file under the target, not just changed files")
.action(async (target: string, opts: { out?: string; run?: string; scanAll?: boolean }) => {
const { verifyTarget } = await import("./fable5/attestation.js");
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
const out = opts.out ?? path.join(".fable", "attestations", stamp + ".json");
const receipt = verifyTarget({ target, out });
const receipt = verifyTarget({ target, out, scanAll: opts.scanAll });
console.log(JSON.stringify(receipt, null, 2));
console.log("\n Attestation: " + out + "\n");
await emitRunEvent(opts.run, { source: "gate", type: receipt.verification_status === "failed" ? "error" : "receipt", data: { kind: "agentic-attestation", receipt, path: out } });