/** * Attachment persistence (Option A) for Studio /agent image attachments. * * User-attached images arrive as relay `ImageAttachment`s — either inline * base64 (`data`, legacy/small) or a backend-hosted url (`url`, optionally * `imageId`). LLM providers see those natively, but agent TOOLS do not: a * tool such as `image_generate` (reference images) or `read` can only use * normal file paths. This module writes every attachment to * * /.spectral/attachments//. * * so the prompt can be annotated with the saved absolute paths (see * `formatAttachmentAnnotation`) and the agent can use the images as files. * * Guarantees: * - NEVER throws and never rejects: every failure (fetch error, fs error, * base64 decode) is reported through `onWarn` and the prompt proceeds. * - Idempotent: the same imageId (or content hash) always maps to the same * path, so re-sending an attachment overwrites the same file. * - Sandboxed: every path segment is stripped to `[A-Za-z0-9_-]`, so a * malicious sessionId/imageId/url can never traverse (`..` contains * only dots → stripped → falls back to a safe name). * - Capped: per-session file count and total size (both named consts). * * The stored session history is NEVER rewritten: callers keep persisting the * original content to SQLite and only annotate the text handed to the agent. * * @module @spectral/server/attachments/persist */ import type { ImageAttachment } from "../wire.js"; /** Max persisted files per session directory — beyond this, persistence is skipped. */ export declare const MAX_FILES_PER_SESSION = 50; /** Max total bytes per session directory (200 MB) — beyond this, persistence is skipped. */ export declare const MAX_TOTAL_ATTACHMENT_BYTES: number; /** Wall-clock budget for one `persistAttachments()` call (url fetches dominate). */ export declare const PERSIST_DEADLINE_MS = 30000; /** Per-request timeout for fetching a hosted/foreign image. */ export declare const FETCH_ATTACHMENT_TIMEOUT_MS = 10000; /** * Strip a path segment to `[A-Za-z0-9_-]` (length-capped). `..`, `/`, `\`, * percent-encodings and unicode are all removed, so traversal is impossible: * a fully-stripped segment collapses to `fallback` instead of becoming `""`. */ export declare function sanitizeSegment(input: string, fallback: string): string; export declare function attachmentExtension(mimeType: string): string | null; /** * Sanitized file stem for an attachment, chosen deterministically so the * same attachment always maps to the same path (idempotency): * 1. sanitized `imageId`, * 2. hosted image id extracted from `url`, * 3. sha256 of `url` (foreign urls), * 4. sha256 of the inline `data` payload. * `null` when the attachment has none of the above. */ export declare function attachmentFileStem(image: ImageAttachment): string | null; /** `stem.ext` for an attachment, or `null` when the mime/stem is unusable. */ export declare function attachmentFileName(image: ImageAttachment): string | null; /** `/.spectral/attachments` — the sweep root. Always absolute. */ export declare function attachmentsRootFor(cwd: string): string; /** Per-session directory under the attachments root. Always absolute. */ export declare function attachmentsSessionDirFor(cwd: string, sessionId: string): string; /** Absolute path an attachment would be persisted to, or `null` when unusable. */ export declare function attachmentPathFor(cwd: string, sessionId: string, image: ImageAttachment): string | null; /** * The persisted paths that still exist on disk for a message's attachments. * Used by history rehydration: the annotation is only appended for files * that are actually readable — an attachment persisted days ago may have * been swept by `sweepAttachments()` in the meantime. */ export declare function existingAttachmentPaths(cwd: string, sessionId: string, images?: ImageAttachment[]): string[]; /** * Prompt annotation for persisted attachments, e.g. * `"\n\n[Attached images saved at: /abs/a.png, /abs/b.jpg]"`. * Empty string when no path survived (nothing persisted / none exist). */ export declare function formatAttachmentAnnotation(paths: readonly string[]): string; /** * Rehydration twin of the prompt annotation: `content` plus the saved-at * annotation for attachments that still exist on disk. Used by * `agent-bridge.ts` when rebuilding prior user messages from * `messages.images_json`, so the LLM context matches what the original * prompt carried. Content is returned unchanged when nothing exists. */ export declare function annotatedUserMessageText(options: { cwd: string; sessionId?: string; content: string; images?: ImageAttachment[]; }): string; /** One successfully persisted attachment. */ export interface PersistedAttachment { path: string; bytes: number; } export interface PersistAttachmentsInput { /** Sanitized into the directory segment — never used raw. */ sessionId: string; /** Project cwd; attachments land under `/.spectral/attachments/`. */ cwd: string; images?: ImageAttachment[]; /** * Backend routing override. `serve.ts` already resolved both (the same * credentials AgentBridge uses for inference), so the server passes them * in instead of re-reading config + machine store. Falls back to * `resolveBackendRouting()` when omitted. */ backendUrl?: string; machineJwt?: string; /** Injectable fetch for tests / alternative runtimes. */ fetchImpl?: typeof fetch; /** Diagnostics sink — failures warn and continue. */ onWarn?: (message: string) => void; /** Deadline override (tests). Defaults to `PERSIST_DEADLINE_MS`. */ deadlineMs?: number; } /** * Persist every usable attachment of a user message to disk and return the * saved absolute paths (annotation input). NEVER throws and never rejects: * every per-image failure warns and continues, and the prompt must always * proceed — a missing file is a degradation, not an error. * * Per-image flow: * - `{data}` → base64-decoded and written directly, * - `{url}` → bytes fetched through the hosted resolver machinery. * * Skips (with a warning) when: the mime type is outside the allowlist, the * per-session file cap / total size cap / wall-clock deadline is reached, or * the fetch/write fails. */ export declare function persistAttachments(input: PersistAttachmentsInput): Promise; //# sourceMappingURL=persist.d.ts.map