feat(design): receipted fetch for OpenAI calls

design/src/receipted-fetch.ts wraps every api.openai.com call: a
content-free egress receipt (sink design-openai, sha256 of the JSON
body — hash only, never the body) is written BEFORE the send. Polarity
is FAIL-OPEN: user-facing generation must not die because an audit log
hiccuped, so a receipt failure warns on stderr and the call proceeds.
Streams pass through untouched (response bodies returned as-is;
non-string request bodies receipted as sha256:null rather than drained
to hash).

All ten call sites converted with per-command payload classes:
generate, variants (injected fetchFn passes through), iterate (both
threaded and fresh paths), evolve (image + screenshot analysis), check,
diff, design-to-code, memory.

Unit-tested with injected fetch: receipt-before-send ordering, stream
passthrough, and fail-open on an unwritable ledger.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit c0e5ff6639414ac2fd98e8ac3affb51401746b55)
This commit is contained in:
Garry Tan
2026-08-12 15:31:48 -07:00
parent a1282c78d4
commit 52288947ec
11 changed files with 218 additions and 11 deletions
+2 -1
View File
@@ -5,6 +5,7 @@
import fs from "fs";
import { requireApiKey } from "./auth";
import { receiptedFetch } from "./receipted-fetch";
export interface CheckResult {
pass: boolean;
@@ -22,7 +23,7 @@ export async function checkMockup(imagePath: string, brief: string): Promise<Che
const timeout = setTimeout(() => controller.abort(), 60_000);
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
const response = await receiptedFetch("check-screenshot-request", "https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
+2 -1
View File
@@ -6,6 +6,7 @@
import fs from "fs";
import { requireApiKey } from "./auth";
import { receiptedFetch } from "./receipted-fetch";
import { readDesignConstraints } from "./memory";
export interface DesignToCodeResult {
@@ -37,7 +38,7 @@ export async function generateDesignToCodePrompt(
? `\n\nExisting DESIGN.md (use these as constraints):\n${designConstraints}`
: "";
const response = await fetch("https://api.openai.com/v1/chat/completions", {
const response = await receiptedFetch("design-to-code-request", "https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
+2 -1
View File
@@ -6,6 +6,7 @@
import fs from "fs";
import { requireApiKey } from "./auth";
import { receiptedFetch } from "./receipted-fetch";
export interface DiffResult {
differences: { area: string; description: string; severity: string }[];
@@ -28,7 +29,7 @@ export async function diffMockups(
const timeout = setTimeout(() => controller.abort(), 60_000);
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
const response = await receiptedFetch("diff-screenshots-request", "https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
+3 -2
View File
@@ -8,6 +8,7 @@
import fs from "fs";
import path from "path";
import { requireApiKey } from "./auth";
import { receiptedFetch } from "./receipted-fetch";
export interface EvolveOptions {
screenshot: string; // Path to current site screenshot
@@ -55,7 +56,7 @@ export async function evolve(options: EvolveOptions): Promise<void> {
const timeout = setTimeout(() => controller.abort(), 240_000);
try {
const response = await fetch("https://api.openai.com/v1/responses", {
const response = await receiptedFetch("evolve-image-request", "https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
@@ -113,7 +114,7 @@ async function analyzeScreenshot(apiKey: string, imageBase64: string): Promise<s
const timeout = setTimeout(() => controller.abort(), 30_000);
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
const response = await receiptedFetch("evolve-screenshot-analysis-request", "https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
+2 -1
View File
@@ -5,6 +5,7 @@
import fs from "fs";
import path from "path";
import { requireApiKey } from "./auth";
import { receiptedFetch } from "./receipted-fetch";
import { parseBrief } from "./brief";
import { createSession, sessionPath } from "./session";
import { checkMockup } from "./check";
@@ -40,7 +41,7 @@ async function callImageGeneration(
const timeout = setTimeout(() => controller.abort(), 240_000);
try {
const response = await fetch("https://api.openai.com/v1/responses", {
const response = await receiptedFetch("generate-image-request", "https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
+3 -2
View File
@@ -9,6 +9,7 @@
import fs from "fs";
import path from "path";
import { requireApiKey } from "./auth";
import { receiptedFetch } from "./receipted-fetch";
import { readSession, updateSession } from "./session";
export interface IterateOptions {
@@ -85,7 +86,7 @@ async function callWithThreading(
const timeout = setTimeout(() => controller.abort(), 240_000);
try {
const response = await fetch("https://api.openai.com/v1/responses", {
const response = await receiptedFetch("iterate-threaded-image-request", "https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
@@ -133,7 +134,7 @@ async function callFresh(
const timeout = setTimeout(() => controller.abort(), 240_000);
try {
const response = await fetch("https://api.openai.com/v1/responses", {
const response = await receiptedFetch("iterate-fresh-image-request", "https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
+2 -1
View File
@@ -14,6 +14,7 @@
import fs from "fs";
import path from "path";
import { requireApiKey } from "./auth";
import { receiptedFetch } from "./receipted-fetch";
export interface ExtractedDesign {
colors: { name: string; hex: string; usage: string }[];
@@ -34,7 +35,7 @@ export async function extractDesignLanguage(imagePath: string): Promise<Extracte
const timeout = setTimeout(() => controller.abort(), 60_000);
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
const response = await receiptedFetch("memory-distill-request", "https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
+57
View File
@@ -0,0 +1,57 @@
/**
* receipted-fetch — egress-receipted wrapper for the design binary's OpenAI
* calls (sink 'design-openai').
*
* Writes a content-free receipt BEFORE the send: sha256 of the JSON body
* plus a byte count — the hash only, never the body itself. FAIL-OPEN: a
* receipt hiccup warns on stderr and the call proceeds. User-facing image
* generation must not die because an audit log could not be written (the
* ledger records ATTEMPTED egress for auditing; it is not a send gate here).
*
* Streams pass through untouched: the response is returned as-is, and a
* non-string request body (e.g. a ReadableStream) is receipted as
* sha256:null rather than being consumed to hash it.
*/
import { sha256Hex, writeReceipt } from "../../lib/egress-receipt";
export type FetchLike = typeof globalThis.fetch;
/**
* Drop-in fetch replacement for api.openai.com calls.
*
* @param payloadClass content-free description of what is being sent
* (e.g. 'generate-image-request', 'check-screenshot-request')
* @param fetchImpl injectable fetch for tests / callers with their own
* fetch (variants.ts passes its stubbed fetchFn through)
*/
export async function receiptedFetch(
payloadClass: string,
url: string,
init?: RequestInit,
fetchImpl: FetchLike = globalThis.fetch,
): Promise<Response> {
try {
const body = init?.body;
let bytes = 0;
let sha256: string | null = null;
if (typeof body === "string") {
bytes = Buffer.byteLength(body);
sha256 = sha256Hex(body);
}
writeReceipt({
sink: "design-openai",
host: new URL(url).host,
payloadClass,
bytes,
sha256,
consent: "user ran design command (OPENAI_API_KEY configured)",
});
} catch (err) {
process.stderr.write(
`[design] egress receipt could not be written (${(err as Error).message}) — proceeding (fail-open). ` +
`gstack records what it ATTEMPTS to send off-machine; see gstack-egress.\n`,
);
}
return fetchImpl(url, init);
}
+3 -2
View File
@@ -7,6 +7,7 @@
import fs from "fs";
import path from "path";
import { requireApiKey } from "./auth";
import { receiptedFetch } from "./receipted-fetch";
import { parseBrief } from "./brief";
import { normalizeIntFlag } from "./flag-utils";
@@ -67,7 +68,7 @@ export async function generateVariant(
const timeout = setTimeout(() => controller.abort(), 240_000);
try {
const response = await fetchFn("https://api.openai.com/v1/responses", {
const response = await receiptedFetch("variants-image-request", "https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"Authorization": `Bearer ${apiKey}`,
@@ -79,7 +80,7 @@ export async function generateVariant(
tools: [{ type: "image_generation", model: "gpt-image-2", size, quality }],
}),
signal: controller.signal,
});
}, fetchFn);
clearTimeout(timeout);