fix: require explicit trans shell operations
This commit is contained in:
+72
-103
@@ -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.",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user