fix(cli): guide ssh argv usage

This commit is contained in:
Codex
2026-05-23 12:32:43 +00:00
parent ca1e2544f0
commit 15d074424c
8 changed files with 171 additions and 23 deletions
+86 -11
View File
@@ -4,6 +4,20 @@ import { type UniDeskConfig, repoRoot } from "./config";
export interface ParsedSshArgs {
remoteCommand: string | null;
requiresStdin: boolean;
invocationKind: SshInvocationKind;
}
export type SshInvocationKind = "interactive" | "argv" | "helper" | "ssh-like";
export interface SshFailureHint {
code: "ssh-like-command-friction";
providerId: string;
trigger: "timeout-or-kex" | "exit-255";
exitCode: number;
message: string;
try: string;
triage: string;
note: string;
}
const argvQuotedSshSubcommands = new Set(["rg", "grep", "sed", "nl", "stat", "du", "ls", "cat", "head", "tail", "wc", "pwd"]);
@@ -479,28 +493,28 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
const subcommand = args[0] ?? "";
if (isSshSkillDiscoveryArgs(args)) {
const toolArgs = subcommand === "skill" ? ["skill-discover", ...args.slice(2)] : ["skill-discover", ...args.slice(1)];
return { remoteCommand: shellArgv(toolArgs), requiresStdin: false };
return { remoteCommand: shellArgv(toolArgs), requiresStdin: false, invocationKind: "helper" };
}
if (subcommand === "apply-patch" || subcommand === "patch") {
const toolArgs = ["apply_patch", ...args.slice(1)];
return { remoteCommand: shellArgv(toolArgs), requiresStdin: true };
return { remoteCommand: shellArgv(toolArgs), requiresStdin: true, invocationKind: "helper" };
}
if (subcommand === "py") {
return { remoteCommand: buildPythonStdinCommand(args.slice(1)), requiresStdin: true };
return { remoteCommand: buildPythonStdinCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
}
if (subcommand === "argv" || subcommand === "exec") {
const toolArgs = args.slice(1);
if (toolArgs.length === 0) throw new Error(`ssh ${subcommand} requires a command`);
return { remoteCommand: shellArgv(toolArgs), requiresStdin: false };
return { remoteCommand: shellArgv(toolArgs), requiresStdin: false, invocationKind: "argv" };
}
if (subcommand === "find") {
return { remoteCommand: buildFindCommand(args.slice(1)), requiresStdin: false };
return { remoteCommand: buildFindCommand(args.slice(1)), requiresStdin: false, invocationKind: "helper" };
}
if (subcommand === "glob") {
return { remoteCommand: shellArgv(["glob", ...args.slice(1)]), requiresStdin: false };
return { remoteCommand: shellArgv(["glob", ...args.slice(1)]), requiresStdin: false, invocationKind: "helper" };
}
if (argvQuotedSshSubcommands.has(subcommand)) {
return { remoteCommand: shellArgv(args), requiresStdin: false };
return { remoteCommand: shellArgv(args), requiresStdin: false, invocationKind: "argv" };
}
const remote: string[] = [];
let remoteStarted = false;
@@ -521,7 +535,11 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
remoteStarted = true;
remote.push(arg);
}
return { remoteCommand: remote.length === 0 ? null : remote.join(" "), requiresStdin: false };
return {
remoteCommand: remote.length === 0 ? null : remote.join(" "),
requiresStdin: false,
invocationKind: remote.length === 0 ? "interactive" : "ssh-like",
};
}
function shellArgv(args: string[]): string {
@@ -658,6 +676,48 @@ export function wrapSshRemoteCommand(command: string | null): string {
return `${bootstrap}; stty -echo 2>/dev/null || true; ${command}`;
}
function safeProviderId(providerId: string): string {
return /^[A-Za-z0-9_.-]{1,64}$/u.test(providerId) ? providerId : "<provider>";
}
function classifySshLikeFailure(exitCode: number, stderrText: string): SshFailureHint["trigger"] | null {
const normalized = stderrText.toLowerCase();
if (
normalized.includes("kex_exchange_identification")
|| normalized.includes("ssh_exchange_identification")
|| normalized.includes("connection closed by remote host")
|| normalized.includes("connection reset by peer")
|| normalized.includes("connection timed out")
|| normalized.includes("operation timed out")
|| normalized.includes("timed out waiting for provider session")
|| normalized.includes("the operation was aborted")
) {
return "timeout-or-kex";
}
return exitCode === 255 ? "exit-255" : null;
}
export function sshFailureHint(providerId: string, parsed: ParsedSshArgs, exitCode: number, stderrText: string): SshFailureHint | null {
if (parsed.invocationKind !== "ssh-like") return null;
const trigger = classifySshLikeFailure(exitCode, stderrText);
if (trigger === null) return null;
const shownProviderId = safeProviderId(providerId);
return {
code: "ssh-like-command-friction",
providerId: shownProviderId,
trigger,
exitCode,
message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer argv form for non-interactive commands.",
try: `bun scripts/cli.ts ssh ${shownProviderId} argv bash -lc '<command>'`,
triage: `bun scripts/cli.ts provider triage ${shownProviderId} --observed-scope ssh --observed-error '<ssh-like timeout or kex failure>'`,
note: "This hint intentionally does not echo the original remote command.",
};
}
export function formatSshFailureHint(hint: SshFailureHint): string {
return `UNIDESK_SSH_HINT ${JSON.stringify(hint)}\n`;
}
function brokerSource(): string {
return String.raw`
const open = JSON.parse(process.argv[2] || process.argv[1] || "{}");
@@ -831,8 +891,16 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
if (rawMode) process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.pipe(child.stdin);
let stderrTail = "";
const appendStderrTail = (chunk: Buffer | string): void => {
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : chunk;
stderrTail = (stderrTail + text).slice(-16_384);
};
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
child.stderr.on("data", (chunk: Buffer) => {
appendStderrTail(chunk);
process.stderr.write(chunk);
});
return await new Promise<number>((resolve) => {
const restore = (): void => {
@@ -841,12 +909,19 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
};
child.on("error", (error) => {
restore();
process.stderr.write(`unidesk ssh failed to start broker: ${error.message}\n`);
const message = `unidesk ssh failed to start broker: ${error.message}\n`;
appendStderrTail(message);
process.stderr.write(message);
const hint = sshFailureHint(providerId, parsed, 255, stderrTail);
if (hint !== null) process.stderr.write(formatSshFailureHint(hint));
resolve(255);
});
child.on("close", (code) => {
restore();
resolve(code ?? 255);
const exitCode = code ?? 255;
const hint = sshFailureHint(providerId, parsed, exitCode, stderrTail);
if (hint !== null) process.stderr.write(formatSshFailureHint(hint));
resolve(exitCode);
});
});
}