diff --git a/COMMANDS.md b/COMMANDS.md index 42d8fcc..556d3a2 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -395,6 +395,8 @@ fable-agent plinius godmode "improve explanation quality" - `--output ` - `fable5 falsification` - `--output ` +- `fable5 external-rsi-evidence` + - `--output ` - `fable5 receipt-health` - `--root ` - `--window-minutes ` diff --git a/src/fable5/external-evidence.test.ts b/src/fable5/external-evidence.test.ts new file mode 100644 index 0000000..8a1f29e --- /dev/null +++ b/src/fable5/external-evidence.test.ts @@ -0,0 +1,32 @@ +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 { createExternalRsiEvidenceReceipt, writeExternalEvidenceReceipt } from "./external-evidence.js"; + +describe("external evidence receipts", () => { + it("records external RSI proof as advisory, not local deploy authority", () => { + const receipt = createExternalRsiEvidenceReceipt(new Date("2026-07-01T00:00:00.000Z")); + + expect(receipt).toMatchObject({ + schema: "fable.external_evidence.receipt.v1", + source: "conversation memory citing pi-real-engineering RSI proof", + decision: "advisory", + deployAttempted: false, + }); + expect(receipt.evidence.every((entry) => entry.scope === "external")).toBe(true); + expect(receipt.notUsableFor).toContain("local fable-agent deploy authority"); + expect(receipt.notUsableFor).toContain("auto_patch_proven=true without fresh local degraded-skill recovery receipt"); + expect(receipt.notUsableFor).toContain("8098/deploy-webhook trust"); + }); + + it("writes external evidence receipts", () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "external-evidence-")); + const file = path.join(dir, "receipt.json"); + + writeExternalEvidenceReceipt(file, createExternalRsiEvidenceReceipt()); + + expect(JSON.parse(fs.readFileSync(file, "utf-8"))).toMatchObject({ schema: "fable.external_evidence.receipt.v1", decision: "advisory" }); + fs.rmSync(dir, { recursive: true, force: true }); + }); +}); diff --git a/src/fable5/external-evidence.ts b/src/fable5/external-evidence.ts new file mode 100644 index 0000000..fba403f --- /dev/null +++ b/src/fable5/external-evidence.ts @@ -0,0 +1,48 @@ +import * as fs from "node:fs"; +import * as path from "node:path"; + +export interface ExternalEvidenceReceipt { + schema: "fable.external_evidence.receipt.v1"; + createdAt: string; + source: string; + evidence: Array<{ claim: string; reference: string; scope: "external" | "local" }>; + usableFor: string[]; + notUsableFor: string[]; + decision: "advisory" | "local-authority"; + deployAttempted: false; +} + +export function createExternalRsiEvidenceReceipt(now = new Date()): ExternalEvidenceReceipt { + return { + schema: "fable.external_evidence.receipt.v1", + createdAt: now.toISOString(), + source: "conversation memory citing pi-real-engineering RSI proof", + evidence: [ + { + claim: "hermes rsi_canary recovered after repair", + reference: "pi-real-engineering:/tmp/rsi-proof-20260619T093724Z.md", + scope: "external", + }, + { + claim: "worker_skill_repair.sh copied skill_health.py and verified rsi_canary", + reference: "pi-real-engineering:/tmp/rsi-proof-20260619T093724Z.md", + scope: "external", + }, + ], + usableFor: ["advisory RSI recovery history", "future local proof import checklist", "red-team/safety context"], + notUsableFor: [ + "local fable-agent deploy authority", + "auto_patch_proven=true without fresh local degraded-skill recovery receipt", + "8098/deploy-webhook trust", + "container count truth without live reconciliation", + ], + decision: "advisory", + deployAttempted: false, + }; +} + +export function writeExternalEvidenceReceipt(file: string, receipt: ExternalEvidenceReceipt): 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 6efc393..ecdbc1f 100644 --- a/src/fable5/index.ts +++ b/src/fable5/index.ts @@ -72,6 +72,9 @@ export type { StaleMemoryClaim, StaleMemoryQuarantineReceipt, StaleMemorySeverit export { createFactoryMemoryFalsificationReceipt, createFalsificationReceipt, writeFalsificationReceipt } from "./falsification-receipt.js"; export type { FalsificationCase, FalsificationReceipt } from "./falsification-receipt.js"; +export { createExternalRsiEvidenceReceipt, writeExternalEvidenceReceipt } from "./external-evidence.js"; +export type { ExternalEvidenceReceipt } from "./external-evidence.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 b60d9d8..1e57c36 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2335,6 +2335,21 @@ fable if (receipt.decision === "failed") process.exit(1); }); +fable + .command("external-rsi-evidence") + .description("Write advisory external RSI evidence receipt without local deploy authority") + .option("--output ", "Receipt output path", path.join(".fable", "evidence", "external-rsi-evidence-live.json")) + .action(async (opts: { output?: string }) => { + const { createExternalRsiEvidenceReceipt, writeExternalEvidenceReceipt } = await import("./fable5/external-evidence.js"); + const receipt = createExternalRsiEvidenceReceipt(); + const out = opts.output ?? path.join(".fable", "evidence", "external-rsi-evidence-live.json"); + writeExternalEvidenceReceipt(out, receipt); + console.log(JSON.stringify(receipt, null, 2)); + console.log(` + Receipt: ${out} +`); + }); + fable .command("receipt-health") .description("Check receipt consumer health over a recent time window")