From 3fa170e2ee7a1539105e61061d19e2171bd8baed Mon Sep 17 00:00:00 2001 From: bashybaranaba Date: Thu, 28 May 2026 00:57:23 +0300 Subject: [PATCH] fix issues with images --- examples/chat/app/api/chat/route.ts | 30 ++- .../app/api/media/generated/[id]/route.ts | 21 ++ examples/chat/app/api/pk-proxy/claim/route.ts | 40 ++-- examples/chat/components/chat/chat-input.tsx | 217 ++++++++++++------ examples/chat/lib/generated-image-cache.ts | 101 ++++++++ examples/chat/lib/governance.ts | 28 ++- examples/chat/lib/provenance.ts | 54 +++-- examples/chat/types/index.ts | 4 +- 8 files changed, 359 insertions(+), 136 deletions(-) create mode 100644 examples/chat/app/api/media/generated/[id]/route.ts create mode 100644 examples/chat/lib/generated-image-cache.ts diff --git a/examples/chat/app/api/chat/route.ts b/examples/chat/app/api/chat/route.ts index 9473d16..e633657 100644 --- a/examples/chat/app/api/chat/route.ts +++ b/examples/chat/app/api/chat/route.ts @@ -23,6 +23,7 @@ import { z } from "zod"; import { v4 as uuidv4 } from "uuid"; import { connectDB, ConversationModel, MessageModel, UserModel } from "@/lib/db"; import { getOpenAIClient } from "@/lib/openai-client"; +import { bufferToArrayBuffer, putGeneratedImage } from "@/lib/generated-image-cache"; import { recordChatProvenance, recordImageProvenance, type SupportedProvider } from "@/lib/provenance"; import { getPKClientAsync } from "@/lib/pk-client"; import { cacheBundle } from "@/lib/bundle-cache"; @@ -113,10 +114,10 @@ function buildImageRequest({ return imageRequest; } -async function blobFromImageResult(imageUrl: string, b64Json?: string): Promise { +async function bufferFromImageResult(imageUrl: string, b64Json?: string): Promise<{ buffer: Buffer; contentType: string } | undefined> { if (b64Json) { const buffer = Buffer.from(b64Json, "base64"); - return new Blob([buffer], { type: "image/png" }); + return { buffer, contentType: "image/png" }; } if (!imageUrl) return undefined; @@ -124,7 +125,9 @@ async function blobFromImageResult(imageUrl: string, b64Json?: string): Promise< if (!imgResp.ok) { throw new Error(`Image download failed with HTTP ${imgResp.status}`); } - return imgResp.blob(); + const contentType = imgResp.headers.get("content-type") ?? "image/png"; + const buffer = Buffer.from(await imgResp.arrayBuffer()); + return { buffer, contentType }; } // ─── Background provenance recorder ────────────────────────────────────────── @@ -441,22 +444,27 @@ export async function POST(req: Request) { const firstImage = response.data?.[0] as { url?: string; b64_json?: string; revised_prompt?: string } | undefined; const b64Json = firstImage?.b64_json; - const imageUrl = firstImage?.url ?? (b64Json ? `data:image/png;base64,${b64Json}` : ""); const revisedPrompt = firstImage?.revised_prompt ?? prompt; - if (!imageUrl) { + if (!firstImage?.url && !b64Json) { throw new Error("Image API returned no image data."); } - // Download the image binary while the generated payload is fresh. - // Storing it here avoids re-fetching later when the URL may have expired, - // and lets the provenance recorder upload the real image to IPFS for embeddings. + // GPT Image models return base64 by default and do not support URL output. + // Store bytes in a short-lived server cache so MongoDB only stores a small URL; + // provenance later replaces this with a durable IPFS gateway URL. let imageBlob: Blob | undefined; + let imageUrl = firstImage.url ?? ""; try { - imageBlob = await blobFromImageResult(imageUrl, b64Json); + const imageBytes = await bufferFromImageResult(firstImage.url ?? "", b64Json); + if (!imageBytes) throw new Error("Image API returned no downloadable image data."); + const cached = await putGeneratedImage(imageBytes.buffer, imageBytes.contentType); + imageUrl = cached.url; + imageBlob = new Blob([bufferToArrayBuffer(imageBytes.buffer)], { type: imageBytes.contentType }); } catch (err) { - console.warn("[chat] generated image download failed:", err); - // Non-fatal: provenance recording will fall back to metadata JSON + console.warn("[chat] generated image byte capture failed:", err); + if (!imageUrl) throw err; + // Non-fatal for legacy URL responses: provenance recording will fall back to metadata JSON. } toolResults.push({ name: "generate_image", result: { imageUrl, revisedPrompt, prompt, model: imageModel }, blob: imageBlob }); diff --git a/examples/chat/app/api/media/generated/[id]/route.ts b/examples/chat/app/api/media/generated/[id]/route.ts new file mode 100644 index 0000000..829187b --- /dev/null +++ b/examples/chat/app/api/media/generated/[id]/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from "next/server"; +import { bufferToArrayBuffer, getGeneratedImage } from "@/lib/generated-image-cache"; + +type Params = { params: Promise<{ id: string }> }; + +export async function GET(_req: Request, { params }: Params): Promise { + const { id } = await params; + const image = await getGeneratedImage(id); + + if (!image) { + return NextResponse.json({ error: "Generated image expired or not found" }, { status: 404 }); + } + + return new NextResponse(bufferToArrayBuffer(image.buffer), { + headers: { + "Content-Type": image.contentType, + "Cache-Control": "private, max-age=1800", + "Content-Length": String(image.buffer.length), + }, + }); +} diff --git a/examples/chat/app/api/pk-proxy/claim/route.ts b/examples/chat/app/api/pk-proxy/claim/route.ts index d2e0d21..21e4b16 100644 --- a/examples/chat/app/api/pk-proxy/claim/route.ts +++ b/examples/chat/app/api/pk-proxy/claim/route.ts @@ -87,48 +87,36 @@ export async function POST(req: NextRequest) { try { const entityId = await pk.entity({ role: "human", name: userId }); + const governanceExtensions = buildGovernanceExtensions({ + governance, + userId: entityId, + fileName: file.name, + roleInWorkflow: owned ? "source" : "reference", + }); const result = await pk.file(file, { entity: { id: entityId, role: "human", name: userId }, action: { type: owned ? "create" : "reference", extensions: { - [AUTHORIZATION_NAMESPACE]: buildGovernanceExtensions({ - governance, - userId: entityId, - fileName: file.name, - roleInWorkflow: owned ? "source" : "reference", - }).authorization, - [GOVERNANCE_NAMESPACE]: buildGovernanceExtensions({ - governance, - userId: entityId, - fileName: file.name, - roleInWorkflow: owned ? "source" : "reference", - }).governance, + [AUTHORIZATION_NAMESPACE]: governanceExtensions.authorization, + [GOVERNANCE_NAMESPACE]: governanceExtensions.governance, }, }, resourceType, resourceExtensions: { - [AUTHORIZATION_NAMESPACE]: buildGovernanceExtensions({ - governance, - userId: entityId, - fileName: file.name, - roleInWorkflow: owned ? "source" : "reference", - }).authorization, - [GOVERNANCE_NAMESPACE]: buildGovernanceExtensions({ - governance, - userId: entityId, - fileName: file.name, - roleInWorkflow: owned ? "source" : "reference", - }).governance, + [AUTHORIZATION_NAMESPACE]: governanceExtensions.authorization, + [GOVERNANCE_NAMESPACE]: governanceExtensions.governance, }, attribution: { contrib: { weight: governance.contributionWeight, basis: "points", - source: owned ? "self-declared" : "default", + source: "self-declared", category: governance.contributorRole, - note: governance.note, + note: + governance.note ?? + `${file.name}: ${owned ? "creator claim" : "external source reference"} with ${governance.licenseType}; authorization ${governance.authorizationStatus}.`, }, license: { type: governance.licenseType, diff --git a/examples/chat/components/chat/chat-input.tsx b/examples/chat/components/chat/chat-input.tsx index 2c0f8b0..304d5f3 100644 --- a/examples/chat/components/chat/chat-input.tsx +++ b/examples/chat/components/chat/chat-input.tsx @@ -8,7 +8,7 @@ import { type KeyboardEvent, type ChangeEvent, } from "react"; -import { Send, Loader2, Paperclip, Mic, MicOff, X, ImageIcon, FileText, ShieldCheck, ShieldOff } from "lucide-react"; +import { Send, Loader2, Paperclip, Mic, MicOff, X, ImageIcon, FileText, ShieldCheck, ShieldOff, AlertTriangle } from "lucide-react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; import { useProvenanceKit } from "@/components/provenance/pk-ui"; @@ -86,10 +86,10 @@ export function ChatInput({ if (!res.ok) throw new Error("Upload failed"); const data = await res.json(); // Store original File object so AttachmentProvenance can run background provenance search. - // cid is set when Pinata is configured; textContent is set for text/* files. + // `cid` is deliberately left unset until ProvenanceKit finds or records a resource. newAttachments.push({ url: data.url, - cid: data.cid, + storageCid: data.cid, mimeType: data.mimeType, name: data.name, textContent: data.textContent, @@ -139,7 +139,14 @@ export function ChatInput({ } catch { /* mic denied */ } } - const canSubmit = (value.trim() || attachments.length > 0) && !isLoading && !disabled && !isUploading && !isTranscribing; + const hasUnregisteredAttachments = attachments.some((att) => att.file && !att.cid); + const canSubmit = + (value.trim() || attachments.length > 0) && + !isLoading && + !disabled && + !isUploading && + !isTranscribing && + !hasUnregisteredAttachments; return (
@@ -209,6 +216,12 @@ export function ChatInput({ {isLoading ? : } + {hasUnregisteredAttachments && ( +
+ + Finish the provenance check or claim step before sending. +
+ )}
); } @@ -223,7 +236,15 @@ function AttachmentGovernanceControls({ if (!onChange) return null; return ( -
+
+
+ + Governance claim + + + required + +
-
+
@@ -308,9 +329,9 @@ function AttachmentGovernanceControls({ value={governance.scope} onChange={(e) => onChange({ ...governance, scope: e.target.value })} placeholder="Authorized scope" - className="mt-1 h-6 w-full rounded border border-border bg-background px-1 text-[10px] text-muted-foreground outline-none" + className="mt-1.5 h-7 w-full rounded-md border border-border bg-background px-2 text-[11px] text-foreground outline-none placeholder:text-muted-foreground focus:ring-1 focus:ring-ring" /> -
+
{governance.authorizationStatus === "authorized" ? ( ) : ( @@ -324,6 +345,72 @@ function AttachmentGovernanceControls({ ); } +function governanceForClaim( + governance: FileAttachment["governance"], + owned: boolean +): NonNullable { + const fallback = defaultAttachmentGovernance(owned ? "own-original" : "licensed-source"); + if (!governance) return fallback; + + if (owned) { + return { + ...governance, + status: "own-original", + authorizationStatus: "authorized", + contributorRole: "creator", + scope: governance.scope || fallback.scope, + note: governance.note || "User asserted this uploaded file is an original source for the workflow.", + }; + } + + if (governance.status === "own-original") { + return fallback; + } + + return { + ...governance, + contributorRole: governance.status === "restricted-source" ? "reference" : "source", + }; +} + +function ClaimActions({ + claiming, + onClaimOriginal, + onRecordSource, +}: { + claiming: boolean; + onClaimOriginal: () => void; + onRecordSource: () => void; +}) { + if (claiming) { + return ( +
+ + Recording provenance… +
+ ); + } + + return ( +
+ + +
+ ); +} + /** Compact provenance indicator for an attachment chip. * Runs a background search on mount; shows a small status row. * On match → clickable badge that opens the ProvenanceFileDialog. @@ -350,6 +437,8 @@ function AttachmentProvenance({ const [dialogOpen, setDialogOpen] = useState(false); const [claiming, setClaiming] = useState(false); const [claimDone, setClaimDone] = useState<"claimed" | "referenced" | null>(null); + const [claimError, setClaimError] = useState(null); + const [searchError, setSearchError] = useState(false); useEffect(() => { if (!pk) return; @@ -370,7 +459,10 @@ function AttachmentProvenance({ } } }) - .catch(() => setStatus("error")); + .catch(() => { + setSearchError(true); + setStatus("not-found"); + }); return () => { if (url) URL.revokeObjectURL(url); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -378,19 +470,23 @@ function AttachmentProvenance({ async function handleClaim(owned: boolean) { setClaiming(true); + setClaimError(null); try { + const claimGovernance = governanceForClaim(governance, owned); const form = new FormData(); form.append("file", file, file.name); form.append("owned", String(owned)); form.append("userId", userId ?? "anonymous"); form.append("mimeType", file.type); - if (governance) form.append("governance", JSON.stringify(governance)); + form.append("governance", JSON.stringify(claimGovernance)); const res = await fetch("/api/pk-proxy/claim", { method: "POST", body: form }); if (!res.ok) throw new Error("Claim failed"); const data = await res.json(); onCidAssigned?.(data.cid); setClaimDone(data.status as "claimed" | "referenced"); - } catch { /* non-fatal */ } + } catch (err) { + setClaimError(err instanceof Error ? err.message : "Claim failed"); + } finally { setClaiming(false); } } @@ -434,35 +530,18 @@ function AttachmentProvenance({ {pct}% match · View details → {!isHigh && governance && ( -
-
- - Low match. Record a clearer claim? +
+
+ + Low-confidence match. Register only after confirming rights.
-
- {claiming ? ( - - ) : ( - <> - - / - - - )} -
+ handleClaim(true)} + onRecordSource={() => handleClaim(false)} + /> + {claimError &&

{claimError}

}
)} -
- - No match found. Register this file? +
+
+ + + {searchError ? "Provenance lookup unavailable" : "No provenance match found"} +
+

+ {searchError + ? "Register this file now so the workflow can still cite it as an input resource." + : "Register this file before sending so it appears as an input resource in the provenance explorer."} +

{governance && ( )} -
- {claiming ? ( - - ) : ( - <> - - / - - - )} -
+ handleClaim(true)} + onRecordSource={() => handleClaim(false)} + /> + {claimError &&

{claimError}

}
); } diff --git a/examples/chat/lib/generated-image-cache.ts b/examples/chat/lib/generated-image-cache.ts new file mode 100644 index 0000000..a88f1d6 --- /dev/null +++ b/examples/chat/lib/generated-image-cache.ts @@ -0,0 +1,101 @@ +import { randomUUID } from "node:crypto"; +import { Readable } from "node:stream"; +import mongoose from "mongoose"; +import { connectDB } from "@/lib/db"; + +type GeneratedImageEntry = { + buffer: Buffer; + contentType: string; +}; + +type StoredGridFile = { + _id: mongoose.mongo.ObjectId; + contentType?: string; + metadata?: { expiresAt?: Date | string }; +}; + +const BUCKET_NAME = "generated_images"; +const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; +const PRUNE_INTERVAL_MS = 10 * 60 * 1000; + +declare global { + // eslint-disable-next-line no-var + var _pkGeneratedImageLastPrune: number | undefined; +} + +function bufferToArrayBuffer(buffer: Buffer): ArrayBuffer { + return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength) as ArrayBuffer; +} + +function getBucket() { + const db = mongoose.connection.db; + if (!db) throw new Error("MongoDB is not connected."); + return new mongoose.mongo.GridFSBucket(db, { bucketName: BUCKET_NAME }); +} + +async function pruneExpiredImages(now = Date.now()) { + if (global._pkGeneratedImageLastPrune && now - global._pkGeneratedImageLastPrune < PRUNE_INTERVAL_MS) { + return; + } + global._pkGeneratedImageLastPrune = now; + + const bucket = getBucket(); + const expired = await bucket + .find({ "metadata.expiresAt": { $lte: new Date(now) } }) + .limit(25) + .toArray(); + + await Promise.allSettled(expired.map((file) => bucket.delete(file._id))); +} + +export async function putGeneratedImage(buffer: Buffer, contentType = "image/png", ttlMs = DEFAULT_TTL_MS) { + await connectDB(); + await pruneExpiredImages(); + + const id = randomUUID(); + const bucket = getBucket(); + const upload = bucket.openUploadStream(id, { + contentType, + metadata: { expiresAt: new Date(Date.now() + ttlMs) }, + }); + + await new Promise((resolve, reject) => { + Readable.from(buffer).pipe(upload).on("error", reject).on("finish", resolve); + }); + + return { + id, + url: `/api/media/generated/${id}`, + }; +} + +export async function getGeneratedImage(id: string): Promise { + await connectDB(); + await pruneExpiredImages(); + + const bucket = getBucket(); + const file = (await bucket.find({ filename: id }).limit(1).next()) as StoredGridFile | null; + if (!file) return null; + + const expiresAt = file.metadata?.expiresAt ? new Date(file.metadata.expiresAt).getTime() : null; + if (expiresAt && expiresAt <= Date.now()) { + await bucket.delete(file._id).catch(() => undefined); + return null; + } + + const chunks: Buffer[] = []; + await new Promise((resolve, reject) => { + bucket + .openDownloadStream(file._id) + .on("data", (chunk) => chunks.push(Buffer.from(chunk))) + .on("error", reject) + .on("end", resolve); + }); + + return { + buffer: Buffer.concat(chunks), + contentType: file.contentType ?? "image/png", + }; +} + +export { bufferToArrayBuffer }; diff --git a/examples/chat/lib/governance.ts b/examples/chat/lib/governance.ts index a79744e..34e11e8 100644 --- a/examples/chat/lib/governance.ts +++ b/examples/chat/lib/governance.ts @@ -53,6 +53,16 @@ export function buildGovernanceExtensions(opts: { }) { const now = new Date().toISOString(); const { governance, userId, fileName, roleInWorkflow } = opts; + const fileLabel = fileName ?? "attachment"; + const needsReview = + governance.status === "restricted-source" || + governance.authorizationStatus !== "authorized"; + const riskFlags = [ + ...(governance.authorizationStatus === "pending" ? ["authorization-pending"] : []), + ...(governance.authorizationStatus === "unauthorized" ? ["authorization-missing"] : []), + ...(governance.status === "restricted-source" ? ["rights-restricted"] : []), + ...(governance.aiTraining === "reserved" ? ["ai-training-reserved"] : []), + ]; return { authorization: { @@ -60,22 +70,26 @@ export function buildGovernanceExtensions(opts: { authorizedBy: governance.authorizationStatus === "authorized" ? userId : undefined, authorizedAt: governance.authorizationStatus === "authorized" ? now : undefined, scope: governance.scope, - reference: `chat-demo:${governance.status}:${fileName ?? "attachment"}`, + reference: `chat-attachment:${governance.status}:${fileLabel}`, }, governance: { - workflow: "Human-AI governance demo chat", - purpose: "Demonstrate attribution, rights, authorisation, AI disclosure, derivation, and selective disclosure readiness.", + workflow: "Chat file-to-output provenance workflow", + purpose: `Record declared rights, authorization, and AI-use constraints for ${fileLabel}.`, roleInWorkflow, requirements: ["R1", "R2", "R3", "R5", "R6"], policyBasis: [ "Copyright authorship: human contribution and transformation history", "DSM Art. 4(3): machine-readable AI training rights reservation", "EU AI Act Art. 50: AI involvement disclosure", - "GDPR/data governance: consent and scoped disclosure", + ...(governance.status === "restricted-source" + ? ["GDPR/data governance: consent and scoped disclosure"] + : []), ], - riskFlags: governance.status === "restricted-source" ? ["authorisation-review", "rights-restricted"] : [], - reviewStatus: governance.status === "restricted-source" ? "review-required" : "reviewed", - note: governance.note, + riskFlags, + reviewStatus: needsReview ? "review-required" : "reviewed", + note: + governance.note ?? + `${fileLabel}: ${governance.licenseType}; authorization ${governance.authorizationStatus}; AI training ${governance.aiTraining}.`, }, }; } diff --git a/examples/chat/lib/provenance.ts b/examples/chat/lib/provenance.ts index d2c3f76..32b6f28 100644 --- a/examples/chat/lib/provenance.ts +++ b/examples/chat/lib/provenance.ts @@ -228,11 +228,26 @@ export async function recordChatProvenance(opts: { // Include attachment CIDs as additional inputs to the response action const attachmentCids = (opts.attachments ?? []).filter((a) => a.cid).map((a) => a.cid!); - const restrictedInputs = (opts.attachments ?? []).filter( + const declaredInputs = opts.attachments ?? []; + const restrictedInputs = declaredInputs.filter( (a) => a.governance?.authorizationStatus !== "authorized" ); + const reviewInputs = declaredInputs.filter( + (a) => + a.governance?.status === "restricted-source" || + a.governance?.authorizationStatus !== "authorized" + ); + const inputSummary = + declaredInputs.length > 0 + ? declaredInputs + .map((a) => { + const gov = a.governance; + return `${a.name} (${a.mimeType}; ${gov?.licenseType ?? "license unspecified"}; authorization ${gov?.authorizationStatus ?? "unspecified"}; AI training ${gov?.aiTraining ?? "unspecified"})`; + }) + .join("; ") + : "No declared file inputs."; const responseGovernance = buildGovernanceExtensions({ - governance: restrictedInputs[0]?.governance ?? defaultAttachmentGovernance("licensed-source"), + governance: reviewInputs[0]?.governance ?? defaultAttachmentGovernance("licensed-source"), userId: agentEntityId, roleInWorkflow: "derived-output", }); @@ -248,18 +263,18 @@ export async function recordChatProvenance(opts: { extensions: { [AUTHORIZATION_NAMESPACE]: { ...responseGovernance.authorization, - status: restrictedInputs.length > 0 ? "pending" : "authorized", + status: reviewInputs.length > 0 ? "pending" : "authorized", scope: - restrictedInputs.length > 0 - ? `Generated response depends on ${restrictedInputs.length} input(s) requiring governance review.` - : "Generated response authorised for this chat demonstration workflow.", + reviewInputs.length > 0 + ? `Generated response depends on ${reviewInputs.length} input(s) requiring governance review: ${reviewInputs.map((a) => a.name).join(", ")}.` + : `Generated response authorised from declared inputs: ${inputSummary}`, }, [GOVERNANCE_NAMESPACE]: { ...responseGovernance.governance, requirements: ["R1", "R2", "R3", "R4", "R5", "R6"], disclosure: { aiInvolved: true, - humanContribution: "User prompt and attached source material selected the task, constraints, and inputs.", + humanContribution: `User prompt and attached source material selected the task, constraints, and inputs. Inputs: ${inputSummary}`, aiContribution: `${opts.provider}/${opts.model} generated the response from declared inputs.`, publicSummary: "AI-assisted response with recorded source CIDs, rights metadata, and authorisation status.", }, @@ -275,22 +290,27 @@ export async function recordChatProvenance(opts: { resourceType: "text", resourceExtensions: { [AUTHORIZATION_NAMESPACE]: { - status: restrictedInputs.length > 0 ? "pending" : "authorized", - authorizedBy: restrictedInputs.length > 0 ? undefined : agentEntityId, - authorizedAt: restrictedInputs.length > 0 ? undefined : new Date().toISOString(), + status: reviewInputs.length > 0 ? "pending" : "authorized", + authorizedBy: reviewInputs.length > 0 ? undefined : agentEntityId, + authorizedAt: reviewInputs.length > 0 ? undefined : new Date().toISOString(), scope: - restrictedInputs.length > 0 - ? `Output depends on ${restrictedInputs.length} input(s) requiring rights or consent review.` - : "Output authorised for the active Human-AI governance demonstration workflow.", + reviewInputs.length > 0 + ? `Output depends on ${reviewInputs.length} input(s) requiring rights or consent review: ${reviewInputs.map((a) => a.name).join(", ")}.` + : `Output authorised from declared inputs: ${inputSummary}`, reference: `chat-demo:response:${opts.provider}/${opts.model}`, }, [GOVERNANCE_NAMESPACE]: { - workflow: "Human-AI governance demo chat", + workflow: "Chat file-to-output provenance workflow", roleInWorkflow: "derived-output", requirements: ["R1", "R2", "R3", "R4", "R5", "R6"], - reviewStatus: restrictedInputs.length > 0 ? "review-required" : "reviewed", - riskFlags: restrictedInputs.length > 0 ? ["input-authorisation-pending"] : [], - note: "Assistant response produced as a derived artefact from the user prompt and declared source CIDs.", + reviewStatus: reviewInputs.length > 0 ? "review-required" : "reviewed", + riskFlags: [ + ...(reviewInputs.length > 0 ? ["input-authorisation-review"] : []), + ...declaredInputs + .filter((a) => a.governance?.aiTraining === "reserved") + .map((a) => `ai-training-reserved:${a.name}`), + ], + note: `Assistant response produced as a derived artefact from the user prompt and recorded source CIDs. Inputs: ${inputSummary}`, }, }, attribution: { diff --git a/examples/chat/types/index.ts b/examples/chat/types/index.ts index 556c0e8..6f0f1e3 100644 --- a/examples/chat/types/index.ts +++ b/examples/chat/types/index.ts @@ -76,8 +76,10 @@ export interface FileAttachment { file?: File; /** Pinata/IPFS gateway URL (preferred) or base64 data URL fallback */ url?: string; - /** IPFS CID — set when uploaded to Pinata; used for provenance inputCids */ + /** ProvenanceKit resource CID — set only after an exact match or explicit claim/reference registration */ cid?: string; + /** Raw storage CID from Pinata/IPFS, used only for retrieval/preview before provenance registration */ + storageCid?: string; mimeType: string; name: string; /** Extracted text content for text/* files — included inline for the LLM */