fix: enforce k3s ssh route syntax

This commit is contained in:
Codex
2026-05-25 06:39:16 +00:00
parent d161bd4850
commit ea56559796
4 changed files with 73 additions and 41 deletions
+6 -3
View File
@@ -19,14 +19,14 @@ 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 <route> [operation args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge; route syntax such as `D601:k3s:hwlab-dev:hwlab-cloud-api` only locates distributed targets." },
{ command: "ssh <route> [operation args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge; route syntax such as `G14:k3s` only locates distributed targets." },
{ 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> script [--shell sh|bash] [script-args...] <<'SCRIPT' ...", description: "Run a remote shell script from local stdin using shell -s, avoiding shell-command strings and nested remote heredocs." },
{ 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[:namespace:workload[:container]] <kubectl|logs|exec|script|apply-patch|command> ...", description: "Locate the D601 native k3s control plane or workload with route syntax, then run a separate operation with KUBECONFIG fixed and argv assembled by the CLI." },
{ command: "ssh <providerId>:k3s[:namespace:workload[:container]] <kubectl|logs|exec|script|apply-patch|command> ...", description: "Locate a native k3s control plane or workload with route syntax, then run a separate operation 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 `ssh <providerId> script` 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." },
@@ -155,6 +155,8 @@ export function sshHelp(): unknown {
"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 G14:k3s",
"bun scripts/cli.ts ssh G14:k3s kubectl get pipelineruns -n hwlab-ci",
"bun scripts/cli.ts ssh D601:k3s script <<'SCRIPT'",
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api apply-patch <<'PATCH'",
"bun scripts/cli.ts ssh D601:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'",
@@ -164,7 +166,8 @@ export function sshHelp(): unknown {
notes: [
"ssh --help and ssh <route> --help print this JSON help and never open an interactive session.",
"For non-interactive remote commands, prefer argv for a single process and script/stdin for shell logic.",
"Route syntax is provider:plane[:namespace:resource[:container]] and locates a distributed target only. For D601 native k3s, D601:k3s locates the control plane, D601:k3s:<namespace>:<workload> locates a workload, and kubectl/script/logs/apply-patch/exec are operations placed after the route.",
"Route syntax is provider:plane[:namespace:resource[:container]] and locates a distributed target only. For native k3s providers such as D601 and G14, <provider>:k3s locates the control plane, <provider>:k3s:<namespace>:<workload> locates a workload, and kubectl/script/logs/apply-patch/exec are operations placed after the route.",
"Do not use post-provider shorthand such as `ssh G14 k3s ...`; write `ssh G14:k3s ...` so location and operation stay separated.",
"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.",
],
+44 -31
View File
@@ -39,7 +39,7 @@ 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 nativeK3sKubeconfig = "/etc/rancher/k3s/k3s.yaml";
const legacyK3sOperationRouteSegments = new Set([
"guard",
"kubectl",
@@ -815,8 +815,7 @@ export function parseSshArgs(args: string[]): ParsedSshArgs {
return { remoteCommand: shellArgv(["glob", ...args.slice(1)]), requiresStdin: false, invocationKind: "helper" };
}
if (subcommand === "k3s") {
if (args[1] === "apply-patch") return buildK3sApplyPatchCommand(args.slice(2));
return { remoteCommand: buildK3sCommand(args.slice(1)), requiresStdin: false, invocationKind: "helper" };
throw new Error("ssh k3s shorthand is unsupported; put k3s in the route, for example: ssh D601:k3s kubectl get nodes");
}
if (argvQuotedSshSubcommands.has(subcommand)) {
return { remoteCommand: shellArgv(args), requiresStdin: false, invocationKind: "argv" };
@@ -849,8 +848,13 @@ 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 };
if (route.plane === "k3s") {
return { providerId: route.providerId, route, parsed: parseK3sRouteArgs(route, args) };
}
if ((args[0] ?? "") === "k3s") {
throw new Error(`ssh k3s shorthand is unsupported; use route syntax instead: ssh ${route.providerId}:k3s ${args.slice(1).join(" ")}`.trim());
}
return { providerId: route.providerId, route, parsed: parseSshArgs(args) };
}
export function parseSshRoute(target: string): ParsedSshRoute {
@@ -877,11 +881,12 @@ export function parseSshRoute(target: string): ParsedSshRoute {
}
function k3sOperationInRouteMessage(target: string, operation: string, namespace: string | undefined, resource: string | undefined): string {
if (operation === "kubectl") return `ssh k3s route must locate a target only; use "ssh D601:k3s kubectl ..." instead of "${target}"`;
if (operation === "script" && namespace === undefined) return `ssh k3s route must locate a target only; use "ssh D601:k3s script <<'SCRIPT'" instead of "${target}"`;
if (operation === "guard") return `ssh k3s route must locate a target only; use "ssh D601:k3s guard" or "ssh D601:k3s" instead of "${target}"`;
if (namespace !== undefined && resource !== undefined) return `ssh k3s route must locate a target only; use "ssh D601:k3s:${namespace}:${resource} ${operation} ..." instead of "${target}"`;
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh D601:k3s ${operation} ..."`;
const providerId = target.split(":")[0] || "<provider>";
if (operation === "kubectl") return `ssh k3s route must locate a target only; use "ssh ${providerId}:k3s kubectl ..." instead of "${target}"`;
if (operation === "script" && namespace === undefined) return `ssh k3s route must locate a target only; use "ssh ${providerId}:k3s script <<'SCRIPT'" instead of "${target}"`;
if (operation === "guard") return `ssh k3s route must locate a target only; use "ssh ${providerId}:k3s guard" or "ssh ${providerId}:k3s" instead of "${target}"`;
if (namespace !== undefined && resource !== undefined) return `ssh k3s route must locate a target only; use "ssh ${providerId}:k3s:${namespace}:${resource} ${operation} ..." instead of "${target}"`;
return `ssh k3s route must locate a target only; put operation "${operation}" after the route, for example "ssh ${providerId}:k3s ${operation} ..."`;
}
function shellArgv(args: string[]): string {
@@ -990,7 +995,7 @@ function buildFindCommand(args: string[]): string {
function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
if (route.entry === null && route.namespace === null && route.resource === null) {
return parseK3sControlPlaneOperation(args);
return parseK3sControlPlaneOperation(route, args);
}
if (route.namespace === null || route.resource === null) {
throw new Error("ssh k3s target route requires provider:k3s:<namespace>:<deployment|pod/resource>");
@@ -998,19 +1003,19 @@ function parseK3sRouteArgs(route: ParsedSshRoute, args: string[]): ParsedSshArgs
return parseK3sTargetOperation(route, args);
}
function parseK3sControlPlaneOperation(args: string[]): ParsedSshArgs {
function parseK3sControlPlaneOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
const operation = args[0] ?? "guard";
if (operation === "apply-patch" || operation === "patch") {
throw new Error("ssh D601:k3s apply-patch requires a workload route: ssh D601:k3s:<namespace>:<workload> apply-patch");
throw new Error(`ssh ${route.providerId}:k3s apply-patch requires a workload route: ssh ${route.providerId}:k3s:<namespace>:<workload> apply-patch`);
}
if (operation === "script" || operation === "sh") {
return { remoteCommand: buildK3sScriptCommand(args.slice(1)), requiresStdin: true, invocationKind: "helper" };
}
if (operation === "guard") {
if (args.length > 1) throw new Error("ssh D601:k3s guard does not accept extra arguments");
return { remoteCommand: buildD601K3sGuardCommand(), requiresStdin: false, invocationKind: "helper" };
if (args.length > 1) throw new Error(`ssh ${route.providerId}:k3s guard does not accept extra arguments`);
return { remoteCommand: buildK3sGuardCommand(route.providerId), requiresStdin: false, invocationKind: "helper" };
}
return { remoteCommand: buildK3sCommand(args), requiresStdin: false, invocationKind: "helper" };
return { remoteCommand: buildK3sCommand(route.providerId, args), requiresStdin: false, invocationKind: "helper" };
}
function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedSshArgs {
@@ -1030,7 +1035,7 @@ function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedS
if (operation === "get" || operation === "describe") {
return { remoteCommand: buildK3sTargetObjectCommand(operation, route, operationArgs), requiresStdin: false, invocationKind: "helper" };
}
if (operation === "kubectl") throw new Error("ssh k3s kubectl is a control-plane operation; use ssh D601:k3s kubectl ...");
if (operation === "kubectl") throw new Error(`ssh k3s kubectl is a control-plane operation; use ssh ${route.providerId}:k3s kubectl ...`);
if (operation === "exec") {
return { remoteCommand: buildK3sExecCommand([...targetArgs, ...k3sRouteCommandArgs(operationArgs)]), requiresStdin: false, invocationKind: "helper" };
}
@@ -1040,7 +1045,7 @@ function parseK3sTargetOperation(route: ParsedSshRoute, args: string[]): ParsedS
function buildK3sTargetObjectCommand(action: "get" | "describe", route: ParsedSshRoute, args: string[]): string {
if (route.namespace === null || route.resource === null) throw new Error(`ssh k3s ${action} target requires namespace and workload route`);
if (args.includes("--follow") || args.includes("-f")) throw new Error(`ssh k3s target ${action} does not support follow mode`);
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", action, "-n", route.namespace, normalizeK3sRouteResource(route.resource), ...args]);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", action, "-n", route.namespace, normalizeK3sRouteResource(route.resource), ...args]);
}
function k3sRouteTargetArgs(route: ParsedSshRoute): string[] {
@@ -1058,37 +1063,45 @@ function k3sRouteCommandArgs(args: string[]): string[] {
return args[0] === "--" ? args : ["--", ...args];
}
function buildK3sCommand(args: string[]): string {
function buildK3sCommand(providerId: string, 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 === "guard") return buildK3sGuardCommand(providerId);
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);
if (kubectlArgs.length === 0) throw new Error("ssh k3s kubectl requires kubectl arguments");
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...args]);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...args]);
}
function buildD601K3sGuardCommand(): string {
function buildK3sGuardCommand(providerId: string): string {
const provider = providerId.toUpperCase();
const providerNodeCheck = provider === "D601"
? "printf '%s\\n' \"$nodes\" | grep -Fx d601 >/dev/null || { printf 'native_k3s_guard=blocked provider=%s reason=d601-node-missing\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }"
: provider === "G14"
? "printf '%s\\n' \"$nodes\" | grep -Fx ubuntu-rog-zephyrus-g14-ga401iv-ga401iv >/dev/null || { printf 'native_k3s_guard=blocked provider=%s reason=g14-node-missing\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }"
: "[ -n \"$nodes\" ] || { printf 'native_k3s_guard=blocked provider=%s reason=node-list-empty\\n' \"$UNIDESK_K3S_PROVIDER_ID\" >&2; exit 1; }";
const script = [
"set -euo pipefail",
`export KUBECONFIG=${shellQuote(d601NativeKubeconfig)}`,
`export KUBECONFIG=${shellQuote(nativeK3sKubeconfig)}`,
`export UNIDESK_K3S_PROVIDER_ID=${shellQuote(providerId)}`,
"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 'provider=%s\\n' \"$UNIDESK_K3S_PROVIDER_ID\"",
"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; }",
providerNodeCheck,
"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'",
"printf 'native_k3s_guard=ok provider=%s\\n' \"$UNIDESK_K3S_PROVIDER_ID\"",
].join("; ");
return shellArgv(["bash", "-c", script]);
}
@@ -1125,7 +1138,7 @@ function buildK3sExecCommand(args: string[]): string {
"--",
...parsed.command,
];
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sScriptCommand(args: string[]): string {
@@ -1148,7 +1161,7 @@ function buildK3sScriptCommand(args: string[]): string {
"--",
...parsed.command,
];
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
@@ -1173,7 +1186,7 @@ function buildK3sApplyPatchCommand(args: string[]): ParsedSshArgs {
];
const wrapper = podApplyPatchStdinWrapper();
return {
remoteCommand: shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]),
remoteCommand: shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]),
requiresStdin: true,
invocationKind: "helper",
stdinPrefix: wrapper.prefix,
@@ -1187,7 +1200,7 @@ function buildK3sHostScriptCommand(parsed: K3sTargetOptions): string {
if (parsed.container !== null) throw new Error("ssh k3s script without a workload does not accept --container");
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";
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, shell, "-s", "--", ...parsed.command]);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, shell, "-s", "--", ...parsed.command]);
}
function buildK3sLogsCommand(args: string[]): string {
@@ -1202,7 +1215,7 @@ function buildK3sLogsCommand(args: string[]): string {
...(parsed.container === null ? [] : ["-c", parsed.container]),
...parsed.kubectlOptions,
];
return shellArgv(["env", `KUBECONFIG=${d601NativeKubeconfig}`, "kubectl", ...kubectlArgs]);
return shellArgv(["env", `KUBECONFIG=${nativeK3sKubeconfig}`, "kubectl", ...kubectlArgs]);
}
function parseK3sTargetOptions(args: string[], commandName: string, options: ParseK3sTargetOptionsOptions): K3sTargetOptions {