From 6872cb3a6d09e6736880d518bdb44598dac2bea7 Mon Sep 17 00:00:00 2001 From: artale Date: Wed, 1 Jul 2026 14:55:18 +0200 Subject: [PATCH] feat: add external project review receipts --- COMMANDS.md | 5 ++ src/fable5/external-project-review.test.ts | 56 +++++++++++++ src/fable5/external-project-review.ts | 96 ++++++++++++++++++++++ src/fable5/index.ts | 3 + src/index.ts | 32 ++++++++ 5 files changed, 192 insertions(+) create mode 100644 src/fable5/external-project-review.test.ts create mode 100644 src/fable5/external-project-review.ts diff --git a/COMMANDS.md b/COMMANDS.md index 35435ed..99facc6 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -386,6 +386,11 @@ fable-agent plinius godmode "improve explanation quality" - `--output ` - `fable5 context-repo-snapshot ` - `--output ` +- `fable5 opencoven-review` + - `--output ` +- `fable5 permission-posture` + - `--mode ` + - `--output ` - `fable5 receipt-health` - `--root ` - `--window-minutes ` diff --git a/src/fable5/external-project-review.test.ts b/src/fable5/external-project-review.test.ts new file mode 100644 index 0000000..a77031f --- /dev/null +++ b/src/fable5/external-project-review.test.ts @@ -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 }); + }); +}); diff --git a/src/fable5/external-project-review.ts b/src/fable5/external-project-review.ts new file mode 100644 index 0000000..d8d722c --- /dev/null +++ b/src/fable5/external-project-review.ts @@ -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> = { + "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; +} diff --git a/src/fable5/index.ts b/src/fable5/index.ts index 796cd03..c6e2a69 100644 --- a/src/fable5/index.ts +++ b/src/fable5/index.ts @@ -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"; diff --git a/src/index.ts b/src/index.ts index 306d755..5e3c714 100644 --- a/src/index.ts +++ b/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 ", "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 ", "read-only|plan|edit|deploy-gated", "read-only") + .option("--output ", "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")