feat: add structured ssh route passthrough

This commit is contained in:
Codex
2026-05-25 05:35:33 +00:00
parent 93e634c608
commit 5d636d2d5b
5 changed files with 378 additions and 10 deletions
+9 -3
View File
@@ -19,12 +19,13 @@ export function rootHelp(): unknown {
{ command: "server cleanup plan [--min-age-hours N] [--limit N]", description: "Dry-run Docker image cleanup plan only: list active/protected images, stale candidates older than the default 24h threshold, risk, estimated reclaim, and manual review commands without deleting anything." },
{ command: "server rebuild <backend-core|frontend|dev-frontend-proxy|provider-gateway|todo-note|code-queue-mgr|project-manager|baidu-netdisk|oa-event-flow>", description: "Maintenance-only local Compose rebuild for reviewed main-server services; frontend standard release must use CI artifact plus deploy apply dev/prod artifact consumers." },
{ command: "provider attach <providerId> [--master-server URL] [--up] [--force] | provider triage <providerId> [--observed-error text] [--observed-scope scope] [--microservice id ...] [--full|--raw]", description: "Generate the minimal external provider-gateway env/compose bundle or run the low-noise read-only provider health triage contract." },
{ command: "ssh <providerId> [ssh-like args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge; prefer `ssh <providerId> argv ...` for non-interactive remote commands." },
{ command: "ssh <route> [ssh-like args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge; route syntax such as `D601:k3s:kubectl` keeps distributed targets structured." },
{ command: "ssh <providerId> apply-patch [tool args...] < patch.diff", description: "Invoke the injected remote apply_patch helper directly over SSH passthrough and stream the patch from local stdin." },
{ command: "ssh <providerId> py [script-args...] < script.py", description: "Run remote Python from local stdin through SSH passthrough without nested shell quoting; extra args become script argv." },
{ command: "ssh <providerId> skills [--scope all|wsl|windows] [--limit N]", description: "Discover WSL/Linux and, for WSL providers, Windows skill directories in one SSH passthrough call." },
{ command: "ssh <providerId> find <path...> [--max-depth N] [--type d|f|l] [--contains TEXT] [--iname PATTERN] [--limit N] [--sort]", description: "Run a structured remote find command without nested shell quoting or parentheses." },
{ command: "ssh <providerId> glob [--root DIR] [--pattern PATTERN] [--contains TEXT] [--type any|f|d] [--limit N] [--sort]", description: "Run remote glob matching through the injected helper without shell glob expansion." },
{ command: "ssh D601:k3s[:kubectl|:namespace:workload[:container]] ...", description: "Run D601 native k3s kubectl or direct workload exec through route syntax with KUBECONFIG fixed and argv assembled by the CLI." },
{ command: "ssh <providerId> argv <command> [args...]", description: "Run a non-interactive remote command with each argv token shell-quoted by UniDesk before SSH passthrough; use `argv bash -lc '<command>'` when shell features are required." },
{ command: "microservice list", description: "List UniDesk-managed user services and their provider/runtime mapping." },
{ command: "microservice status <id>", description: "Show one user service config, repository reference, backend mapping, and runtime status." },
@@ -143,7 +144,7 @@ export function sshHelp(): unknown {
output: "json",
description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge.",
usage: [
"bun scripts/cli.ts ssh <providerId>",
"bun scripts/cli.ts ssh <route>",
"bun scripts/cli.ts ssh <providerId> argv <command> [args...]",
"bun scripts/cli.ts ssh D601 argv bash -lc '<command>'",
"bun scripts/cli.ts ssh <providerId> apply-patch < patch.diff",
@@ -151,10 +152,15 @@ export function sshHelp(): unknown {
"bun scripts/cli.ts ssh <providerId> skills [--scope all|wsl|windows] [--limit N]",
"bun scripts/cli.ts ssh <providerId> find <path...> [--contains TEXT] [--limit N]",
"bun scripts/cli.ts ssh <providerId> glob [--root DIR] [--pattern PATTERN]",
"bun scripts/cli.ts ssh D601:k3s",
"bun scripts/cli.ts ssh D601:k3s:kubectl get pods -n hwlab-dev",
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'",
"bun scripts/cli.ts ssh D601:k3s:logs:hwlab-dev:hwlab-cloud-api --tail 80",
],
notes: [
"ssh --help and ssh <providerId> --help print this JSON help and never open an interactive session.",
"ssh --help and ssh <route> --help print this JSON help and never open an interactive session.",
"For non-interactive remote commands, prefer argv: bun scripts/cli.ts ssh D601 argv bash -lc '<command>'.",
"Route syntax is provider:plane:entry-or-namespace:resource:container. For D601 native k3s, D601:k3s controls the cluster, D601:k3s:kubectl exposes kubectl, and D601:k3s:<namespace>:<workload> defaults to kubectl exec.",
"If an ssh-like remote command fails with timeout/kex/exit-255 friction, stderr includes one low-noise UNIDESK_SSH_HINT JSON line with the argv retry command.",
"Use -- before a remote command that intentionally starts with a dash.",
],
+315 -5
View File
@@ -9,6 +9,22 @@ export interface ParsedSshArgs {
export type SshInvocationKind = "interactive" | "argv" | "helper" | "ssh-like";
export interface ParsedSshRoute {
providerId: string;
plane: "host" | "k3s";
entry: string | null;
namespace: string | null;
resource: string | null;
container: string | null;
raw: string;
}
export interface ParsedSshInvocation {
providerId: string;
route: ParsedSshRoute;
parsed: ParsedSshArgs;
}
export interface SshFailureHint {
code: "ssh-like-command-friction";
providerId: string;
@@ -21,6 +37,24 @@ export interface SshFailureHint {
}
const argvQuotedSshSubcommands = new Set(["rg", "grep", "sed", "nl", "stat", "du", "ls", "cat", "head", "tail", "wc", "pwd"]);
const d601NativeKubeconfig = "/etc/rancher/k3s/k3s.yaml";
const k3sRouteEntries = new Set([
"guard",
"kubectl",
"exec",
"logs",
"get",
"describe",
"top",
"rollout",
"wait",
"config",
"version",
"cluster-info",
"auth",
"api-resources",
"api-versions",
]);
const remoteApplyPatchSource = String.raw`#!/usr/bin/env python3
import sys
@@ -513,6 +547,9 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
if (subcommand === "glob") {
return { remoteCommand: shellArgv(["glob", ...args.slice(1)]), requiresStdin: false, invocationKind: "helper" };
}
if (subcommand === "k3s") {
return { remoteCommand: buildK3sCommand(args.slice(1)), requiresStdin: false, invocationKind: "helper" };
}
if (argvQuotedSshSubcommands.has(subcommand)) {
return { remoteCommand: shellArgv(args), requiresStdin: false, invocationKind: "argv" };
}
@@ -542,6 +579,46 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
};
}
export function parseSshInvocation(target: string, args: string[]): ParsedSshInvocation {
const route = parseSshRoute(target);
const parsed = route.plane === "k3s" ? parseK3sRouteArgs(route, args) : parseSshArgs(args);
return { providerId: route.providerId, route, parsed };
}
export function parseSshRoute(target: string): ParsedSshRoute {
if (!target) throw new Error("ssh requires provider id, for example: bun scripts/cli.ts ssh D601");
const parts = target.split(":");
const [providerId, plane, ...rest] = parts;
if (!providerId) throw new Error("ssh route requires a provider id before ':'");
if (plane === undefined || plane.length === 0 || plane === "host") {
return { providerId, plane: "host", entry: null, namespace: null, resource: null, container: null, raw: target };
}
if (plane !== "k3s") throw new Error(`unsupported ssh route plane: ${plane}`);
const [first, second, third, fourth, ...extra] = rest;
if (first && k3sRouteEntries.has(first)) {
if (extra.length > 0) throw new Error("ssh k3s command route supports at most provider:k3s:entry:namespace:resource:container");
return {
providerId,
plane: "k3s",
entry: first,
namespace: second && second.length > 0 ? second : null,
resource: third && third.length > 0 ? third : null,
container: fourth && fourth.length > 0 ? fourth : null,
raw: target,
};
}
if (fourth !== undefined) throw new Error("ssh k3s target route supports at most provider:k3s:namespace:resource:container");
return {
providerId,
plane: "k3s",
entry: null,
namespace: first && first.length > 0 ? first : null,
resource: second && second.length > 0 ? second : null,
container: third && third.length > 0 ? third : null,
raw: target,
};
}
function shellArgv(args: string[]): string {
return args.map(shellQuote).join(" ");
}
@@ -556,6 +633,10 @@ function positiveInt(value: string, option: string): number {
return parsed;
}
function optionalPositiveInt(value: string, option: string): string {
return String(positiveInt(value, option));
}
function findOptionValue(args: string[], index: number, option: string): string {
const value = args[index + 1];
if (value === undefined || value.length === 0) throw new Error(`ssh find ${option} requires a value`);
@@ -642,6 +723,235 @@ function buildFindCommand(args: string[]): string {
return command;
}
function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
if (route.entry === null && route.namespace === null && route.resource === null) {
const k3sArgs = args.length === 0 ? ["guard"] : args;
return { remoteCommand: buildK3sCommand(k3sArgs), requiresStdin: false, invocationKind: "helper" };
}
if (route.entry === "guard") {
if (args.length > 0) throw new Error("ssh route D601:k3s:guard does not accept extra arguments");
return { remoteCommand: buildD601K3sGuardCommand(), requiresStdin: false, invocationKind: "helper" };
}
if (route.entry === "kubectl") {
if (args.length === 0) throw new Error("ssh route D601:k3s:kubectl requires kubectl arguments");
return { remoteCommand: buildK3sCommand(["kubectl", ...args]), requiresStdin: false, invocationKind: "helper" };
}
if (route.entry === "logs") {
return { remoteCommand: buildK3sLogsCommand([...k3sRouteTargetArgs(route), ...args]), requiresStdin: false, invocationKind: "helper" };
}
if (route.entry === "exec") {
return { remoteCommand: buildK3sExecCommand([...k3sRouteTargetArgs(route), ...k3sRouteCommandArgs(args)]), requiresStdin: false, 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" };
}
if (route.namespace === null || route.resource === null) {
throw new Error("ssh k3s target route requires provider:k3s:<namespace>:<deployment|pod/resource>");
}
if (args.length === 0) {
return {
remoteCommand: shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", "get", "-n", route.namespace, normalizeK3sRouteResource(route.resource), "-o", "wide"]),
requiresStdin: false,
invocationKind: "helper",
};
}
return { remoteCommand: buildK3sExecCommand([...k3sRouteTargetArgs(route), ...k3sRouteCommandArgs(args)]), requiresStdin: false, invocationKind: "helper" };
}
function k3sRouteTargetArgs(route: ParsedSshRoute): string[] {
if (route.namespace === null) throw new Error(`ssh route ${route.raw} requires a namespace segment`);
if (route.resource === null) throw new Error(`ssh route ${route.raw} requires a workload or pod segment`);
return [
"--namespace", route.namespace,
"--resource", normalizeK3sRouteResource(route.resource),
...(route.container === null ? [] : ["--container", route.container]),
];
}
function k3sRouteCommandArgs(args: string[]): string[] {
if (args.length === 0) throw new Error("ssh k3s target route requires a command to exec");
return args[0] === "--" ? args : ["--", ...args];
}
function buildK3sCommand(args: string[]): string {
const action = args[0] ?? "";
if (action.length === 0 || action === "--help" || action === "-h" || action === "help") {
throw new Error("ssh k3s requires a subcommand: guard, kubectl, get, describe, logs or exec");
}
if (action === "guard") return buildD601K3sGuardCommand();
if (action === "exec") return buildK3sExecCommand(args.slice(1));
if (action === "logs") return buildK3sLogsCommand(args.slice(1));
if (action === "kubectl") {
const kubectlArgs = args.slice(1);
if (kubectlArgs.length === 0) throw new Error("ssh k3s kubectl requires kubectl arguments");
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
}
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...args]);
}
function buildD601K3sGuardCommand(): string {
const script = [
"set -euo pipefail",
`export KUBECONFIG=${shellQuote(d601NativeKubeconfig)}`,
"context=$(kubectl config current-context)",
"server=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')",
"nodes=$(kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{\"\\n\"}{end}')",
"printf 'kubeconfig=%s\\n' \"$KUBECONFIG\"",
"printf 'context=%s\\n' \"$context\"",
"printf 'server=%s\\n' \"$server\"",
"printf 'nodes=%s\\n' \"$(printf '%s' \"$nodes\" | tr '\\n' ' ')\"",
"printf '%s\\n' \"$nodes\" | grep -Fx d601 >/dev/null || { printf 'native_k3s_guard=blocked reason=d601-node-missing\\n' >&2; exit 1; }",
"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]);
}
interface K3sTargetOptions {
namespace: string | null;
resource: string | null;
container: string | null;
stdin: boolean;
tty: boolean;
command: string[];
kubectlOptions: string[];
}
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>");
if (parsed.resource === null) throw new Error("ssh k3s exec requires --deployment <name>, --pod <name> or --resource <type/name>");
const kubectlArgs = [
"exec",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
...(parsed.stdin ? ["-i"] : []),
...(parsed.tty ? ["-t"] : []),
...parsed.kubectlOptions,
"--",
...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>");
if (parsed.resource === null) throw new Error("ssh k3s logs requires --deployment <name>, --pod <name> or --resource <type/name>");
if (parsed.stdin || parsed.tty) throw new Error("ssh k3s logs does not support --stdin or --tty");
const kubectlArgs = [
"logs",
"-n", parsed.namespace,
parsed.resource,
...(parsed.container === null ? [] : ["-c", parsed.container]),
...parsed.kubectlOptions,
];
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function parseK3sTargetOptions(args: string[], commandName: string, options: { requireCommand: boolean }): K3sTargetOptions {
let namespace: string | null = null;
let resource: string | null = null;
let container: string | null = null;
let stdin = false;
let tty = false;
const kubectlOptions: string[] = [];
const command: string[] = [];
let afterDoubleDash = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (afterDoubleDash) {
command.push(arg);
continue;
}
if (arg === "--") {
afterDoubleDash = true;
continue;
}
if (arg === "--namespace" || arg === "-n") {
namespace = k3sOptionValue(args, index, `${commandName} ${arg}`);
index += 1;
continue;
}
if (arg === "--deployment" || arg === "--deploy") {
resource = `deployment/${k3sOptionValue(args, index, `${commandName} ${arg}`)}`;
index += 1;
continue;
}
if (arg === "--pod") {
resource = `pod/${k3sOptionValue(args, index, `${commandName} ${arg}`)}`;
index += 1;
continue;
}
if (arg === "--resource") {
resource = normalizeK3sResource(k3sOptionValue(args, index, `${commandName} ${arg}`));
index += 1;
continue;
}
if (arg === "--container" || arg === "-c") {
container = k3sOptionValue(args, index, `${commandName} ${arg}`);
index += 1;
continue;
}
if (arg === "--stdin" || arg === "-i") {
stdin = true;
continue;
}
if (arg === "--tty" || arg === "-t") {
tty = true;
continue;
}
if (arg === "--tail") {
kubectlOptions.push("--tail", optionalPositiveInt(k3sOptionValue(args, index, `${commandName} ${arg}`), `${commandName} --tail`));
index += 1;
continue;
}
if (arg === "--since" || arg === "--since-time" || arg === "--limit-bytes") {
kubectlOptions.push(arg, k3sOptionValue(args, index, `${commandName} ${arg}`));
index += 1;
continue;
}
if (arg === "--previous" || arg === "--timestamps" || arg === "--all-containers" || arg === "--prefix") {
kubectlOptions.push(arg);
continue;
}
if (arg === "--follow" || arg === "-f") {
throw new Error(`${commandName} does not support ${arg}; use a bounded logs command and poll again`);
}
if (arg.startsWith("-")) throw new Error(`unsupported ${commandName} option: ${arg}`);
if (resource === null) {
resource = normalizeK3sResource(arg);
continue;
}
throw new Error(`unexpected ${commandName} argument before --: ${arg}`);
}
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 };
}
function k3sOptionValue(args: string[], index: number, option: string): string {
const value = args[index + 1];
if (value === undefined || value.length === 0) throw new Error(`${option} requires a value`);
return value;
}
function normalizeK3sResource(value: string): string {
if (value.includes("/")) return value;
return `pod/${value}`;
}
function normalizeK3sRouteResource(value: string): string {
if (value.startsWith("deploy/")) return `deployment/${value.slice("deploy/".length)}`;
if (value.startsWith("po/")) return `pod/${value.slice("po/".length)}`;
if (value.includes("/")) return value;
return `deployment/${value}`;
}
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"';
@@ -850,12 +1160,12 @@ function terminalSize(): { cols: number; rows: number } {
}
export async function runSsh(config: UniDeskConfig, providerId: string, args: string[]): Promise<number> {
if (!providerId) throw new Error("ssh requires provider id, for example: bun scripts/cli.ts ssh D518");
const parsed = parseSshArgs(args);
const invocation = parseSshInvocation(providerId, args);
const parsed = invocation.parsed;
const size = terminalSize();
const openTimeoutMs = Math.max(15000, Number(process.env.UNIDESK_SSH_OPEN_TIMEOUT_MS || 60000));
const payload = {
providerId,
providerId: invocation.providerId,
command: wrapSshRemoteCommand(parsed.remoteCommand),
tty: parsed.remoteCommand === null,
stdinEotOnEnd: parsed.remoteCommand !== null,
@@ -912,14 +1222,14 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
const message = `unidesk ssh failed to start broker: ${error.message}\n`;
appendStderrTail(message);
process.stderr.write(message);
const hint = sshFailureHint(providerId, parsed, 255, stderrTail);
const hint = sshFailureHint(invocation.providerId, parsed, 255, stderrTail);
if (hint !== null) process.stderr.write(formatSshFailureHint(hint));
resolve(255);
});
child.on("close", (code) => {
restore();
const exitCode = code ?? 255;
const hint = sshFailureHint(providerId, parsed, exitCode, stderrTail);
const hint = sshFailureHint(invocation.providerId, parsed, exitCode, stderrTail);
if (hint !== null) process.stderr.write(formatSshFailureHint(hint));
resolve(exitCode);
});
+23 -1
View File
@@ -1,6 +1,6 @@
import { sshHelp } from "./src/help";
import { providerTriageRecommendedCrossChecks } from "./src/provider-triage";
import { formatSshFailureHint, parseSshArgs, sshFailureHint } from "./src/ssh";
import { formatSshFailureHint, parseSshArgs, parseSshInvocation, sshFailureHint } from "./src/ssh";
type JsonRecord = Record<string, unknown>;
@@ -19,6 +19,26 @@ export function runSshArgvGuidanceContract(): JsonRecord {
assertCondition(shortcut.invocationKind === "argv", "safe command shortcuts must use argv quoting", shortcut);
assertCondition(shortcut.remoteCommand === "'pwd'", "safe command shortcut should be shell-quoted", shortcut);
const k3sGuard = parseSshArgs(["k3s", "guard"]);
assertCondition(k3sGuard.invocationKind === "helper", "k3s guard must be classified as helper", k3sGuard);
assertCondition(k3sGuard.remoteCommand?.includes("KUBECONFIG") && k3sGuard.remoteCommand.includes("/etc/rancher/k3s/k3s.yaml"), "k3s guard must force native k3s kubeconfig", k3sGuard);
const k3sExec = parseSshArgs(["k3s", "exec", "--namespace", "hwlab-dev", "--deployment", "hwlab-cloud-api", "--", "node", "-e", "console.log(process.version)"]);
assertCondition(k3sExec.invocationKind === "helper", "k3s exec must be classified as helper", k3sExec);
assertCondition(k3sExec.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'node' '-e' 'console.log(process.version)'", "k3s exec must assemble kubectl argv without nested shell quoting", k3sExec);
const routeKubectl = parseSshInvocation("D601:k3s:kubectl", ["get", "pods", "-n", "hwlab-dev"]);
assertCondition(routeKubectl.providerId === "D601", "route must preserve provider id", routeKubectl);
assertCondition(routeKubectl.route.plane === "k3s" && routeKubectl.route.entry === "kubectl", "route must parse k3s kubectl entry", routeKubectl);
assertCondition(routeKubectl.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'get' 'pods' '-n' 'hwlab-dev'", "D601:k3s:kubectl must map to kubectl argv", routeKubectl);
const routeTarget = parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api", ["node", "-e", "console.log(process.version)"]);
assertCondition(routeTarget.route.namespace === "hwlab-dev" && routeTarget.route.resource === "hwlab-cloud-api", "route target must parse namespace and workload", routeTarget);
assertCondition(routeTarget.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'deployment/hwlab-cloud-api' '--' 'node' '-e' 'console.log(process.version)'", "D601:k3s:<namespace>:<workload> must default to deployment exec", routeTarget);
const routePodTarget = parseSshInvocation("D601:k3s:hwlab-dev:pod/hwlab-cloud-api-abc:api", ["--", "sh", "-lc", "echo ok"]);
assertCondition(routePodTarget.parsed.remoteCommand === "'env' 'KUBECONFIG=/etc/rancher/k3s/k3s.yaml' 'kubectl' 'exec' '-n' 'hwlab-dev' 'pod/hwlab-cloud-api-abc' '-c' 'api' '--' 'sh' '-lc' 'echo ok'", "pod route with container must preserve explicit pod kind", routePodTarget);
const sshLike = parseSshArgs(["echo hello"]);
const hint = sshFailureHint("D601", sshLike, 255, "kex_exchange_identification: Connection closed by remote host");
assertCondition(hint !== null, "ssh-like kex failure must produce a hint", sshLike);
@@ -33,6 +53,7 @@ export function runSshArgvGuidanceContract(): JsonRecord {
const helpText = JSON.stringify(sshHelp());
assertCondition(helpText.includes("ssh D601 argv bash -lc '<command>'"), "ssh help must recommend argv bash -lc for non-interactive commands", helpText);
assertCondition(helpText.includes("ssh D601:k3s:kubectl get pods -n hwlab-dev"), "ssh help must document k3s kubectl route", helpText);
assertCondition(helpText.includes("UNIDESK_SSH_HINT"), "ssh help must document structured failure hint", helpText);
const crossChecks = providerTriageRecommendedCrossChecks("D601");
@@ -42,6 +63,7 @@ export function runSshArgvGuidanceContract(): JsonRecord {
ok: true,
checks: [
"argv form is classified and quoted as the success path for non-interactive commands",
"k3s route fixes native kubeconfig and assembles kubectl exec as argv",
"ssh-like timeout/kex failures emit one structured argv retry hint",
"help text documents argv bash -lc and UNIDESK_SSH_HINT",
"provider triage recommendedCrossChecks keeps ssh D601 argv true",