feat: quarantine stale factory memory
This commit is contained in:
parent
6872cb3a6d
commit
b958364338
|
|
@ -391,6 +391,8 @@ fable-agent plinius godmode "improve explanation quality"
|
|||
- `fable5 permission-posture`
|
||||
- `--mode <mode>`
|
||||
- `--output <path>`
|
||||
- `fable5 stale-memory-quarantine`
|
||||
- `--output <path>`
|
||||
- `fable5 receipt-health`
|
||||
- `--root <path>`
|
||||
- `--window-minutes <n>`
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ export type { ExternalToolAuditReceipt, ExternalToolAuditRow, ExternalToolAuditS
|
|||
export { createOpenCovenReviewReceipt, createPermissionPostureReceipt, writeExternalProjectReviewReceipt } from "./external-project-review.js";
|
||||
export type { ExternalProjectDecision, ExternalProjectReviewReceipt, PermissionMode, PermissionPostureReceipt } from "./external-project-review.js";
|
||||
|
||||
export { createFactoryMemoryQuarantineReceipt, createStaleMemoryQuarantineReceipt, writeStaleMemoryQuarantineReceipt } from "./stale-memory-quarantine.js";
|
||||
export type { StaleMemoryClaim, StaleMemoryQuarantineReceipt, StaleMemorySeverity } from "./stale-memory-quarantine.js";
|
||||
|
||||
export { channelPath, emitChannelEvent, readChannelEvents } from "./channel.js";
|
||||
export type { ChannelEvent, ChannelEventSource, ChannelEventType, NewChannelEvent } from "./channel.js";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
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 { createFactoryMemoryQuarantineReceipt, createStaleMemoryQuarantineReceipt, writeStaleMemoryQuarantineReceipt } from "./stale-memory-quarantine.js";
|
||||
|
||||
describe("stale memory quarantine receipts", () => {
|
||||
it("quarantines dangerous factory memory without deploy authority", () => {
|
||||
const receipt = createFactoryMemoryQuarantineReceipt(new Date("2026-07-01T00:00:00.000Z"));
|
||||
|
||||
expect(receipt).toMatchObject({
|
||||
schema: "fable.stale_memory.quarantine.v1",
|
||||
source: "conversation memory summary",
|
||||
decision: "quarantined",
|
||||
deployAttempted: false,
|
||||
});
|
||||
expect(receipt.claims.map((claim) => claim.claim)).toContain("8098/deploy-webhook is trusted deploy path");
|
||||
expect(receipt.claims.some((claim) => claim.severity === "dangerous")).toBe(true);
|
||||
expect(receipt.blockedTrustPaths).toContain("8098/deploy-webhook");
|
||||
expect(receipt.canonicalTrustPaths).toContain("8099/deploy via git-proxy");
|
||||
});
|
||||
|
||||
it("marks empty claim sets ready", () => {
|
||||
const receipt = createStaleMemoryQuarantineReceipt({ source: "none", claims: [] });
|
||||
|
||||
expect(receipt).toMatchObject({ decision: "ready", deployAttempted: false });
|
||||
});
|
||||
|
||||
it("writes quarantine receipts", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "stale-memory-quarantine-"));
|
||||
const file = path.join(dir, "receipt.json");
|
||||
|
||||
writeStaleMemoryQuarantineReceipt(file, createFactoryMemoryQuarantineReceipt());
|
||||
|
||||
expect(JSON.parse(fs.readFileSync(file, "utf-8"))).toMatchObject({ schema: "fable.stale_memory.quarantine.v1" });
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
export type StaleMemorySeverity = "advisory" | "dangerous";
|
||||
|
||||
export interface StaleMemoryClaim {
|
||||
claim: string;
|
||||
staleBecause: string;
|
||||
replacementTruth: string;
|
||||
severity: StaleMemorySeverity;
|
||||
}
|
||||
|
||||
export interface StaleMemoryQuarantineReceipt {
|
||||
schema: "fable.stale_memory.quarantine.v1";
|
||||
createdAt: string;
|
||||
source: string;
|
||||
claims: StaleMemoryClaim[];
|
||||
blockedTrustPaths: string[];
|
||||
canonicalTrustPaths: string[];
|
||||
decision: "quarantined" | "ready";
|
||||
deployAttempted: false;
|
||||
}
|
||||
|
||||
export function createStaleMemoryQuarantineReceipt(opts: { source: string; claims: StaleMemoryClaim[]; now?: Date }): StaleMemoryQuarantineReceipt {
|
||||
return {
|
||||
schema: "fable.stale_memory.quarantine.v1",
|
||||
createdAt: (opts.now ?? new Date()).toISOString(),
|
||||
source: opts.source,
|
||||
claims: opts.claims,
|
||||
blockedTrustPaths: ["8098/deploy-webhook", "memory-only deploy authority", "autonomous deploy without fresh gate receipt"],
|
||||
canonicalTrustPaths: ["live receipt > committed receipt > memory", "8099/deploy via git-proxy", "explicit approval + fresh valid ZTE/factory gate receipt"],
|
||||
decision: opts.claims.length > 0 ? "quarantined" : "ready",
|
||||
deployAttempted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function createFactoryMemoryQuarantineReceipt(now = new Date()): StaleMemoryQuarantineReceipt {
|
||||
return createStaleMemoryQuarantineReceipt({
|
||||
now,
|
||||
source: "conversation memory summary",
|
||||
claims: [
|
||||
{ claim: "25 containers", staleBecause: "fresh factory receipts verified 18 containers", replacementTruth: "container count must come from live factory reconciliation receipt", severity: "advisory" },
|
||||
{ claim: "8098/deploy-webhook is trusted deploy path", staleBecause: "current trust boundary marks 8098 legacy/stale only", replacementTruth: "8099/deploy via git-proxy is the canonical gated deploy route", severity: "dangerous" },
|
||||
{ claim: "agents can deploy without human intervention", staleBecause: "current policy requires explicit approval and fresh gate receipts", replacementTruth: "agents draft, receipts prove, humans approve, guarded 8099/deploy ships", severity: "dangerous" },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function writeStaleMemoryQuarantineReceipt(file: string, receipt: StaleMemoryQuarantineReceipt): string {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
fs.writeFileSync(file, `${JSON.stringify(receipt, null, 2)}\n`);
|
||||
return file;
|
||||
}
|
||||
15
src/index.ts
15
src/index.ts
|
|
@ -2304,6 +2304,21 @@ fable
|
|||
if (receipt.decision === "blocked") process.exit(1);
|
||||
});
|
||||
|
||||
fable
|
||||
.command("stale-memory-quarantine")
|
||||
.description("Quarantine stale factory memory so it cannot become deploy authority")
|
||||
.option("--output <path>", "Receipt output path", path.join(".fable", "memory", "stale-memory-quarantine-live.json"))
|
||||
.action(async (opts: { output?: string }) => {
|
||||
const { createFactoryMemoryQuarantineReceipt, writeStaleMemoryQuarantineReceipt } = await import("./fable5/stale-memory-quarantine.js");
|
||||
const receipt = createFactoryMemoryQuarantineReceipt();
|
||||
const out = opts.output ?? path.join(".fable", "memory", "stale-memory-quarantine-live.json");
|
||||
writeStaleMemoryQuarantineReceipt(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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue