fix: clarify k3s route container syntax
This commit is contained in:
@@ -38,13 +38,13 @@ trans G14:k3s kubectl get pods -n hwlab-dev
|
||||
trans D601:k3s kubectl get pods -A
|
||||
|
||||
# 指定 namespace + workload
|
||||
trans G14:k3s:hwlab-dev:hwlab-cloud-web-abc/app cat /app/version.txt
|
||||
trans G14:k3s:hwlab-dev:pod-name/workspace apply-patch <<'PATCH'
|
||||
trans G14:k3s:hwlab-dev:hwlab-cloud-web:app exec --cwd /app -- cat /app/version.txt
|
||||
trans G14:k3s:hwlab-dev:pod:hwlab-cloud-web-abc:app apply-patch --cwd /workspace <<'PATCH'
|
||||
...
|
||||
PATCH
|
||||
```
|
||||
|
||||
CLI 自动注入 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`。
|
||||
CLI 自动注入 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`。k3s route 中 `:` 是分布式路由分隔符,`/` 只表示容器内文件系统 cwd;容器选择必须写 `:<container>` 或 operation 参数 `--container <container>`,不要写成 `pod/<pod>/<container>`。
|
||||
|
||||
### Windows route
|
||||
|
||||
@@ -233,11 +233,11 @@ for i in $(seq 1 10); do echo "[$i]"; cat "$tmp/$i.out"; cat "$tmp/$i.err" >&2;
|
||||
|
||||
### 远端 patch 正确姿势
|
||||
|
||||
第一个 route token 直接定位到目标 pod+workspace,不要从 host 生成 diff 再改路径上传:
|
||||
第一个 route token 直接定位到目标 pod/container,容器内 cwd 用 operation 参数 `--cwd /path`,不要从 host 生成 diff 再改路径上传:
|
||||
|
||||
```bash
|
||||
# ✅ 正确
|
||||
trans G14:k3s:hwlab-dev:hwlab-cloud-web-abc/app/web apply-patch <<'PATCH'
|
||||
trans G14:k3s:hwlab-dev:pod:hwlab-cloud-web-abc:app apply-patch --cwd /web <<'PATCH'
|
||||
*** Begin Patch
|
||||
*** Update File: app.mjs
|
||||
@@
|
||||
|
||||
@@ -81,7 +81,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
|
||||
## Critical Remote Patch Transport Rule
|
||||
|
||||
- P0: 对 G14/D601/远端 worktree 做文本源码修改时,必须优先使用 UniDesk SSH workspace route 的 `apply-patch` 透传入口;不要优先用远端 Python/Perl/sed heredoc 或复杂 shell quoting 拼接大段文本补丁。
|
||||
- P0: 对 k3s pod 内文本热修必须把第一个 route token 直接定位到目标 pod/workspace 后再执行 `apply-patch`;禁止从 host/worktree 取 diff、改路径、再管道拼到 pod,反面案例和正确写法见 `docs/reference/cli.md`。
|
||||
- P0: 对 k3s pod 内文本热修必须把第一个 route token 直接定位到目标 pod/container,容器内 cwd 用 `--cwd /path` 表达后再执行 `apply-patch`;`:` 是分布式路由符号,`/` 是文件系统 cwd 符号,禁止用 `pod/<pod>/<container>` 选择容器,也禁止从 host/worktree 取 diff、改路径、再管道拼到 pod,反面案例和正确写法见 `docs/reference/cli.md`。
|
||||
- P0: 只有在 `apply-patch` 本身不可用或需要处理非文本/批量机械生成文件时,才使用其他受控方式;使用前必须说明原因,并在修改后立即用 `git diff`、语法检查或文件尾部检查确认没有截断或污染。
|
||||
- P0: `apply-patch` 一旦出现误删、尾部截断、匹配漂移或其他正确性问题,必须立即优先修复 UniDesk `apply-patch` 本身;算法必须按 Codex 开源 `apply_patch` 源码语义做 1:1 对齐,不能用局部护栏、兼容绕行、分支开关或改用其他 patch 入口掩盖基础链路缺陷。
|
||||
- P0: Codex 开源 `apply_patch` 参考源码已固定缓存到 `/tmp/codex-apply-patch/codex/codex-rs/apply-patch/`,core 侧相关文件和 commit 记录在 `/tmp/codex-apply-patch/`;排查或对齐 `apply-patch` 算法时必须先读该本地缓存,只有缓存缺失或明确需要更新时才重新联网拉取。
|
||||
|
||||
+13
-12
@@ -242,7 +242,7 @@ GitHub issue/PR 写操作必须优先使用 `bun scripts/cli.ts gh issue|pr ...
|
||||
exec /root/unidesk/scripts/trans "$@"
|
||||
```
|
||||
|
||||
主 server 上的人工/Codex 分布式敏捷操作必须直接写 `trans ...`,不要在 Codex 工具调用里退回完整 `bun scripts/cli.ts ssh ...` 前缀。例如 `trans D601:/home/ubuntu/workspace/hwlab-dev git status --short --branch`、`trans D601:k3s kubectl get pods -n hwlab-dev` 或 `trans D601:k3s:hwlab-dev:hwlab-cloud-web/tmp pwd`。`tran` 是历史兼容 wrapper 和 runner 固化入口;新写长期参考、AGENTS 索引和 CLI help 时优先写 `trans ...`。
|
||||
主 server 上的人工/Codex 分布式敏捷操作必须直接写 `trans ...`,不要在 Codex 工具调用里退回完整 `bun scripts/cli.ts ssh ...` 前缀。例如 `trans D601:/home/ubuntu/workspace/hwlab-dev git status --short --branch`、`trans D601:k3s kubectl get pods -n hwlab-dev` 或 `trans D601:k3s:hwlab-dev:hwlab-cloud-web exec --cwd /tmp -- pwd`。`tran` 是历史兼容 wrapper 和 runner 固化入口;新写长期参考、AGENTS 索引和 CLI help 时优先写 `trans ...`。
|
||||
|
||||
`trans` 同样遵守 route/operation 解析器;route 后面的第一个 token 不是原生 ssh 命令字符串。不要写 `trans G14:/root/hwlab sh -lc '...'`,因为 `sh` 会被解析为 stdin script helper 的别名,`-lc` 会变成不受支持的 script 选项。带变量展开、管道、重定向或多条命令的远端逻辑,默认使用 `trans G14:/root/hwlab script <<'SCRIPT'`;默认 `script` 走目标节点 `/bin/sh`,并继承 provider-gateway/G14 已长期化的 proxy 环境。需要临时单步执行一行远端 shell 逻辑、且不想先创建脚本文件或 heredoc 时,优先使用 `trans G14:/root/hwlab script -- 'sed -n "1,20p" a && sed -n "1,20p" b'`,CLI 会把单个字符串放进目标节点的 `sh -c`,第二个 `sed`、管道和重定向都会留在远端;等价 `shell '<command>'` 仍保留为显式 shell operation。`script` 和 `shell` helper 会在用户 shell 文本前注入一个极小的 POSIX 兼容 `printf` wrapper,使 `printf "--- section ---\n"` 这类高频排障分隔标题在 dash/sh 与 bash 下行为一致;direct argv 形态不注入该 wrapper。`script --` 后跟多个 token 时保持 direct argv,例如 `trans G14:/root/hwlab script -- sed -n '1,20p' AGENTS.md`。只有脚本确实使用 `pipefail`、数组、`[[ ... ]]` 等 bash 专有语义时才加 `--shell bash`,不能把 `--shell bash` 当作 proxy 修复手段。单进程命令才直接写成 argv,例如 `trans G14:/root/hwlab git status --short --branch`。遇到分布式开发摩擦时,优先补强 `trans`/`tran` 的 route/operation、stdin helper 或目标节点环境,并把稳定解法写回长期参考文档,不要退回多层 shell 字符串拼接。
|
||||
|
||||
@@ -255,7 +255,7 @@ exec /root/unidesk/scripts/trans "$@"
|
||||
- 例外只限于一次性探测、临时 heredoc 草稿或旧文档复用;任何被复用第二次的 `cd <workspace> && ...` 都必须重写成 `trans <provider>:/absolute/workspace` 形式。
|
||||
- 当远端存在多个并行 workspace(例如 `G14:/root/hwlab` 与 `G14:/root/hwlab-v02`)时,route 必须显式带 workspace,CLI 的 `pwd` 输出、后续 `apply-patch` 的相对路径和 `script` 的 cwd 全部跟随该 workspace;切换 workspace 必须切换 route,不允许在同一次 `trans` 链里再 `cd`。
|
||||
- 本规则覆盖所有 host workspace 形态,包括 `G14:/root/hwlab`、`G14:/root/hwlab-v02`、`G14:/root/agentrun-v01`、`D601:/home/ubuntu/workspace/unidesk-dev`、`D601:/home/ubuntu/workspace/hwlab-dev`;provider-gateway 侧已经把它们注册为 host workspace route。
|
||||
- 与 `k3s` route 的分工不变:定位控制面继续写 `trans G14:k3s`、定位 workload 继续写 `trans G14:k3s:<namespace>:<workload>[/<workspace>]`;host workspace 路径里的 `cd` 才需要被替换,控制面或 pod 内的多层 shell 不在本规则的清理范围。
|
||||
- 与 `k3s` route 的分工不变:定位控制面继续写 `trans G14:k3s`、定位 workload/container 继续写 `trans G14:k3s:<namespace>:<workload>[:<container>]`;pod/container 内 cwd 用 operation 参数 `--cwd /path`,或在已经明确选出 container 后才使用 `/path` route 后缀表达文件系统位置。host workspace 路径里的 `cd` 才需要被替换,控制面或 pod 内的多层 shell 不在本规则的清理范围。
|
||||
|
||||
非交互 `ssh`/`trans`/`tran` 不是登录 shell,不能依赖 `.bashrc`、`.profile` 或交互 alias。
|
||||
CLI 会在 Host route、workspace route、k3s 控制面脚本和 pod route 的 shell/script helper
|
||||
@@ -298,12 +298,12 @@ ssh-like 远端命令如果出现 `kex_exchange_identification`、`Connection cl
|
||||
|
||||
远端文本 patch 默认使用 `apply-patch` 的 v2 引擎:它不把 hunk 解析交给远端 shell/perl helper,而是在本地按行序列匹配,支持长中文/Unicode 行、纯新增 hunk、低上下文插入和 `@@` 上下文定位,再把完整新内容写回远端。v2 的文件操作提交顺序按 Codex 标准 `apply_patch` 语义执行:空 patch 会失败;删除不存在的文件会失败;`Add File` 可覆盖已有文件;`Move to` 可覆盖目标文件;当大 patch 后续 hunk 不匹配时,已成功提交的前序文件操作会保留,并在错误详情中记录 `partialChanges`,调用方应基于当前文件内容继续补一个更小的 patch,而不是期待全量事务回滚。若 stderr 报 `failed to find expected lines` 且显示 partial context match,尤其是大块/函数替换,调用方必须先重读目标文件当前块,再用更少稳定上下文、`@@ <unique anchor>` 或多个小 hunk 重试;该失败不构成改用 `download`/`upload`、远端脚本整文件替换或 `apply-patch-v1` 的理由。`apply_patch` 旧 helper 默认拒绝低上下文 update hunk:空搜索/纯插入无锚点、只在插入点前有上下文而没有插入点后上下文、或同一 hunk search 在目标文件中匹配多个位置时,都会结构化失败并提示补充上下文。成功应用时每个 hunk 会在 stderr 输出 `apply_patch: hunk N matched path:line`,用于复核实际落点;只有人工确认确实需要旧 helper 行为或 `--allow-loose` 时,才显式调用 `apply-patch-v1 --allow-loose`。
|
||||
|
||||
如果只是远端打文本补丁,不需要再手写 `ssh D601 'apply_patch' < patch.diff` 这种命令拼接;正式默认入口是 `trans D601:/absolute/workspace apply-patch < patch.diff`、`trans D601:k3s:<namespace>:<workload>/<workspace> apply-patch < patch.diff` 或 `trans D601:win/c/test apply-patch < patch.diff`。旧 helper 只有 `apply-patch-v1` 一个入口,附加参数会原样透传给远端 `apply_patch`,例如 `trans D601 apply-patch-v1 --help` 或 `trans D601 apply-patch-v1 --allow-loose < reviewed.patch`。标准单命令用法如下,不需要先创建本地 patch 临时文件:
|
||||
如果只是远端打文本补丁,不需要再手写 `ssh D601 'apply_patch' < patch.diff` 这种命令拼接;正式默认入口是 `trans D601:/absolute/workspace apply-patch < patch.diff`、`trans D601:k3s:<namespace>:<workload>[:<container>] apply-patch --cwd /workspace < patch.diff` 或 `trans D601:win/c/test apply-patch < patch.diff`。旧 helper 只有 `apply-patch-v1` 一个入口,附加参数会原样透传给远端 `apply_patch`,例如 `trans D601 apply-patch-v1 --help` 或 `trans D601 apply-patch-v1 --allow-loose < reviewed.patch`。标准单命令用法如下,不需要先创建本地 patch 临时文件:
|
||||
|
||||
pod 内文本热修的反面案例是:先在 host/source worktree 生成 `git diff`,再用本地 `sed` 改路径、拼 `*** Begin Patch` 包头,最后管道到 `G14:k3s:<namespace>:<pod>`。这种做法把 source workspace、local shell、远端 shell 和 pod workspace 四层混在一起,容易出现 patch 格式错误、路径漂移、部分成功后误判、以及“看起来在热修 pod,实际主要在搬运 host diff”的错误行为。正确做法是把第一个 route token 直接定位到目标 pod 和 pod 内 workspace,然后在同一条 route 上写标准 Codex patch:
|
||||
pod 内文本热修的反面案例是:先在 host/source worktree 生成 `git diff`,再用本地 `sed` 改路径、拼 `*** Begin Patch` 包头,最后管道到 `G14:k3s:<namespace>:<pod>`。这种做法把 source workspace、local shell、远端 shell 和 pod workspace 四层混在一起,容易出现 patch 格式错误、路径漂移、部分成功后误判、以及“看起来在热修 pod,实际主要在搬运 host diff”的错误行为。正确做法是把第一个 route token 直接定位到目标 pod/container,容器内 cwd 用 `--cwd /path` 表达,然后在同一条 route 上写标准 Codex patch:
|
||||
|
||||
```bash
|
||||
trans G14:k3s:hwlab-dev:pod:hwlab-cloud-web-abc/app/web/hwlab-cloud-web apply-patch <<'PATCH'
|
||||
trans G14:k3s:hwlab-dev:pod:hwlab-cloud-web-abc:hwlab-cloud-web apply-patch --cwd /app/web/hwlab-cloud-web <<'PATCH'
|
||||
*** Begin Patch
|
||||
*** Update File: app.mjs
|
||||
@@
|
||||
@@ -313,7 +313,7 @@ trans G14:k3s:hwlab-dev:pod:hwlab-cloud-web-abc/app/web/hwlab-cloud-web apply-pa
|
||||
PATCH
|
||||
```
|
||||
|
||||
如果运行面实际加载 `dist/` 产物,同样直接定位到 `.../app/web/hwlab-cloud-web/dist apply-patch` 再改 `app.mjs`,不要从源码 worktree 生成 diff 后改路径上传。热修后立即用同一个 pod route 做 `grep`/`sha256sum`/语法检查或浏览器 smoke 确认落点;若热修内容已经等同于上传一份较大源码,优先停止热修,改走 PR/CI/CD。
|
||||
如果运行面实际加载 `dist/` 产物,同样直接定位到 `...:hwlab-cloud-web apply-patch --cwd /app/web/hwlab-cloud-web/dist` 再改 `app.mjs`,不要从源码 worktree 生成 diff 后改路径上传。热修后立即用同一个 pod route 做 `grep`/`sha256sum`/语法检查或浏览器 smoke 确认落点;若热修内容已经等同于上传一份较大源码,优先停止热修,改走 PR/CI/CD。
|
||||
|
||||
```bash
|
||||
trans D601:/home/ubuntu/pipeline apply-patch <<'PATCH'
|
||||
@@ -369,7 +369,7 @@ SCRIPT
|
||||
|
||||
这个入口的目标是分布式调试的“0 shell-command-string”路径:本地 shell 只负责 heredoc/stdin,UniDesk 只负责 provider 路由,远端 shell 只解释脚本正文。脚本正文里仍然要遵守 shell 语言自身的规则,但不再穿过本地 shell、远端 shell、kubectl exec 和容器 shell 的多重字符串转义。
|
||||
|
||||
`trans <providerId> shell '<command>'` 是一行远端 shell 逻辑的逃生阀,不取代 `script`。它的输入必须作为一个 quoted argv 到达 CLI,适合 `sed ... && sed ...`、`kubectl get ... | head` 或一次性环境探测;它仍然只穿过一次目标 shell,不能解决本地 shell 已经拆开的外层 `&&`、`|`、`>`。k3s 控制面同样支持 `trans G14:k3s shell 'kubectl get nodes && kubectl get pods -A'`,并默认注入 `/etc/rancher/k3s/k3s.yaml`;pod route 支持 `trans D601:k3s:hwlab-dev:hwlab-cloud-api/app shell 'pwd && ls'`,先进入 pod workspace 再执行一行 shell 逻辑。
|
||||
`trans <providerId> shell '<command>'` 是一行远端 shell 逻辑的逃生阀,不取代 `script`。它的输入必须作为一个 quoted argv 到达 CLI,适合 `sed ... && sed ...`、`kubectl get ... | head` 或一次性环境探测;它仍然只穿过一次目标 shell,不能解决本地 shell 已经拆开的外层 `&&`、`|`、`>`。k3s 控制面同样支持 `trans G14:k3s shell 'kubectl get nodes && kubectl get pods -A'`,并默认注入 `/etc/rancher/k3s/k3s.yaml`;pod route 需要 cwd 时使用 `exec --cwd /path -- sh -c '<command>'`,例如 `trans D601:k3s:hwlab-dev:hwlab-cloud-api exec --cwd /app -- sh -c 'pwd && ls'`。
|
||||
|
||||
`trans <providerId> skills` 是远端 skill 发现入口,也可写作 `trans <providerId> skill discover`。输出固定为 JSON,包含 `node`、`roots`、`counts` 和 `skills`:`roots` 会显示每个候选 skill 根目录是否存在、扫描到多少 skill 以及错误;`skills` 会给出 `scope`、`name`、`description`、`path`、`skillMd` 和可转换时的 `windowsPath`。默认扫描远端用户的 `~/.agents/skills`、`~/.codex/skills`、可访问的 `/root/.agents/skills`、`/root/.codex/skills`;如果目标是 WSL,还会扫描 `/mnt/c/Users/*/.agents/skills` 与 `/mnt/c/Users/*/.codex/skills`,从而一次性看清 WSL 和 Windows 两套 skill。常用参数是 `--scope wsl`、`--scope windows`、`--limit N`、`--max-depth N`、`--root <path>` 和 `--windows-root <path>`;不要用宽泛的 Linux `find /mnt/*` 扫 Windows 盘,优先用这个结构化入口避免卡在 Windows 挂载层。
|
||||
|
||||
@@ -407,11 +407,11 @@ PS
|
||||
|
||||
`<provider>:win skills [--scope agents|codex|all] [--limit N]` 是 Windows 用户 skill 发现入口,默认只读取当前 Windows 用户的 `%USERPROFILE%\.agents\skills`,输出 JSON 中包含 `roots`、`counts` 和每个 skill 的 `name`、`path`、`skillFile`、`description`。需要同时检查 `%USERPROFILE%\.codex\skills` 时显式加 `--scope all`;不要为了列 skill 手写 `cmd dir` 或宽泛扫描整个用户目录。
|
||||
|
||||
`D601:k3s` 或 `G14:k3s` 定位到对应 provider 的原生 k3s 控制面;`<provider>:k3s:<namespace>:<workload>[:container]` 定位到 namespace 下的一个默认 deployment workload;若目标是具体 Pod,标准 workload 段写成 `pod:<podid>`,旧 `pod/<podid>` 只作为兼容输入继续接受;若目标是 Deployment,也可以显式写 `deployment/<name>` 或简写 `<name>`。pod 内 workspace 使用 slash 后缀表达,slash 只用于 pod/container 内文件系统定位,例如 `D601:k3s:hwlab-dev:hwlab-cloud-api/app` 会定位到 deployment `hwlab-cloud-api` 并在 pod 内先 `cd /app`,`D601:k3s:hwlab-dev:pod:hwlab-cloud-api-abc/workspace/app:api` 会定位到 pod、container 和 `/workspace/app`。`kubectl`、`logs`、`script`、`apply-patch`、旧 `apply-patch-v1` fallback、`exec` 和普通容器命令都是 route 后面的 operation,这样路由子模块和操作子模块可以独立扩展。
|
||||
`D601:k3s` 或 `G14:k3s` 定位到对应 provider 的原生 k3s 控制面;`<provider>:k3s:<namespace>:<workload>[:container]` 定位到 namespace 下的一个默认 deployment workload;若目标是具体 Pod,标准 workload 段写成 `pod:<podid>`,例如 `D601:k3s:hwlab-v03:pod:hwlab-cloud-api-abc:hwlab-cloud-api`。`pod/<podid>` 不是合法 route 写法,因为 `:` 是分布式路由分隔符,`/` 只表示目标容器里的文件系统 cwd;如果调用者写成 `pod/<podid>/<container>`,CLI 必须在连接运行面前报错并提示改用 `pod:<podid>:<container>` 或 `--container <container>`。若目标是 Deployment,也可以显式写 `deployment:<name>` 或简写 `<name>`;容器选择写 `:<container>` 或 operation 参数 `--container <container>`。pod 内 cwd 推荐用 operation 参数 `--cwd /path`,例如 `D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api exec --cwd /app -- pwd`。`kubectl`、`logs`、`script`、`apply-patch`、旧 `apply-patch-v1` fallback、`exec` 和普通容器命令都是 route 后面的 operation,这样路由子模块和操作子模块可以独立扩展。
|
||||
|
||||
`k3s` 必须出现在 route 的 plane 段里,禁止使用 `trans G14 k3s ...` 或 `trans D601 k3s ...` 这类 post-provider shorthand;正确形态是 `trans G14:k3s kubectl ...` 或 `trans D601:k3s kubectl ...`。定位和操作必须保持分离,`kubectl`、`logs`、`script`、`apply-patch`、旧 `apply-patch-v1` fallback、`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 workspace `apply-patch` 读写和旧 `apply-patch-v1` fallback 组装成 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>/<workspace> apply-patch` 是 pod 内文本 patch 默认入口;旧 helper 仅通过 `<provider>:k3s:<namespace>:<workload> apply-patch-v1` 显式调用。典型用法:
|
||||
该入口解决运行面调试中最常见的多层 shell 引号问题。它不要求升级 provider-gateway,也不新增业务 API,只复用现有 Host SSH 维护桥;CLI 在本地把 Kubernetes 目标、namespace、container、log 限制、容器命令、stdin script、pod cwd `apply-patch` 读写和旧 `apply-patch-v1` fallback 组装成 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>[:container] logs` 读取有界日志;`<provider>:k3s:<namespace>:<workload>[:container] exec ...` 和 `<provider>:k3s:<namespace>:<workload>[:container] <command> ...` 进入目标 workload/container;`<provider>:k3s:<namespace>:<workload>[:container] script` 把本地 stdin 作为 pod 内 shell 脚本执行;`<provider>:k3s:<namespace>:<workload>[:container] apply-patch --cwd /workspace` 是 pod 内文本 patch 默认入口;旧 helper 仅通过 `<provider>:k3s:<namespace>:<workload> apply-patch-v1` 显式调用。典型用法:
|
||||
|
||||
```bash
|
||||
trans D601:k3s
|
||||
@@ -427,13 +427,14 @@ trans G14:k3s
|
||||
trans G14:k3s kubectl get pipelineruns -n hwlab-ci
|
||||
printf 'kubectl get deploy -n hwlab-dev\n' | trans D601:k3s script
|
||||
trans D601:k3s:hwlab-dev:hwlab-cloud-api logs --tail 80
|
||||
trans D601:k3s:hwlab-v03:pod:hwlab-cloud-api-abc:hwlab-cloud-api script -- 'curl -fsS http://user-billing/health'
|
||||
trans G14:k3s logs --namespace=devops-infra --deployment=git-mirror-http --tail=80
|
||||
trans G14:k3s logs -n agentrun-ci -l tekton.dev/pipelineRun=agentrun-v01-ci-xxxx --tail=120
|
||||
trans D601:k3s:hwlab-dev:hwlab-cloud-api node -e 'console.log(process.version)'
|
||||
trans D601:k3s:hwlab-dev:hwlab-cloud-api/app pwd
|
||||
trans D601:k3s:hwlab-dev:hwlab-cloud-api exec --cwd /app -- pwd
|
||||
printf 'printf "pod=%s\n" "$HOSTNAME"\n' | trans D601:k3s:hwlab-dev:hwlab-cloud-api script
|
||||
tar -C /tmp/patched-files -cf - . | trans D601:k3s:unidesk:code-queue/root/unidesk exec --stdin -- tar -xf - -C /root/unidesk
|
||||
trans D601:k3s:hwlab-dev:hwlab-cloud-api/app apply-patch <<'PATCH'
|
||||
tar -C /tmp/patched-files -cf - . | trans D601:k3s:unidesk:code-queue exec --cwd /root/unidesk --stdin -- tar -xf - -C /root/unidesk
|
||||
trans D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api apply-patch --cwd /app <<'PATCH'
|
||||
*** Begin Patch
|
||||
*** Update File: /tmp/example.txt
|
||||
@@
|
||||
|
||||
@@ -62,12 +62,12 @@ If a manual repair is needed to unblock the platform, the durable fix must be co
|
||||
|
||||
Distributed runtime work should prefer structured CLI passthrough over ad-hoc nested shell strings. The standard escalation order is:
|
||||
|
||||
1. Use a purpose-built UniDesk route plus operation or helper such as `trans D601:k3s kubectl ...`, `trans D601:k3s script`, `trans D601:k3s:<namespace>:<workload> logs`, `trans D601:k3s:<namespace>:<workload> script`, `trans D601:k3s:<namespace>:<workload>/<workspace> apply-patch`, `trans <providerId>:/absolute/workspace apply-patch`, `trans <providerId> py`, `trans <providerId> find`, `trans <providerId> glob` or `trans <providerId> skills`. Use legacy `apply-patch-v1` only when the old remote helper is explicitly required.
|
||||
1. Use a purpose-built UniDesk route plus operation or helper such as `trans D601:k3s kubectl ...`, `trans D601:k3s script`, `trans D601:k3s:<namespace>:<workload> logs`, `trans D601:k3s:<namespace>:<workload> script`, `trans D601:k3s:<namespace>:<workload>[:<container>] apply-patch --cwd /workspace`, `trans <providerId>:/absolute/workspace apply-patch`, `trans <providerId> py`, `trans <providerId> find`, `trans <providerId> glob` or `trans <providerId> skills`. Use legacy `apply-patch-v1` only when the old remote helper is explicitly required.
|
||||
2. If no helper exists, use `trans <providerId> argv <command> [args...]` so the CLI quotes each argv token once.
|
||||
3. If shell features such as pipes, redirects, loops or variable expansion are required, use a single quoted heredoc with `trans <providerId> script` or `trans D601:k3s:<namespace>:<workload> script` so the script body travels over stdin instead of through shell command-string arguments.
|
||||
4. Treat free-form ssh-like command strings as an interactive compatibility path, not as the default automation surface.
|
||||
|
||||
For D601 Kubernetes work, route syntax is preferred over positional shell recipes, but the route must stay a pure locator. `D601:k3s` means the native k3s control plane, and `D601:k3s:<namespace>:<workload>[:container]` means a namespaced workload or pod. Operations come after the route: `kubectl` runs on the control plane, `logs` reads bounded workload logs, `script` streams a local heredoc/stdin script into the host or target pod, and `apply-patch` is the default remote text patch operation for host or pod workspaces. The route-operation split keeps distributed location and execution behavior independently extensible, fixes `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`, refuses long-follow logs, and assembles common `kubectl exec` / `kubectl logs` / stdin script / pod patch target arguments without adding a provider-gateway protocol change. This prevents the common failure mode where a command crosses local shell, UniDesk SSH broker, remote shell command strings, `kubectl exec`, and container shell quoting layers before reaching the process that should run it.
|
||||
For D601 Kubernetes work, route syntax is preferred over positional shell recipes, but the route must stay a pure locator. `D601:k3s` means the native k3s control plane, and `D601:k3s:<namespace>:<workload>[:container]` means a namespaced workload or pod/container. `:` is the distributed route separator; `/` is only an in-container filesystem cwd, so container selection must use `:<container>` or `--container <container>`, not `pod/<pod>/<container>`. Operations come after the route: `kubectl` runs on the control plane, `logs` reads bounded workload logs, `script` streams a local heredoc/stdin script into the host or target pod, and `apply-patch --cwd /workspace` is the default remote text patch operation for pod workspaces. The route-operation split keeps distributed location and execution behavior independently extensible, fixes `KUBECONFIG=/etc/rancher/k3s/k3s.yaml`, refuses long-follow logs, and assembles common `kubectl exec` / `kubectl logs` / stdin script / pod patch target arguments without adding a provider-gateway protocol change. This prevents the common failure mode where a command crosses local shell, UniDesk SSH broker, remote shell command strings, `kubectl exec`, and container shell quoting layers before reaching the process that should run it.
|
||||
|
||||
Longer scripts should move across stdin (`trans py`, `trans script` or k3s `script` operation), and remote text patches should default to `apply-patch` with a host or pod workspace route. Legacy `apply-patch-v1` remains available as the explicit fallback and uses the injected `sh` helper path instead of assuming target containers have `python3`, `node` or repository-local tools. Avoid heredocs nested inside remote command strings, `python - <<EOF` inside SSH strings, or JSON/Markdown bodies passed through shell arguments. These patterns often bind stdin to the wrong process, strip quotes, or leave a half-open provider SSH session that looks like a platform outage.
|
||||
|
||||
|
||||
+4
-4
@@ -183,10 +183,10 @@ export function sshHelp(): unknown {
|
||||
"trans G14:k3s",
|
||||
"trans G14:k3s kubectl get pipelineruns -n hwlab-ci",
|
||||
"trans D601:k3s script <<'SCRIPT'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api/app pwd",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api/app apply-patch <<'PATCH'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api exec --cwd /app -- pwd",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api:hwlab-cloud-api apply-patch --cwd /app <<'PATCH'",
|
||||
"trans D601:k3s:hwlab-dev:hwlab-cloud-api apply-patch-v1 <<'PATCH'",
|
||||
"tar -C /path/to/files -cf - . | trans D601:k3s:unidesk:code-queue/root/unidesk exec --stdin -- tar -xf - -C /root/unidesk",
|
||||
"tar -C /path/to/files -cf - . | trans D601:k3s:unidesk:code-queue exec --cwd /root/unidesk --stdin -- tar -xf - -C /root/unidesk",
|
||||
"trans D601:win/c/test apply-patch <<'PATCH'",
|
||||
"trans D601:win upload ./tool.mjs F:\\Work\\hwlab\\.tmp\\tool.mjs",
|
||||
"trans D601:win download F:\\Work\\hwlab\\.tmp\\tool.mjs ./tool.mjs",
|
||||
@@ -210,7 +210,7 @@ export function sshHelp(): unknown {
|
||||
"`upload` and `download` are the default whole-file transfer entries for non-text and generated files. They write through remote temp files, verify byte count and SHA-256 on both sides, and return `verification.automatic=true`, `verification.verified=true`, and `verification.match.{bytes,sha256}=true`; this JSON is the transfer integrity proof, so callers do not need a separate manual `sha256sum` check. Downloads stream over `host.ssh.tcp-pool`, emit progress JSON, and may receive a caller-supplied `--inactivity-timeout-ms` from async artifact/deploy jobs so active large transfers are not killed by the generic short-command budget.",
|
||||
"`apply-patch-v1` is the only legacy fallback entry: it rejects low-context update hunks by default, reports the matched file:line for each hunk on stderr, and only accepts --allow-loose when the caller has manually reviewed an intentionally ambiguous insertion.",
|
||||
"script defaults to target /bin/sh and inherits provider proxy variables such as HTTP_PROXY/HTTPS_PROXY/ALL_PROXY/NO_PROXY; it is for host/k3s POSIX shell only. Use --shell bash only for bash syntax such as pipefail, arrays, or [[ ... ]], not as a proxy workaround.",
|
||||
"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. Host workspace routes use `<provider>:/absolute/workspace`; WSL providers can use `<provider>:win ps` for Windows PowerShell and `<provider>:win cmd` for Windows cmd.exe, with `<provider>:win/c/test ...` mapping the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use <provider>:k3s for the control plane, <provider>:k3s:<namespace>:<workload> for a workload, and <provider>:k3s:<namespace>:<workload>/<pod-workspace> for a pod workspace.",
|
||||
"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. Host workspace routes use `<provider>:/absolute/workspace`; WSL providers can use `<provider>:win ps` for Windows PowerShell and `<provider>:win cmd` for Windows cmd.exe, with `<provider>:win/c/test ...` mapping the Windows cwd to `C:\\test`; native k3s providers such as D601 and G14 use `<provider>:k3s` for the control plane and `<provider>:k3s:<namespace>:<workload>[:<container>]` for a workload/container. In k3s routes, `:` is the distributed route separator; `/...` is only an in-container filesystem cwd and never selects a container. Prefer operation `--cwd /path` when a container is also specified.",
|
||||
"Use `win`, not `win32`; the win route is a Windows operation plane. `ps` and `cmd` both set UTF-8/Python encoding defaults, while `cmd` also sets `chcp 65001`.",
|
||||
"`<provider>:win skills` discovers the current Windows user's `%USERPROFILE%\\.agents\\skills` by default; use `--scope all` to include `%USERPROFILE%\\.codex\\skills`.",
|
||||
"Do not put operation names in any colon route segment, including nested k3s namespace/workload/container segments.",
|
||||
|
||||
+103
-16
@@ -49,6 +49,11 @@ export interface ParsedSshInvocation {
|
||||
parsed: ParsedSshArgs;
|
||||
}
|
||||
|
||||
interface EffectiveApplyPatchV2Invocation {
|
||||
invocation: ParsedSshInvocation;
|
||||
argv: string[];
|
||||
}
|
||||
|
||||
export interface SshCaptureResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
@@ -149,7 +154,6 @@ export const sshUserToolPathPrelude = [
|
||||
"export PATH",
|
||||
].join("\n");
|
||||
const k3sResourceKindAliases = new Set(["pod", "po", "pods", "deployment", "deploy", "deployments", "statefulset", "sts", "daemonset", "ds", "job", "jobs"]);
|
||||
const k3sPodRoutePrefixes = ["pod:", "po:", "pods:"];
|
||||
const legacyK3sOperationRouteSegments = new Set([
|
||||
"guard",
|
||||
"kubectl",
|
||||
@@ -1031,6 +1035,7 @@ export function parseSshRoute(target: string): ParsedSshRoute {
|
||||
return hostSshRoute(providerId, target, workspace);
|
||||
}
|
||||
if (plane !== "k3s") throw new Error(`unsupported ssh route plane: ${plane}`);
|
||||
rejectK3sSlashKindRouteSegments(target, rest);
|
||||
const normalizedRest = normalizeK3sRouteRestSegments(rest);
|
||||
const [first, second, third, fourth] = normalizedRest;
|
||||
const operationInRoute = [first, second, third].map((segment) => segment === undefined ? undefined : routeSegmentHead(segment)).find((segment) => segment !== undefined && legacyK3sOperationRouteSegments.has(segment));
|
||||
@@ -1414,25 +1419,48 @@ function normalizeK3sRouteRestSegments(rest: string[]): string[] {
|
||||
const normalized: string[] = [];
|
||||
for (let index = 0; index < rest.length; index += 1) {
|
||||
const current = rest[index] ?? "";
|
||||
const podPrefix = k3sPodRoutePrefixes.find((prefix) => current === prefix.slice(0, -1) || current.startsWith(prefix));
|
||||
if (podPrefix === undefined) {
|
||||
const kindRoute = k3sColonKindRoute(current);
|
||||
if (kindRoute === null) {
|
||||
normalized.push(current);
|
||||
continue;
|
||||
}
|
||||
if (current === podPrefix.slice(0, -1)) {
|
||||
if (kindRoute.name === null) {
|
||||
const next = rest[index + 1];
|
||||
if (next === undefined || next.length === 0) throw new Error("ssh k3s pod: route requires a pod name after pod:");
|
||||
normalized.push(`pod/${next}`);
|
||||
if (next === undefined || next.length === 0) throw new Error(`ssh k3s ${kindRoute.kind}: route requires a resource name after ${kindRoute.kind}:`);
|
||||
normalized.push(`${kindRoute.kind}/${next}`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const podName = current.slice(podPrefix.length);
|
||||
if (podName.length === 0) throw new Error("ssh k3s pod: route requires a pod name after pod:");
|
||||
normalized.push(`pod/${podName}`);
|
||||
normalized.push(`${kindRoute.kind}/${kindRoute.name}`);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function rejectK3sSlashKindRouteSegments(target: string, rest: string[]): void {
|
||||
const resourceSegment = rest[1];
|
||||
if (resourceSegment === undefined || resourceSegment.length === 0) return;
|
||||
const slashIndex = resourceSegment.indexOf("/");
|
||||
if (slashIndex < 0) return;
|
||||
const head = resourceSegment.slice(0, slashIndex);
|
||||
if (!isK3sResourceKindAlias(head)) return;
|
||||
throw new Error(
|
||||
`ssh k3s route segment "${resourceSegment}" in "${target}" uses "/" between Kubernetes route parts. ` +
|
||||
`":" is the distributed route separator and "/" is only for the in-container filesystem cwd. ` +
|
||||
`Use "trans <provider>:k3s:<namespace>:${head}:<name>[:<container>] ..." for ${head} routes, ` +
|
||||
`and use "--cwd /path" or a route cwd suffix only for filesystem paths.`
|
||||
);
|
||||
}
|
||||
|
||||
function k3sColonKindRoute(value: string): { kind: string; name: string | null } | null {
|
||||
const colonIndex = value.indexOf(":");
|
||||
const head = colonIndex < 0 ? value : value.slice(0, colonIndex);
|
||||
if (!isK3sResourceKindAlias(head)) return null;
|
||||
if (colonIndex < 0) return { kind: head, name: null };
|
||||
const name = value.slice(colonIndex + 1);
|
||||
if (name.length === 0) throw new Error(`ssh k3s ${head}: route requires a resource name after ${head}:`);
|
||||
return { kind: head, name };
|
||||
}
|
||||
|
||||
function splitK3sResourceWorkspace(value: string | null): { resource: string | null; workspace: string | null } {
|
||||
if (value === null || value.length === 0) return { resource: null, workspace: null };
|
||||
const parts = value.split("/");
|
||||
@@ -1714,6 +1742,55 @@ function k3sRouteExecOperationArgs(args: string[]): string[] {
|
||||
throw new Error("ssh k3s target exec operation requires a command to exec");
|
||||
}
|
||||
|
||||
function effectiveApplyPatchV2Invocation(invocation: ParsedSshInvocation, argv: string[]): EffectiveApplyPatchV2Invocation {
|
||||
if (invocation.route.plane !== "k3s" || isApplyPatchV2HelpArgs(argv)) return { invocation, argv };
|
||||
let container: string | null = null;
|
||||
let workspace: string | null = null;
|
||||
const remaining: string[] = [];
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index] ?? "";
|
||||
if (arg === "--container" || arg === "-c") {
|
||||
container = k3sOptionValue(argv, index, `ssh ${invocation.route.raw} apply-patch ${arg}`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const containerValue = k3sEqualsOptionValue(arg, "--container", `ssh ${invocation.route.raw} apply-patch`);
|
||||
if (containerValue !== null) {
|
||||
container = containerValue;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--workdir" || arg === "--cwd") {
|
||||
workspace = k3sWorkspaceValue(k3sOptionValue(argv, index, `ssh ${invocation.route.raw} apply-patch ${arg}`), `ssh ${invocation.route.raw} apply-patch ${arg}`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
const workdirValue = k3sEqualsOptionValue(arg, "--workdir", `ssh ${invocation.route.raw} apply-patch`) ?? k3sEqualsOptionValue(arg, "--cwd", `ssh ${invocation.route.raw} apply-patch`);
|
||||
if (workdirValue !== null) {
|
||||
workspace = k3sWorkspaceValue(workdirValue, `ssh ${invocation.route.raw} apply-patch ${arg.split("=", 1)[0]}`);
|
||||
continue;
|
||||
}
|
||||
remaining.push(arg);
|
||||
}
|
||||
|
||||
const route = invocation.route;
|
||||
if (container !== null && route.container !== null && container !== route.container) {
|
||||
throw new Error("ssh k3s apply-patch container can be specified once, either in the route :<container> segment or with --container");
|
||||
}
|
||||
|
||||
return {
|
||||
invocation: {
|
||||
...invocation,
|
||||
route: {
|
||||
...route,
|
||||
container: route.container ?? container,
|
||||
workspace: combineK3sRouteWorkspace(route.workspace, workspace),
|
||||
},
|
||||
},
|
||||
argv: remaining,
|
||||
};
|
||||
}
|
||||
|
||||
function buildK3sCommand(providerId: string, args: string[]): string {
|
||||
const action = args[0] ?? "";
|
||||
if (action.length === 0 || action === "--help" || action === "-h" || action === "help") {
|
||||
@@ -2109,7 +2186,16 @@ function k3sWorkspaceValue(value: string, option: string): string {
|
||||
function withK3sWorkspace(parsed: K3sTargetOptions, command: string[]): string[] {
|
||||
if (parsed.workspace === null) return command;
|
||||
if (command.length === 0) throw new Error("ssh k3s workspace route requires a command to execute");
|
||||
return ["sh", "-c", 'cd "$1" || exit; shift; exec "$@"', "unidesk-cwd", parsed.workspace, ...command];
|
||||
const cwdScript = [
|
||||
"if ! cd \"$1\"; then",
|
||||
"rc=$?",
|
||||
"printf 'UNIDESK_K3S_CWD_HINT cwd=%s message=%s\\n' \"$1\" '/... is interpreted as remote cwd, not container; use :<container> or --container <container> for container selection.' >&2",
|
||||
"exit \"$rc\"",
|
||||
"fi",
|
||||
"shift",
|
||||
"exec \"$@\"",
|
||||
].join("\n");
|
||||
return ["sh", "-c", cwdScript, "unidesk-cwd", parsed.workspace, ...command];
|
||||
}
|
||||
|
||||
function normalizeK3sResource(value: string): string {
|
||||
@@ -3130,18 +3216,19 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
|
||||
});
|
||||
}
|
||||
if (operationName === "apply-patch") {
|
||||
const executor: ApplyPatchV2Executor = invocation.route.plane === "win"
|
||||
? { fs: createWindowsApplyPatchFileSystem(config, invocation) }
|
||||
: { run: (command, input) => runSshCaptureCommand(config, invocation, command, input) };
|
||||
const applyPatch = effectiveApplyPatchV2Invocation(invocation, normalizedArgs.slice(1));
|
||||
const executor: ApplyPatchV2Executor = applyPatch.invocation.route.plane === "win"
|
||||
? { fs: createWindowsApplyPatchFileSystem(config, applyPatch.invocation) }
|
||||
: { run: (command, input) => runSshCaptureCommand(config, applyPatch.invocation, command, input) };
|
||||
return await runApplyPatchV2({
|
||||
executor,
|
||||
stdin: process.stdin,
|
||||
stdout: process.stdout,
|
||||
stderr: process.stderr,
|
||||
argv: normalizedArgs.slice(1),
|
||||
argv: applyPatch.argv,
|
||||
timing: {
|
||||
providerId: invocation.providerId,
|
||||
route: invocation.route.raw,
|
||||
providerId: applyPatch.invocation.providerId,
|
||||
route: applyPatch.invocation.route.raw,
|
||||
transport: "backend-core-broker",
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user