/** * The user's file drawer: its path law, its upload cap, and the three tools * that read and fill it. * * Everything here is locked to §3.1's frozen `/user/files` mount, and the lock * is structural rather than a check bolted onto a path argument: the tools take * a NAME and build the path themselves, so no caller-supplied path exists for a * `..` to climb through. `userFilePath` is the single authority the write doors * (`POST /files`, `vendo_user_files_put`, `vendo.putUserFile`) and these reads * all go through. */ import { UPLOAD_MAX_BYTES, VendoError, type Principal, type ThreadId, type ToolRegistry, type WorkspaceFs } from "./core/index.js"; import type { FilesVenue } from "./compose-store.js"; import type { CreateVendoConfig } from "./types.js"; /** §3.1's frozen drawer: per subject, and outliving every conversation. */ export declare const USER_FILES = "/user/files"; /** Where a dropped file LANDS, for the moments between `POST /files` and the turn that claims it. A drop finishes before the conversation it belongs to exists (the composer uploads pre-send, and a first turn's thread id is minted server-side), so there has to be an address that means "received, not yet homed". Nothing but the re-homer and its sweep ever reads it. */ export declare const USER_UPLOADS = "/user/uploads"; /** Where a file BELONGS once a turn has claimed it: with the conversation, like a Claude Code project — so it is in reach of every later turn on that thread, and it dies when the thread does. */ export declare const USER_THREADS = "/user/threads"; /** Does this message part address bytes the SERVER holds, rather than carry them? All three addresses answer yes: the shelf, a staged drop, and a conversation's own files. */ export declare const isUserFilePath: (path: string) => boolean; /** * The ONE name check every door into a user's files shares, so a file lands and * is fetched at the same address by the same rule. * * A name is a FILE name, never a path. Refusing `/`, `\` and the `.`/`..` * dot-segments AT THE SOURCE is what contains the whole feature — every path * below is BUILT from a name that provably carries no separator, so there is * nothing to escape with. Same posture as the route-tool traversal fix * (b9392b92c): reject the segment rather than sanitize it, because the values * reaching here are steerable by end-user chat. */ /** The longest leaf any door accepts. Named because the re-homer builds a leaf out of one staging already prefixed and has to cut it to the same limit. */ export declare const MAX_LEAF_NAME = 200; /** The keep-shelf's address. */ export declare function userFilePath(name: string): string; /** A staged drop's address. The random prefix is OURS, never the caller's: two conversations may drop `report.pdf` in the same second and neither may overwrite the other before its turn claims it. */ export declare function uploadStagingPath(name: string): string; /** Everything one conversation owns. The delete cascade sweeps this whole subtree, so nothing may live under it that should outlive the thread. The id goes through the SAME rule as a name: it is a path segment like any other, and the only shape the wire checks is `thr_` + `.+` (core ids.ts), whose `.+` matches a slash. Without this a client-chosen id climbed out of this mount — and took the delete cascade's recursive rm with it. */ export declare const threadFilesDir: (threadId: ThreadId) => string; /** One file, homed with its conversation. */ export declare const threadFilePath: (threadId: ThreadId, name: string) => string; /** The drop door's default cap. It lives in core beside `UPLOAD_HEADER`, the other half of the same wire contract, because `vendo doctor` reports it from another package; named here because this is the door the default describes. */ export { UPLOAD_MAX_BYTES }; export declare const overCap: (name: string, bytes: number, max: number, venue: FilesVenue) => VendoError; /** The cap and its backing, resolved from config ONCE: the drop door (`POST /files`) and the upload tool both read this, so they can never refuse at different sizes or name a different destination. The `files` predicate belongs to `config`, not to the resolved adapter — `selectFiles` returns one FilesAdapter either way, and the interface has no name to ask for. */ export declare const uploadCapOf: (config: Pick) => { uploadMaxBytes: number; files: FilesVenue; }; export declare const VENDO_USER_FILES_LIST_TOOL = "vendo_user_files_list"; export declare const VENDO_USER_FILES_READ_TOOL = "vendo_user_files_read"; export declare const VENDO_USER_FILES_PUT_TOOL = "vendo_user_files_put"; /** One read's window. Line-oriented, because a spreadsheet row cut in half is unusable — and sized well under the 32,000-char global tool-output cap (tool-bridge's `capOutcome`), whose blunt truncation would replace this whole result with a preview string and destroy its structure. */ export declare const LINES_PER_READ = 200; export declare const CHARS_PER_READ = 12000; /** * The drawer's hands, on the ONE registry — guarded, audited and projected * exactly like a host tool, with no privileged side door. Each descriptor's * `risk` is the whole guard story: `guard.bind` keys off it. * * Every hand opens the workspace for `ctx.principal` and NOBODY else, so one * user's drawer is unreachable from another's session by construction — there * is no subject argument to get wrong. */ export declare function createUserFilesTools(open: (principal: Principal) => Promise, cap: { uploadMaxBytes: number; files: FilesVenue; }): ToolRegistry;