feat: add external project review receipts
This commit is contained in:
parent
208d27e2cf
commit
6872cb3a6d
|
|
@ -386,6 +386,11 @@ fable-agent plinius godmode "improve explanation quality"
|
|||
- `--output <path>`
|
||||
- `fable5 context-repo-snapshot <root>`
|
||||
- `--output <path>`
|
||||
- `fable5 opencoven-review`
|
||||
- `--output <path>`
|
||||
- `fable5 permission-posture`
|
||||
- `--mode <mode>`
|
||||
- `--output <path>`
|
||||
- `fable5 receipt-health`
|
||||
- `--root <path>`
|
||||
- `--window-minutes <n>`
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createOpenCovenReviewReceipt, createPermissionPostureReceipt, writeExternalProjectReviewReceipt } from "./external-project-review.js";
|
||||
|
||||
describe("external project review receipts", () => {
|
||||
it("records OpenCoven as learn-only because GPL code is not safe to vendor", () => {
|
||||
const receipt = createOpenCovenReviewReceipt(new Date("2026-07-01T00:00:00.000Z"));
|
||||
|
||||
expect(receipt).toMatchObject({
|
||||
schema: "fable.external_project.review.v1",
|
||||
repo: "OpenCoven/coven-code",
|
||||
license: "GPL-3.0",
|
||||
safeToVendor: false,
|
||||
decision: "learn-only",
|
||||
policy: "learn-patterns-no-vendored-code",
|
||||
deployAttempted: false,
|
||||
});
|
||||
expect(receipt.patternsLearned).toContain("explicit permission modes");
|
||||
expect(receipt.risks).toContain("GPL code cannot be copied into Fable");
|
||||
});
|
||||
|
||||
it("records deploy-gated permission posture without opening 8098", () => {
|
||||
const receipt = createPermissionPostureReceipt("deploy-gated", new Date("2026-07-01T00:00:00.000Z"));
|
||||
|
||||
expect(receipt).toMatchObject({
|
||||
schema: "fable.permission_posture.receipt.v1",
|
||||
mode: "deploy-gated",
|
||||
approvalRequired: true,
|
||||
decision: "ready",
|
||||
deployAttempted: false,
|
||||
});
|
||||
expect(receipt.allowed[0]).toContain("8099/deploy");
|
||||
expect(receipt.blocked).toContain("8098/deploy-webhook");
|
||||
expect(receipt.blocked).toContain("memory-only deploy claims");
|
||||
});
|
||||
|
||||
it("keeps read-only mode non-mutating", () => {
|
||||
const receipt = createPermissionPostureReceipt("read-only");
|
||||
|
||||
expect(receipt.allowed).toContain("emit non-mutating receipts");
|
||||
expect(receipt.blocked).toContain("deploy");
|
||||
expect(receipt.approvalRequired).toBe(false);
|
||||
});
|
||||
|
||||
it("writes receipts", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "external-project-review-"));
|
||||
const file = path.join(dir, "receipt.json");
|
||||
|
||||
writeExternalProjectReviewReceipt(file, createOpenCovenReviewReceipt());
|
||||
|
||||
expect(JSON.parse(fs.readFileSync(file, "utf-8"))).toMatchObject({ schema: "fable.external_project.review.v1" });
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
export type ExternalProjectDecision = "learn-only" | "blocked";
|
||||
export type PermissionMode = "read-only" | "plan" | "edit" | "deploy-gated";
|
||||
|
||||
export interface ExternalProjectReviewReceipt {
|
||||
schema: "fable.external_project.review.v1";
|
||||
createdAt: string;
|
||||
repo: string;
|
||||
url: string;
|
||||
license: string;
|
||||
safeToVendor: boolean;
|
||||
patternsLearned: string[];
|
||||
adaptations: string[];
|
||||
risks: string[];
|
||||
decision: ExternalProjectDecision;
|
||||
policy: "learn-patterns-no-vendored-code";
|
||||
deployAttempted: false;
|
||||
}
|
||||
|
||||
export interface PermissionPostureReceipt {
|
||||
schema: "fable.permission_posture.receipt.v1";
|
||||
createdAt: string;
|
||||
mode: PermissionMode;
|
||||
allowed: string[];
|
||||
blocked: string[];
|
||||
approvalRequired: boolean;
|
||||
decision: "ready" | "blocked";
|
||||
deployAttempted: false;
|
||||
}
|
||||
|
||||
export function createOpenCovenReviewReceipt(now = new Date()): ExternalProjectReviewReceipt {
|
||||
return {
|
||||
schema: "fable.external_project.review.v1",
|
||||
createdAt: now.toISOString(),
|
||||
repo: "OpenCoven/coven-code",
|
||||
url: "https://github.com/OpenCoven/coven-code",
|
||||
license: "GPL-3.0",
|
||||
safeToVendor: false,
|
||||
patternsLearned: [
|
||||
"local-first agent runtime",
|
||||
"explicit permission modes",
|
||||
"session resume and branching",
|
||||
"agent identity contract",
|
||||
"plugin and bridge seams separated from authority",
|
||||
"no-telemetry-by-default posture",
|
||||
],
|
||||
adaptations: [
|
||||
"record external reviews as receipts",
|
||||
"make permission posture explicit",
|
||||
"keep adapters below CLI and receipts as authority",
|
||||
"treat GPL projects as learn-only unless legal review says otherwise",
|
||||
],
|
||||
risks: ["GPL code cannot be copied into Fable", "remote bridge or MCP must not become deploy authority", "plugin systems add attack surface"],
|
||||
decision: "learn-only",
|
||||
policy: "learn-patterns-no-vendored-code",
|
||||
deployAttempted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function createPermissionPostureReceipt(mode: PermissionMode, now = new Date()): PermissionPostureReceipt {
|
||||
const rows: Record<PermissionMode, Omit<PermissionPostureReceipt, "schema" | "createdAt" | "mode" | "deployAttempted">> = {
|
||||
"read-only": {
|
||||
allowed: ["read receipts", "inspect files", "summarize repositories", "emit non-mutating receipts"],
|
||||
blocked: ["write code", "mutate factory", "deploy", "use secrets"],
|
||||
approvalRequired: false,
|
||||
decision: "ready",
|
||||
},
|
||||
plan: {
|
||||
allowed: ["read receipts", "write plan receipts", "propose diffs"],
|
||||
blocked: ["apply diffs", "deploy", "mutate factory"],
|
||||
approvalRequired: false,
|
||||
decision: "ready",
|
||||
},
|
||||
edit: {
|
||||
allowed: ["write code", "run tests", "write receipts"],
|
||||
blocked: ["deploy", "mutate factory without gate", "use secrets unless explicitly approved"],
|
||||
approvalRequired: true,
|
||||
decision: "ready",
|
||||
},
|
||||
"deploy-gated": {
|
||||
allowed: ["deploy only through guarded 8099/deploy with fresh valid gate receipt and explicit approval"],
|
||||
blocked: ["8098/deploy-webhook", "memory-only deploy claims", "deploy without fresh ZTE/factory gate"],
|
||||
approvalRequired: true,
|
||||
decision: "ready",
|
||||
},
|
||||
};
|
||||
return { schema: "fable.permission_posture.receipt.v1", createdAt: now.toISOString(), mode, ...rows[mode], deployAttempted: false };
|
||||
}
|
||||
|
||||
export function writeExternalProjectReviewReceipt(file: string, receipt: ExternalProjectReviewReceipt | PermissionPostureReceipt): string {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
||||
return file;
|
||||
}
|
||||
|
|
@ -63,6 +63,9 @@ export type { FlueInteropRow, FlueInteropStatus } from "./flue-interop.js";
|
|||
export { createExternalToolAuditReceipt, DICKLESWORTHSTONE_TOOL_ROWS, writeExternalToolAuditReceipt } from "./external-tool-audit.js";
|
||||
export type { ExternalToolAuditReceipt, ExternalToolAuditRow, ExternalToolAuditStatus } from "./external-tool-audit.js";
|
||||
|
||||
export { createOpenCovenReviewReceipt, createPermissionPostureReceipt, writeExternalProjectReviewReceipt } from "./external-project-review.js";
|
||||
export type { ExternalProjectDecision, ExternalProjectReviewReceipt, PermissionMode, PermissionPostureReceipt } from "./external-project-review.js";
|
||||
|
||||
export { channelPath, emitChannelEvent, readChannelEvents } from "./channel.js";
|
||||
export type { ChannelEvent, ChannelEventSource, ChannelEventType, NewChannelEvent } from "./channel.js";
|
||||
|
||||
|
|
|
|||
32
src/index.ts
32
src/index.ts
|
|
@ -2272,6 +2272,38 @@ fable
|
|||
`);
|
||||
});
|
||||
|
||||
fable
|
||||
.command("opencoven-review")
|
||||
.description("Write a learn-only OpenCoven/coven-code external project review receipt")
|
||||
.option("--output <path>", "Receipt output path", path.join(".fable", "research", "opencoven-review-live.json"))
|
||||
.action(async (opts: { output?: string }) => {
|
||||
const { createOpenCovenReviewReceipt, writeExternalProjectReviewReceipt } = await import("./fable5/external-project-review.js");
|
||||
const receipt = createOpenCovenReviewReceipt();
|
||||
const out = opts.output ?? path.join(".fable", "research", "opencoven-review-live.json");
|
||||
writeExternalProjectReviewReceipt(out, receipt);
|
||||
console.log(JSON.stringify(receipt, null, 2));
|
||||
console.log(`
|
||||
Receipt: ${out}
|
||||
`);
|
||||
});
|
||||
|
||||
fable
|
||||
.command("permission-posture")
|
||||
.description("Write an explicit permission posture receipt")
|
||||
.option("--mode <mode>", "read-only|plan|edit|deploy-gated", "read-only")
|
||||
.option("--output <path>", "Receipt output path", path.join(".fable", "safety", "permission-posture-live.json"))
|
||||
.action(async (opts: { mode?: string; output?: string }) => {
|
||||
const { createPermissionPostureReceipt, writeExternalProjectReviewReceipt } = await import("./fable5/external-project-review.js");
|
||||
const receipt = createPermissionPostureReceipt((opts.mode ?? "read-only") as never);
|
||||
const out = opts.output ?? path.join(".fable", "safety", "permission-posture-live.json");
|
||||
writeExternalProjectReviewReceipt(out, receipt);
|
||||
console.log(JSON.stringify(receipt, null, 2));
|
||||
console.log(`
|
||||
Receipt: ${out}
|
||||
`);
|
||||
if (receipt.decision === "blocked") process.exit(1);
|
||||
});
|
||||
|
||||
fable
|
||||
.command("receipt-health")
|
||||
.description("Check receipt consumer health over a recent time window")
|
||||
|
|
|
|||
Loading…
Reference in New Issue