From 2eaad15fb27bc0842c606b708d865f9ad937ad98 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 25 May 2026 06:56:47 +0000 Subject: [PATCH] fix: keep ssh route operation boundaries --- docs/reference/cli.md | 4 ++-- scripts/src/help.ts | 3 ++- scripts/src/ssh.ts | 12 +++++------- scripts/ssh-argv-guidance-contract-test.ts | 12 +++++++++++- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 1a562ee5..94b92c24 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -177,9 +177,9 @@ bun scripts/cli.ts ssh D601 find /home/ubuntu --max-depth 4 --type d --icontains bun scripts/cli.ts ssh D601 glob --root /home/ubuntu/pikapython --pattern '**/*-test.cpp' --limit 20 --sort ``` -`ssh` 的 route 语法是 `provider:plane[:namespace:resource[:container]]`,只负责定位分布式目标,不表达操作。当前稳定 plane 是 `k3s`:`D601:k3s` 或 `G14:k3s` 定位到对应 provider 的原生 k3s 控制面;`:k3s::[:container]` 定位到 namespace 下的一个默认 deployment workload;若目标是具体 Pod,workload 段写成 `pod/`,若目标是 Deployment,也可以显式写 `deployment/` 或简写 ``。`kubectl`、`logs`、`script`、`apply-patch`、`exec` 和普通容器命令都是 route 后面的 operation,这样路由子模块和操作子模块可以独立扩展。 +`ssh` 的 route 语法是 `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`。第一个 argv token 只负责定位分布式目标,不表达操作;第一个 token 后面的所有 token 才进入 operation 解析器。当前稳定 plane 是 `k3s`:`D601:k3s` 或 `G14:k3s` 定位到对应 provider 的原生 k3s 控制面;`:k3s::[:container]` 定位到 namespace 下的一个默认 deployment workload;若目标是具体 Pod,workload 段写成 `pod/`,若目标是 Deployment,也可以显式写 `deployment/` 或简写 ``。`kubectl`、`logs`、`script`、`apply-patch`、`exec` 和普通容器命令都是 route 后面的 operation,这样路由子模块和操作子模块可以独立扩展。 -`k3s` 必须出现在 route 的 plane 段里,禁止使用 `ssh G14 k3s ...` 或 `ssh D601 k3s ...` 这类 post-provider shorthand;正确形态是 `ssh G14:k3s kubectl ...` 或 `ssh D601:k3s kubectl ...`。定位和操作必须保持分离,新增分布式目标时也按 `provider:plane[:scope...]` 扩展 route,而不是在 operation args 中新增另一套定位语法。 +`k3s` 必须出现在 route 的 plane 段里,禁止使用 `ssh G14 k3s ...` 或 `ssh D601 k3s ...` 这类 post-provider shorthand;正确形态是 `ssh G14:k3s kubectl ...` 或 `ssh D601:k3s kubectl ...`。定位和操作必须保持分离,`kubectl`、`logs`、`script`、`apply-patch`、`exec` 等 operation 名也不得放进任何 colon route 段,包括 namespace、workload 或 container 段;新增分布式目标时按 `{provider}:{plane}:{scope}` 扩展 route,而不是在 operation args 中新增另一套定位语法。 该入口解决运行面调试中最常见的多层 shell 引号问题。它不要求升级 provider-gateway,也不新增业务 API,只复用现有 Host SSH 维护桥;CLI 在本地把 Kubernetes 目标、namespace、container、log 限制、容器命令、stdin script 和 pod apply-patch 组装成 kubectl argv,并固定远端 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`。`:k3s` 无后续参数时执行 native k3s guard;`:k3s kubectl ...` 接收原始 kubectl argv;`:k3s script` 执行带 native kubeconfig 的 host stdin 脚本;`:k3s:: logs` 读取有界日志;`:k3s:: exec ...` 和 `:k3s:: ...` 进入目标 workload;`:k3s:: script` 把本地 stdin 作为 pod 内 shell 脚本执行;`:k3s:: apply-patch` 把本地标准 patch 作为 stdin 送入 pod 内 `apply_patch`。典型用法: diff --git a/scripts/src/help.ts b/scripts/src/help.ts index ec4786a8..52a192ff 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -166,7 +166,8 @@ export function sshHelp(): unknown { notes: [ "ssh --help and ssh --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 native k3s providers such as D601 and G14, :k3s locates the control plane, :k3s:: locates a workload, and kubectl/script/logs/apply-patch/exec are operations placed after the route.", + "Route syntax is `{provider}:{plane}[:{scope...}] {operation} [operation-args...]`: the first argv token locates a distributed target only, and every following token belongs to the operation parser. For native k3s providers such as D601 and G14, :k3s locates the control plane, :k3s:: locates a workload, and kubectl/script/logs/apply-patch/exec are operations placed after the route.", + "Do not put operation names in any colon route segment, including nested k3s namespace/workload/container segments.", "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.", diff --git a/scripts/src/ssh.ts b/scripts/src/ssh.ts index e9dd154e..66790ccc 100644 --- a/scripts/src/ssh.ts +++ b/scripts/src/ssh.ts @@ -867,7 +867,8 @@ export function parseSshRoute(target: string): ParsedSshRoute { } if (plane !== "k3s") throw new Error(`unsupported ssh route plane: ${plane}`); const [first, second, third, fourth] = rest; - if (first && legacyK3sOperationRouteSegments.has(first)) throw new Error(k3sOperationInRouteMessage(target, first, second, third)); + const operationInRoute = [first, second, third].find((segment) => segment !== undefined && legacyK3sOperationRouteSegments.has(segment)); + if (operationInRoute !== undefined) throw new Error(k3sOperationInRouteMessage(target, operationInRoute)); if (fourth !== undefined) throw new Error("ssh k3s target route supports at most provider:k3s:namespace:resource:container"); return { providerId, @@ -880,13 +881,10 @@ export function parseSshRoute(target: string): ParsedSshRoute { }; } -function k3sOperationInRouteMessage(target: string, operation: string, namespace: string | undefined, resource: string | undefined): string { +function k3sOperationInRouteMessage(target: string, operation: string): string { const providerId = target.split(":")[0] || ""; - 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} ..."`; + 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:: ${operationExample}" instead of "${target}"`; } function shellArgv(args: string[]): string { diff --git a/scripts/ssh-argv-guidance-contract-test.ts b/scripts/ssh-argv-guidance-contract-test.ts index 833e63ab..22c584d2 100644 --- a/scripts/ssh-argv-guidance-contract-test.ts +++ b/scripts/ssh-argv-guidance-contract-test.ts @@ -82,6 +82,16 @@ export function runSshArgvGuidanceContract(): JsonRecord { /route must locate a target only.*ssh D601:k3s kubectl/u, "operation names must not be accepted as k3s route segments", ); + assertThrows( + () => parseSshInvocation("D601:k3s:hwlab-ci:kubectl", ["get", "pods"]), + /route must locate a target only.*operation "kubectl" after the route/u, + "operation names must not be accepted as nested k3s route segments", + ); + assertThrows( + () => parseSshInvocation("D601:k3s:hwlab-dev:hwlab-cloud-api:logs", []), + /route must locate a target only.*operation "logs" after the route/u, + "operation names must not be accepted as k3s container route segments", + ); assertThrows( () => parseSshInvocation("D601:k3s:apply-patch:hwlab-dev:hwlab-cloud-api", []), /route must locate a target only.*apply-patch/u, @@ -121,7 +131,7 @@ export function runSshArgvGuidanceContract(): JsonRecord { "argv form is classified and quoted as the success path for non-interactive commands", "stdin script form removes shell-command strings for host and k3s workload scripts", "pod apply-patch operation injects helper and forwards patch stdin", - "legacy operation-in-route forms are rejected with canonical route-plus-operation guidance", + "legacy operation-in-route forms are rejected in any k3s route segment with canonical route-plus-operation guidance", "post-provider k3s shorthand is rejected so location and operation stay separated", "k3s route stays location-only while operations fix native kubeconfig and assemble kubectl exec as argv", "ssh-like timeout/kex failures emit one structured argv retry hint",