fix: expose ssh transfer verification

This commit is contained in:
Codex
2026-05-30 08:31:52 +00:00
parent 277e28ce97
commit 91564b3784
4 changed files with 67 additions and 6 deletions
+2 -2
View File
@@ -24,7 +24,7 @@ export function rootHelp(): unknown {
{ command: "ssh <route> [operation args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge; route syntax such as `G14:k3s` or `D601:win/c/test` only locates distributed targets." },
{ command: "ssh gh:/owner/repo[/pr|/issue][/number[/1]] ls|cat|rg|patch-apply", description: "Treat GitHub PRs/issues as virtual text directories; `ls --full` shows state/floors/body length, and `patch-apply` updates first-floor `body.md` through UniDesk gh plus apply-patch v2." },
{ command: "ssh <route> apply-patch < patch.diff", description: "Default remote text patch entry: apply a standard patch with the local TypeScript v2 engine while the remote route only reads and writes files." },
{ command: "ssh <route> upload <local-file> <remote-file> | ssh <route> download <remote-file> <local-file>", description: "Transfer whole files through SSH passthrough with remote temp files, byte-count checks, SHA-256 verification, and client-side chunk fallback." },
{ command: "ssh <route> upload <local-file> <remote-file> | ssh <route> download <remote-file> <local-file>", description: "Transfer whole files through SSH passthrough with remote temp files, automatic endpoint byte/SHA-256 verification, and client-side chunk fallback." },
{ command: "ssh <providerId> apply-patch-v1 [tool args...] < patch.diff", description: "Fallback to the injected legacy remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },
{ command: "ssh <providerId> py [script-args...] < script.py", description: "Run remote Python from local stdin through SSH passthrough without nested shell quoting; extra args become script argv." },
{ command: "ssh <providerId> script [--shell sh|bash] [script-args...] <<'SCRIPT' ...", description: "Run a remote shell script from local stdin using shell -s; default sh inherits provider proxy env and gets the portable printf helper used by shell/script." },
@@ -192,7 +192,7 @@ export function sshHelp(): unknown {
"script and shell helper modes inject a tiny POSIX-compatible printf wrapper before user shell text, so portable printf headings such as `printf \"--- section ---\\n\"` work consistently under dash/sh and bash. Direct argv commands are unchanged.",
"For arbitrary stdin streams into a workload command, use a workload route plus `exec --stdin -- <command> ...`; this keeps the route as location-only and avoids heredoc/base64/tar shell wrapping.",
"`apply-patch` is the default remote text patch entry and uses the v2 local line-based patch engine with remote read/write operations, including Windows routes such as `D601:win/c/test`, so long Unicode/Chinese lines and pure insertion hunks avoid the legacy remote shell hunk parser. Its stdout/stderr follows Codex apply_patch text output rather than UniDesk JSON output; on multi-file failure, stderr lists applied hunks before the first failed hunk and the failed hunk, then stops like Codex apply_patch.",
"`upload` and `download` are the default whole-file transfer entries for non-text and generated files. They write through remote temp files, verify byte count and SHA-256 on both sides, and fall back from a single stdin payload to bounded client-side chunks before treating provider-gateway limits as a server-side problem.",
"`upload` and `download` are the default whole-file transfer entries for non-text and generated files. They write through remote temp files, verify byte count and SHA-256 on both sides, and return `verification.automatic=true`, `verification.verified=true`, and `verification.match.{bytes,sha256}=true`; this JSON is the transfer integrity proof, so callers do not need a separate manual `sha256sum` check. The client falls back from a single stdin payload to bounded chunks before treating provider-gateway limits as a server-side problem.",
"`apply-patch-v1` is the only legacy fallback entry: it rejects low-context update hunks by default, reports the matched file:line for each hunk on stderr, and only accepts --allow-loose when the caller has manually reviewed an intentionally ambiguous insertion.",
"script defaults to target /bin/sh and inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY; use --shell bash only for bash syntax such as pipefail, arrays, or [[ ... ]], not as a proxy workaround.",
"Route syntax is `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`: the first argv token locates a distributed target only, and every following token belongs to the operation parser. Host workspace routes use `<provider>:/absolute/workspace`; WSL providers can use `<provider>:win cmd <command-line>` to run Windows host cmd.exe with UTF-8 defaults, and `<provider>:win/c/test cmd cd` maps the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use <provider>:k3s for the control plane, <provider>:k3s:<namespace>:<workload> for a workload, and <provider>:k3s:<namespace>:<workload>/<pod-workspace> for a pod workspace.",
+30
View File
@@ -33,6 +33,11 @@ interface SshFileTransferStat {
sha256: string;
}
interface SshFileTransferEndpointStat extends SshFileTransferStat {
side: "local" | "remote";
path: string;
}
interface SshFileTransferWriteResult {
strategy: "argv" | "stdin" | "chunked-stdin";
chunks: number;
@@ -68,6 +73,10 @@ export async function runSshFileTransferOperation(
const write = await writeRemoteFileVerified(invocation, executor, builders, options.remotePath, content);
const remote = await statRemoteFile(invocation, executor, builders, options.remotePath);
assertTransferStat("upload final remote verification", options.remotePath, expected, remote);
const verification = buildTransferVerification(
{ side: "local", path: localPath, ...expected },
{ side: "remote", path: options.remotePath, ...remote },
);
process.stdout.write(`${JSON.stringify({
ok: true,
command: "ssh upload",
@@ -78,6 +87,7 @@ export async function runSshFileTransferOperation(
bytes: expected.bytes,
sha256: expected.sha256,
verified: true,
verification,
transfer: write,
}, null, 2)}\n`);
return 0;
@@ -89,6 +99,10 @@ export async function runSshFileTransferOperation(
const local = await readFile(localPath);
const localStat = { bytes: local.length, sha256: sha256HexBuffer(local) };
assertTransferStat("download final local verification", localPath, read.remote, localStat);
const verification = buildTransferVerification(
{ side: "remote", path: options.remotePath, ...read.remote },
{ side: "local", path: localPath, ...localStat },
);
process.stdout.write(`${JSON.stringify({
ok: true,
command: "ssh download",
@@ -99,6 +113,7 @@ export async function runSshFileTransferOperation(
bytes: read.remote.bytes,
sha256: read.remote.sha256,
verified: true,
verification,
transfer: {
strategy: "chunked-read",
chunks: read.chunks,
@@ -274,6 +289,21 @@ function assertTransferStat(label: string, pathName: string, expected: SshFileTr
});
}
function buildTransferVerification(source: SshFileTransferEndpointStat, target: SshFileTransferEndpointStat): Record<string, unknown> {
return {
automatic: true,
algorithm: "sha256",
checked: ["bytes", "sha256"],
verified: source.bytes === target.bytes && source.sha256 === target.sha256,
source,
target,
match: {
bytes: source.bytes === target.bytes,
sha256: source.sha256 === target.sha256,
},
};
}
function sha256HexBuffer(value: Buffer): string {
return createHash("sha256").update(value).digest("hex");
}