fix: 收敛 trans 远端文本短路径
This commit is contained in:
@@ -38,6 +38,7 @@ interface SshFileTransferCliOptions {
|
||||
localPath: string;
|
||||
remotePath: string;
|
||||
inactivityTimeoutMs?: number;
|
||||
allowTextTransfer: boolean;
|
||||
}
|
||||
|
||||
interface SshFileTransferStat {
|
||||
@@ -99,12 +100,23 @@ const fileTransferWriteRawChunkBytes = 131_072;
|
||||
const fileTransferWriteB64ChunkChars = 1_398_104;
|
||||
const fileTransferProgressEveryChunks = 16;
|
||||
|
||||
const knownTextExtensions = new Set([
|
||||
".bat", ".bib", ".c", ".cc", ".cfg", ".cmd", ".conf", ".cpp", ".css", ".csv", ".cts", ".cxx",
|
||||
".diff", ".env", ".go", ".h", ".hpp", ".htm", ".html", ".ini", ".java", ".js", ".json", ".jsonl",
|
||||
".jsx", ".less", ".log", ".md", ".markdown", ".mts", ".mjs", ".patch", ".ps1", ".py", ".rs", ".scss",
|
||||
".sh", ".sql", ".svg", ".tex", ".text", ".toml", ".ts", ".tsv", ".tsx", ".txt", ".xml", ".yaml", ".yml",
|
||||
]);
|
||||
|
||||
const knownTextBasenames = new Set([
|
||||
".editorconfig", ".gitattributes", ".gitignore", "agents.md", "dockerfile", "license", "makefile", "mdtodo", "readme",
|
||||
]);
|
||||
|
||||
const fileTransferUsageHint = {
|
||||
code: "prefer-remote-text-operations",
|
||||
message: "避免对远端文本反复 upload/download;读取优先使用 cat/rg,修改优先使用 apply-patch。upload/download 仅用于必要的二进制文件或生成物传输。",
|
||||
message: "已知文本路径会在传输前被拦截;读取使用 cat/head/tail/rg,修改直接使用 apply-patch quoted heredoc。upload/download 仅用于必要的二进制文件或生成物。",
|
||||
preferredOperations: {
|
||||
read: ["cat", "rg"],
|
||||
edit: ["apply-patch"],
|
||||
read: ["cat", "head", "tail", "rg"],
|
||||
edit: ["apply-patch <<'PATCH'"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -121,6 +133,7 @@ export async function runSshFileTransferOperation(
|
||||
): Promise<number> {
|
||||
const options = parseSshFileTransferCliOptions(args);
|
||||
const localPath = path.resolve(options.localPath);
|
||||
const transferPolicy = enforceTextTransferPolicy(invocation, options, localPath);
|
||||
if (options.action === "upload") {
|
||||
const content = await readFile(localPath);
|
||||
const expected = { bytes: content.length, sha256: sha256HexBuffer(content) };
|
||||
@@ -141,6 +154,7 @@ export async function runSshFileTransferOperation(
|
||||
bytes: expected.bytes,
|
||||
sha256: expected.sha256,
|
||||
verified: true,
|
||||
transferPolicy,
|
||||
hint: fileTransferUsageHint,
|
||||
verification,
|
||||
transfer: write,
|
||||
@@ -159,6 +173,7 @@ export async function runSshFileTransferOperation(
|
||||
bytes: download.bytes,
|
||||
sha256: download.sha256,
|
||||
verified: true,
|
||||
transferPolicy,
|
||||
hint: fileTransferUsageHint,
|
||||
verification: download.verification,
|
||||
transfer: download.transfer,
|
||||
@@ -204,6 +219,7 @@ function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptio
|
||||
if (action !== "upload" && action !== "download") throw new Error("ssh file transfer requires upload or download");
|
||||
const positionals: string[] = [];
|
||||
let inactivityTimeoutMs: number | undefined;
|
||||
let allowTextTransfer = false;
|
||||
for (let index = 1; index < args.length; index += 1) {
|
||||
const arg = args[index] ?? "";
|
||||
if (arg === "--") {
|
||||
@@ -213,6 +229,10 @@ function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptio
|
||||
if (arg === "--chunk-bytes" || arg === "--block-bytes") {
|
||||
throw new Error(`unsupported ssh ${action} option: ${arg}; downloads use host.ssh.tcp-pool stdout streaming and no longer support the legacy base64 block reader`);
|
||||
}
|
||||
if (arg === "--allow-text-transfer") {
|
||||
allowTextTransfer = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--inactivity-timeout-ms" || arg === "--runtime-timeout-ms") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined) throw new Error(`ssh ${action} ${arg} requires a positive integer value`);
|
||||
@@ -240,8 +260,55 @@ function parseSshFileTransferCliOptions(args: string[]): SshFileTransferCliOptio
|
||||
const [first, second] = positionals;
|
||||
if (!first || !second) throw new Error(`ssh ${action} paths must be non-empty`);
|
||||
return action === "upload"
|
||||
? { action, localPath: first, remotePath: second, inactivityTimeoutMs }
|
||||
: { action, remotePath: first, localPath: second, inactivityTimeoutMs };
|
||||
? { action, localPath: first, remotePath: second, inactivityTimeoutMs, allowTextTransfer }
|
||||
: { action, remotePath: first, localPath: second, inactivityTimeoutMs, allowTextTransfer };
|
||||
}
|
||||
|
||||
function enforceTextTransferPolicy(
|
||||
invocation: ParsedSshInvocation,
|
||||
options: SshFileTransferCliOptions,
|
||||
resolvedLocalPath: string,
|
||||
): Record<string, unknown> {
|
||||
const knownTextPaths = [
|
||||
knownTextPath(options.remotePath),
|
||||
knownTextPath(resolvedLocalPath),
|
||||
].filter((item): item is { path: string; reason: string } => item !== null);
|
||||
if (knownTextPaths.length > 0 && !options.allowTextTransfer) {
|
||||
const readCommand = `trans ${invocation.route.raw} cat ${JSON.stringify(options.remotePath)}`;
|
||||
const editCommand = `trans ${invocation.route.raw} apply-patch <<'PATCH'`;
|
||||
throw new SshFileTransferError("remote text transfer is discouraged; use the native text operation", {
|
||||
code: "text-transfer-discouraged",
|
||||
action: options.action,
|
||||
route: invocation.route.raw,
|
||||
knownTextPaths,
|
||||
preferred: options.action === "download"
|
||||
? { operation: "cat", command: readCommand }
|
||||
: { operation: "apply-patch", command: editCommand },
|
||||
alternatives: options.action === "download"
|
||||
? [readCommand, `trans ${invocation.route.raw} head -n 80 ${JSON.stringify(options.remotePath)}`, `trans ${invocation.route.raw} rg <pattern> ${JSON.stringify(options.remotePath)}`]
|
||||
: [editCommand],
|
||||
override: {
|
||||
option: "--allow-text-transfer",
|
||||
purpose: "仅用于确需整体搬运的生成物;成功结果会记录 overrideRequested=true。",
|
||||
},
|
||||
targetCreated: false,
|
||||
remoteOperationStarted: false,
|
||||
});
|
||||
}
|
||||
return {
|
||||
code: "binary-or-generated-artifact-transfer",
|
||||
knownTextPaths,
|
||||
overrideRequested: options.allowTextTransfer,
|
||||
overrideUsed: knownTextPaths.length > 0 && options.allowTextTransfer,
|
||||
};
|
||||
}
|
||||
|
||||
function knownTextPath(rawPath: string): { path: string; reason: string } | null {
|
||||
const normalized = rawPath.trim().replace(/\\/gu, "/").replace(/\/+$/gu, "");
|
||||
const basename = normalized.slice(normalized.lastIndexOf("/") + 1).toLowerCase();
|
||||
if (knownTextBasenames.has(basename)) return { path: rawPath, reason: `basename:${basename}` };
|
||||
const extension = path.posix.extname(basename);
|
||||
return knownTextExtensions.has(extension) ? { path: rawPath, reason: `extension:${extension}` } : null;
|
||||
}
|
||||
|
||||
function parsePositiveRuntimeTimeoutMs(value: string, label: string): number {
|
||||
|
||||
Reference in New Issue
Block a user