fix: require explicit trans shell operations

This commit is contained in:
Codex
2026-06-15 04:01:04 +00:00
parent b2c2819fe2
commit b8fb3c41a8
18 changed files with 241 additions and 273 deletions
+72 -103
View File
@@ -187,6 +187,9 @@ const legacyK3sOperationRouteSegments = new Set([
"kubectl",
"exec",
"script",
"shell",
"sh",
"bash",
"apply-patch",
"apply-patch-v1",
"patch",
@@ -945,11 +948,11 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
if (subcommand === "playwright") {
return { remoteCommand: null, requiresStdin: true, invocationKind: "helper" };
}
if (subcommand === "script" || subcommand === "sh") {
return buildShellCommand(args.slice(1));
if (subcommand === "script" || subcommand === "shell") {
throw removedShellAliasError(subcommand);
}
if (subcommand === "shell") {
return buildShellStringCommand(args.slice(1));
if (subcommand === "sh" || subcommand === "bash") {
return buildShellCommand(args.slice(1), subcommand, `ssh ${subcommand}`);
}
if (subcommand === "argv" || subcommand === "exec") {
const toolArgs = args.slice(1);
@@ -1003,7 +1006,7 @@ export function sshRouteSeparatorCompatibilityHint(rawArgs: string[], normalized
if (rawArgs === normalizedArgs || rawArgs[0] !== "--") return "";
const operation = normalizedArgs[0] ?? "";
const operationText = operation.length === 0 ? "operation" : `operation \`${operation}\``;
return `UNIDESK_SSH_HINT route-level -- is ignored before ${operationText}; canonical form is \`trans <route> ${normalizedArgs.join(" ")}\`. Keep -- only inside operations such as \`script -- <command>\` or \`exec -- <command>\`.\n`;
return `UNIDESK_SSH_HINT route-level -- is ignored before ${operationText}; canonical form is \`trans <route> ${normalizedArgs.join(" ")}\`. Keep -- only inside operations such as \`sh -- <command>\`, \`bash -- <command>\`, or \`exec -- <command>\`.\n`;
}
function validateDirectArgvCommand(commandName: string, toolArgs: string[]): void {
@@ -1012,7 +1015,7 @@ function validateDirectArgvCommand(commandName: string, toolArgs: string[]): voi
if (!looksLikeShellCommandString(command)) return;
throw new Error(
`ssh ${commandName} received one shell-like command string; ${commandName} executes a single process and treats that string as the executable path. ` +
`Use \`trans <route> script -- ${shellQuote(command)}\` for one-line shell logic, or split argv tokens, for example \`trans <route> ${commandName} ls -la\`.`
`Use \`trans <route> sh -- ${shellQuote(command)}\` or \`trans <route> bash -- ${shellQuote(command)}\` for one-line shell logic, or split argv tokens, for example \`trans <route> ${commandName} ls -la\`.`
);
}
@@ -1533,6 +1536,9 @@ function k3sOperationInRouteMessage(target: string, operation: string): string {
if (operation === "v2" || operation === "patch" || operation === "patch-v1") {
return `ssh k3s route must locate a target only; remote patch entrypoints are "apply-patch" for the default v2 engine and "apply-patch-v1" for the legacy helper instead of "${target}"`;
}
if (operation === "script" || operation === "shell") {
return `ssh k3s route must locate a target only; operation "${operation}" has been removed because it hides the shell dialect; use explicit "sh" or "bash" after the route instead of "${target}"`;
}
const operationExample = operation === "guard" ? "guard" : `${operation} ...`;
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh ${providerId}:k3s ${operationExample}" or "ssh ${providerId}:k3s:<namespace>:<workload> ${operationExample}" instead of "${target}"`;
}
@@ -1662,12 +1668,11 @@ function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): P
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (operation === "script" || operation === "sh") {
return buildK3sScriptOperation(args.slice(1));
if (operation === "script" || operation === "shell") {
throw removedShellAliasError(operation);
}
if (operation === "shell") {
const parsed = parseShellStringOperationArgs(args.slice(1), `ssh ${route.providerId}:k3s shell`);
return { remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
if (operation === "sh" || operation === "bash") {
return buildK3sScriptOperation(args.slice(1), operation, `ssh ${route.providerId}:k3s ${operation}`);
}
if (operation === "guard") {
if (args.length > 1) throw new Error(`ssh ${route.providerId}:k3s guard does not accept extra arguments`);
@@ -1700,10 +1705,11 @@ function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedS
if (operation === "patch" || operation === "patch-v1" || operation === "v2") {
throw new Error("remote patch entrypoints are `apply-patch` for the default v2 engine and `apply-patch-v1` for the legacy helper");
}
if (operation === "script") return buildK3sScriptOperation([...targetArgs, ...operationArgs]);
if (operation === "shell") {
const parsed = parseShellStringOperationArgs(operationArgs, `ssh ${route.raw} shell`);
return { remoteCommand: buildK3sExecCommand([...targetArgs, "--", parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
if (operation === "script" || operation === "shell") {
throw removedShellAliasError(operation);
}
if (operation === "sh" || operation === "bash") {
return buildK3sScriptOperation([...targetArgs, ...operationArgs], operation, `ssh ${route.raw} ${operation}`);
}
if (operation === "logs") return { remoteCommand: buildK3sLogsCommand([...targetArgs, ...operationArgs]), requiresStdin: false, invocationKind: "helper" };
if (operation === "argv") {
@@ -1832,9 +1838,12 @@ function buildK3sCommand(providerId: string, args: string[]): string {
}
if (action === "guard") return buildK3sGuardCommand(providerId);
if (action === "exec") return buildK3sExecCommand(args.slice(1));
if (action === "script") {
const parsed = buildK3sScriptOperation(args.slice(1));
if (parsed.remoteCommand === null) throw new Error("ssh k3s script resolved to an interactive command unexpectedly");
if (action === "script" || action === "shell") {
throw removedShellAliasError(action);
}
if (action === "sh" || action === "bash") {
const parsed = buildK3sScriptOperation(args.slice(1), action, `ssh k3s ${action}`);
if (parsed.remoteCommand === null) throw new Error(`ssh k3s ${action} resolved to an interactive command unexpectedly`);
return parsed.remoteCommand;
}
if (action === "logs") return buildK3sLogsCommand(args.slice(1));
@@ -1911,21 +1920,24 @@ function buildK3sExecCommand(args: string[]): string {
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sScriptOperation(args: string[]): ParsedSshArgs {
const parsed = parseK3sTargetOptions(args, "ssh k3s script", { requireCommand: false, allowCommand: true, allowShell: true });
if (parsed.shell === null && parsed.command.length > 0) {
return { remoteCommand: buildK3sInlineScriptCommand(parsed), requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: buildK3sStdinScriptCommand(parsed), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
function removedShellAliasError(operation: string): Error {
return new Error(`ssh ${operation} operation has been removed because it hides the shell dialect; use explicit \`sh\` for POSIX /bin/sh or \`bash\` for Bash syntax`);
}
function buildK3sStdinScriptCommand(parsed: K3sTargetOptions): string {
if (parsed.namespace === null && parsed.resource === null) return buildK3sHostScriptCommand(parsed);
if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace <name>");
if (parsed.selector !== null) throw new Error("ssh k3s script does not support --selector");
if (parsed.resource === null) throw new Error("ssh k3s script target requires --deployment <name>, --pod <name> or --resource <type/name>");
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
const shell = parsed.shell ?? "sh";
function buildK3sScriptOperation(args: string[], shell: "sh" | "bash", commandName: string): ParsedSshArgs {
const parsed = parseK3sTargetOptions(args, commandName, { requireCommand: false, allowCommand: true });
if (parsed.command.length > 0) {
return { remoteCommand: buildK3sInlineScriptCommand(parsed, shell, commandName), requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: buildK3sStdinScriptCommand(parsed, shell, commandName), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
}
function buildK3sStdinScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
if (parsed.namespace === null && parsed.resource === null) return buildK3sHostScriptCommand(parsed, shell, commandName);
if (parsed.namespace === null) throw new Error(`${commandName} target requires --namespace <name>`);
if (parsed.selector !== null) throw new Error(`${commandName} does not support --selector`);
if (parsed.resource === null) throw new Error(`${commandName} target requires --deployment <name>, --pod <name> or --resource <type/name>`);
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
const kubectlArgs = [
"exec",
"-i",
@@ -1939,20 +1951,21 @@ function buildK3sStdinScriptCommand(parsed: K3sTargetOptions): string {
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sInlineScriptCommand(parsed: K3sTargetOptions): string {
if (parsed.command.length === 0) throw new Error("ssh k3s script -- requires a command");
if (parsed.selector !== null) throw new Error("ssh k3s script -- does not support --selector");
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
if (parsed.stdin) throw new Error("ssh k3s script -- does not accept --stdin");
const command = parsed.command.length === 1 ? ["sh", "-c", shellScriptWithCompatibility(parsed.command[0] ?? "")] : parsed.command;
function buildK3sInlineScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
if (parsed.command.length === 0) throw new Error(`${commandName} -- requires a command`);
if (parsed.command.length !== 1) throw new Error(`${commandName} -- requires exactly one shell command string; use exec/argv for direct argv commands`);
if (parsed.selector !== null) throw new Error(`${commandName} -- does not support --selector`);
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
if (parsed.stdin) throw new Error(`${commandName} -- does not accept --stdin`);
const command = [shell, "-c", shellScriptWithCompatibility(parsed.command[0] ?? "")];
if (parsed.namespace === null && parsed.resource === null) {
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
if (parsed.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir");
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options");
if (parsed.container !== null) throw new Error(`${commandName} without a workload does not accept --container`);
if (parsed.workspace !== null) throw new Error(`${commandName} without a workload does not accept --workdir`);
if (parsed.kubectlOptions.length > 0) throw new Error(`${commandName} without a workload does not accept kubectl log options`);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, ...command]);
}
if (parsed.namespace === null) throw new Error("ssh k3s script target requires --namespace <name>");
if (parsed.resource === null) throw new Error("ssh k3s script target requires --deployment <name>, --pod <name> or --resource <type/name>");
if (parsed.namespace === null) throw new Error(`${commandName} target requires --namespace <name>`);
if (parsed.resource === null) throw new Error(`${commandName} target requires --deployment <name>, --pod <name> or --resource <type/name>`);
const kubectlArgs = [
"exec",
"-n", parsed.namespace,
@@ -1992,36 +2005,15 @@ function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
};
}
function buildK3sHostScriptCommand(parsed: K3sTargetOptions): string {
if (parsed.tty) throw new Error("ssh k3s script does not support --tty; stdin is reserved for the script body");
if (parsed.stdin) throw new Error("ssh k3s script does not accept --stdin; stdin is always the script body");
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
if (parsed.workspace !== null) throw new Error("ssh k3s script without a workload does not accept --workdir");
if (parsed.kubectlOptions.length > 0) throw new Error("ssh k3s script without a workload does not accept kubectl log options");
const shell = parsed.shell ?? "sh";
function buildK3sHostScriptCommand(parsed: K3sTargetOptions, shell: "sh" | "bash", commandName: string): string {
if (parsed.tty) throw new Error(`${commandName} does not support --tty; stdin is reserved for the shell body`);
if (parsed.stdin) throw new Error(`${commandName} does not accept --stdin; stdin is always the shell body`);
if (parsed.container !== null) throw new Error(`${commandName} without a workload does not accept --container`);
if (parsed.workspace !== null) throw new Error(`${commandName} without a workload does not accept --workdir`);
if (parsed.kubectlOptions.length > 0) throw new Error(`${commandName} without a workload does not accept kubectl log options`);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, shell, "-s", "--", ...parsed.command]);
}
function shellStringFromArgs(args: string[], commandName = "ssh shell"): string {
if (args.length === 0) throw new Error(`${commandName} requires a command string`);
return args.join(" ");
}
function parseShellStringOperationArgs(args: string[], commandName: string): { shell: string; command: string } {
let shell = "sh";
const commandArgs: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--shell") {
shell = k3sScriptShell(k3sOptionValue(args, index, `${commandName} --shell`), `${commandName} --shell`);
index += 1;
continue;
}
commandArgs.push(arg);
}
return { shell, command: shellStringFromArgs(commandArgs, commandName) };
}
function buildK3sLogsCommand(args: string[]): string {
const parsed = parseK3sTargetOptions(args, "ssh k3s logs", { requireCommand: false, allowSelector: true });
if (parsed.namespace === null) throw new Error("ssh k3s logs requires --namespace <name>");
@@ -2264,49 +2256,26 @@ function shellScriptStdinPrefix(): string {
return `${shellScriptPrelude()}\n`;
}
function buildShellCommand(args: string[]): ParsedSshArgs {
let shell = "sh";
function buildShellCommand(args: string[], shell: "sh" | "bash", commandName: string): ParsedSshArgs {
const scriptArgs: string[] = [];
let afterDoubleDash = false;
let directArgvMode = false;
let shellOptionSeen = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (afterDoubleDash) {
scriptArgs.push(arg);
continue;
}
if (arg === "--") {
if (!shellOptionSeen && scriptArgs.length === 0) {
directArgvMode = true;
scriptArgs.push(...args.slice(index + 1));
break;
if (scriptArgs.length === 0) {
const command = args.slice(index + 1);
if (command.length === 0) throw new Error(`${commandName} -- requires a command`);
if (command.length !== 1) throw new Error(`${commandName} -- requires exactly one shell command string; use argv or a direct subcommand for direct argv commands`);
return { remoteCommand: shellArgv([shell, "-c", shellScriptWithCompatibility(command[0] ?? "")]), requiresStdin: false, invocationKind: "helper" };
}
afterDoubleDash = true;
continue;
scriptArgs.push(...args.slice(index + 1));
break;
}
if (arg === "--shell") {
shell = k3sScriptShell(k3sOptionValue(args, index, "ssh script --shell"), "ssh script --shell");
shellOptionSeen = true;
index += 1;
continue;
}
if (arg.startsWith("-")) throw new Error(`unsupported ssh script option: ${arg}`);
if (arg.startsWith("-")) throw new Error(`unsupported ${commandName} option: ${arg}`);
scriptArgs.push(arg);
}
if (directArgvMode) {
if (scriptArgs.length === 0) throw new Error("ssh script -- requires a command");
if (scriptArgs.length === 1) return { remoteCommand: shellArgv([shell, "-c", shellScriptWithCompatibility(scriptArgs[0] ?? "")]), requiresStdin: false, invocationKind: "helper" };
return { remoteCommand: shellArgv(scriptArgs), requiresStdin: false, invocationKind: "argv" };
}
return { remoteCommand: shellArgv([shell, "-s", "--", ...scriptArgs]), requiresStdin: true, invocationKind: "helper", stdinPrefix: shellScriptStdinPrefix() };
}
function buildShellStringCommand(args: string[]): ParsedSshArgs {
const parsed = parseShellStringOperationArgs(args, "ssh shell");
return { remoteCommand: shellArgv([parsed.shell, "-c", shellScriptWithCompatibility(parsed.command)]), requiresStdin: false, invocationKind: "helper" };
}
function podApplyPatchStdinWrapper(): { prefix: string; suffix: string } {
const toolMarker = "__UNIDESK_APPLY_PATCH_TOOL__";
const patchMarker = "__UNIDESK_APPLY_PATCH_PAYLOAD__";
@@ -2399,8 +2368,8 @@ export function sshFailureHint(providerId: string, parsed: ParsedSshArgs, exitCo
providerId: shownProviderId,
trigger,
exitCode,
message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer structured argv or stdin script passthrough for non-interactive commands.",
try: `trans ${shownProviderId} script <<'SCRIPT'`,
message: "ssh-like remote command failed before proving Host SSH is globally unavailable; prefer structured argv or explicit sh/bash stdin passthrough for non-interactive commands.",
try: `trans ${shownProviderId} sh <<'SH'`,
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.",
};