Merge remote-tracking branch 'origin/master' into fix/worktree-independent-state

# Conflicts:
#	docs/reference/nc01.md
This commit is contained in:
Codex
2026-07-10 05:09:34 +02:00
12 changed files with 145 additions and 109 deletions
+23 -50
View File
@@ -2,80 +2,53 @@
UniDesk 以主 server 为统一入口。本文件只做自动加载的顶级索引;长期规则、流程和判定标准必须放到 skill 或 `docs/reference/*.md`
## P0: 文件体积与脚本分流
- P0: 任何源码/CLI 文件超过 3000 行必须先按职责差分拆到 2000 行以下再继续,禁止卡在 3000 行边界反复触发。
- P0: 禁止把 shell/Node/Python 等脚本作为大段字符串内嵌;脚本必须放入原生后缀文件(如 `.sh`/`.mjs`/`.py`)并从文件加载。
## P0: 主 worktree 同步提交第一原则
- P0: 发现固定主/目标 worktree 落后 remote 时,必须立刻先 `git stash push -u` 保存脏改(如有,含 untracked),再 `git pull --ff-only` 快进到最新 remote,然后 `git stash apply` 并按语义合并;主工作区恢复出的并行改动必须先直接提交,再继续后续任务;禁止用 reset、drop 或覆盖式 checkout 丢弃并行改动。
## P0: NC01 k8s server 固定运行面
## P0: 损害预防与文档治理
- P0: NC01 用 k8s 部署 UniDesk server/backend-core 是固定原则;恢复、验证和 `trans` 必须以 `config/unidesk-host-k8s.yaml` 与 k8s `unidesk` namespace 为准,禁止把 Docker Compose/`unidesk-backend-core` 容器当 NC01 运行面;healthy 后非明确要求不得重建/重启/替换
## P0: AGENTS.md 体积与分流
- P0: 本地和远端 `AGENTS.md` 硬上限是 10 KiB;超过或即将超过时,必须先分流到对应 skill 或 `docs/reference/*.md`,再在本文件保留一句话索引
- P0: `AGENTS.md` 触发 dump、输出爆炸或上下文膨胀时,修文件结构,不提高 dump 阈值,不继续追加规则。长期入口见 `docs/reference/devops-hygiene.md``docs/reference/cli.md`
- P0: 新增、修改或蒸馏长期参考文档必须先遵循 `$docs-spec``AGENTS.md` 只保留入口,不承载长文、日志、JSON、trace dump 或一次性排障过程。
## P0: 工作区、语义合并与并行修改
- P0: 本机可能有多个并行开发任务;发现未预期本地修改时默认保留,不得自动 `git reset``git checkout --`、删除或回滚他人修改。
- P0: 当前目录的主工作区(非 `.worktree/` 路径)必须固定停留在 `master` 分支;禁止把主工作区切到其他分支,非 `master` 开发必须使用独立 `.worktree/<task>`
- P0: 主 worktree 同步冲突处理以文件开头的“主 worktree 同步提交第一原则”为最高入口;语义合并时保留并行改动的有效意图,不回滚到旧运行面或旧配置路径。
- P0: 固定主 repo 通常只做 source-truth 预检、fetch、status、快进同步和 worktree anchor;源码、部署脚本、issue closeout 和高风险 dad-dev 工作使用独立 `.worktree/<task>`。简单 YAML-only 版本/数值变更、文档/skill/长期参考轻量修改可直接在当前主 worktree 处理;复杂配置/运行面重构仍用独立 worktree。细则见 `docs/reference/devops-hygiene.md`
- P0: 清理任务 worktree 前必须按语义确认已合入 `master`:工作区 clean,相关提交是 `origin/master` 祖先或被 `--cherry-pick` 判定为等价吸收,必要时检查关键文件 diff/PR merge commit;不得仅因分支落后就删除。仍有未合入语义、未提交文件或不确定归属时,先合并/提交/推送到 `master` 或记录阻塞,不清理 worktree。
- P0: UniDesk 工具链自身修复必须先合并到 `master`,再把实际执行工具命令的 checkout 更新到包含该 merge 的最新 `master` 后继续使用;若当前 checkout 有并行脏改或非 master 分支导致不能安全 fast-forward,禁止 reset/stash/checkout 清理,改用干净 `origin/master` worktree 运行工具并说明原因。
- P0: Master server 禁止作为通用构建机;不得在 master server 执行 Docker/Rust/Go/前端等高 CPU 构建。受控例外见 `docs/reference/dev-environment.md`
- P0: 源码/CLI 超过 3000 行必须按职责拆到 2000 行以下;大段脚本放入 `.sh`/`.mjs`/`.py` 等原生文件。细则见 `docs/reference/devops-hygiene.md`
- P0: `/root/unidesk` 主 worktree 固定停留在 `master`;未预期修改默认属于并行任务,禁止 reset/checkout/删除,非 `master` 开发、语义合并和 worktree 清理由 `docs/reference/devops-hygiene.md` 约束。
- P0: Master server 禁止通用 Docker/Rust/Go/前端重型构建;执行面和受控例外见 `docs/reference/dev-environment.md`
- P0: `AGENTS.md` 硬上限 10 KiB,只保留损害预防规则和索引;文档修改先加载 `$docs-spec`,细则见 `docs/reference/agent-instruction-hygiene.md``docs/reference/docs-governance.md`
- P0: NC01 UniDesk server/backend-core 只认 `config/unidesk-host-k8s.yaml` 选中的 k8s `unidesk` 运行面;禁止用 Compose 替代,healthy 后非明确要求不扰动。细则见 `docs/reference/nc01.md`
## P0: 最新要求、可见性与验证
- P0: 用户最新明确要求优先于旧测试、旧门禁、旧预检、旧断言和旧 guard;阻碍最新目标的旧入口应删除,不做兼容保留。
- P0: 发现验证所需依赖缺失时必须直接按仓库声明和锁文件使用对应包管理器安装,然后继续原验证;不得以环境缺依赖为由跳过、降级或结束验证,除非安装会变更锁文件或引入未声明依赖,此时先按仓库规范补齐依赖声明
- P0: 可见性问题优先修复;状态、耗时、失败原因、trace、命令结果或关键证据不可见时,先补 CLI/日志/状态输出再继续。细则见 `docs/reference/observability.md`
- P0: CLI 改动默认不做单元测试、合同测试或新增测试脚本;除非用户明确要求,最多做语法检查和必要命令形态确认。
- P0: 不得用裸 `tsc`/`bun --bun tsc` 当语法验证;它会长时间卡住。CLI/TS 轻量验证用 `bun --check <具体文件>``bun scripts/cli.ts check` 默认 syntax transpile 和必要命令形态确认;细则见 `docs/reference/cli.md`
- P0: CLI 默认输出应是 Kubernetes 风格的简洁表格、短摘要和 drill-down 命令;JSON 只用于 `--json``--raw``--full``-o json` 或机器消费。超长输出 dump 是兜底,不是长期交互入口。细则见 `docs/reference/cli.md`
- P0: 缺失的已声明验证依赖按仓库包管理器和锁文件安装后继续;状态、耗时、失败或关键证据不可见时先修可见性。细则见 `docs/reference/devops-hygiene.md``docs/reference/observability.md`
- P0: CLI 改动、轻量语法验证和 compact text/显式 JSON 输出契约统一遵循 `docs/reference/cli.md`;默认不新增测试或运行裸 `tsc`
## P0: 配置、Secret 与 YAML-first
- P0: UniDesk 自有配置优先使用 YAML;节点/lane、namespace、endpoint、版本、容量、退避窗口、Secret sourceRef 等可调项必须从 YAML 进入受控 CLI,不得写成隐藏默认。细则见 `docs/reference/yaml-first-ops.md``$unidesk-ymalops`
- P0: node/lane 必须作为 YAML 的变量值、selector、target key 或 CLI 参数参与渲染;禁止把具体 node/lane 写入 YAML 文件名、函数名、helper/module 名、长期 schema 名或隐藏默认
- P0: Secret、API key、JWT/encryption key、`DATABASE_URL` 等凭据只能通过 YAML 声明 sourceRef/targetKey 和受控 CLI 下发;输出只披露对象名、key 名、presence、fingerprint 和摘要,不得打印完整凭据
- P0: Credentials: `docs/reference/nc01.md`.
- P0: `hyueapi.com` / `.hyueapi.com` 是 Codex API 直连域名,必须保留在 `NO_PROXY` / `no_proxy` 中。
- P0: UniDesk 可调运维事实以 owning YAML 为唯一真相,代码只校验/渲染,变更走受控 CLI;细则见 `docs/reference/yaml-first-ops.md``docs/reference/platform-infra.md``$unidesk-ymalops`
- P0: Secret 仅由 YAML `sourceRef`/`targetKey` 和受控 CLI 下发;禁止从运行面反解,输出只披露对象/key、presence、fingerprint 与摘要
- P0: `hyueapi.com` / `.hyueapi.com` 必须保留在 `NO_PROXY` / `no_proxy`
## P0: 受控入口与技能路由
- P0: 分布式敏捷现场修复必须使用 `$dad-dev`:P1 目标运行面探测,P2 最小热补闭环,P3 Git/项目持久化,P4 原入口复测。
- P0: CI/CD、GitOps、rollout、PipelineRun、Argo、git-mirror 和 AgentRun 部署必须`$unidesk-cicd` 与受控 CLI禁止把原生 `kubectl``argo``tkn``curl` 或临时 shell 当正式控制入口
- P0: GitHub issue/PR 正式写入必须`$unidesk-gh` / `bun scripts/cli.ts gh ...`,禁止原生 `gh`手写 GitHub API 绕过;正文、评论和 closeout 默认中文。
- P0: 远端文本修改优先走 `$unidesk-trans``trans <route> apply-patch`route 定位和容器 cwd 规则`docs/reference/cli.md`
- P0: WebWorkbenchPlaywright/web-probe、前端状态投影和线上 Web bug 复测使用 `$unidesk-webdev`OTel/Tempo/trace 追踪使用 `$unidesk-otel`
- P0: 用户要求“离线调查”时,按 `$unidesk-otel` + `$unidesk-webdev` + `$unidesk-gh` 执行:OTel 离线 analyze、已有 web-probe artifact/analyze、代码静态分析和 `../opencode` 对比;不得新开 web-probe 或改运行面;工具不好用先改工具
- P0: Web 哨兵、`web-probe sentinel``monitor.pikapython.com`、定期/周期巡检和新建巡检使用 `$unidesk-monitor`,涉及页面复现或截图时同时使用 `$unidesk-webdev`
- P0: 跨 host/lane 现场修复加载 `$dad-dev`,完成真实运行面探测、最小验证、持久化和原入口复测。
- P0: CI/CD、GitOps、rollout、PipelineRun、Argo、git-mirror 和 AgentRun 部署走 `$unidesk-cicd` 与受控 CLI原生工具仅限有界诊断
- P0: GitHub issue/PR 正式写走 `$unidesk-gh` / `bun scripts/cli.ts gh ...`,禁止原生 `gh`/手写 API;写入默认中文。
- P0: 远端文本修改优先走 `$unidesk-trans``trans <route> apply-patch`route/cwd 见 `docs/reference/cli.md`
- P0: Web/Workbench/Playwright/web-probe `$unidesk-webdev`OTel/Tempo/trace `$unidesk-otel`Web 哨兵和周期巡检走 `$unidesk-monitor`
- P0: “离线调查”只分析既有 trace/artifact/源码,不新开探针或改运行面;边界见 `docs/reference/observability.md`
## P0: HWLAB、AgentRun 与节点边界
## P0: 固定开发与运行面
- P0: HWLAB 开发和部署固定使用 NC01:固定 workspace 是 `NC01:/root/hwlab-v03`k3s route 是 `NC01:k3s`,配置真相是 `config/hwlab-node-lanes.yaml`。细则见 `docs/reference/hwlab.md`
- P0: BK7258 项目/苗总项目/飞思创/脚本适配/中间件适配/MicroPython 相关材料常用目录是 `D518:D:\Work\HWLabOA\Project Management\[EPIC002][MIAO][PRJ001][BK7258]\`,进入后先读 `D002001001-SUMMARY.md`
- P0: D601 UniDesk 固定开发 workspace 是 `/home/ubuntu/workspace/unidesk-dev`;源码/配置任务先从最新 `origin/master` 创建独立 worktree。细则见 `docs/reference/dev-environment.md`
- P0: AgentRun 开发和部署固定只使用 NC01:固定 workspace 是 `NC01:/root/agentrun-v02`k3s route 是 `NC01:k3s`,配置真相是 `config/agentrun.yaml`。细则见 `docs/reference/agentrun.md`
## P0: HWLAB API key 与 issue 关闭
- P0: `HWLAB_API_KEY` 必须按 issue/CLI 选中的 node/lane 和目标 HWLAB repo 规则解析;只能输出 source path、present/missing 或 redacted prefix,不得打印完整 key。细则见 `docs/reference/hwlab.md`
- P0: HWLAB 用户反馈、CLI、Cloud Web、AgentRun、device-pod、公开 API 或运行面工作流 issue,关闭前必须按明确 lane+node 完成用户入口或原入口真实验证;仅源码检查、PR 合并或构建检查不得关闭。
- P0: HWLAB 固定使用 NC01/v03AgentRun 固定使用 NC01/nc01-v02workspace、route、配置真相、CI/CD 和原入口验收分别见 `docs/reference/hwlab.md` `docs/reference/agentrun.md`
- P0: D601 UniDesk workspace、任务 worktree 和 Master 构建边界见 `docs/reference/dev-environment.md`
- P0: HWLAB 凭据脱敏和 issue 关闭遵循 `docs/reference/hwlab.md`;关闭前必须在选中 node/lane 走原入口真实验证,源码、PR 或构建通过不能替代
## P0: 长期参考入口
- CLI 与 route: `docs/reference/cli.md`
- 文档治理与过程蒸馏: `docs/reference/docs-governance.md`
- Agent 指令体积与分流: `docs/reference/agent-instruction-hygiene.md`
- DevOps hygiene、source truth、worktree 稳定本地状态、语义合并与清理: `docs/reference/devops-hygiene.md`
- 开发环境和 D601/Master 边界: `docs/reference/dev-environment.md`
- 主 server Codex profiles/resume: `docs/reference/master-server-ops.md`
- HWLAB: `docs/reference/hwlab.md`
- AgentRun: `docs/reference/agentrun.md`
- NC01: `docs/reference/nc01.md`
+6 -5
View File
@@ -8,15 +8,16 @@ metadata:
relatedIssues:
- 2243
- 2449
- 2462
defaults:
targetId: NC01
switch:
enabled: true
mode: shadow-produce-only
mode: durable-event-stream
appIntegrationEnabled: true
shadowProduceEnabled: true
shadowConsumeEnabled: false
produceEnabled: true
consumeEnabled: true
operator:
implementation: strimzi
@@ -178,5 +179,5 @@ management:
pattern: ^hwlab-v03-cloud-api-sse-([0-9]{13})-[0-9a-f]{8}$
captureGroup: 1
unit: unix-ms
defaultShadowTopic: hwlab.event.v1
shadowProducerId: unidesk-platform-infra-kafka-cli
defaultTopic: hwlab.event.v1
diagnosticProducerId: unidesk-platform-infra-kafka-cli
+2 -2
View File
@@ -1,6 +1,6 @@
# UniDesk CLI Reference
UniDesk 的统一 CLI 实现入口是根目录 `scripts/cli.ts`,运行方式固定为 `bun scripts/cli.ts <command>`;普通根 CLI 子命令仍使用该入口。`trans <route> ...` 是 SSH/WSL/k3s 透传专用入口,wrapper 委托轻量 `scripts/ssh-cli.ts` 启动链路,避免被无关根 CLI 子命令模块的解析或语法错误拖垮;长期参考文档、AGENTS 索引、CLI help 和人工远端操作示例都必须优先写 `trans ...`,不得再把 `bun scripts/cli.ts ssh ...` 作为默认透传入口。CLI 默认输出 JSON所有成功和失败路径必须向 stdout 写出结构化对象,避免无输出造成状态不可观测。
UniDesk 的统一 CLI 实现入口是根目录 `scripts/cli.ts`,运行方式固定为 `bun scripts/cli.ts <command>`;普通根 CLI 子命令仍使用该入口。`trans <route> ...` 是 SSH/WSL/k3s 透传专用入口,wrapper 委托轻量 `scripts/ssh-cli.ts` 启动链路,避免被无关根 CLI 子命令模块的解析或语法错误拖垮;长期参考文档、AGENTS 索引、CLI help 和人工远端操作示例都必须优先写 `trans ...`,不得再把 `bun scripts/cli.ts ssh ...` 作为默认透传入口。面向人工的默认输出采用 Kubernetes 风格的简洁表格、短摘要和 drill-down 命令;JSON 只用于 `--json``--raw``--full``-o json` 或其他明确机器消费模式。所有成功和失败路径必须给出可判定状态,避免无输出造成不可观测。
主 server 必须在 PATH 上提供 `/root/.local/bin/trans` 可执行 wrapper,内容委托 repo 内版本化 `scripts/trans` 并执行 `bun scripts/ssh-cli.ts ssh "$@"`;交互 shell 可额外提供 alias,但非交互 Codex `exec` 和脚本不能依赖 alias 展开。
@@ -167,7 +167,7 @@ UniDesk/HWLAB Web 开发、HWLAB `web-probe run|script|observe|screenshot`、fak
## Output Contract
每条命令的最外层 JSON 包含 `ok``command``data``error`。失败时 CLI 设置非零退出码,但仍然输出 JSON 错误对象;错误对象应包含 `name``message` 和可用 `stack`
显式 JSON 或机器消费模式的最外层对象包含 `ok``command``data``error`。失败时 CLI 设置非零退出码JSON 模式返回包含 `name``message` 和可用 `stack` 的错误对象,默认人工模式返回有界错误摘要和后续诊断命令。预期参数错误不得默认打印 stack trace
诊断命令默认采用渐进披露:`server logs``job list/status``codex task/trace/output``microservice health code-queue``microservice proxy` 和 AgentRun control-plane/resource primitive 都必须有默认条数、字节数、表格或文本预览上限;用户显式传 `--limit``--tail-bytes``--full-text``--raw``--full``-o json` 或等价机器消费参数才扩大单次输出。AgentRun `control-plane plan|refresh|cleanup-runners|trigger-current` 默认输出短摘要、关键字段和下一步命令;`describe task -o json` 默认仍是 compact client schema,完整资源用 `--full -o json``result --raw` 属于显式 raw 路径,可以触发 dump 兜底。CLI stdout 遇到下游 pipe 关闭的 `EPIPE` 必须安静退出,不得打印 Bun stack trace。
+1 -1
View File
@@ -34,7 +34,7 @@ Compose v2 安装后仍然必须遵守 UniDesk 的服务控制入口:全栈生
## Host Codex Profiles
主 server 上的 `codex``mycx``mxcx``acx` 都是人工/Codex 运维入口,不属于 UniDesk Compose 服务、Code Queue runner、AgentRun runtime 或发布流水线。`mxcx` 只用于快速以 MiniMax-M3 provider 启动同一套 Codex CLI`acx` 是主 server 统一多模型 Codex profile 入口,默认使用 `gpt-5.5-only`,并允许通过 `/model``-m` 切换到 OpenCode Zen Go 与 Sub2API GPT aliases。它们必须复用系统 `codex` 二进制,不安装第二套 Codex,不替换 `/usr/bin/codex`,也不改变 `mycx` 指向的默认 Codex 启动方式。主机 Codex profile 的长期运维细节`docs/reference/master-server-ops.md` 为准;UniDesk 仓库文档只记录部署边界和与服务运行面相关的约束。
主 server 上的 `codex``mycx``oncx``mxcx``acx` 都是人工/Codex 运维入口,不属于 UniDesk Compose 服务、Code Queue runner、AgentRun runtime 或发布流水线。`mxcx` 只用于快速以 MiniMax-M3 provider 启动同一套 Codex CLI`acx` 是主 server 统一多模型 Codex profile 入口,默认使用 `gpt-5.5-only`,并允许通过 `/model``-m` 切换到 OpenCode Zen Go 与 Sub2API GPT aliases。它们必须复用系统 `codex` 二进制,不安装第二套 Codex,不替换 `/usr/bin/codex`,也不改变 `mycx`/`oncx` 的系统 Codex 启动方式。主机 Codex profile、模型持久化、resume 分桶和凭据继承的长期判定标准`docs/reference/master-server-ops.md` 为准;UniDesk 仓库文档只记录部署边界和与服务运行面相关的约束。
MiniMax-M3 配置必须保持 profile/provider 级隔离。当前 `mxcx` 的稳定形态是 `CODEX_HOME=/root/.codex-minimax-m3``model=MiniMax-M3``model_provider=minimax``wire_api=responses`,通过本机 Moon Bridge loopback endpoint 转换到 MiniMax 上游;profile config 中的 Codex `base_url` 应指向本机 bridgeMoon Bridge runtime config 再持有 MiniMax upstream `https://api.minimaxi.com`。密钥通过 profile `auth.json` 或环境中的 `OPENAI_API_KEY`/`MINIMAX_API_KEY` 注入,文件只能由 root 读取,权限应为 `0600` 或更严;不得提交到 Git、写入 `AGENTS.md`/`docs/reference`、进入 Docker image、Compose env、GitOps manifest、issue/PR 评论、CLI 输出或任务 trace。
+3 -3
View File
@@ -68,13 +68,13 @@ The persistent dev rollout currently supports:
## Rust Backend-Core Boundary
backend-core is implemented as a Rust service. The default dev and CI path compiles backend-core on D601 CI and consumes commit-pinned artifacts. The master server may inspect files, run TypeScript CLI checks, render Compose config, dispatch jobs and proxy traffic, but it must not run Rust compilation for ordinary backend-core iteration.
backend-core is implemented as a Rust service. The default dev and CI path compiles backend-core on D601 CI and consumes commit-pinned artifacts. The master server may inspect files, run concrete-file lightweight syntax checks, dispatch jobs and proxy traffic, but it must not run repository-level checks, Compose checks or Rust compilation for ordinary backend-core iteration.
Narrow production-online exception: when a user or issue explicitly asks to put the current backend-core fix online on the main-server Compose runtime, master server may run backend-core-only Rust compilation with constrained parallelism (`CARGO_BUILD_JOBS=1`, `--jobs 1`, or the CLI's equivalent setting) and must use async job/status/health evidence. This exception does not allow repository-wide checks, Rust tests, Go builds, frontend builds, or other service builds on the master server.
Allowed on the master server:
- `bun scripts/cli.ts check --files --scripts-typecheck --compose --logs`
- `git diff --check` and `bun --check <specific-file>`
- `bun scripts/cli.ts check --help`
- `bun scripts/cli.ts deploy plan --env dev --service backend-core`
- `bun scripts/cli.ts deploy apply --env dev --service backend-core`
@@ -116,7 +116,7 @@ The persistent dev environment follows the shared Git-backed deployment hygiene
Use this sequence for backend-core Rust and frontend dev work:
1. Preflight the fixed workspace, then create the task-scoped worktree with `bun scripts/cli.ts dev-env worktree add .worktree/<task> --branch <branch> --from origin/master`; keep unrelated parallel changes separated with `git status`/`git diff`.
2. Run local non-Rust checks on the master server, for example `bun scripts/cli.ts check --files --scripts-typecheck --compose --logs`.
2. On the master server, limit validation to diff review and concrete-file lightweight syntax checks such as `bun --check <specific-file>`. Run repository-level checks, type checks, Compose checks, frontend checks and all Rust checks on D601, CI or another approved execution surface.
3. Commit the code from the task worktree and merge/push it to `origin/master` through the chosen lightweight PR or direct integration path; `deploy apply --env dev` cannot deploy unpushed local changes.
4. Update `deploy.json` `environments.dev.services` so `backend-core` and `frontend` point at the pushed commit, then commit and push that manifest update.
5. Preflight backend-core publication: `bun scripts/cli.ts ci publish-backend-core --commit <full-sha> --dry-run`. The result must have no `blockedScopes`, `wouldBuildOnD601=true`, D601 `unidesk-ci` Tekton runner metadata, D601 registry target `127.0.0.1:5000/unidesk/backend-core`, required labels for service id/source repo/source commit/Dockerfile, and `recommendedAction` pointing to the real publish command.
+10
View File
@@ -16,6 +16,10 @@ When stable release lanes such as `release/v1` are enabled, the desired-state re
Source and CLI files must not be kept near the 3000-line split boundary. Once a file exceeds 3000 lines, split it by responsibility until the original file is below 2000 lines before continuing feature or fix work. Do not make token-preserving micro-edits that leave the file just under or exactly at 3000 lines; that only guarantees the next small change will trigger the same split problem again.
Large shell, Node, Python or other script bodies must live in files with their native suffix, such as `.sh`, `.mjs` or `.py`, and be loaded or streamed from those files. Do not embed operational scripts as large string literals in TypeScript, shell wrappers, YAML or another host language; small command fragments and bounded quoted heredocs remain acceptable when they are the native interface of a controlled command.
When declared validation dependencies are missing, install them with the repository's package manager and lockfile, then continue the original validation. Do not skip, downgrade or terminate validation merely because the local environment is incomplete. If the required dependency is undeclared or installation would introduce a lockfile change, first add the dependency through the repository's normal declaration workflow and keep that change in the task's reviewed scope.
## Prohibited Deployment Truth
The following practices are not acceptable as the long-term or hidden source of a working environment:
@@ -52,6 +56,8 @@ If a manual repair is needed to unblock the platform, the durable fix must be co
固定主 repo 是 source truth anchor,不是源码/运行面 scratch 区。会产生源码、配置、issue closeout、部署脚本、验收产物或高风险 dad-dev / post-task 交付的工作,执行前必须先从目标 fixed repo 的最新 remote/base 创建任务专属 `.worktree/<task>`,后续编辑、验证、提交、push 和受控 CLI 写操作都在该 worktree 内完成。UniDesk 任务 worktree 必须通过 `bun scripts/cli.ts dev-env worktree add .worktree/<task> --branch <branch> [--from origin/master]` 创建,让 `.worktreecopy` 白名单内的本地配置自动复制到新 worktreefixed repo 只用于 `git fetch``git status`、快进同步、读取规则和这个受控创建入口。其中已有的并行未提交修改默认保持不动,不纳入当前任务,也不要用 reset、checkout 或删除来“清理”。
The root UniDesk checkout at `/root/unidesk` is the fixed main worktree and must stay on `master`. Non-`master` work belongs in a task-specific `.worktree/<task>`. Unexpected local changes are presumed to belong to another concurrent task: preserve them, do not reset, checkout, delete or rewrite them, and scope the current task's commit to its own files.
## Worktree-Independent Local State
Tracked source, YAML and scripts belong to the selected checkout; operator credentials and mutable runtime state do not. UniDesk's canonical owner-level roots are `/root/.unidesk/.env` for credentials and `/root/.unidesk/.state` for mutable state, logs, caches, generated artifacts and Secret source files. YAML `sourceRef`, CLI path configuration and source defaults that consume these materials must use those fixed absolute roots. They must not derive the path from `process.cwd()`, the module's task-worktree root, or a relative `.env`/`.state`/`logs` path.
@@ -72,6 +78,8 @@ When diagnosing a missing worktree file, first classify ownership. A tracked fil
允许不创建新 `.worktree` 的场景包括 P1 只读探测、运行面临时热补、上述文档/skill/长期参考轻量修改,或目标项目长期参考明确声明的直接修改例外。例外必须能解释为什么不会污染 fixed repo source truth,并且不得触碰无关并行修改;一旦需要写源码、配置、issue closeout、部署脚本、验收产物或其他高风险交付记录,立即切回独立 `.worktree`
When UniDesk's own CLI, wrapper or controlled toolchain is repaired, merge the durable fix into `master` before relying on it for the original operation. The checkout that actually executes the tool must then contain that merged revision. If a fixed checkout cannot fast-forward because of unrelated changes, preserve it and run the tool from a clean worktree based on current `origin/master`; never reset or overwrite concurrent work to update the executable path.
在模型 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` 的现场修复闭环。
如果某个现场步骤因为 quoting、route 定位、kubeconfig、输出体积或缺少 helper 而反复痛苦,优先改进 UniDesk passthrough / CLI 并在本文件的 `Distributed Command Passthrough``docs/reference/cli.md` 中记录稳定入口,不要沉淀一批一次性 shell 菜谱。
@@ -131,6 +139,8 @@ If a CI repo-check task fails at `git clone` because credentials are unavailable
## Verification Priority
The user's latest explicit objective takes precedence over stale tests, guards, preflights and assertions. When an old gate blocks the current requirement without protecting a still-valid high-value risk, remove it from the active path instead of preserving a fallback, legacy mode or compatibility branch around it.
When checks disagree, use this priority order:
1. The pushed desired commit and `deploy.json`/manifest contract.
+4
View File
@@ -15,6 +15,10 @@
- k3s 操作必须使用 YAML 解析出的 route 语法,例如 `trans NC01:k3s ...`。第一个 route token 必须定位分布式目标,后续 token 才是 operation。
- 非 NC01 workspace、`/root/HWLAB``/workspace/hwlab``/tmp/hwlab-*`、无关 runner clone、master-server checkout 或未由 YAML 选中的 workspace 都不能作为当前 HWLAB 开发 source truthCI/CD source authority 只看 NC01 YAML `sourceAuthority` 的受控 source snapshot。
BK7258、苗总项目、飞思创、脚本/中间件适配和 MicroPython 材料的常用项目目录是 `D518:D:\Work\HWLabOA\Project Management\[EPIC002][MIAO][PRJ001][BK7258]\`。进入该目录后先读取 `D002001001-SUMMARY.md`,再按摘要定位具体规格、任务书或阶段材料;不要把该资料目录当成 HWLAB 源码 workspace。
HWLAB 用户反馈、CLI、Cloud Web、AgentRun、device-pod、公开 API 或运行面工作流 issue,关闭前都必须在 issue/CLI 明确选中的 node/lane 通过用户入口或原入口完成真实验证。源码检查、测试、PR 合并、PipelineRun 或 Secret 存在性只能作为支持证据,不能单独满足关闭条件。`HWLAB_API_KEY` 必须按选中的 node/lane 和目标 HWLAB repo 规则解析,输出只允许 source path、presence、redacted prefix 或 fingerprint,不得打印完整 key。
## 关键 GitHub 入口
- UniDesk 总看板:`pikasTech/unidesk#20`
+39 -5
View File
@@ -1,6 +1,6 @@
# Master Server Ops
This document records master-server architecture and decision rules. **Operations commands (Moon Bridge management, profile smoke tests, MiniMax session recovery) have moved to the `unidesk-ops` skill** (`~/.agents/skills/unidesk-ops/SKILL.md`). Do not place secrets, one-off incident logs, or dated changelog notes here.
This document records master-server architecture and decision rules. Installed Codex wrappers own their command syntax; this reference owns profile boundaries, safety rules and acceptance criteria. General server lifecycle and GC commands remain in the `unidesk-ops` skill. Do not place secrets, one-off incident logs, or dated changelog notes here.
## Execution Boundary
@@ -10,7 +10,7 @@ This document records master-server architecture and decision rules. **Operation
## Codex Provider Profiles
`dscx`, `mxcx`, and `acx` are local Codex profile wrappers under `/root/.local/bin/`. They are host-level tools, not UniDesk service code. `acx` is the unified multi-model entry; `gocx` remains the OpenCode Zen Go compatibility entry, and older single-model `dscx-go`, `dfcx-go`, and `glcx-go` wrappers are compatibility entries. Usage commands are in `unidesk-ops` skill.
`mycx` and `oncx` are stable system Codex wrappers under `/usr/local/bin/`; both use the default `/root/.codex` home and are described in the shared-state subsection below. `dscx`, `mxcx`, and `acx` are isolated local Codex profile wrappers under `/root/.local/bin/`. They are host-level tools, not UniDesk service code. `acx` is the unified multi-model entry; `gocx` remains the OpenCode Zen Go compatibility entry, and older single-model `dscx-go`, `dfcx-go`, and `glcx-go` wrappers are compatibility entries.
- `dscx` uses `CODEX_HOME=/root/.codex-deepseek-v4-pro`, model `deepseek-v4-pro`, Codex custom provider `deepseek`, and local Moon Bridge at `http://127.0.0.1:38440/v1`.
- `mxcx` uses `CODEX_HOME=/root/.codex-minimax-m3`, model `MiniMax-M3`, Codex custom provider `minimax`, and local Moon Bridge at `http://127.0.0.1:38441/v1`.
@@ -22,7 +22,7 @@ This document records master-server architecture and decision rules. **Operation
- `acx` includes all OpenCode Zen Go upstream slugs plus `gpt-5.5-only` and `gpt-5.5-sub2api` in one `model-catalog.json` so Codex can use `/model` or `-m <model>` within the same profile.
- `acx` routes OpenCode Zen Go models to the existing `gocx` Moon Bridge. GPT models must not pass through Moon Bridge or the ACX router: `gpt-5.5-only` uses the direct `only` OpenAI-compatible Responses endpoint and `gpt-5.5-sub2api` uses the Sub2API pool endpoint, both with upstream model `gpt-5.5`.
- GPT direct providers must receive their API key through an environment-key path such as `ACX_GPT_DIRECT_API_KEY`, read from the matching `/root/.codex/auth.json.<profile>` file by the wrapper. Do not let direct GPT calls fall back to `/root/.codex-acx/auth.json`; that file may contain only the local-router dummy key.
- All wrappers read upstream API keys from profile auth files or wrapper-injected environment variables; generated Moon Bridge or router runtime configs live under the profile `.tmp/` directory with mode `0600`. Do not copy upstream keys into documentation.
- Isolated Moon Bridge and router wrappers read upstream API keys from profile auth files or, where the compatibility path still requires it, wrapper-injected environment variables; generated runtime configs live under the profile `.tmp/` directory with mode `0600`. This compatibility rule does not apply to `mycx` Pika auth, whose command-backed contract is defined below. Do not copy upstream keys into documentation.
- Each profile must include `model_catalog_json` in `config.toml` pointing to a profile-local `model-catalog.json` entry for its active model. Missing catalog metadata causes Codex to fall back to default metadata, which lowers the effective context window and prints `Model metadata ... not found`.
- Profile context metadata must match the intended upstream limit closely enough for Codex auto-compact to fire before provider rejection. Keep the local profile metadata in sync with the actual model family you are routing to.
- Current master-server profile baselines:
@@ -32,9 +32,43 @@ This document records master-server architecture and decision rules. **Operation
- Keep the wrapper-generated Moon Bridge/router metadata aligned with the profile `config.toml` and `model-catalog.json`. If these diverge, Codex and the local admission layer may disagree about compaction and request size behavior.
- `hyueapi.com` / `.hyueapi.com` must remain in `NO_PROXY` / `no_proxy` for Codex API channels.
### Shared CODEX_HOME Wrappers (`mycx` / `oncx`)
`mycx` and `oncx` reuse the same system `codex` binary and the same `/root/.codex` state tree. Their stable entrypoints are native executable wrappers, not shell functions:
- `/usr/local/bin/mycx` selects profile `pika`; its writable profile layer is `/root/.codex/pika.config.toml`.
- `/usr/local/bin/oncx` selects profile `only`; its writable profile layer is `/root/.codex/only.config.toml`.
- `/root/.codex/config.toml` is the shared base layer, while `/root/.codex/state_5.sqlite` and `/root/.codex/sessions/**/rollout-*.jsonl` hold the shared session index and durable transcripts.
- The Webterm declaration in `config/platform-infra/webterm.yaml` starts `mycx`; wrapper path or startup changes must preserve that original entrypoint and include a Webterm launch check.
- `codex-pool configure-local` owns only the unsuffixed shared `config.toml` and `auth.json` consumer files described in `docs/reference/platform-infra.md`. It does not own the profile-v2 `pika.config.toml`, `only.config.toml`, or `auth.json.pika` files and must not regenerate them as Sub2API consumer state.
- Shell startup files must not define same-name functions that shadow these executables or regenerate profile files on launch. After changing a wrapper, existing Codex processes must be exited and existing shells must clear stale functions or start a new shell before validation.
For profile-v2, `/model` persists `model` and reasoning effort to the active profile file. The two wrappers therefore remember their last selections independently. `-m` and `-c model...` / `-c model_reasoning_effort...` are runtime overrides and must not be treated as persistent model selection. A fresh same-profile restart, a switch between profiles, and resuming an existing session are distinct behaviors and must be diagnosed separately.
Session discovery has an additional identity boundary:
- Sharing `CODEX_HOME` does not guarantee identical `/resume` results. The picker also filters by the effective provider ID and by cwd, source, and archive state.
- Provider IDs are persisted, case-sensitive session bucket keys. The active wrappers use distinct provider identities, and historical sessions can retain earlier capitalization or provider names.
- In the deployed picker behavior, `--all` removes the cwd restriction but does not remove the provider restriction. Revalidate this contract after Codex upgrades before changing wrappers or migrating history.
- Resume uses the current profile's effective model, provider, and reasoning overrides; it does not necessarily restore the model last recorded by the selected session. Fresh model persistence and session-model restoration must therefore have separate acceptance criteria.
- Rollout JSONL is durable history and can repopulate the SQLite index. Never change only `state_5.sqlite` to merge provider buckets. A reviewed history migration must stop active Codex processes, update rollout metadata with a structured JSON parser, update the SQLite index transactionally, preserve provenance, and verify the same session ID set after reindexing.
Provider identity is a compatibility contract. Any change to `CODEX_HOME`, profile name, provider ID, or provider-ID capitalization must preflight existing history and warn or block when it would create a new hidden bucket. Do not make different real backends impersonate one provider solely to merge picker lists. The preferred future unification is a picker policy with explicit `current`, `all`, or configured aliases, visible origin-provider metadata, and an explicit continue/fork decision for cross-provider resume.
Pika authentication must use command-backed provider auth from `[model_providers.Pika.auth]`, with the credential source `/root/.codex/auth.json.pika` restricted to mode `0600`. Do not export the Pika API key into the wrapper or Codex environment: inherited environment variables can be captured by shell snapshots and propagated to child tools. Shell snapshots must also be restricted to mode `0600` and must not contain reusable credentials. Treat any plaintext snapshot occurrence as credential exposure, remove the copy, and rotate the credential.
Diagnostics and validation must remain secret-safe:
- Report only effective `CODEX_HOME`, wrapper path, active profile file, provider ID, model/reasoning, session counts by provider, credential presence or fingerprint, and relevant file modes.
- Verify each profile by selecting a non-baseline `/model`, exiting, fresh-starting the same wrapper, and confirming the selection remains while the other profile file is unchanged.
- Verify a temporary CLI override does not rewrite the profile, and verify Pika command-backed auth works with the corresponding API-key environment variable absent.
- When `/resume` differs, compare `CODEX_HOME`, provider ID, cwd, source, and archive filters before attributing the result to storage loss or caching.
The investigation evidence and source-level rationale for these rules are retained in [UniDesk issue #1640](https://github.com/pikasTech/unidesk/issues/1640); this document is the authority for the active long-term contract.
## Moon Bridge
Moon Bridge is installed as `/root/.local/bin/moonbridge`. The binary exposes OpenAI Responses endpoints and bridges Codex to provider-specific upstream APIs. Operations commands are in `unidesk-ops` skill.
Moon Bridge is installed as `/root/.local/bin/moonbridge`. The binary exposes OpenAI Responses endpoints and bridges Codex to provider-specific upstream APIs. The installed profile wrappers own bridge command syntax; this section records the supported runtime behavior.
The local source copy for the installed patched build is `/root/src/moon-bridge-sanitize`. When changing Moon Bridge, keep the change narrow, run package-level tests, build a replacement binary, back up the previous binary, then restart affected profile bridges. Default master-server build restrictions still apply.
@@ -77,7 +111,7 @@ Sanitizer rules: recursively scans `ResponsesRequest.input`, repairs tool-call `
## MiniMax Session Recovery
`mxcx` includes a cleanup and guard layer for corrupted MiniMax-backed session JSONL. **Recovery operations commands are in `unidesk-ops` skill.** This section only documents behavioral rules.
`mxcx` includes a cleanup and guard layer for corrupted MiniMax-backed session JSONL. The wrapper owns recovery command syntax; this section documents behavioral rules.
- `mxcx resume` auto-runs `session-clean` + `session-guard` before invoking Codex. Uses `CODEX_HOME=/root/.codex-minimax-m3` profile.
- `session-clean` is strictly scoped to invalid tool-call `arguments`: malformed JSON, MiniMax sentinel text, and schema-invalid cases. Must preserve line order, non-tool messages, reasoning, outputs, token records, and session metadata. Must not compact/summarize/truncate/reorder transcript.
+8 -4
View File
@@ -11,17 +11,21 @@ NC01 is the current host registered as a UniDesk provider-gateway node. Host mai
- AgentRun config: `config/agentrun.yaml`, lane `nc01-v02`.
- NC01 host PostgreSQL config: `config/platform-db/postgres-nc01.yaml`.
## UniDesk Server Runtime
NC01 runs the UniDesk server/backend-core on Kubernetes in namespace `unidesk`, with `config/unidesk-host-k8s.yaml` as the configuration authority. Recovery, validation and `trans` operations must target `NC01:k3s` and those Kubernetes objects. A Docker Compose project or a container named `unidesk-backend-core` is not the NC01 server runtime and must not be used as recovery evidence. Once the Kubernetes runtime is healthy, do not rebuild, restart or replace it unless the user or owning runbook explicitly requires that action.
## Fixed Repos
- HWLAB v0.3 fixed repo: `/root/hwlab`, branch `v0.3`.
- AgentRun v0.2 fixed repo: `/root/agentrun`, branch `v0.2`.
- HWLAB v0.3 fixed repo: `/root/hwlab-v03`; branch and remote come from `config/hwlab-node-lanes.yaml`.
- AgentRun v0.2 fixed repo: `/root/agentrun-v02`; branch and remote come from `config/agentrun.yaml`.
- UniDesk source/config authority remains `/root/unidesk`.
## Database Boundary
NC01 databases run on host-native PostgreSQL, not Docker or Kubernetes. Kubernetes workloads that need PostgreSQL must use YAML-declared external PostgreSQL bridge objects and Secrets.
NC01 database placement is selected by each owning YAML, not by a host-wide assumption. `config/unidesk-host-k8s.yaml`, `config/agentrun.yaml` and `config/hwlab-node-lanes.yaml` independently declare whether their workloads consume host-native/external PostgreSQL or a lane-local Kubernetes PostgreSQL. Validation must report the selected mode and corresponding Service/SecretRef; it must not require objects belonging to an unselected mode.
For HWLAB v0.3, the Kubernetes Service `nc01-host-postgres` in namespace `hwlab-v03` points to host PostgreSQL at `10.42.0.1:5432`. Runtime database Secrets are sourced from `/root/.unidesk/.state/secrets/hwlab/*` via YAML sourceRef and must only be reported as present/fingerprint metadata, never as full values.
For HWLAB v0.3, `runtimeStore.postgres.mode` in `config/hwlab-node-lanes.yaml` owns this choice. A platform-service mode uses the YAML-declared external PostgreSQL bridge such as `nc01-host-postgres`; a local-k3s mode uses its lane-local PostgreSQL objects and must report the external bridge as not required. Runtime database Secrets come only from the selected mode's YAML sourceRef, with local source material under `/root/.unidesk/.state/secrets/hwlab/*`, and may be reported as presence/fingerprint metadata, never as full values.
## Decision Center
+1 -1
View File
@@ -5,7 +5,7 @@
## Source Of Truth
- UniDesk-owned platform configuration must be YAML-first. `config/platform-infra/*.yaml` is the durable source for images, versions, endpoints, FRP exposure, account profile selection, and local consumer configuration.
- Runtime Secrets and local `~/.codex/config.toml*` / `auth.json*` files are inputs or generated local state, not committed truth. CLI output may show Secret paths, byte counts, fingerprints, and short previews only; it must not print complete API keys.
- Runtime Secrets and local `~/.codex/config.toml*` / `auth.json*` files are inputs or generated local state, not committed truth. CLI output may show Secret object/key names, source paths, byte counts, presence and fingerprints. Short previews are allowed only for non-secret configuration; Secret and credential values are never previewed or printed.
- Code that reads platform YAML must validate object shape, field types, required fields, Kubernetes names, image strings, and ports before mutating G14 k3s or local consumer files.
- Do not hide image versions, namespace names, endpoint URLs, FRP ports, or profile lists in Python/TOML/JSON helper constants when they are UniDesk-owned choices. External tools may still require their own TOML/JSON/env file formats at the edge.
- Fresh node outbound bootstrap uses the zero-dependency host proxy boundary defined in `docs/reference/yaml-first-ops.md`. Platform-infra egress proxy settings may provide the benchmark-validated proxy source and workload consumer settings, but a new node must not depend on Docker, k3s, containerd or package-manager network access to install the initial host proxy client.
+9 -3
View File
@@ -347,13 +347,19 @@ export async function status(config: UniDeskConfig, options: StatusOptions): Pro
}
function isPipelinesAsCodeMigratedLane(spec: AgentRunLaneSpec): boolean {
return spec.gitMirror.readUrl.includes("gitea-http.")
return spec.source.remote.includes("gitea-http.")
|| spec.source.remote.includes("/mirrors/")
|| spec.gitMirror.readUrl.includes("gitea-http.")
|| spec.gitops.repoURL.includes("gitea-http.")
|| spec.gitMirror.readUrl.includes("/mirrors/");
}
function pacStatusCommand(spec: AgentRunLaneSpec, full = false): string {
return `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${spec.nodeId}${full ? " --full" : ""}`;
return `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${spec.nodeId} --consumer ${pacConsumerId(spec)}${full ? " --full" : ""}`;
}
function pacConsumerId(spec: AgentRunLaneSpec): string {
return `agentrun-${spec.lane.toLowerCase()}`;
}
function giteaMirrorStatusCommand(spec: AgentRunLaneSpec): string {
@@ -362,7 +368,7 @@ function giteaMirrorStatusCommand(spec: AgentRunLaneSpec): string {
export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
const spec = target.spec;
const pacStatus = record(await runPlatformInfraPipelinesAsCodeCommand(config, ["status", "--target", spec.nodeId, "--full"]));
const pacStatus = record(await runPlatformInfraPipelinesAsCodeCommand(config, ["status", "--target", spec.nodeId, "--consumer", pacConsumerId(spec), "--full"]));
const pacSummary = record(pacStatus.summary);
const latestPipelineRun = record(pacSummary.latestPipelineRun);
const artifact = record(pacSummary.artifact);
+39 -35
View File
@@ -74,10 +74,10 @@ interface PlatformKafkaConfig {
targetId: string;
switch: {
enabled: boolean;
mode: string;
mode: "durable-event-stream";
appIntegrationEnabled: boolean;
shadowProduceEnabled: boolean;
shadowConsumeEnabled: boolean;
produceEnabled: boolean;
consumeEnabled: boolean;
};
};
operator: {
@@ -127,8 +127,8 @@ interface PlatformKafkaConfig {
deleteBatchSize: number;
policies: KafkaGroupCleanupPolicy[];
};
defaultShadowTopic: string;
shadowProducerId: string;
defaultTopic: string;
diagnosticProducerId: string;
};
}
@@ -230,7 +230,7 @@ export function kafkaHelp(): Record<string, unknown> {
"bun scripts/cli.ts platform-infra kafka tail --node NC01 --topic hwlab.event.v1 --limit 5",
"bun scripts/cli.ts platform-infra kafka produce --node NC01 --topic hwlab.event.v1 --key <trace-or-session>",
],
boundary: "Kafka runtime belongs to platform-infra. P2 supports shadow produce/query only; Kafka consumer cutover stays disabled.",
boundary: "Kafka runtime belongs to platform-infra. Durable producers and fixed-group consumers are enabled; browser delivery remains a database-projected SSE concern.",
};
}
@@ -262,10 +262,10 @@ function readKafkaConfig(): PlatformKafkaConfig {
targetId: y.stringField(defaults, "targetId", "defaults"),
switch: {
enabled: y.booleanField(switchRecord, "enabled", "defaults.switch"),
mode: y.stringField(switchRecord, "mode", "defaults.switch"),
mode: y.enumField(switchRecord, "mode", "defaults.switch", ["durable-event-stream"] as const),
appIntegrationEnabled: y.booleanField(switchRecord, "appIntegrationEnabled", "defaults.switch"),
shadowProduceEnabled: y.booleanField(switchRecord, "shadowProduceEnabled", "defaults.switch"),
shadowConsumeEnabled: y.booleanField(switchRecord, "shadowConsumeEnabled", "defaults.switch"),
produceEnabled: y.booleanField(switchRecord, "produceEnabled", "defaults.switch"),
consumeEnabled: y.booleanField(switchRecord, "consumeEnabled", "defaults.switch"),
},
},
operator: {
@@ -363,8 +363,8 @@ function parseManagement(root: Record<string, unknown>): PlatformKafkaConfig["ma
deleteBatchSize: positiveInteger(cleanup, "deleteBatchSize", "management.consumerGroupCleanup"),
policies: y.arrayOfRecords(cleanup.policies, "management.consumerGroupCleanup.policies").map(parseGroupCleanupPolicy),
},
defaultShadowTopic: topicNameField(management, "defaultShadowTopic", "management"),
shadowProducerId: y.stringField(management, "shadowProducerId", "management"),
defaultTopic: topicNameField(management, "defaultTopic", "management"),
diagnosticProducerId: y.stringField(management, "diagnosticProducerId", "management"),
};
}
@@ -406,7 +406,7 @@ function validateConfig(kafka: PlatformKafkaConfig): void {
if (kafka.cluster.replicas !== 1) throw new Error(`${configLabel}.cluster.replicas must be 1 for the target-scoped Kafka POC`);
if (!/^[0-9]+Gi$/u.test(kafka.cluster.storage.size)) throw new Error(`${configLabel}.cluster.storage.size must be a Gi quantity`);
if (!kafka.topics.some((topic) => topic.name === kafka.validation.smokeTopic)) throw new Error(`${configLabel}.validation.smokeTopic must reference one of topics[].name`);
if (!kafka.topics.some((topic) => topic.name === kafka.management.defaultShadowTopic)) throw new Error(`${configLabel}.management.defaultShadowTopic must reference one of topics[].name`);
if (!kafka.topics.some((topic) => topic.name === kafka.management.defaultTopic)) throw new Error(`${configLabel}.management.defaultTopic must reference one of topics[].name`);
if (kafka.management.defaultTailLimit > kafka.management.maxTailLimit) throw new Error(`${configLabel}.management.defaultTailLimit must be <= management.maxTailLimit`);
if (kafka.management.defaultGroupListLimit > kafka.management.maxGroupListLimit) throw new Error(`${configLabel}.management.defaultGroupListLimit must be <= management.maxGroupListLimit`);
if (kafka.management.consumerGroupCleanup.defaultCandidateLimit > kafka.management.consumerGroupCleanup.maxCandidateLimit) throw new Error(`${configLabel}.management.consumerGroupCleanup.defaultCandidateLimit must be <= maxCandidateLimit`);
@@ -418,7 +418,9 @@ function validateConfig(kafka: PlatformKafkaConfig): void {
cleanupPolicyIds.add(policy.id);
cleanupPrefixes.add(policy.prefix);
}
if (kafka.defaults.switch.shadowConsumeEnabled) throw new Error(`${configLabel}.defaults.switch.shadowConsumeEnabled must stay false for shadow-produce-only stage`);
if (!kafka.defaults.switch.enabled || !kafka.defaults.switch.appIntegrationEnabled || !kafka.defaults.switch.produceEnabled || !kafka.defaults.switch.consumeEnabled) {
throw new Error(`${configLabel}.defaults.switch must enable durable event production and consumption`);
}
const topicNames = new Set<string>();
for (const topic of kafka.topics) {
if (topicNames.has(topic.name)) throw new Error(`${configLabel}.topics contains duplicate topic ${topic.name}`);
@@ -644,7 +646,7 @@ async function offsets(config: UniDeskConfig, options: KafkaInspectOptions): Pro
async function tail(config: UniDeskConfig, options: KafkaInspectOptions): Promise<Record<string, unknown>> {
const kafka = readKafkaConfig();
const target = resolveTarget(kafka, options.targetId);
const topic = resolveTopic(kafka, options.topic, kafka.management.defaultShadowTopic);
const topic = resolveTopic(kafka, options.topic, kafka.management.defaultTopic);
const limit = boundedLimit(kafka, options.limit);
const result = await capture(config, target.route, ["sh"], tailScript(kafka, target, topic, limit));
const parsed = parseJsonOutput(result.stdout);
@@ -662,15 +664,15 @@ async function tail(config: UniDeskConfig, options: KafkaInspectOptions): Promis
async function produce(config: UniDeskConfig, options: KafkaProduceOptions): Promise<Record<string, unknown>> {
const kafka = readKafkaConfig();
const target = resolveTarget(kafka, options.targetId);
const topic = resolveTopic(kafka, options.topic, kafka.management.defaultShadowTopic);
const payload = buildShadowPayload(kafka, target, options, topic);
const topic = resolveTopic(kafka, options.topic, kafka.management.defaultTopic);
const payload = buildDiagnosticPayload(kafka, target, options, topic);
const result = await capture(config, target.route, ["sh"], produceScript(kafka, target, topic, options.key, payload));
const parsed = parseJsonOutput(result.stdout);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-kafka-produce",
mutation: true,
mode: "shadow-produce-only",
mode: "diagnostic-produce",
target: targetSummary(target),
config: compactConfigSummary(kafka, target),
production: parsed ?? null,
@@ -763,6 +765,9 @@ metadata:
annotations:
unidesk.ai/spec: ${kafka.metadata.spec}
unidesk.ai/app-integration-enabled: "${kafka.defaults.switch.appIntegrationEnabled}"
unidesk.ai/event-stream-mode: "${kafka.defaults.switch.mode}"
unidesk.ai/event-produce-enabled: "${kafka.defaults.switch.produceEnabled}"
unidesk.ai/event-consume-enabled: "${kafka.defaults.switch.consumeEnabled}"
spec:
kafka:
version: ${kafka.cluster.kafkaVersion}
@@ -1453,7 +1458,7 @@ PY
}
function produceScript(kafka: PlatformKafkaConfig, target: KafkaTarget, topic: string, key: string | null, payload: string): string {
const keyValue = key ?? `${kafka.management.shadowProducerId}-${Date.now()}`;
const keyValue = key ?? `${kafka.management.diagnosticProducerId}-${Date.now()}`;
const separator = "|";
if (keyValue.includes(separator) || keyValue.includes("\n")) throw new Error("kafka produce --key must not contain | or newline");
return `
@@ -1496,9 +1501,9 @@ payload_obj = {
"bytes": len(payload.encode()),
"valuesPrinted": False,
},
"producer": "${kafka.management.shadowProducerId}",
"mode": "shadow-produce-only",
"consume": {"enabled": False, "performed": False},
"producer": "${kafka.management.diagnosticProducerId}",
"mode": "diagnostic-produce",
"consume": {"enabled": True, "performed": False},
"steps": {"produce": {"exitCode": produce_rc, "stdoutTail": text("produce.out"), "stderrTail": text("produce.err")}},
"valuesPrinted": False,
}
@@ -1543,13 +1548,13 @@ function compactConfigSummary(kafka: PlatformKafkaConfig, target: KafkaTarget):
topicCount: kafka.topics.length,
clientCount: kafka.clients.length,
management: {
defaultShadowTopic: kafka.management.defaultShadowTopic,
defaultTopic: kafka.management.defaultTopic,
defaultTailLimit: kafka.management.defaultTailLimit,
maxTailLimit: kafka.management.maxTailLimit,
defaultGroupListLimit: kafka.management.defaultGroupListLimit,
maxGroupListLimit: kafka.management.maxGroupListLimit,
consumerGroupCleanup: kafka.management.consumerGroupCleanup,
shadowProducerId: kafka.management.shadowProducerId,
diagnosticProducerId: kafka.management.diagnosticProducerId,
},
valuesPrinted: false,
};
@@ -1622,9 +1627,9 @@ function policyChecks(kafka: PlatformKafkaConfig, target: KafkaTarget, manifest:
{ name: "no-public-exposure", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(manifest) && !/^\s*kind:\s*Ingress\s*$/mu.test(manifest), detail: "Kafka POC is ClusterIP/internal only." },
{ name: "single-broker-poc", ok: kafka.cluster.replicas === 1, detail: "The target-scoped POC uses one KRaft broker/controller; production HA remains out of scope." },
{ name: "allow-all-network-policy", ok: manifest.includes("kind: NetworkPolicy") && manifest.includes("name: allow-all") && manifest.includes(`namespace: ${target.namespace}`), detail: `NetworkPolicy/allow-all is rendered in ${target.namespace}.` },
{ name: "shadow-produce-enabled", ok: kafka.defaults.switch.shadowProduceEnabled === true, detail: "Apps may publish shadow events to Kafka." },
{ name: "app-integration-shadow-only", ok: kafka.defaults.switch.appIntegrationEnabled === true && kafka.defaults.switch.mode === "shadow-produce-only", detail: "App integration is limited to shadow production in this stage." },
{ name: "shadow-consume-disabled", ok: kafka.defaults.switch.shadowConsumeEnabled === false, detail: "Kafka consumer cutover remains disabled." },
{ name: "durable-produce-enabled", ok: kafka.defaults.switch.produceEnabled === true, detail: "Transactional outbox relays may publish durable application events." },
{ name: "durable-event-stream-mode", ok: kafka.defaults.switch.appIntegrationEnabled === true && kafka.defaults.switch.mode === "durable-event-stream", detail: "Application integration uses the durable event stream contract." },
{ name: "fixed-group-consume-enabled", ok: kafka.defaults.switch.consumeEnabled === true, detail: "Durable projector consumers are enabled." },
];
}
@@ -1686,7 +1691,7 @@ function renderPlan(result: Record<string, unknown>): RenderedCliResult {
` status: ${stringValue(next.status)}`,
` validate: ${stringValue(next.validate)}`,
"",
"Boundary: Kafka runtime is platform-infra only; P2 allows shadow produce/query and keeps consumer cutover disabled.",
"Boundary: Kafka runtime is platform-infra only; durable event production and fixed-group projector consumption are enabled.",
"Disclosure: Secret values are not printed; client entries show only object/key metadata.",
]);
}
@@ -1960,7 +1965,7 @@ async function parseProduceOptions(args: string[]): Promise<KafkaProduceOptions>
let topic: string | null = null;
let key: string | null = null;
let source = "manual-cli";
let eventType = "platform-infra.kafka.shadow-produce.v1";
let eventType = "platform-infra.kafka.diagnostic-produce.v1";
let payload: string | null = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
@@ -2026,7 +2031,7 @@ function boundedGroupCleanupLimit(kafka: PlatformKafkaConfig, limit: number | nu
return value;
}
function buildShadowPayload(kafka: PlatformKafkaConfig, target: KafkaTarget, options: KafkaProduceOptions, topic: string): string {
function buildDiagnosticPayload(kafka: PlatformKafkaConfig, target: KafkaTarget, options: KafkaProduceOptions, topic: string): string {
let userPayload: unknown = null;
if (options.payload !== null) {
const trimmed = options.payload.trim();
@@ -2042,20 +2047,19 @@ function buildShadowPayload(kafka: PlatformKafkaConfig, target: KafkaTarget, opt
eventId: `evt_${target.id}_${Date.now()}_${Math.random().toString(16).slice(2)}`,
schemaVersion: 1,
eventType: options.eventType,
producer: kafka.management.shadowProducerId,
producer: kafka.management.diagnosticProducerId,
source: options.source,
node: target.id,
namespace: target.namespace,
topic,
mode: "shadow-produce-only",
consumeEnabled: false,
mode: kafka.defaults.switch.mode,
appIntegrationEnabled: kafka.defaults.switch.appIntegrationEnabled,
shadowProduceEnabled: kafka.defaults.switch.shadowProduceEnabled,
shadowConsumeEnabled: kafka.defaults.switch.shadowConsumeEnabled,
produceEnabled: kafka.defaults.switch.produceEnabled,
consumeEnabled: kafka.defaults.switch.consumeEnabled,
partitionKey: options.key ?? null,
occurredAt: new Date().toISOString(),
payload: userPayload ?? {
kind: "manual-shadow-produce",
kind: "manual-diagnostic-produce",
spec: kafka.metadata.spec,
valuesPrinted: false,
},