fix: keep ssh route operation boundaries

This commit is contained in:
Codex
2026-05-25 06:56:47 +00:00
parent 0d6bcd9f26
commit 2eaad15fb2
4 changed files with 20 additions and 11 deletions
+2 -2
View File
@@ -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 控制面;`<provider>:k3s:<namespace>:<workload>[:container]` 定位到 namespace 下的一个默认 deployment workload;若目标是具体 Podworkload 段写成 `pod/<podid>`,若目标是 Deployment,也可以显式写 `deployment/<name>` 或简写 `<name>``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 控制面;`<provider>:k3s:<namespace>:<workload>[:container]` 定位到 namespace 下的一个默认 deployment workload;若目标是具体 Podworkload 段写成 `pod/<podid>`,若目标是 Deployment,也可以显式写 `deployment/<name>` 或简写 `<name>``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``<provider>:k3s` 无后续参数时执行 native k3s guard`<provider>:k3s kubectl ...` 接收原始 kubectl argv`<provider>:k3s script` 执行带 native kubeconfig 的 host stdin 脚本;`<provider>:k3s:<namespace>:<workload> logs` 读取有界日志;`<provider>:k3s:<namespace>:<workload> exec ...``<provider>:k3s:<namespace>:<workload> <command> ...` 进入目标 workload`<provider>:k3s:<namespace>:<workload> script` 把本地 stdin 作为 pod 内 shell 脚本执行;`<provider>:k3s:<namespace>:<workload> apply-patch` 把本地标准 patch 作为 stdin 送入 pod 内 `apply_patch`。典型用法:
+2 -1
View File
@@ -166,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 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.",
"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, <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 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.",
+5 -7
View File
@@ -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] || "<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} ..."`;
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}"`;
}
function shellArgv(args: string[]): string {
+11 -1
View File
@@ -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",