fix(ssh): dispatch transfer snippets through verified transferShell
The previous fix gated on a verified `transferShell` but still let OpenSSH hand the snippet to whatever `$SHELL` happens to be on the remote. On a fish/csh/tcsh host the new gate would accept the host, then fail anyway because the login shell can't parse `if [ ... ]; then ...` (P1 from PR #3722 review). Each transfer command (read, write, stat, list) is now wrapped in `<transferShell> -c '...'` via a shared `wrapInPosixShell` helper, so the snippet is parsed by the same shell OMP's capability probe verified can run it. `ensurePosixRemote` returns the verified shell so each call site can do the wrap. `ssh-executor.ts`'s identical Windows-compat helper is consolidated onto the same primitive (`buildCompatCommand` / `quoteForCompatShell` removed). Stays POSIX-clean across all four call sites; `-c` (not `-lc`) since the snippets only call absolute builtins and don't need login-profile setup. Capability *probing* still uses `-lc` to mirror the user env. Test additions: one case asserts every dispatch starts with `bash -c '...'` and embeds the original POSIX snippet (read/write/stat/list) when transferShell is bash and login shell is unknown; another covers the `sh -c` happy path.
This commit is contained in:
@@ -4,6 +4,7 @@ import { OutputSink } from "../session/streaming-output";
|
||||
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
||||
import { buildRemoteCommand, ensureConnection, ensureHostInfo, type SSHConnectionTarget } from "./connection-manager";
|
||||
import { hasSshfs, mountRemote } from "./sshfs-mount";
|
||||
import { wrapInPosixShell } from "./utils";
|
||||
|
||||
export interface SSHExecutorOptions {
|
||||
/** Timeout in milliseconds */
|
||||
@@ -78,18 +79,6 @@ function createAbortWaiter(
|
||||
return { promise, cleanup: () => signal.removeEventListener("abort", onAbort) };
|
||||
}
|
||||
|
||||
function quoteForCompatShell(command: string): string {
|
||||
if (command.length === 0) {
|
||||
return "''";
|
||||
}
|
||||
const escaped = command.replace(/'/g, "'\\''");
|
||||
return `'${escaped}'`;
|
||||
}
|
||||
|
||||
function buildCompatCommand(shell: "bash" | "sh", command: string): string {
|
||||
return `${shell} -c ${quoteForCompatShell(command)}`;
|
||||
}
|
||||
|
||||
export async function executeSSH(
|
||||
host: SSHConnectionTarget,
|
||||
command: string,
|
||||
@@ -108,7 +97,7 @@ export async function executeSSH(
|
||||
if (options?.compatEnabled) {
|
||||
const info = await ensureHostInfo(host);
|
||||
if (info.compatShell) {
|
||||
resolvedCommand = buildCompatCommand(info.compatShell, command);
|
||||
resolvedCommand = wrapInPosixShell(info.compatShell, command);
|
||||
} else {
|
||||
logger.warn("SSH compat enabled without detected compat shell", { host: host.name });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user