feat: add stdin script ssh routes

This commit is contained in:
Codex
2026-05-25 05:58:51 +00:00
parent 5d636d2d5b
commit 64c82b5be8
5 changed files with 132 additions and 33 deletions
+82 -8
View File
@@ -42,6 +42,7 @@ const k3sRouteEntries = new Set([
"guard",
"kubectl",
"exec",
"script",
"logs",
"get",
"describe",
@@ -536,6 +537,9 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
if (subcommand === "py") {
return { remoteCommand: buildPythonStdinCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
}
if (subcommand === "script" || subcommand === "sh") {
return { remoteCommand: buildShellStdinCommand(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`);
@@ -742,6 +746,9 @@ function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
if (route.entry === "exec") {
return { remoteCommand: buildK3sExecCommand([...k3sRouteTargetArgs(route), ...k3sRouteCommandArgs(args)]), requiresStdin: false, invocationKind: "helper" };
}
if (route.entry === "script") {
return { remoteCommand: buildK3sScriptCommand([...k3sRouteTargetArgs(route), ...args]), requiresStdin: true, invocationKind: "helper" };
}
if (route.entry !== null) {
const k3sArgs = [route.entry, ...(route.namespace === null ? [] : [route.namespace]), ...(route.resource === null ? [] : [route.resource]), ...args];
return { remoteCommand: buildK3sCommand(k3sArgs), requiresStdin: false, invocationKind: "helper" };
@@ -781,6 +788,7 @@ function buildK3sCommand(args: string[]): string {
}
if (action === "guard") return buildD601K3sGuardCommand();
if (action === "exec") return buildK3sExecCommand(args.slice(1));
if (action === "script") return buildK3sScriptCommand(args.slice(1));
if (action === "logs") return buildK3sLogsCommand(args.slice(1));
if (action === "kubectl") {
const kubectlArgs = args.slice(1);
@@ -805,7 +813,7 @@ function buildD601K3sGuardCommand(): string {
"case \"$server\" in *127.0.0.1:11700*|*docker-desktop*) printf 'native_k3s_guard=blocked reason=docker-desktop-context server=%s\\n' \"$server\" >&2; exit 1;; esac",
"printf 'native_k3s_guard=ok\\n'",
].join("; ");
return shellArgv(["bash", "-lc", script]);
return shellArgv(["bash", "-c", script]);
}
interface K3sTargetOptions {
@@ -814,10 +822,17 @@ interface K3sTargetOptions {
container: string | null;
stdin: boolean;
tty: boolean;
shell: string | null;
command: string[];
kubectlOptions: string[];
}
interface ParseK3sTargetOptionsOptions {
requireCommand: boolean;
allowCommand?: boolean;
allowShell?: boolean;
}
function buildK3sExecCommand(args: string[]): string {
const parsed = parseK3sTargetOptions(args, "ssh k3s exec", { requireCommand: true });
if (parsed.namespace === null) throw new Error("ssh k3s exec requires --namespace <name>");
@@ -836,6 +851,28 @@ function buildK3sExecCommand(args: string[]): string {
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sScriptCommand(args: string[]): string {
const parsed = parseK3sTargetOptions(args, "ssh k3s script", { requireCommand: false, allowCommand: true, allowShell: true });
if (parsed.namespace === null) throw new Error("ssh k3s script requires --namespace <name>");
if (parsed.resource === null) throw new Error("ssh k3s script 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";
const kubectlArgs = [
"exec",
"-i",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
...parsed.kubectlOptions,
"--",
shell,
"-s",
"--",
...parsed.command,
];
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sLogsCommand(args: string[]): string {
const parsed = parseK3sTargetOptions(args, "ssh k3s logs", { requireCommand: false });
if (parsed.namespace === null) throw new Error("ssh k3s logs requires --namespace <name>");
@@ -851,12 +888,13 @@ function buildK3sLogsCommand(args: string[]): string {
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function parseK3sTargetOptions(args: string[], commandName: string, options: { requireCommand: boolean }): K3sTargetOptions {
function parseK3sTargetOptions(args: string[], commandName: string, options: ParseK3sTargetOptionsOptions): K3sTargetOptions {
let namespace: string | null = null;
let resource: string | null = null;
let container: string | null = null;
let stdin = false;
let tty = false;
let shell: string | null = null;
const kubectlOptions: string[] = [];
const command: string[] = [];
let afterDoubleDash = false;
@@ -896,6 +934,12 @@ function parseK3sTargetOptions(args: string[], commandName: string, options: { r
index += 1;
continue;
}
if (arg === "--shell") {
if (!options.allowShell) throw new Error(`${commandName} does not support --shell`);
shell = k3sScriptShell(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} ${arg}`);
index += 1;
continue;
}
if (arg === "--stdin" || arg === "-i") {
stdin = true;
continue;
@@ -930,8 +974,8 @@ function parseK3sTargetOptions(args: string[], commandName: string, options: { r
}
if (options.requireCommand && command.length === 0) throw new Error(`${commandName} requires -- <command> [args...]`);
if (!options.requireCommand && command.length > 0) throw new Error(`${commandName} does not accept a command after --`);
return { namespace, resource, container, stdin, tty, command, kubectlOptions };
if (!options.requireCommand && options.allowCommand !== true && command.length > 0) throw new Error(`${commandName} does not accept a command after --`);
return { namespace, resource, container, stdin, tty, shell, command, kubectlOptions };
}
function k3sOptionValue(args: string[], index: number, option: string): string {
@@ -952,6 +996,36 @@ function normalizeK3sRouteResource(value: string): string {
return `deployment/${value}`;
}
function k3sScriptShell(value: string, option: string): string {
if (!/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${option} must be a shell executable name or path without whitespace`);
return value;
}
function buildShellStdinCommand(args: string[]): string {
let shell = "sh";
const scriptArgs: string[] = [];
let afterDoubleDash = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (afterDoubleDash) {
scriptArgs.push(arg);
continue;
}
if (arg === "--") {
afterDoubleDash = true;
continue;
}
if (arg === "--shell") {
shell = k3sScriptShell(k3sOptionValue(args, index, "ssh script --shell"), "ssh script --shell");
index += 1;
continue;
}
if (arg.startsWith("-")) throw new Error(`unsupported ssh script option: ${arg}`);
scriptArgs.push(arg);
}
return shellArgv([shell, "-s", "--", ...scriptArgs]);
}
function buildPythonStdinCommand(args: string[]): string {
const pythonArgs = args.map(shellQuote).join(" ");
const execArgs = pythonArgs.length > 0 ? ` "$UNIDESK_SSH_PY_FILE" ${pythonArgs}` : ' "$UNIDESK_SSH_PY_FILE"';
@@ -1017,8 +1091,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 argv form for non-interactive commands.",
try: `bun scripts/cli.ts ssh ${shownProviderId} argv bash -lc '<command>'`,
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: `bun scripts/cli.ts ssh ${shownProviderId} script <<'SCRIPT'`,
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.",
};
@@ -1176,7 +1250,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
const payloadJson = JSON.stringify(payload);
const encodedBrokerSource = Buffer.from(brokerSource(), "utf8").toString("base64");
const script = [
"set -euo pipefail",
"set -eu",
`payload=${shellQuote(payloadJson)}`,
"if command -v backend-core >/dev/null 2>&1; then",
' exec backend-core --ssh-broker "$payload"',
@@ -1190,7 +1264,7 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
"-i",
"unidesk-backend-core",
"sh",
"-lc",
"-c",
script,
], {
cwd: repoRoot,