diff --git a/AGENTS.md b/AGENTS.md index bbe44dca..f9756812 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,11 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - P0: 任何新增、修改或蒸馏 `docs/reference/*.md` 长期参考文档的动作,必须遵循 `docs-spec` 规范;禁止绕过 `docs-spec` 凭临时习惯改写长期参考。 - `AGENTS.md` 只做顶级索引和一句话规则摘要;长期稳定、可复用的约束、入口、判定标准必须落到 `docs/reference/`,再由 `AGENTS.md` 提供索引。 - 过程记录、一次性排障、临时结论、带日期的流水账不得直接写成长期参考;需要沉淀时必须按 `docs-spec` 先蒸馏,再写入 `docs/reference/`。 +## Critical Secretary Long-Term Reference Rule + +- P0: 任何秘书日程、时间盒、Todo Note / Decision Center 分流、滚排、复盘、外部催办登记、委托跟踪、回读与回写动作之前,必须先加载并以 `docs/reference/secretary-reference.md` 为唯一长期参考;AGENTS.md 索引条目只作入口,不替代该参考的完整约束。 +- P0: 秘书工作面的最小核查(北京时、Todo Note、Decision Center 日记、schedule list)、反馈格式("完成/未完成 + 卡点" 或 "路径 + 当前状态 + 下一步阻塞")和"维护 Todo Note、只读 Decision Center diary"边界必须按 `secretary-reference.md` 执行;任何与之冲突的临时记忆、旧版规则或对话印象一律以该参考为准。 +- P0: 禁止在未读完 `secretary-reference.md` 的情况下拍时间块、滚动调整、跨日复盘或代替用户写 Decision Center diary;时间分配由秘书主导,不向用户要选项,例外只有用户原话冲突、硬约束缺失或涉及外部承诺与身份判断。 ## Critical Decision Consensus Anti-Regression Rule @@ -48,12 +53,9 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 ## Critical Distributed Agile Validation Rule -- P0: 分布式敏捷开发默认先在目标 provider/pod/host 透传环境做最小真实闭环验证,再进入完整 CI/CD、GitOps 或发布流水线;禁止把完整 CI/CD 当作每轮兼容性探索和试错工具。 -- P0: 对第三方模型、API provider、硬件、跨平台 bridge、CLI/trans/tran 或高频工具链异常,禁止在未做目标运行面实地探测前定性为外部不可用;必须先核对当前 runtime config/Secret/env/proxy,使用受控透传在真实 pod/host/provider 上复现,并与 UniDesk/HWLAB 等成熟实现做对照。 -- 对第三方模型、硬件、跨平台 bridge、CLI/trans/tran 和高频工具链摩擦的修复,必须先用目标运行面上的最小脚本、临时 pod exec、真实端口或受控透传命令证明核心链路跑通;只有闭环通过后,才把修复固化到源码、测试、长期参考和正式发布。 -- P0: 用户反馈或现场证据推翻初始 blocker 判断时,必须立即停止原 blocker 叙事,改走“透传探测 -> 单变量实验 -> 必要热修复验证 -> 源码 PR -> CI/CD -> 原入口复测”的闭环;运行面热修复只能作为实验和恢复证据,不能替代正式 PR/CI/CD。 -- P0: 如果运行面热补被正常 GitOps、rollout、CI/CD 或镜像重建覆盖,或热补载体与正式发布路径冲突,不要反复拉扯热补;保留已获得的最小闭环证据,直接转入 PR/CI/CD 正式流程,并以正式 rollout 后的原入口复测作为最终结论。 -- 外部 API 或模型行为可能变化时,先查官方/一手文档和成熟实践,再在目标运行面做实验验证;不要凭旧记忆反复推 CI/CD 试错。 +- P0: 分布式敏捷开发通用流程以 `$dad-dev` skill 为唯一权威:P1 目标运行面探测、P2 运行面最小热补丁闭环、P3 源码 PR/CI/CD、P4 原入口复测;UniDesk 项目特有入口和判定边界见 `docs/reference/devops-hygiene.md`。 +- P0: 对第三方模型、API provider、硬件、跨平台 bridge、CLI/trans/tran 或高频工具链异常,禁止在未核对当前 runtime config/Secret/env/proxy 并完成目标运行面实地探测前定性为外部不可用。 +- P0: 运行面热补只能作为实验和恢复证据,不能替代正式 PR/CI/CD;若热补被正常 GitOps、rollout、CI/CD 或镜像重建覆盖,保留最小闭环证据后直接转入 `$dad-dev` 的 P3/P4。 ## Critical HWLAB Issue Closure CLI Validation Rule @@ -158,9 +160,37 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - P0: `trans ...` 后面禁止裸放本地 shell 续接控制符,包括 `&&`、`;` 和 `|`;需要在远端执行多步命令时,必须使用 `trans script -- '远端完整脚本'`、`trans script <<'SCRIPT'` 或等价的单一 stdin/script 参数,避免后半段被本地 shell 执行。`tran` 兼容入口遵守同一规则。`script -- '<单个字符串>'` 会按远端 shell one-liner 执行;`script -- <多个 argv>` 才是 direct argv。`apply-patch`、`apply-patch-v1`、`script`、`py` 等 stdin/capture-backed operation 可以使用 heredoc 或 `< patch.diff` 作为本地输入。 - P0: 新增或扩展 `ssh`/`trans`/`tran` 高频 operation 不得把完整实现继续堆进 `scripts/src/ssh.ts`;`ssh.ts` 只保留 route/parser/broker/dispatch,共享能力拆到 `scripts/src/ssh-*.ts` 专门模块,细则见 `docs/reference/cli.md`。 +## Critical Apply Patch Syntax + +- `apply_patch` 是 Codex 本地文本 patch CLI,通过 stdin 吃 patch 文本;所有高频源码编辑(包括远端 `trans apply-patch` 也走同一 v2 引擎)都依赖它,语法错(首行错、绝对路径、unified diff hunk header、裸空行)会被原样拒绝,下手前必须确认格式正确。 +- 语法是 Codex 专用 envelope,不是 unified diff: + - 第一行 `*** Begin Patch`、最后一行 `*** End Patch`,外层不出现空行。 + - 操作 header 必须是 `*** Update File: ` / `*** Add File: ` / `*** Delete File: `;**路径必须相对路径**(如 `AGENTS.md`),禁止 `/root/unidesk/AGENTS.md` 这类绝对路径。 + - hunk 分隔符**只写 `@@`**,**禁止写 `@@ -N,M +K,L @@`** 这种 unified diff 头;上下文不唯一时追加 `@@ class Foo` 或 `@@ def bar()` 提示定位。 + - hunk 内每行首字符是前缀:` `(上下文,原样保留)、`-`(删除)、`+`(新增)。**文件里的空行在 hunk 内也必须写为 ` \n`(一个空格 + 换行)**,禁止裸 `\n`。 + - 默认 3 行上下前文 + 3 行上下后文;同一 hunk 在文件中重复出现时必须用 `@@` 跳到 class/function。 +- 最小可用示例(修改 `AGENTS.md` 的一行): + + ``` + *** Begin Patch + *** Update File: AGENTS.md + @@ + ## Heading + context line + -old line + +new line + more context + *** End Patch + ``` + +- 成功 stdout 为 `Success. Updated the following files:` 后接文件列表;失败 stdout 为空、stderr 写原因。多文件 hunk 中途失败时前序成功的 hunk 按 Codex 语义保留(`partialChanges`),必须基于当前文件状态补小 patch,不要期望回滚。 +- 完整 Codex 规范缓存于 `/tmp/codex-apply-patch/codex/codex-rs/apply-patch/apply_patch_tool_instructions.md`;本节只写最易踩的高频坑,必要时再回查。 + ## CLI -- P0: `trans ...` 是 `bun scripts/cli.ts ssh ...` 的短 alias,只用于 SSH/WSL/k3s 透传;人工/Codex 远端操作、长期参考、AGENTS 索引和 CLI help 中禁止继续把 `bun scripts/cli.ts ssh ...` 作为默认透传入口。普通根 CLI 子命令仍使用 `bun scripts/cli.ts `,分工见 `docs/reference/cli.md`。 +- P0: `trans ...` 是 `bun scripts/cli.ts ssh ...` 的短 alias,只用于 SSH/WSL/k3s 透传;人工/Codex 远端操作、长期参考、AGENTS 索引、CLI help、非交互脚本和非交互 `exec` 都必须直接调主 server PATH 上的 `/root/.local/bin/trans` wrapper,禁止把 `bun scripts/cli.ts ssh ...`、`bun scripts/cli.ts trans ...` 或任何带 `bun scripts/cli.ts` 前缀的透传写法作为默认入口。普通根 CLI 子命令仍使用 `bun scripts/cli.ts `,分工见 `docs/reference/cli.md`。 + +- P0: `trans ` 的 host workspace 形态必须把已知远端 workspace 写在 route 第一个 token,例如 `trans G14:/root/hwlab-v02 ...`、`trans D601:/home/ubuntu/workspace/unidesk-dev ...`;禁止把 `cd /root/hwlab-v02 && ...` 塞进 `script`/`shell` 字符串或 long-form `bun scripts/cli.ts ssh` 串,长期规则见 `docs/reference/cli.md` 的 Standard Workspace-Prefixed Passthrough。 - `bun scripts/cli.ts help`:输出所有可用命令的 JSON 索引,详细规范见 `docs/reference/cli.md`。 - `bun scripts/cli.ts --main-server-ip `:默认通过公网 frontend 登录态远程执行调试、用户服务(底层命令名 `microservice`)、Code Queue 查询与节点自测命令,不要求主 server SSH key,详细规范见 `docs/reference/cli.md`。 - `bun scripts/cli.ts config show`:校验并展示根目录 `config.json`,配置来源规则见 `docs/reference/config.md`。 @@ -245,3 +275,8 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文 - `docs/reference/dev-ci-runner.md`:`ci run-dev-e2e` 的 Git 控制 runner、短 launcher、结果目录和 no-CD 边界。 - `docs/reference/codex-deploy.md`:D601 Code Queue 旧 `codex deploy ` 入口禁用原因、受控部署边界和后续 CD 目标行为。 - `reference`:兼容旧路径的符号链接,指向 `docs/reference/`。 + +## Critical v0.2 device-pod cloud-api Architecture + +- v0.2 device-pod 集成 = cloud-api `internal/cloud/access-control.ts` + device-pod executor `cmd/hwlab-device-pod/main.ts` + 主机端 `device-host-cli.mjs`;意图/子动作/原因矩阵、`output.text` 三层透传链、cloud-api vs 主机端的运维边界、worktree/closeout 验证顺序都在 [`docs/reference/g14.md#v0.2-device-pod-cloud-api-architecture`](docs/reference/g14.md) 一节,不重复抄写。 +- 主机端 `F:\Work\ConStart\tools\device-host-cli.mjs` 与 v0.2 仓库内的 `skills/device-pod-cli/assets/device-host-cli.mjs` 是 D601 ops 侧副本关系,v0.2 lane CI/CD 不会自动同步;任何 host-side 行为变更需要 D601 ops 手动覆盖主机副本,详见 [`docs/reference/g14.md`](docs/reference/g14.md)。 diff --git a/docs/reference/devops-hygiene.md b/docs/reference/devops-hygiene.md index 8313ccad..62042a1c 100644 --- a/docs/reference/devops-hygiene.md +++ b/docs/reference/devops-hygiene.md @@ -46,31 +46,11 @@ If a manual repair is needed to unblock the platform, the durable fix must be co ## 分布式敏捷流程 -“分布式敏捷”是 UniDesk 对 distributed agile field repair 的固定流程名。后续 issue、PR、指挥记录或用户反馈提到“分布式敏捷”时,默认指下面这套流程:先在真实分布式运行面快速探测和实验补丁,形成可复现的证据与复盘 issue,再把有效修复收敛为 Git/PR/CI/CD 的持久化交付,最后从原始用户入口复测。它允许快速现场学习,但不允许运行面改动变成隐藏部署真相。 +“分布式敏捷”是 UniDesk 对 distributed agile field repair 的固定流程名;通用 P1/P2/P3/P4 阶段、禁止行为和证据边界由 `$dad-dev` skill 维护,本参考不再重复展开。UniDesk 项目内只保留下面的特有约束:必须使用结构化 `trans`/UniDesk CLI 进入真实 provider、pod、host bridge 或 service port;运行面热补只能证明方向或临时恢复,不能成为隐藏部署真相;持久化完成必须回到 Git/PR/CI/CD 后原入口复测。 -Before classifying a failure as an external blocker, the operator must complete the field anti-misclassification check. This is P0 for model providers, API providers, hardware links, cross-platform bridges, CLI/trans/tran paths and frequently used tooling: +在模型 provider、API provider、硬件链路、跨平台 bridge、CLI/trans/tran 或高频工具链问题上,判定外部 blocker 前仍需完成 UniDesk 的防误判核查:确认当前 runtime config / Secret key presence / env / proxy / NO_PROXY / endpoint / args,使用实际目标运行面复现,并尽量与 UniDesk/HWLAB 成熟实现对照。用户反馈或新证据推翻 blocker 判断时,立即切回 `$dad-dev` 的现场修复闭环。 -1. Confirm the exact runtime configuration used by the failing path: committed source ref, deployed image or script revision, redacted Secret names and key presence, env/proxy/NO_PROXY shape, endpoint identity and command args. Do not infer these values from memory or from a different workspace. -2. Reproduce the symptom from the actual target provider, pod, host bridge or service port through UniDesk passthrough or the service entry that failed. A commander-machine-only check is supporting evidence, not classification evidence. -3. Compare with the mature local implementation when one exists. For Codex/model-provider work, inspect the current UniDesk/HWLAB stdio, forwarder, proxy, env-stripping and config-loading paths before concluding the provider itself is broken. -4. Run narrow one-variable experiments in the live target environment. Typical variables are explicit versus config-derived model, endpoint, proxy or NO_PROXY, env inheritance, secret mount shape, CLI version, protocol start parameters and request payload. Record the success case and the failure case with trace ids, run ids, job names, rollout objects or bounded logs. -5. Only call the condition an external blocker after the current runtime config has been verified, the minimal real-path probe still fails, a mature reference path or equivalent cross-check also fails, and the evidence rules out local adapter/config mistakes. - -If user feedback or fresh evidence contradicts an initial blocker claim, the operator must stop repeating the blocker narrative and switch to field repair mode immediately. The expected sequence is passthrough probing, single-variable live experiments, a bounded hotfix experiment when needed, a source PR, CI/CD rollout and re-test from the original entry point. The hotfix proves direction or restores a live path; it does not complete the task. - -The standard flow is: - -1. Probe the real runtime surface first. Use structured UniDesk passthrough, service health endpoints, trace/result polling, bounded logs, object metadata and user-entry requests to reproduce the symptom on the actual target environment. Prefer short single-step commands that return promptly and can be repeated. -2. Apply an experimental runtime patch only when it is needed to prove a fix direction. The patch must be narrow, named, reversible and scoped to the affected deployment, pod, ConfigMap, env key, script mount or file. It must not include secrets, broad filesystem rewrites, unmanaged image builds, destructive resets or unrelated cleanup. -3. Validate the runtime patch from the user or service entry that exposed the problem. Supporting internal checks are useful, but the decisive evidence should include the external URL, API route, trace id, operation id, rollout object, health metadata or other runtime identity that proves the real path changed. -4. Write a recap issue before treating the fix as complete. The issue must include reproduction steps, runtime evidence, root cause, exact experimental patch shape, rollback or cleanup notes, durable source changes needed, and post-CD validation criteria. Sensitive values stay redacted; copy-pastable secret or credential mutation commands do not belong in the recap. -5. Convert the working runtime patch into the owning repository. The formal fix must be committed, pushed, reviewed through PR when applicable, and validated by the smallest appropriate CI gate on an approved execution surface. Master server local checks remain prohibited for heavy gates. -6. Roll the fix through the standard CD path. CD must consume commit-pinned artifacts or desired-state manifests rather than the live hotfix. If publish and desired-state commits differ, the rollout target is the already published source commit, not a later documentation-only or desired-state commit. -7. Re-test after CD rollout from the original user or service entry. The final evidence must show the deployed commit/image/runtime metadata, the relevant health or trace result, and that the temporary runtime patch is gone or no longer active. - -This flow deliberately separates agility from persistence: runtime probing and experimental patches are allowed to shorten diagnosis, while Git, PR, CI/CD and post-rollout validation remain the only durable completion path. If a recurring field step is painful because of quoting, target routing, kubeconfig selection, output volume or missing helpers, improve the UniDesk passthrough tool and document the new helper instead of preserving another one-off command recipe. - -Full CI/CD, GitOps rollout, image build, hardware run, long trace replay and model-provider compatibility work must not be used as the inner loop. First prove the smallest real loop inside the target provider, Pod, host bridge or service port, then promote the proven change into source and the normal release path. For model-provider work, prefer official docs and mature protocol bridges over hand-written protocol translation; a local compatibility shim should stay thin and preserve provider cache/tool semantics. +如果某个现场步骤因为 quoting、route 定位、kubeconfig、输出体积或缺少 helper 而反复痛苦,优先改进 UniDesk passthrough / CLI 并在本文件的 `Distributed Command Passthrough` 或 `docs/reference/cli.md` 中记录稳定入口,不要沉淀一批一次性 shell 菜谱。 ## Distributed Command Passthrough diff --git a/docs/reference/g14.md b/docs/reference/g14.md index b36a9ab6..6570a6d5 100644 --- a/docs/reference/g14.md +++ b/docs/reference/g14.md @@ -100,24 +100,14 @@ Do not turn `v0.2` expansion governance into a stack of broad compatibility gate ## v0.2 Worktree + PR Workflow -`v0.2` source-of-truth changes must enter through a task-scoped worktree on a feature branch and then merge through a PR, not by direct commits to `v0.2`. The canonical sequence is: +`v0.2` source-of-truth changes must enter through a task-scoped worktree on a feature branch and then merge through a PR, not by direct commits to `v0.2`. The generic P2/P3/P4 flow is owned by `$dad-dev`; this section only fixes the G14/v0.2-specific source route, branch and lane: ```bash trans G14:/root/hwlab-v02 script -- 'git fetch origin v0.2 && git pull --ff-only origin v0.2 && git status --short --branch' -# 1. Create a task-scoped worktree from latest origin/v0.2 trans G14:/root/hwlab-v02 script -- 'git worktree add .worktree/ -b fix/issue- origin/v0.2' -# 2. Develop, commit, and push the feature branch from inside the worktree -trans G14:/root/hwlab-v02/.worktree/ script -- 'git push -u origin fix/issue-' -# 3. Open a PR through UniDesk CLI (no native gh, no curl) -bun scripts/cli.ts gh pr create --repo pikasTech/HWLAB \ - --head fix/issue- --base v0.2 --title "..." --body-file - -# 4. Merge through UniDesk CLI; do not bypass to native gh pr merge -bun scripts/cli.ts gh pr merge --repo pikasTech/HWLAB --squash -# 5. Pull the merged v0.2 head back into the fixed workspace -trans G14:/root/hwlab-v02 script -- 'git pull --ff-only origin v0.2' ``` -The fixed repo at `/root/hwlab-v02` is not a scratch area and must not carry parallel worktree state on the `v0.2` branch itself. All worktree branches should follow the `fix/issue-` naming so PR titles and merge commits stay scannable. +The fixed repo at `/root/hwlab-v02` is not a scratch area and must not carry parallel worktree state on the `v0.2` branch itself. All worktree branches should follow the `fix/issue-` naming so PR titles and merge commits stay scannable. GitHub PR writes, merge, rollout trigger and final original-entry validation follow `$dad-dev` plus the UniDesk CLI control rules in `AGENTS.md`. ### Recovery From a Direct Commit To v0.2 @@ -218,3 +208,51 @@ docker build --network host \ The backup proxy uses `HTTP_PROXY=http://127.0.0.1:11809`, `HTTPS_PROXY=http://127.0.0.1:11809` and `ALL_PROXY=socks5h://127.0.0.1:11808`. This proxy is not a replacement for UniDesk runtime egress. k3s workloads such as Code Queue must still use the cataloged `g14-provider-egress-proxy` Kubernetes Service and `g14-tcp-egress-gateway` for normal runtime access to PostgreSQL, OA Event Flow and external APIs. The node-local VPN proxy is allowed only for G14 host-side bootstrap, image build, cache prewarm or recovery steps, and those steps should record the proxy choice in issue or deployment evidence. + +## v0.2 device-pod cloud-api architecture + +`v0.2` device-pod integration is the cloud-api → executor → D601 Windows `device-host-cli.mjs` chain under `internal/cloud/access-control.ts`, `cmd/hwlab-device-pod/main.ts` and the host-side `F:\Work\ConStart\tools\device-host-cli.mjs`. PR #765 (selector cheat sheet + fail-fast) and PR #778 (output.text propagation + evidence selector + read-only sub-action `--reason` exemption) are the two anchor PRs; PR #779 tracks the still-open host-side ops work. Earlier work used raw `MUTATING_INTENTS.has(intent) && !reason` and a single-pass `textOr(output.text, …)` extractor; both are obsolete and must not be re-introduced. + +### Intent / sub-action / reason matrix + +`DEVICE_JOB_INTENTS` (cloud-api) enumerates the full supported surface; `MUTATING_INTENTS` is the strict subset whose default sub-action is mutating. Only `workspace.build` and `debug.download` carry a structured sub-action (`start` / `status` / `output` / `wait` / `cancel` / `evidence`) and are listed in `DEVICE_JOB_ACTIONABLE_INTENTS`; for those two, `_deviceJobRequiresReason(intent, args, reason)` returns `false` when `reason` is provided OR when `args.action` is in `DEVICE_JOB_READ_ONLY_SUB_ACTIONS`. Any other mutating intent (`workspace.apply-patch`, `workspace.put`, `debug.reset`, `io.uart.write`, `io.uart.jsonrpc`, `io.uart.read-after-launch-flash`, etc.) still always requires a non-empty `reason`. Adding a new actionable mutating intent requires extending both `MUTATING_INTENTS` and `DEVICE_JOB_ACTIONABLE_INTENTS` together; adding a new read-only sub-action requires only the `DEVICE_JOB_READ_ONLY_SUB_ACTIONS` set. + +The `evidence` sub-action on `workspace.evidence` / `debug.evidence` is a first-class intent, not a `workspace.build` sub-action. Code Agent sees `:workspace:/ build evidence [jobId]` and `:debug-probe download evidence [jobId]`; cloud-api maps to a new device-pod executor job, the executor maps to `deviceHostArgs = ["workspace", "evidence", kind, ...]`, and the host-side `device-host-cli.mjs` dispatches via `if (command === "evidence")` at the top level of `main()` (not nested under `if (command === "build")`). `workspace.evidence kind=build` → `keil-build` job; `debug.evidence kind=download` → `keil-download` job; the kind sub-arg must be `build` / `download` and the optional jobId selects a specific past job. + +### Output text propagation chain + +`body.output.text` flows through three layers in order; each layer tries more fields and only falls back when earlier sources are empty: + +1. **host `device-host-cli.mjs`** returns a JSON envelope that already contains `stdout` / `stderr` / `summary` / `logTail` / `buildSummary` for build/download ops; `workspace.ls` / `workspace.cat` / `workspace.rg` are inline and include a JSON body. +2. **executor `cmd/hwlab-device-pod/main.ts` `gatewayDispatchText(result, dispatch)`** walks `result.stdout` → `result.stderr` → `dispatch.stdout` → `dispatch.stderr` → `result.evidence.{text,logTail,summary}` → `dispatch.message` (only when `dispatchStatus === "completed"`) → `dispatch.summary` → `result.summary` → `dispatch.buildSummary` → `result.text` → `JSON.stringify(result)`. The executor stores this as `job.output` and exposes it via `boundedOutput()` which clips at `DEVICE_JOB_OUTPUT_MAX_BYTES` (12000) and drops `executor` / nested `output` when truncated. +3. **cloud-api `executorOutputPayload(body, httpStatus)`** wraps what the executor sent and exposes `body.text` / `body.output` / `body.bytes` / `body.truncation` to the `/v1/device-pods/{id}/jobs/{jobId}/output` endpoint. `text` is `firstString(body?.text, output.text, nestedOutput.text, output.summary, nestedOutput.summary, evidence.text, evidence.logTail, evidence.summary)`; the matched key is recorded by caller convention. `executor` payload stays on the response so callers can read `dispatch.exitCode` / `dispatch.message` / `dispatch.stdout` even when text is empty. + +The `evidence.*` and `*summary` lookups exist so a dispatcher that already includes host `logTail` / `buildSummary` becomes visible without a separate `bootsharp` re-run on the Code Agent side. The `summary` lookups also keep error messages (`dispatch.message`) in the response even when `dispatchStatus` is not `completed`; this is the reason `body.error.message` always has something to show for failed host dispatches. + +### Cloud-api vs host-side boundary + +`/root/hwlab-v02/skills/device-pod-cli/assets/device-host-cli.mjs` is the v0.2-shipped copy of the host-side CLI. The actual hardware host runs a separate `F:\Work\ConStart\tools\device-host-cli.mjs` that is **not** a deployment of the v0.2 repo; it is a D601 ops-side copy that must be synced manually when the v0.2 repo changes host-side behavior. The two-step contract is: + +- v0.2 cloud-api / executor changes are valid once `PipelineRun Succeeded` + `git mirror flush` complete; runtime revision is `commit.id` from `/health/live` and source commit can be forced to match via the next `trigger-current`. +- v0.2 host-side `device-host-cli.mjs` changes are NOT visible until someone replaces `F:\Work\ConStart\tools\device-host-cli.mjs` on the D601 Windows host; cloud-api `body.text` will faithfully surface the "unsupported command" JSON error from the stale host binary, which proves the cloud-api propagation chain works but the host side is stale. + +A live `workspace.evidence` / `debug.evidence` / `download evidence` selector that returns the host `logTail` end-to-end therefore requires both (a) the v0.2 PR merged and rolled, and (b) the D601 host binary replaced; missing either half is a known gap tracked in #779. + +### v0.2 device-pod closeout checks + +Device-pod fixes still follow `$dad-dev` and the `## v0.2 Worktree + PR Workflow` route above. The device-pod-specific closeout is the three-layer runtime matrix below; keep these checks because they prove the cloud-api -> executor -> D601 host chain, while generic PR/CI/CD and worktree mechanics stay in `$dad-dev`. + +```bash +trans G14:/root/hwlab-v02/.worktree/ script -- 'cd tools && bun test device-pod-cli.test.ts' +trans G14:/root/hwlab-v02/.worktree/ script -- 'cd cmd/hwlab-device-pod && bun test main.test.ts' +trans G14:/root/hwlab-v02/.worktree/ script -- 'cd internal/cloud && bun test access-control.test.ts' +trans G14:/root/hwlab-v02/.worktree/ script -- 'node --check skills/device-pod-cli/assets/device-host-cli.mjs' +``` + +Treat `access-control.test.ts` workbench failures as pre-existing on the v0.2 base unless the new test list explicitly covers them. After PR merge and `trigger-current --lane v02 --confirm`, the live `http://74.48.78.17:19667/` CLI 验收 must hit all three layers: + +1. `body.output.text` non-empty for at least one happy-path intent (`workspace.ls` / `workspace.cat` are the cheapest ones to verify propagation without needing a real D601 build). +2. `workspace.evidence kind=build` / `kind=download` accepted by cloud-api, dispatched to executor, executor `blocker === null` and `job.reason === ""`. +3. ` action=status` accepted without `--reason` while the same intent with `action=start` is still rejected with `device_job_reason_required`. + +There is no separate `device-pod` doc; this section is the single authoritative reference for the architecture, and the AGENTS.md index points to it. diff --git a/docs/reference/microservices.md b/docs/reference/microservices.md index 79bbb95b..2e563ab4 100644 --- a/docs/reference/microservices.md +++ b/docs/reference/microservices.md @@ -60,6 +60,10 @@ Code Queue runner 也是分布式开发执行面。runner 镜像必须内置 `tr - 代理路径:只允许 `/api/` 前缀;允许方法为 `GET`、`HEAD`、`POST`、`DELETE`,用于保持 Todo Note 原有清单创建/删除、任务增删改、提醒、展开/收起、移动、撤销/重做等功能。 - **写操作端点形态(2026-06-01 复盘 [#188](https://github.com/pikasTech/unidesk/issues/188) 固化)**:Todo Note 不走 REST 集合(如 `/api/instances/:id/todos`),所有 task 写都走 **action 队列**模式 `POST /api/instances/:id/actions` + body `{action: {type, ...}}`。已注册 action type:`addTodo` / `updateTodoTitle` / `toggleTodoCompleted` / `toggleTodoExpanded` / `setAllTodosExpanded` / `moveTodo` / `deleteTodo` / `renameInstance` / `setTodoReminder`。其他写端点:`POST /api/instances`(body `{name}` 新建清单)、`DELETE /api/instances/:id`、`POST /api/instances/:id/undo`、`POST /api/instances/:id/redo`。 - **`TODO_NOTE_BACKEND_ONLY=1` 真实语义**:仅关闭 Todo Note 自带 Vite 前端 SPA,不阻挡任何已注册 API 路由(包括所有 POST/DELETE 写)。`/api/health` 暴露的 `backendOnly` 字段是观察用,不是读路径开关。看到 `404 {"error":"Todo Note is running in backend-only mode"}` 时的第一反应是路径写错(用了不存在的 REST 端点被 catch-all 兜底),不是写被 mode 锁。CLI 写操作范式见 `docs/reference/cli.md` 的 `microservice proxy` 段。 + +- **Catch-all 误导文案 + CLI 改写 / 预检(issue #198 interim)**:上游 `gitee.com/Lyon1998/todo_note` 的 catch-all 把所有未注册路径都回 `404 {"ok":false,"error":"Todo Note is running in backend-only mode"}`,秘书/agent 看到这条第一反应容易误判成 "写被 mode 锁了"。UniDesk CLI 侧在 `microservice proxy todo-note` 检测到该特征 body 时会做就地改写,输出结构化诊断(`writableApiEndpoints`、`actionTypes`、`backendOnly: true`、`method`、`path`、`issueReference`、`upstreamBody` 完整保留原始误导文案用于审计),并把 `bodyRewritten: true` 显式返回;上游 catch-all 修复 PR 合并后改写自动退化为 no-op,CLI 不再覆盖。 + - 同样在 CLI 侧提供 `--check-path` 预检:未命中时返回同源结构化诊断并**不调用 upstream**;命中时返回 `ok: true, matched: true, endpoint: {...}`。当前 `--check-path` 只支持 `service=todo-note`,对其他服务返回结构化 `unsupported` 错误。命令样例见 `docs/reference/cli.md` 的 `microservice proxy` 段。 + - 验证门禁走 `scripts/src/e2e.ts` 的 `microservice:todo-note-route-diagnostic`(覆盖错路径 rewrite、命中 / 未命中 check-path、非 todo-note 服务的 unsupported)。该门禁是 CLI 侧 interim 自检,不替代上游 catch-all 修复 PR;上游落地后由 `bun scripts/cli.ts server rebuild todo-note` 拉新 commit 镜像,并继续用 `microservice health todo-note` + `microservice proxy todo-note /api/instances//actions` 验证。 - UniDesk 前端:`用户服务 / Todo Note` React 页面负责展示清单列表、树形任务、筛选、提醒、拖放/上移下移、撤销/重做、字号控制和显式原始 JSON 按钮。 Todo Note 在 UniDesk 语境中按纯后端服务管理:不得继续公开 Todo Note 自身 Vite/Web 前端,也不得把 `4211` 映射为公网端口。浏览器只能通过 UniDesk frontend 的 `/api/microservices/todo-note/...` 同源代理访问 Todo Note 后端。标准 artifact consumer 路径为 `bun scripts/cli.ts deploy apply --env dev|prod --service todo-note`;由于 Todo Note 源码仍在外部 Gitee 仓库,D601 registry 中必须先已有 `127.0.0.1:5000/unidesk/todo-note:`。Compose 在 recreate 时注入 `UNIDESK_TODO_NOTE_DEPLOY_*`,artifact consumer 的健康探针读取 `/api/health` 并合成 `deploy.commit` 和 `deploy.requestedCommit` 供强校验。 diff --git a/scripts/src/e2e.ts b/scripts/src/e2e.ts index d6834134..d650d76f 100644 --- a/scripts/src/e2e.ts +++ b/scripts/src/e2e.ts @@ -96,6 +96,7 @@ const SERVICE_CHECK_NAMES = [ "microservice:todo-note-health", "microservice:todo-note-migrated-data", "microservice:todo-note-write-path", + "microservice:todo-note-route-diagnostic", "microservice:code-queue-status", "microservice:code-queue-health", "microservice:code-queue-workdirs", @@ -1761,6 +1762,68 @@ function runPsql(config: UniDeskConfig, sql: string): { ok: boolean; stdout: str return { ok: result.exitCode === 0, stdout: result.stdout.trim(), stderr: result.stderr.trim(), exitCode: result.exitCode }; } +function runTodoNoteCliProbe(args: string[]): { ok: boolean; exitCode: number | null; payload: unknown; stderr: string } { + const result = runCommand(["bun", "scripts/cli.ts", "microservice", "proxy", "todo-note", ...args], repoRoot, { timeoutMs: 30_000 }); + let payload: unknown = null; + if (result.stdout.trim().length > 0) { + try { + payload = JSON.parse(result.stdout.trim()) as unknown; + } catch { + payload = null; + } + } + return { ok: result.exitCode === 0, exitCode: result.exitCode, payload, stderr: result.stderr.trim() }; +} + +function runTodoNoteRouteDiagnosticChecks(): { ok: boolean; detail: unknown } { + const issues: string[] = []; + // 1) Bad path: CLI must rewrite the misleading 404 to the structured diagnostic. + const badPathRewrite = runTodoNoteCliProbe(["/api/instances/instance_probe_bad/todos", "--method", "POST", "--body-json", JSON.stringify({ title: "e2e-route-diagnostic" })]); + const badPathBody = (badPathRewrite.payload as { data?: { body?: Record } } | null)?.data?.body ?? null; + const rewriteOk = ( + badPathRewrite.exitCode === 1 + && badPathBody !== null + && badPathBody.error === "Todo Note route not found" + && badPathBody.backendOnly === true + && badPathBody.method === "POST" + && badPathBody.path === "/api/instances/instance_probe_bad/todos" + && Array.isArray(badPathBody.writableApiEndpoints) + && (badPathBody.writableApiEndpoints as Array<{ path?: string }>).some((endpoint) => endpoint.path === "/api/instances/:instanceId/actions") + && Array.isArray(badPathBody.actionTypes) + && (badPathBody.actionTypes as string[]).includes("addTodo") + && (badPathRewrite.payload as { data?: { bodyRewritten?: boolean } } | null)?.data?.bodyRewritten === true + && (badPathRewrite.payload as { data?: { upstreamBody?: { error?: string } } } | null)?.data?.upstreamBody?.error === "Todo Note is running in backend-only mode" + ); + if (!rewriteOk) issues.push("bad-path-rewrite"); + // 2) --check-path matched: POST /api/instances must succeed and return matched=true. + const checkPathMatched = runTodoNoteCliProbe(["/api/instances", "--method", "POST", "--check-path"]); + const checkPathMatchedData = (checkPathMatched.payload as { data?: { matched?: boolean; endpoint?: { path?: string } } } | null)?.data ?? null; + const matchOk = checkPathMatched.exitCode === 0 && checkPathMatchedData?.matched === true && checkPathMatchedData?.endpoint?.path === "/api/instances"; + if (!matchOk) issues.push("check-path-match"); + // 3) --check-path mismatched: must return the structured diagnostic and skip the upstream call. + const checkPathMissed = runTodoNoteCliProbe(["/api/todos", "--method", "POST", "--check-path"]); + const checkPathMissedData = (checkPathMissed.payload as { data?: { checkPath?: boolean; error?: string; path?: string } } | null)?.data ?? null; + const missOk = checkPathMissed.exitCode === 1 && checkPathMissedData?.checkPath === true && checkPathMissedData?.error === "Todo Note route not found" && checkPathMissedData?.path === "/api/todos"; + if (!missOk) issues.push("check-path-miss"); + // 4) --check-path on a non-todo-note service must return a structured unsupported error. + const unsupportedProbe = runCommand(["bun", "scripts/cli.ts", "microservice", "proxy", "code-queue", "/api/foo", "--method", "GET", "--check-path"], repoRoot, { timeoutMs: 30_000 }); + let unsupportedPayload: unknown = null; + try { unsupportedPayload = JSON.parse(unsupportedProbe.stdout.trim()) as unknown; } catch { unsupportedPayload = null; } + const unsupportedData = (unsupportedPayload as { data?: { error?: string; supportedServices?: string[] } } | null)?.data ?? null; + const unsupportedOk = unsupportedProbe.exitCode === 1 && typeof unsupportedData?.error === "string" && unsupportedData.error.includes("--check-path currently only supports service=todo-note") && Array.isArray(unsupportedData.supportedServices) && unsupportedData.supportedServices.includes("todo-note"); + if (!unsupportedOk) issues.push("check-path-unsupported"); + return { + ok: issues.length === 0, + detail: { + issues, + badPathRewrite: { exitCode: badPathRewrite.exitCode, body: badPathBody }, + checkPathMatched, + checkPathMissed: { exitCode: checkPathMissed.exitCode, data: checkPathMissedData }, + unsupportedProbe: { exitCode: unsupportedProbe.exitCode, data: unsupportedData }, + }, + }; +} + function dockerCoreJson(path: string, init?: { method?: string; body?: unknown }): unknown { const method = init?.method ?? "GET"; const body = init?.body === undefined ? "" : JSON.stringify(init.body); @@ -2474,6 +2537,10 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2 addSelectedCheck(checks, options, "microservice:todo-note-health", (todoNoteHealth as { ok?: boolean; body?: { ok?: boolean; storage?: string } }).ok === true && (todoNoteHealth as { body?: { ok?: boolean; storage?: string } }).body?.ok === true && (todoNoteHealth as { body?: { storage?: string } }).body?.storage === "postgres", todoNoteHealth); addSelectedCheck(checks, options, "microservice:todo-note-migrated-data", (todoNoteInstances as { ok?: boolean }).ok === true && todoNoteRows.length >= 5 && ["CONSTAR", "大论文", "找工作", "小论文", "事务"].every((name) => todoNoteNames.includes(name)) && todoNoteRows.reduce((sum, row) => sum + Number(row.todoCount ?? 0), 0) >= 100, { todoNoteInstances }); addSelectedCheck(checks, options, "microservice:todo-note-write-path", (todoNoteCreate as { ok?: boolean }).ok === true && (todoNoteAdd as { ok?: boolean }).ok === true && (todoNoteToggle as { ok?: boolean }).ok === true && (todoNoteUndo as { ok?: boolean }).ok === true && (todoNoteDelete as { ok?: boolean }).ok === true, { todoNoteCreate, todoNoteAdd, todoNoteToggle, todoNoteUndo, todoNoteDelete }); + // Issue #198: assert the CLI-side rewrite of the Todo Note catch-all misleading 404. + // When the upstream fix lands, this gate will pass through the upstream body instead of the rewritten one — keep the assertion focused on the CLI-side shape (the upstream body is still preserved under upstreamBody so a future flip is auditable). + const todoNoteRouteDiagnostic = runTodoNoteRouteDiagnosticChecks(); + addSelectedCheck(checks, options, "microservice:todo-note-route-diagnostic", todoNoteRouteDiagnostic.ok, todoNoteRouteDiagnostic.detail); addSelectedCheck(checks, options, "microservice:oa-event-flow-status", (oaEventFlowStatus as { ok?: boolean }).ok === true && (oaEventFlowStatus as { body?: { microservice?: { id?: string; providerId?: string } } }).body?.microservice?.providerId === config.providerGateway.id, oaEventFlowStatus); addSelectedCheck(checks, options, "microservice:oa-event-flow-health", (oaEventFlowHealth as { ok?: boolean }).ok === true && oaEventFlowHealthBody?.ok === true && oaEventFlowHealthBody.service === "oa-event-flow" && oaEventFlowHealthBody.databaseReady === true, oaEventFlowHealth); addSelectedCheck(checks, options, "microservice:oa-event-flow-diagnostics", (oaEventFlowDiagnostics as { ok?: boolean }).ok === true && oaEventFlowDiagnosticsBody?.ok === true && oaEventFlowDiagnosticsBody.service === "oa-event-flow" && oaEventFlowDiagnosticsBody.databaseReady === true && Number.isFinite(oaEventFlowDiagnosticsBody.eventCount) && Array.isArray(oaEventFlowDiagnosticsBody.eventTypes), oaEventFlowDiagnostics); diff --git a/scripts/src/help.ts b/scripts/src/help.ts index fd826166..b99ade0b 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -38,7 +38,7 @@ export function rootHelp(): unknown { { command: "microservice list", description: "List UniDesk-managed user services and their provider/runtime mapping." }, { command: "microservice status ", description: "Show one user service config, repository reference, backend mapping, and runtime status." }, { command: "microservice health [--compact|--raw|--full]", description: "Probe one user service through backend-core -> provider-gateway HTTP proxy; default output is compact, and code-queue uses a commander-safe liveness summary unless raw/full is requested." }, - { command: "microservice proxy [--method GET|POST|PUT|PATCH|DELETE] [--body-json JSON|--body-file path|--body-stdin] [--raw] [--max-body-bytes N]", description: "Access a private user-service backend path through the same frontend-only proxy used by WebUI; JSON request bodies are supported for controlled write/debug endpoints." }, + { command: "microservice proxy [--method GET|POST|PUT|PATCH|DELETE] [--body-json JSON|--body-file path|--body-stdin] [--check-path] [--raw] [--max-body-bytes N]", description: "Access a private user-service backend path through the same frontend-only proxy used by WebUI; JSON request bodies are supported for controlled write/debug endpoints; --check-path (currently todo-note only) validates the path/method against the registered CLI-side endpoint catalog without contacting upstream." }, { command: "microservice diagnostics [--full|--raw]", description: "Split k3sctl-managed proxy health into a compact summary by default; use --full/--raw for complete evidence." }, { command: "microservice tunnel-self-test ", description: "Trigger an expected provider HTTP tunnel failure and verify requestId/stage diagnostics are returned." }, { command: "decision upload [--title text] [--type meeting|decision|goal|external_goal|internal_goal|blocker|debt|experiment] [--level|--priority G0|G1|G2|G3|P0|P1|P2|P3|none] [--doc-no DC-...] [--doc-type DCSN|GOAL|PLAN|RPRT|ACTN|ISSU|RETR|RQST|RESP|MINS] [--doc-priority P0|P1|P2|P3] [--signer text] [--issued-at ISO]", description: "Upload a meeting note or decision/requirement record through backend-core -> decision-center user-service proxy." }, @@ -233,7 +233,7 @@ function microserviceHelp(): unknown { "bun scripts/cli.ts microservice health [--compact|--full|--raw]", "bun scripts/cli.ts microservice diagnostics [--full|--raw]", "bun scripts/cli.ts microservice tunnel-self-test ", - "bun scripts/cli.ts microservice proxy [--method GET|POST|PUT|PATCH|DELETE] [--body-json JSON|--body-file path|--body-stdin] [--raw] [--full] [--max-body-bytes N]", + "bun scripts/cli.ts microservice proxy [--method GET|POST|PUT|PATCH|DELETE] [--body-json JSON|--body-file path|--body-stdin] [--check-path] [--raw] [--full] [--max-body-bytes N]", ], description: "Access UniDesk-managed user services through the same backend-core/provider proxy path used by the WebUI.", }; diff --git a/scripts/src/microservices.ts b/scripts/src/microservices.ts index 7c603a28..411d45a6 100644 --- a/scripts/src/microservices.ts +++ b/scripts/src/microservices.ts @@ -4,6 +4,138 @@ import { runCommand } from "./command"; import { type UniDeskConfig, repoRoot } from "./config"; import { jsonByteLength, previewJson } from "./preview"; +// Todo Note misleading-404 rewrite (issue #198) — CLI-side interim workaround +// for the upstream todo_note catch-all handler. The upstream repo +// (https://gitee.com/Lyon1998/todo_note) wraps every unregistered path in +// `404 {"ok":false,"error":"Todo Note is running in backend-only mode"}`, +// which makes agents and humans misdiagnose a path typo as a write lock. +// When the upstream fix lands, this rewrite becomes a no-op: the upstream +// body will not match `isTodoNoteMisleadingRouteNotFoundBody` anymore and +// the CLI will pass the upstream response through unchanged. +// +// Source of truth for the writable endpoint list below is +// docs/reference/microservices.md "Todo Note On Main Server" and +// apps/server/src/server.ts in the upstream todo_note repo. If the +// upstream registered routes change, update TODO_NOTE_WRITABLE_ENDPOINTS +// and TODO_NOTE_ACTION_TYPES in lock-step. + +interface TodoNoteEndpoint { + method: string; + path: string; + hint: string; +} + +export const TODO_NOTE_ACTION_TYPES = [ + "addTodo", + "updateTodoTitle", + "toggleTodoCompleted", + "toggleTodoExpanded", + "setAllTodosExpanded", + "moveTodo", + "deleteTodo", + "renameInstance", + "setTodoReminder", +] as const; + +export const TODO_NOTE_WRITABLE_ENDPOINTS: readonly TodoNoteEndpoint[] = [ + { method: "POST", path: "/api/instances", hint: "Create a new todo list; body {name}." }, + { method: "DELETE", path: "/api/instances/:instanceId", hint: "Delete a todo list." }, + { + method: "POST", + path: "/api/instances/:instanceId/actions", + hint: "Apply a typed action; body {action: {type, ...}}. Use the action queue pattern, not REST collection paths like /api/instances/:id/todos.", + }, + { method: "POST", path: "/api/instances/:instanceId/undo", hint: "Undo the last applied action." }, + { method: "POST", path: "/api/instances/:instanceId/redo", hint: "Redo the last undone action." }, +]; + +function isTodoNoteMisleadingRouteNotFoundBody(body: unknown): boolean { + if (typeof body !== "object" || body === null || Array.isArray(body)) return false; + const record = body as Record; + return record.error === "Todo Note is running in backend-only mode" && record.ok === false; +} + +function splitPathQuery(path: string): string { + const questionIndex = path.indexOf("?"); + return questionIndex === -1 ? path : path.slice(0, questionIndex); +} + +export function matchTodoNoteEndpoint(method: string, path: string): TodoNoteEndpoint | null { + const upperMethod = method.toUpperCase(); + const targetSegments = splitPathQuery(path).split("/").filter(Boolean); + for (const endpoint of TODO_NOTE_WRITABLE_ENDPOINTS) { + if (endpoint.method !== upperMethod) continue; + const patternSegments = endpoint.path.split("/").filter(Boolean); + if (patternSegments.length !== targetSegments.length) continue; + let matched = true; + for (let i = 0; i < patternSegments.length; i++) { + const pattern = patternSegments[i]; + const actual = targetSegments[i]; + if (pattern.startsWith(":")) continue; + if (pattern !== actual) { matched = false; break; } + } + if (matched) return endpoint; + } + return null; +} + +export function buildTodoNoteRouteNotFoundDiagnostic(method: string, path: string): Record { + return { + ok: false, + error: "Todo Note route not found", + backendOnly: true, + method, + path, + writableApiEndpoints: TODO_NOTE_WRITABLE_ENDPOINTS.map((endpoint) => ({ method: endpoint.method, path: endpoint.path, hint: endpoint.hint })), + actionTypes: [...TODO_NOTE_ACTION_TYPES], + hint: "Write operations use the action queue pattern: POST /api/instances/:instanceId/actions with body {action: {type, ...}}. See docs/reference/microservices.md (Todo Note On Main Server) and the upstream apps/server/src/server.ts:97-126 for the registered action types. The CLI rewrote this 404 because the upstream catch-all body was misleading (issue #198).", + issueReference: "pikasTech/unidesk#198", + }; +} + +export function rewriteTodoNoteMisleadingRouteNotFound(serviceId: string, method: string, path: string, response: unknown): unknown { + if (serviceId !== "todo-note") return response; + if (typeof response !== "object" || response === null || Array.isArray(response)) return response; + const record = response as Record; + if (record.status !== 404) return response; + if (!isTodoNoteMisleadingRouteNotFoundBody(record.body)) return response; + const diagnostic = buildTodoNoteRouteNotFoundDiagnostic(method, path); + return { + ...record, + body: diagnostic, + bodyRewritten: true, + rewriteReason: "Todo Note catch-all 404 body was misleading; CLI wrapped it with a structured route diagnostic. See docs/reference/cli.md and pikasTech/unidesk issue #198 for context.", + upstreamBody: record.body, + }; +} + +export function runTodoNoteCheckPath(method: string, path: string): Record { + const matched = matchTodoNoteEndpoint(method, path); + if (matched) { + return { + ok: true, + service: "todo-note", + method, + path, + matched: true, + endpoint: { method: matched.method, path: matched.path, hint: matched.hint }, + hint: "Path and method match a registered Todo Note write endpoint; safe to send the request.", + }; + } + return { + ok: false, + command: `microservice proxy todo-note ${path} --method ${method} --check-path`, + data: { + ok: false, + status: 404, + method, + path, + checkPath: true, + ...buildTodoNoteRouteNotFoundDiagnostic(method, path), + }, + }; +} + function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'`; } @@ -840,9 +972,28 @@ export async function runMicroserviceCommand(_config: UniDeskConfig, args: strin const body = requestBodyOption(args); const full = hasFlag(args, "--full"); const raw = hasFlag(args, "--raw"); + const checkPath = hasFlag(args, "--check-path"); + const method = methodOption(args, body !== undefined); + if (checkPath) { + if (id !== "todo-note") { + return { + ok: false, + command: `microservice proxy ${id} ${path} --method ${method} --check-path`, + data: { + ok: false, + error: `--check-path currently only supports service=todo-note; got ${id}`, + supportedServices: ["todo-note"], + hint: "Open docs/reference/cli.md (microservice proxy --check-path) for the supported endpoint catalog. Other services do not yet ship a CLI-side writable endpoint list; file a follow-up if you need one.", + }, + }; + } + return runTodoNoteCheckPath(method, path); + } const maxBodyBytes = full ? numberOption(args, "--max-body-bytes", 5_000_000) : cappedNumberOption(args, "--max-body-bytes", raw ? 120_000 : 60_000, 500_000); const maxResponseBytes = full ? Math.min(Math.max(maxBodyBytes, 120_000), 5_000_000) : Math.min(Math.max(maxBodyBytes * 3, 240_000), 1_500_000); - return summarizeMicroserviceProxyResponse(coreInternalFetch(`/api/microservices/${encodeId(id)}/proxy${path}`, { method: methodOption(args, body !== undefined), body, maxResponseBytes }), args); + const fetched = coreInternalFetch(`/api/microservices/${encodeId(id)}/proxy${path}`, { method, body, maxResponseBytes }); + const rewritten = rewriteTodoNoteMisleadingRouteNotFound(id, method, path, fetched); + return summarizeMicroserviceProxyResponse(rewritten, args); } throw new Error("microservice command must be one of: list, status, health, diagnostics, tunnel-self-test, proxy"); }