fix: 收敛 trans 远端文本短路径

This commit is contained in:
Codex
2026-07-13 05:20:27 +02:00
parent 18937da9db
commit c04983455e
16 changed files with 498 additions and 70 deletions
+72 -5
View File
@@ -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 {