/** * Transfer Tool * scp-based binary-safe file/directory upload & download * with sha256 verification and atomic rename semantics. * * - ssh_upload — upload a file or directory; verify via sha256; atomic rename. * - ssh_download — download a file or directory via scp (binary-safe). * * Heredoc / cat>file / base64-chunks are intentionally NOT used here. * For sudo writes into protected paths, the file travels to /tmp under the * SSH user, is copied next to the target under sudo and takes the target * path by rename. */ import { CallToolRequest, Tool } from '@modelcontextprotocol/sdk/types.js'; import { type ToolResult } from '../utils/tool-result.js'; export declare class TransferTool { private executor; constructor(); getTools(): Tool[]; handleCall(request: CallToolRequest): Promise; private handleUpload; /** * Upload response. * * The path printed is where the file actually landed: with `~` it differs * from what was requested. Warnings go into the same place — both the ones * the installer collected (an uncleaned old copy) and the ones that came up * while expanding the path; without this they would simply vanish. */ private formatUploadResult; /** * Upload a single file. * * Both paths, plain and under sudo, go through the installer: data lands on * a staging path next to the target and takes its place by rename. The only * difference is how that staging path gets filled — a direct transfer, or a * copy from /tmp, because the transfer itself does not run as root. */ private uploadFile; /** * Put a single file on the server. * * The parent directory is not created here: the staging path always sits * next to the target, and the installer has already created its directory * before the transfer starts. On the sudo path it is `/tmp` outright. An * extra command here would cost one round trip to the server per upload. */ private putFile; /** * Fill the staging path next to the target when only root can write there. * * The transfer runs as the connecting user into /tmp, and the file appears * next to the target as a sudo copy: the transport itself has no root * privileges. The intermediate file is removed either way. */ private stageUnderSudo; /** * Fill the staging path with a whole tree the connecting user cannot write. * * The transport never runs as root, so the tree lands in /tmp first and is * copied next to the target from there. The handoff copy goes away whatever * happens: it is a full second copy of the data on a shared path. */ private stageTreeUnderSudo; /** * Mode and owner on the staging path — before it becomes the target. * * Otherwise a window opens on the live path where the data is already * visible but access to it is still wrong. Owner applies only under sudo: * under a regular user `chown` refuses on a name that isn't its own. */ private applyOwnership; /** * Whether the path exists on the server. "Failed to check" is a separate * outcome: a failed check answering "no file" would let the * `overwrite: false` guard write over something it never managed to see. */ private remoteExists; /** * Verify the transferred data against the source. * * The three outcomes differ in kind: a mismatch is a transfer failure (we * throw), "nothing to verify with" is a success with a note. Mixing them up * means a server without sha256sum gets the same reaction as a corrupted * file, and the error handler goes on to delete what was just written. */ private verify; private uploadDirectory; /** * Build the staging tree out of the target's own content with the new tree * on top. * * The order is the whole point. The new tree crosses the network first, into * a temporary path of its own next to the target; only then is the target * copied, and only then does the new tree land on top of that copy. Copying * the target first would widen the window in which writes into the live * directory — a log line, an upload — are missed by the copy that replaces it. * * The composition is two plain `cp -a`: the same two on BusyBox and on * coreutils. `cp -a -n` reads better and would do it in one command, but on * BusyBox it skips the whole tree and still exits 0 — merge would quietly * become a full replacement, which is the loss the flag exists to prevent. */ private stageMerged; private handleDownload; private isRemoteDir; /** * Bring a source the connecting user cannot read within its reach. * * The transport never runs as root, so the data is copied under root into * /tmp, handed over to the connecting user and fetched from there. The copy * goes away whatever happens next — it is a second copy of someone else's * data sitting on a shared path, not something to leave behind. The * original is only ever read, so no failure here can cost anything. */ private fetchUnderSudo; private downloadFile; /** * Download a directory as a whole. * * Walking the remote tree is the transport's job, so what remains here is * just recounting what arrived and, if asked, verifying hashes — without * this, `verify` for a directory would silently do nothing. */ private downloadDirectory; }