/** * Work-item attachments — content-addressed files on Todos and comments * (Todos v2 slice 5). Bytes live at `/attachments//`; * the DB row carries the original filename, mime, and the RELATIVE storage * path. Agents CONSUME attachments by reading the absolute `storagePath` the * read surface returns — the gateway and its agents share a filesystem by * architecture (local-first), so nothing streams over MCP. * * Semantics (design decisions, locked): * - Content-addressed: identical content dedupes to ONE file. Removing a row * never deletes the file while another row references the same hash * (refcount by query, not by column); the LAST removal unlinks it. * - `comment_id` NULL = attached to the Todo; set = attached to that comment, * which must belong to the same item and be live (not tombstoned) at attach * time. Tombstoning a comment later does NOT delete its attachment rows. * - Authority: any identified caller attaches to an item or to their OWN * comment (pair-matched on author + authorKind, operator overrides); * removal is uploader-or-operator. * - Caps: 25 MB per file, 200 MB per item (sum of its rows' bytes, comment * rows included — dedup does not discount the budget). * - `attachment_added` bumps the Todo version (new material resorts lists); * `attachment_removed` is audit-only. */ export declare const ATTACHMENT_MAX_BYTES: number; export declare const ATTACHMENT_ITEM_MAX_BYTES: number; export interface WorkItemAttachment { id: string; workItemId: string; commentId: string | null; filename: string; mime: string; bytes: number; sha256: string; /** Absolute on-disk path — agents Read this directly. The DB stores the * relative form (spec §3.2). */ storagePath: string; uploadedBy: string; createdAt: string; } /** Who is uploading/removing: the comments identity model — the author string * ('operator' | employee slug | `session:`) plus its kind, so a slug * colliding with a sentinel can never claim another principal's comments * (the org boundary also reserves those names — belt and suspenders). */ export interface AttachmentActor { author: string; authorKind: 'operator' | 'employee' | 'system'; operator: boolean; } export interface AddAttachmentInput { workItemId: string; /** NULL/absent = attached to the Todo itself; set = attached to that comment. */ commentId?: string | null; filename: string; /** Falls back to application/octet-stream — mime sniffing from the original * filename is the upload boundary's job (route/MCP), not the store's. */ mime?: string; /** Staged temp file the store CONSUMES: renamed into the content-addressed * location, or deleted when the content already exists (dedup). */ stagedPath: string; uploader: AttachmentActor; } export type WorkItemAttachmentErrorCode = 'attachment-not-found' | 'attachment-forbidden' | 'attachment-too-large' | 'attachment-item-budget' | 'comment-not-found' | 'comment-deleted'; export declare class WorkItemAttachmentError extends Error { readonly code: WorkItemAttachmentErrorCode; constructor(code: WorkItemAttachmentErrorCode, message: string); } /** Absolute content-addressed path for a stored hash. */ export declare function attachmentPath(sha256: string): string; /** Write a buffer to the staging area (same filesystem as the store, so the * content-addressed move is an atomic rename). Upload boundaries stage here * before calling addAttachment. */ export declare function stageAttachmentBuffer(buffer: Buffer): string; export declare function getAttachment(id: string): WorkItemAttachment | undefined; /** All of a Todo's attachment rows — item-level and per-comment — oldest first. */ export declare function listAttachments(workItemId: string): WorkItemAttachment[]; /** Sum of every attachment row's bytes on the item — comment rows included, * dedup NOT discounted (the budget is per row, decision 1). */ export declare function itemBytesUsed(workItemId: string): number; /** Attach a staged file to a Todo (or to one of its live comments). The staged * file is consumed on success AND on refusal (nothing is left behind). */ export declare function addAttachment(input: AddAttachmentInput): WorkItemAttachment; /** Remove an attachment row (uploader or operator). The stored file is * unlinked only when no other row references the same content hash. Returns * false for an unknown id. */ export declare function removeAttachment(id: string, remover: AttachmentActor): boolean; //# sourceMappingURL=attachments.d.ts.map