/**
 * Composer attachment resolution (composer-vision-attach, generalized in PR3).
 *
 * The chat composer lets a user attach any READABLE file — uploaded to
 * conversation scratch storage OR picked from the asset library — and have the
 * agent get its contents for the conversation. The client never sends bytes; it
 * sends durable **refs**. This service resolves a ref to what the agent actually
 * consumes:
 *  - **images** → an `ImageContent` vision block (the model sees the pixels).
 *  - **docs**   → extracted TEXT injected as context (pdf/docx/xlsx extracted at
 *    upload and stored; txt/md/csv stored raw). See `chat_scratch_files`.
 *
 * The mechanism differs by type; the resolver branches on the scratch row's
 * `kind` (or, for a library image, the asset index row). Two focused entry
 * points keep the two concerns clean:
 *  - `resolveImageRefs` — CURRENT-turn images → vision blocks (PR2 behavior).
 *  - `resolveDocContext` — doc refs (current turn + replayed from session
 *    history, D7) → a single capped, prompt-injection-wrapped text block.
 *
 * Design (see `~/.claude/plans/composer-vision-attach.md`):
 *  - **A1 (IDOR):** trust ONLY the index/scratch row, never client-supplied
 *    values. Repo access (library) / row ownership (scratch) is re-checked on
 *    EVERY resolve — there is no "trusted" skip.
 *  - **A3/D8/D11 (limits):** a fixed cross-model MIME allowlist + byte cap +
 *    count cap for images; per-doc + total char caps for doc text.
 *  - **D12 (injection):** untrusted doc text is wrapped in labeled delimiters
 *    with a "data, not instructions" preamble — it must never sit at the
 *    user-prompt level unmarked.
 *  - A denied / deleted / unsupported ref never throws and never leaks: it is
 *    dropped and replaced with a `[file: name]` text placeholder.
 *
 * Deps are injected (not WorkspaceManager directly) so the IDOR/format/size
 * logic is unit-testable without standing up a workspace.
 */
import type { ImageContent } from '@mariozechner/pi-ai';
import type { AssetIndexRow } from './assets/asset-index-service.js';
import type { ScratchFileKind, ScratchFileSource } from './chat-session-service.js';
/**
 * Durable attachment reference the client sends (NOT bytes), a discriminated union:
 *  - `asset`   — a curated library IMAGE pick; resolved via the asset index + repo
 *    access. (Library DOC picks are materialized into a scratch row server-side,
 *    so they arrive as `scratch` refs — there is no asset-doc ref kind.)
 *  - `scratch` — a conversation file (uploaded image/doc, or a library doc copied
 *    into scratch); resolved via the chat scratch row + chat ownership. The row's
 *    `kind` decides image-vs-doc handling; no `repo` (it's not a catalogued asset).
 */
export type AttachmentRef = {
    kind: 'asset';
    assetId: string;
    filename: string;
} | {
    kind: 'scratch';
    sessionId: string;
    fileId: string;
    filename: string;
};
/** Back-compat alias: PR2 shipped the scratch image union with `imageId`. */
export type ImageRef = AttachmentRef;
/** Minimal scratch-file metadata the resolver needs (from chat_scratch_files). */
export interface ScratchFileLite {
    filename: string;
    contentType: string;
    userId: number;
    blobKey: string;
    kind: ScratchFileKind;
    /** Doc text (already capped at upload). Null for images. */
    extractedText: string | null;
    /** 'library' rows reference a permanent asset; their repo access must be
     *  RE-checked on every replay (access may be revoked after attach). */
    source?: ScratchFileSource;
    sourceRepo?: string | null;
}
/**
 * Normalize a possibly-legacy/loosely-typed ref into the tagged union, or null
 * if it carries neither a usable asset nor scratch identity. Defensive parsing
 * for `chat_messages.meta` (PR2 refs carry `imageId`, not `fileId`; older refs
 * may lack `kind`).
 */
export declare function normalizeAttachmentRef(raw: unknown): AttachmentRef | null;
/** PR2 export name kept for back-compat importers. */
export declare const normalizeImageRef: typeof normalizeAttachmentRef;
/**
 * Image MIME types vision models actually decode, cross-model. Mirrors
 * `CAPTIONABLE_MIME` in asset-caption-service.ts — Anthropic + most providers
 * only accept these four; shipping the others 400s. No per-provider matrix (A3).
 */
export declare const VISION_MIME_ALLOWLIST: Set<string>;
/** Doc MIME types the composer extracts text from (D11). pdf/docx/xlsx via the
 *  shared extractors; txt/md/csv read raw. */
export declare const DOC_MIME_ALLOWLIST: Set<string>;
/**
 * The composer-READABLE allowlist (D11): the union of vision images + extractable
 * docs. This is DELIBERATELY narrower than the asset/import allowlist (which
 * accepts opaque zip/video/audio that the agent can't read) — every accepted file
 * must produce either pixels or text. The upload route enforces this.
 */
export declare const COMPOSER_READABLE_MIME: Set<string>;
/**
 * Max decoded bytes per image. Base64 inflates ~33% into the request body and
 * large payloads 413/timeout. Matches the caption path's DEFAULT_CAPTION_MAX_BYTES.
 */
export declare const VISION_MAX_IMAGE_BYTES: number;
/** Max bytes for an uploaded DOC (D8). Tighter than the image cap because the
 *  extractors (pdf-parse/mammoth/SheetJS) run synchronously on the request
 *  thread — a tight cap keeps extraction latency bounded. */
export declare const DOC_MAX_UPLOAD_BYTES: number;
/** Max images carried by a single turn. Vision is metered — keep it bounded. */
export declare const VISION_MAX_IMAGES = 8;
/** Per-doc extracted-text cap (chars). ~12k chars ≈ 3k tokens. Doc text is
 *  STORED truncated to this too (D8), so `chat-sessions.db` can't balloon. */
export declare const DOC_PER_FILE_CHARS = 12000;
/** Total doc-context cap across all docs in one turn (chars). ~40k ≈ 10k tokens. */
export declare const DOC_TOTAL_CHARS = 40000;
/** Max docs injected into one turn's context. */
export declare const DOC_MAX_FILES = 5;
/** The text placeholder substituted for an attachment that couldn't be shown. */
export declare function attachmentPlaceholder(name: string): string;
/** PR2 export name kept for back-compat importers. */
export declare const imagePlaceholder: typeof attachmentPlaceholder;
/** The note substituted for an image dropped by the per-message image cap. The
 *  composer blocks attaching past the cap, so this is the backstop for anything
 *  that slips by the UI — instructive, so the model tells the user instead of
 *  silently analyzing a subset. */
export declare function overCapImagePlaceholder(name: string): string;
/** Prompt used when the user sends attachments with no text (#5). */
export declare const ATTACHMENT_ONLY_FALLBACK_PROMPT = "Please look at the attached file(s).";
/** PR2 export name kept for back-compat. */
export declare const IMAGE_ONLY_FALLBACK_PROMPT = "Please look at the attached file(s).";
/** #5: a chat turn is valid with text OR at least one attachment (image or doc);
 *  empty-everything is not. */
export declare function isValidAttachmentTurn(message: string | undefined | null, attachmentCount: number): boolean;
/** PR2 export name kept for back-compat. */
export declare const isValidVisionTurn: typeof isValidAttachmentTurn;
/**
 * Compose the user-message text for a (possibly attachment-bearing) turn: the
 * typed text, or the attachment-only fallback when there's no text but there are
 * attachments, with any dropped-file placeholders appended. Caller prepends its
 * own context prefixes (and the doc context block, separately).
 */
export declare function composeAttachmentMessageText(messageText: string, hasAttachments: boolean, placeholders?: string[]): string;
/** PR2 export name kept for back-compat. */
export declare const composeVisionMessageText: typeof composeAttachmentMessageText;
/** Turn raw image bytes into the agent runtime's image content block. */
export declare function bufferToImageContent(buffer: Buffer, mimeType: string): ImageContent;
/**
 * Injected dependencies. Each is pre-bound to the current request's user, so the
 * service itself holds no auth knowledge — keeping the access decision in ONE
 * place (the route) and the resolution logic trivially testable.
 */
export interface ResolveAttachmentDeps {
    /** The authoritative asset-index row for an id, or null if unknown/deleted. */
    getAssetRow: (assetId: string) => AssetIndexRow | null;
    /** Whether the CURRENT user may read the given repo. Re-checked every call. */
    canAccessRepo: (repo: string) => boolean;
    /** Fetch the stored bytes for an asset (by its index-row repo + filename). */
    downloadAsset: (repo: string, assetId: string, filename: string) => Promise<Buffer>;
    /** The scratch metadata row for (sessionId, fileId), or null if unknown. */
    getScratchRow: (sessionId: string, fileId: string) => ScratchFileLite | null;
    /** Fetch a scratch image's bytes by its stored blob key. */
    downloadScratch: (blobKey: string) => Promise<Buffer>;
    /** The current request's user id — scratch authz is row.userId === this. */
    currentUserId: number | undefined;
}
/** PR2 type name kept for back-compat. */
export type ResolveImageRefsDeps = ResolveAttachmentDeps;
export interface ResolvedImages {
    /** Vision blocks for the refs that resolved cleanly, in input order. */
    blocks: ImageContent[];
    /** `[file: name]` notes for refs that were dropped (deleted/denied/etc.). */
    placeholders: string[];
}
/**
 * Resolve client refs → vision blocks. Handles only IMAGE refs (library `asset`
 * images + `scratch` rows whose `kind==='image'`); doc refs are skipped here
 * (they flow through {@link resolveDocContext}). Never throws for a bad/forbidden
 * ref — it drops to a placeholder. Resolves at most `VISION_MAX_IMAGES`.
 */
export declare function resolveImageRefs(refs: AttachmentRef[] | undefined | null, deps: ResolveAttachmentDeps): Promise<ResolvedImages>;
export interface ResolvedDocs {
    /** A single text block to append to the prompt (already injection-wrapped +
     *  capped), or '' if no docs resolved. */
    docContext: string;
    /** `[file: name]` notes for doc refs that were dropped. */
    placeholders: string[];
}
/** One resolved doc's text, pre-wrap. */
interface DocPart {
    filename: string;
    text: string;
}
/**
 * Resolve doc refs → ONE prompt-appendable text block (D7 replay target).
 * Handles only `scratch` rows whose `kind==='doc'` (image/asset refs are skipped
 * here). Pass the union of the current turn's refs AND the doc refs replayed from
 * session history — duplicates (same sessionId+fileId) are de-duped. Applies the
 * per-doc + total caps (D1/D8) and wraps the result so the model treats it as
 * data, not instructions (D12). Synchronous: text is already stored on the row.
 */
export declare function resolveDocContext(refs: AttachmentRef[] | undefined | null, deps: ResolveAttachmentDeps): ResolvedDocs;
export declare function wrapDocContext(parts: DocPart[]): string;
export {};
