Merge pull request #525 from pikasTech/fix/device-pod-profile-bootstrap-20260527

fix: bootstrap device pod profiles from canonical skill
This commit is contained in:
Lyon
2026-05-28 08:50:31 +08:00
committed by GitHub
10 changed files with 408 additions and 33 deletions
+1 -1
View File
@@ -220,7 +220,7 @@
"NO_PROXY": "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com",
"no_proxy": "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com",
"OPENAI_API_KEY": "secretRef:hwlab-code-agent-provider/openai-api-key",
"HWLAB_CODE_AGENT_SKILLS_DIRS": "/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills",
"HWLAB_CODE_AGENT_SKILLS_DIRS": "/app/skills",
"HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE": "deepseek",
"HWLAB_CODE_AGENT_DEEPSEEK_MODEL": "deepseek-chat",
"HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL": "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses",
+1 -1
View File
@@ -216,7 +216,7 @@
},
{
"name": "HWLAB_CODE_AGENT_SKILLS_DIRS",
"value": "/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills"
"value": "/app/skills"
},
{
"name": "OPENAI_API_KEY",
+3 -1
View File
@@ -60,6 +60,8 @@ HWLAB coder / code agent runner 镜像必须通过正式 CI/CD 预装 device-pod
- **device-pod-cli 入口**runner 内必须预装 `device-pod-cli` skill,稳定入口为 `/app/skills/device-pod-cli/scripts/device-pod-cli.mjs`,其实现指向 `/app/tools/device-pod-cli.mjs`。code agent 应通过该 CLI 读取 workspace 下 `.device-pod/<devicePodId>.json`,再走 cloud-api/gateway/device-host-cli 调用设备能力。
- **device-host-cli 资产**runner 内必须随 `device-pod-cli` skill 打包 Windows 侧自包含 host CLI 资产,稳定路径为 `/app/skills/device-pod-cli/assets/device-host-cli.mjs`。新 Windows 硬件 PC 接入时,不运行独立安装器;code agent 使用预装 cmd 透传入口把该资产发送到目标 workspace 的 `tools\device-host-cli.mjs`,再通过同一 cmd 透传入口执行 `node tools\device-host-cli.mjs health` 验证。
`/app/skills` 是 HWLAB coder 镜像内 code agent skill 的唯一 canonical 位置。`device-pod-cli` 不再同步到 `/root/.agents/skills``/home/ubuntu/.agents/skills` 或 workspace 下的 skill 副本;这些副本会造成 discovery 口径、wrapper 相对路径和 CI/CD 预装内容不同步。默认 prompt、`HWLAB_CODE_AGENT_SKILLS_DIRS`、skill discovery 和验收都必须指向 `/app/skills`。如果某个运行时只能读取 home 目录,应先修复 runner 的 skill discovery 或环境变量,而不是复制第二份 skill。
`device-host-cli` 不是一次性预装脚本,而是 HWLAB 内部 code agent 可继续热开发的 host 侧工具。DeepSeek、Codex runner 或其他 HWLAB 内部 code agent 在真实硬件闭环中遇到 Keil、debug probe、串口、文件工作区或硬件启动路径不顺手时,优先在目标 Windows workspace 的 `tools\device-host-cli.mjs` 上新增或修复具名设备能力,并通过 `device-pod-cli -> cloud-api -> gateway -> device-host-cli` 的真实链路热验证;验证通过后再把同一实现回填到 `/app/skills/device-pod-cli/assets/device-host-cli.mjs` 对应的 HWLAB repo 资产,进入下一次 CI/CD 预装。不得因为 host CLI 暂时不顺手而把 `device-pod-cli` 退化为泛化 cmd/shell 入口。
目标 Windows workspace 的最小布局为:
@@ -71,7 +73,7 @@ HWLAB coder / code agent runner 镜像必须通过正式 CI/CD 预装 device-pod
完成上述布局后,profile 中的 `hostCli` 应指向 `node tools\device-host-cli.mjs``device-pod-cli` 才能稳定执行 workspace、debug-probe 和 io-probe 操作。`device-host-cli` 必须自包含,不能在运行时依赖 Windows 侧 skill 目录;Keil、serial-monitor、mklink、文件编辑等 skill 只允许作为实现参考。
CI/CD 判定口径是:构建产物中同时存在 `/app/tools/tran.mjs``/app/tools/hwlab-gateway-tran.mjs``/app/tools/device-pod-cli.mjs``/app/skills/device-pod-cli/SKILL.md``/app/skills/device-pod-cli/scripts/device-pod-cli.mjs``/app/skills/device-pod-cli/assets/device-host-cli.mjs``node /app/tools/tran.mjs --help` 能展示 cmd/ps/upload/download 透传帮助,且 code agent skill discovery 发现 `device-pod-cli`。只有这些入口都在 runner 中可见,才能称为 HWLAB coder 已具备 device-pod 最小预装能力。
CI/CD 判定口径是:构建产物中同时存在 `/app/tools/tran.mjs``/app/tools/hwlab-gateway-tran.mjs``/app/tools/device-pod-cli.mjs``/app/skills/device-pod-cli/SKILL.md``/app/skills/device-pod-cli/scripts/device-pod-cli.mjs``/app/skills/device-pod-cli/assets/device-host-cli.mjs``node /app/tools/tran.mjs --help` 能展示 cmd/ps/upload/download 透传帮助,`HWLAB_CODE_AGENT_SKILLS_DIRS=/app/skills`且 code agent skill discovery `/app/skills` 发现 `device-pod-cli`。只有这些入口都在 runner 中可见,才能称为 HWLAB coder 已具备 device-pod 最小预装能力。
## 四要素
+1 -9
View File
@@ -1526,15 +1526,7 @@ function resolveSkillDirs(env = process.env, options = {}) {
.filter((dir) => typeof dir === "string" && dir.trim())
.map((dir) => path.resolve(dir.trim())))];
}
return [...new Set([
...configured,
path.join(os.homedir(), ".agents", "skills"),
"/root/.agents/skills",
"/home/ubuntu/.agents/skills",
path.join(repoRoot, "skills")
]
.filter((dir) => typeof dir === "string" && dir.trim())
.map((dir) => path.resolve(dir.trim())))];
return [path.resolve("/app/skills")];
}
function sanitizeErrorField(key, value) {
@@ -791,6 +791,8 @@ test("Code Agent PC gateway prompt reaches Codex stdio instead of internal hardw
const turn = calls.find((call) => call.method === "turn/start");
assert.ok(turn, "Codex stdio turn/start should be called");
assert.match(turn.args.prompt, /\/app\/tools\/hwlab-gateway-tran\.mjs/u);
assert.match(turn.args.prompt, /canonical skill location is \/app\/skills\/device-pod-cli\/SKILL\.md/u);
assert.match(turn.args.prompt, /node \/app\/skills\/device-pod-cli\/scripts\/device-pod-cli\.mjs/u);
assert.match(turn.args.prompt, /gws_DESKTOP-1MHOD9I:\/f\/work/u);
assert.match(turn.args.prompt, /gws_DESKTOP-1MHOD9I:f:\/work\/hwlab\/\.tmp/u);
assert.match(turn.args.prompt, /improve \/app\/tools\/hwlab-gateway-tran\.mjs first/u);
@@ -924,6 +926,8 @@ test("Code Agent Keil gateway prompt is not blocked by M3 IO intent guard", asyn
const turn = calls.find((call) => call.method === "turn/start");
assert.ok(turn, "Codex stdio turn/start should be called");
assert.match(turn.args.prompt, /\/app\/tools\/hwlab-gateway-tran\.mjs/u);
assert.match(turn.args.prompt, /device-pod-cli/u);
assert.match(turn.args.prompt, /\/app\/skills\/device-pod-cli\/SKILL\.md/u);
assert.match(turn.args.prompt, /gws_DESKTOP-1MHOD9I:\/f\/work/u);
assert.match(turn.args.prompt, /gws_DESKTOP-1MHOD9I:f:\/work\/hwlab\/\.tmp/u);
assert.doesNotMatch(turn.args.prompt, /hwlab-gateway-shell\.mjs|--powershell-stdin/u);
+4 -9
View File
@@ -88,6 +88,8 @@ const CODEX_STDIO_BOUNDARY_INSTRUCTIONS = [
"Use the provided workspace and repo-owned Codex stdio session only.",
"Do not read or print secrets, tokens, kubeconfig files, DB URLs, private keys, or raw environment values.",
"For hardware, gateway, box-simu, patch-panel, DAP, PWM, Keil, serial, and Windows skill requests, execute the requested work through the repo-owned Codex stdio session with the available tran wrapper, skill CLI, or project tool that actually reaches the target.",
"For any request that mentions device-pod, device-pod-cli, device-host-cli, a device-pod profile, device-pod-71-freq, device-pod-71-00075-11, or a registered device-pod Keil build/download/UART/debug-probe operation, first use the HWLAB internal skill named device-pod-cli. The canonical skill location is /app/skills/device-pod-cli/SKILL.md; read that manifest and run node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs for the operation.",
"When a matching device-pod profile exists, do not bypass device-pod-cli with direct hwlab-gateway-tran.mjs, Windows Keil skills, serial-monitor skills, keil-cli.py, or ad hoc path discovery. Only drop to tran or Windows-side skills when the device-pod-cli skill explicitly says to bootstrap, install, or repair the lower layer.",
"For registered PC gateway Windows command or skill requests, use the preloaded tran wrapper: node /app/tools/hwlab-gateway-tran.mjs gws_DESKTOP-1MHOD9I:/f/work <cmd|ps|upload|download> [options] -- <args>. The locator before ':' is the gateway session and the path after ':' is the Windows workspace, with /f/work and f:/work both mapping to F:\\work.",
"For Windows cmd, call tran as: node /app/tools/hwlab-gateway-tran.mjs gws_DESKTOP-1MHOD9I:/f/work cmd -- <command>. For PowerShell, call tran as: node /app/tools/hwlab-gateway-tran.mjs gws_DESKTOP-1MHOD9I:/f/work ps -- <script>. The wrapper owns UTF-8, quoting, cwd, EncodedCommand, and stdout/stderr passthrough; do not hand-build nested cmd /c, cd &&, pipes, or quoting workarounds.",
"For reliable Windows file transfer, use tran upload/download instead of pasting large base64 through chat or shell output. Prefer the canonical tool workspace gws_DESKTOP-1MHOD9I:f:/work/hwlab/.tmp when updating PC-side helper files.",
@@ -2716,14 +2718,7 @@ function resolveSkillDirs(env = process.env) {
if (env.HWLAB_CODE_AGENT_SKILLS_STRICT === "1") {
return [...new Set(configured.map((dir) => path.resolve(dir)))];
}
return [...new Set([
...configured,
"/app/skills",
path.join(repoRoot, "skills"),
path.join(os.homedir(), ".agents", "skills"),
"/root/.agents/skills",
"/home/ubuntu/.agents/skills"
].map((dir) => path.resolve(dir)))];
return [path.resolve("/app/skills")];
}
async function skillManifestPaths(skillsDir) {
@@ -3553,7 +3548,7 @@ function childProcessEnv(env = process.env) {
HOME: env.HOME || process.env.HOME || os.homedir(),
CODEX_HOME: resolveCodexHome(env),
CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "hwlab_code_agent",
UNIDESK_SKILLS_PATH: firstNonEmpty(env.HWLAB_CODE_AGENT_SKILLS_DIRS, env.UNIDESK_SKILLS_PATH, "/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills"),
UNIDESK_SKILLS_PATH: "/app/skills",
NO_PROXY: noProxy,
no_proxy: noProxy,
...(env.LANG ? { LANG: env.LANG } : {}),
+1 -1
View File
@@ -147,7 +147,7 @@ export function ensureCodeAgentRuntimeBase(env = process.env) {
env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR ||= "repo-owned";
env.CODEX_HOME ||= codexHome;
env.HWLAB_CODE_AGENT_CODEX_COMMAND ||= codexCommand;
env.HWLAB_CODE_AGENT_SKILLS_DIRS ||= "/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills";
env.HWLAB_CODE_AGENT_SKILLS_DIRS ||= "/app/skills";
for (const target of [path.dirname(workspace), codexHome]) {
try {
+2 -2
View File
@@ -521,7 +521,7 @@ function ensureCodeAgentRuntimeBase() {
process.env.HWLAB_CODE_AGENT_PROVIDER = process.env.HWLAB_CODE_AGENT_PROVIDER || "codex-stdio";
process.env.CODEX_HOME = codexHome;
process.env.HWLAB_CODE_AGENT_CODEX_COMMAND = process.env.HWLAB_CODE_AGENT_CODEX_COMMAND || "/app/node_modules/.bin/codex";
process.env.HWLAB_CODE_AGENT_SKILLS_DIRS = process.env.HWLAB_CODE_AGENT_SKILLS_DIRS || "/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills";
process.env.HWLAB_CODE_AGENT_SKILLS_DIRS = process.env.HWLAB_CODE_AGENT_SKILLS_DIRS || "/app/skills";
mkdirSync(path.dirname(workspace), { recursive: true });
if (!existsSync(workspace)) {
@@ -947,7 +947,7 @@ function dockerfile(baseImage, port, labels = []) {
"ENV HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR=repo-owned",
"ENV HWLAB_CODE_AGENT_CODEX_COMMAND=/app/node_modules/.bin/codex",
"ENV HWLAB_CODE_AGENT_PROVIDER=codex-stdio",
"ENV HWLAB_CODE_AGENT_SKILLS_DIRS=/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills",
"ENV HWLAB_CODE_AGENT_SKILLS_DIRS=/app/skills",
"COPY package.json ./package.json",
"COPY package-lock.json* ./",
"RUN if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && codex_version=\"$(node -p \"require('./node_modules/@openai/codex/package.json').version\")\" && case \"$(uname -m)\" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts \"@openai/codex-linux-x64@npm:@openai/codex@${codex_version}-linux-x64\" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts \"@openai/codex-linux-arm64@npm:@openai/codex@${codex_version}-linux-arm64\" ;; esac",
+254 -1
View File
@@ -1,6 +1,6 @@
---
name: device-pod-cli
description: Use the HWLAB internal device-pod CLI from a HWLAB code agent runner to operate a profiled target workspace, debug probe, and I/O probe through hwlab-gateway and device-host-cli.
description: Use the HWLAB internal device-pod CLI from a HWLAB code agent runner to create/register device-pod profiles and operate a profiled target workspace, debug probe, and I/O probe through hwlab-gateway and device-host-cli.
version: 0.1.0-mvp
---
@@ -18,6 +18,168 @@ Use this skill when the task mentions `device-pod-cli`, `device-pod`,
`device-host-cli`, HWLAB device-pod profile files, Keil build/download through a
device pod, debug probe control, or I/O probe reads such as UART boot logs.
Canonical skill location: `/app/skills/device-pod-cli/SKILL.md`. Do not look
for or create duplicate skill copies under `/root/.agents/skills`,
`/home/ubuntu/.agents/skills`, or `/workspace/hwlab/skills`; those locations are
not the HWLAB runner contract and cause stale instructions.
## DeepSeek Operation Friction Rules
- For build, download, UART, and workspace actions, use `device-pod-cli` first.
Do not start by calling `/app/tools/hwlab-gateway-tran.mjs` directly, do not
hunt for Windows-side Keil skills, and do not call `keil-cli.py` directly when
a matching device-pod profile exists.
- The profile already carries `hostWorkspaceRoot`, `projectWorkspace`,
`debugInterface.uv4Path`, probe UID, UART port, and gateway route. Let
`device-pod-cli -> device-host-cli` apply those values; do not rediscover the
Windows project path with `dir`, `Get-ChildItem`, or ad hoc `cmd` probes.
- If a command returns `profile-missing`, run profile bootstrap. If it returns a
device-host-cli JSON failure, fix the named device-pod operation or host CLI;
do not bypass the profile with generic gateway shell.
Fast build smoke for `device-pod-71-freq`:
```sh
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile show --pod-id device-pod-71-freq
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:workspace:/ build start
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:workspace:/ build status <jobId>
```
The build status JSON must be treated as the evidence. Report
`hostJson.status`, `hostJson.result.success`, `hostJson.result.exitCode`,
`hostJson.result.project`, `hostJson.result.target`, and the output artifact
paths from the profile; do not describe Windows Keil skill config as the source
of truth for a device-pod build.
## DeepSeek Profile-Recreate Task
When a HWLAB runner asks DeepSeek or another code agent to delete and recreate a
profile, this SKILL.md is the operating procedure. Do not stop after listing the
workspace or describing files. Execute the profile bootstrap and validation
commands, then report the resulting JSON evidence.
For `device-pod-71-00075-11`, the required order is:
1. Prove the profile is missing with
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs health --pod-id device-pod-71-00075-11`.
2. Create `/workspace/hwlab/.device-pod/device-pod-71-00075-11.json` with
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile create --pod-id device-pod-71-00075-11 --force`.
3. Upload `/app/skills/device-pod-cli/assets/device-host-cli.mjs` and the new
profile to `gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11` using
`/app/tools/tran.mjs`.
4. Verify Windows host health through `node tools\device-host-cli.mjs --profile
.device-pod\device-pod-71-00075-11.json --pod-id device-pod-71-00075-11
health`.
5. Run `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile list`
and report the profile `count` plus the hash of each profile.
6. Verify runner health, selector dry-runs, workspace `ls`, build start/status,
download start/status, and UART read/write/read through `device-pod-cli`.
Any failure must be reported as `FAIL` with the exact command and the JSON
`blocker` or stderr/stdout summary. A successful bootstrap report must include
runner profile path, profile SHA-256, `profile list` count, host health or the
observed gateway blocker, runner health, and both selector dry-run results.
Full hardware validation reports must additionally include build job id,
download job id, download `Verify OK` evidence, and UART byte count/summary.
For the first bootstrap turn, run this block exactly. It intentionally performs
only missing-state proof, runner profile creation, host asset/profile upload,
host health, runner health, and selector dry-run checks; run build/download/UART
in a second turn after this block succeeds.
Preferred one-line flow for DeepSeek runners: use the CLI-owned profile template
instead of hand-copying JSON. This avoids heredoc/quoting drift in model-run
shells and still creates the same explicit profile under the persistent profile
directory.
```sh
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile create --pod-id device-pod-71-00075-11 --force
node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload /app/skills/device-pod-cli/assets/device-host-cli.mjs tools/device-host-cli.mjs
node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload /workspace/hwlab/.device-pod/device-pod-71-00075-11.json .device-pod/device-pod-71-00075-11.json
node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 cmd -- node tools\device-host-cli.mjs --profile .device-pod\device-pod-71-00075-11.json --pod-id device-pod-71-00075-11 health
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs health --pod-id device-pod-71-00075-11
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile list
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-00075-11:workspace:/ ls --dry-run
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:workspace:/ ls --dry-run
```
The lower-level JSON block below is a reference for the CLI template and for
manual recovery only.
```sh
set -eu
skill_file=/app/skills/device-pod-cli/SKILL.md
grep -n "DeepSeek Profile-Recreate Task" "$skill_file"
set +e
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs health --pod-id device-pod-71-00075-11
missing_exit=$?
set -e
if [ "$missing_exit" -eq 0 ]; then
echo "FAIL: expected profile-missing before bootstrap" >&2
exit 10
fi
profile_dir="${DEVICE_POD_PROFILE_DIR:-/workspace/hwlab/.device-pod}"
profile_file="$profile_dir/device-pod-71-00075-11.json"
mkdir -p "$profile_dir"
cat > "$profile_file" <<'JSON'
{
"schemaVersion": 1,
"podId": "device-pod-71-00075-11",
"devicePodId": "device-pod-71-00075-11",
"workspaceRoot": "F:\\Work\\ConStart\\projects\\71-00075-11",
"target": {
"id": "constart-71-00075-11",
"mcu": "STM32H723ZGTx",
"flashBase": "0x08000000"
},
"projectWorkspace": {
"projectPath": "FirmWare/MDK-ARM/FREQ_Controller_FW.uvprojx",
"targetName": "FREQ_Controller_FW",
"hexPath": "FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.hex",
"mapPath": "FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.map"
},
"debugInterface": {
"id": "debug-probe",
"type": "cmsis-dap",
"probeUid": "3FD750C63E342E24",
"uv4Path": "C:\\Keil_v5\\UV4\\UV4.exe",
"programBackend": "keil-headless",
"autoBindUvoptx": true
},
"ioInterface": {
"uart": [
{
"id": "uart/1",
"scope": "external",
"port": "COM4",
"baudRate": 921600,
"encoding": "utf8",
"description": "ConStart debug USART1 via CMSIS-DAP CDC UART (verified COM4)"
}
]
},
"cloudApiUrl": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"gatewaySessionId": "gws_DESKTOP-1MHOD9I",
"resourceId": "res_windows_host",
"capabilityId": "cap_windows_cmd_exec",
"hostWorkspaceRoot": "F:\\Work\\ConStart\\projects\\71-00075-11",
"hostCli": "node tools\\device-host-cli.mjs"
}
JSON
sha256sum "$profile_file"
node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload /app/skills/device-pod-cli/assets/device-host-cli.mjs tools/device-host-cli.mjs
node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload "$profile_file" .device-pod/device-pod-71-00075-11.json
node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 cmd -- node tools\device-host-cli.mjs --profile .device-pod\device-pod-71-00075-11.json --pod-id device-pod-71-00075-11 health
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs health --pod-id device-pod-71-00075-11
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile list
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-00075-11:workspace:/ ls --dry-run
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:workspace:/ ls --dry-run
```
## Runtime Contract
- The preinstalled CLI entrypoint is
@@ -56,6 +218,97 @@ device pod, debug probe control, or I/O probe reads such as UART boot logs.
through the real device-pod/gateway route first, then copy the result back into
the HWLAB repo asset for the next CI/CD preinstall.
## Profile Bootstrap
A missing profile is a normal bootstrap state. Do not switch to another profile
and do not guess defaults. Create the selected device-pod profile explicitly in
both places below, then verify it before running build, download, or UART I/O.
- Runner profile: `/workspace/hwlab/.device-pod/<devicePodId>.json`. This is
the persistent HWLAB code agent workspace profile used by `device-pod-cli`.
- Windows host profile: `<hostWorkspaceRoot>\.device-pod\<devicePodId>.json`.
This is the matching profile loaded by `device-host-cli` after the gateway
command reaches the Windows hardware PC.
- Windows host CLI: `<hostWorkspaceRoot>\tools\device-host-cli.mjs`. Upload the
packaged `/app/skills/device-pod-cli/assets/device-host-cli.mjs` asset when a
new profile is bootstrapped or after the host CLI has been hot-fixed.
Minimum profile fields are `schemaVersion`, `podId` or `devicePodId`,
`target.id`, `cloudApiUrl`, `gatewaySessionId`, `resourceId`, `capabilityId`,
`hostWorkspaceRoot`, and `hostCli`. Keep the project, debug probe, and I/O probe
sections explicit so later agents do not depend on hidden CLI overrides.
For G14 DEV ConStart `device-pod-71-00075-11`, use this known hardware profile.
This example is intentionally complete so a DeepSeek runner can recreate the
profile after `/workspace/hwlab/.device-pod/device-pod-71-00075-11.json` and the
matching Windows profile have been deleted.
```sh
profile_dir="${DEVICE_POD_PROFILE_DIR:-/workspace/hwlab/.device-pod}"
profile_file="$profile_dir/device-pod-71-00075-11.json"
mkdir -p "$profile_dir"
cat > "$profile_file" <<'JSON'
{
"schemaVersion": 1,
"podId": "device-pod-71-00075-11",
"devicePodId": "device-pod-71-00075-11",
"workspaceRoot": "F:\\Work\\ConStart\\projects\\71-00075-11",
"target": {
"id": "constart-71-00075-11",
"mcu": "STM32H723ZGTx",
"flashBase": "0x08000000"
},
"projectWorkspace": {
"projectPath": "FirmWare/MDK-ARM/FREQ_Controller_FW.uvprojx",
"targetName": "FREQ_Controller_FW",
"hexPath": "FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.hex",
"mapPath": "FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.map"
},
"debugInterface": {
"id": "debug-probe",
"type": "cmsis-dap",
"probeUid": "3FD750C63E342E24",
"uv4Path": "C:\\Keil_v5\\UV4\\UV4.exe",
"programBackend": "keil-headless",
"autoBindUvoptx": true
},
"ioInterface": {
"uart": [
{
"id": "uart/1",
"scope": "external",
"port": "COM4",
"baudRate": 921600,
"encoding": "utf8",
"description": "ConStart debug USART1 via CMSIS-DAP CDC UART (verified COM4)"
}
]
},
"cloudApiUrl": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"gatewaySessionId": "gws_DESKTOP-1MHOD9I",
"resourceId": "res_windows_host",
"capabilityId": "cap_windows_cmd_exec",
"hostWorkspaceRoot": "F:\\Work\\ConStart\\projects\\71-00075-11",
"hostCli": "node tools\\device-host-cli.mjs"
}
JSON
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs health --pod-id device-pod-71-00075-11
node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload /app/skills/device-pod-cli/assets/device-host-cli.mjs tools/device-host-cli.mjs
node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 upload "$profile_file" .device-pod/device-pod-71-00075-11.json
node /app/tools/tran.mjs gws_DESKTOP-1MHOD9I:/f/Work/ConStart/projects/71-00075-11 cmd -- node tools\\device-host-cli.mjs --profile .device-pod\\device-pod-71-00075-11.json --pod-id device-pod-71-00075-11 health
```
After bootstrap, validate selector isolation before touching hardware:
```text
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-00075-11:workspace:/ ls --dry-run
node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:workspace:/ ls --dry-run
```
The two dry-run outputs must reference different `--profile
.device-pod\<devicePodId>.json` host arguments and different profile hashes.
## Windows Host CLI Bootstrap
Do not run a separate installer. To bring up a new Windows hardware PC, send the
+137 -8
View File
@@ -38,6 +38,7 @@ function classifyBlocker(message) {
function parseTarget(raw) {
if (!raw || raw === '--help' || raw === 'help') return { kind: 'help' };
if (raw === 'health') return { kind: 'health' };
if (raw === 'profile') return { kind: 'profile' };
const match = String(raw).match(/^([^:]+):(workspace|debug-probe|io-probe)(?::(.*))?$/u);
if (!match) throw new Error(`invalid target selector: ${raw}`);
return { kind: 'invoke', podId: match[1], surface: match[2], path: match[3] || '' };
@@ -104,6 +105,112 @@ function hostProfilePath(profile) {
return profile.hostProfilePath || `.device-pod\\${profile.podId}.json`;
}
function writeJsonFile(file, value) {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}
function conStart710007511Profile(overrides = {}) {
return {
schemaVersion: 1,
podId: 'device-pod-71-00075-11',
devicePodId: 'device-pod-71-00075-11',
workspaceRoot: 'F:\\Work\\ConStart\\projects\\71-00075-11',
target: {
id: 'constart-71-00075-11',
mcu: 'STM32H723ZGTx',
flashBase: '0x08000000'
},
projectWorkspace: {
projectPath: 'FirmWare/MDK-ARM/FREQ_Controller_FW.uvprojx',
targetName: 'FREQ_Controller_FW',
hexPath: 'FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.hex',
mapPath: 'FirmWare/MDK-ARM/FREQ_Controller_FW/FREQ_Controller_FW.map'
},
debugInterface: {
id: 'debug-probe',
type: 'cmsis-dap',
probeUid: '3FD750C63E342E24',
uv4Path: 'C:\\Keil_v5\\UV4\\UV4.exe',
programBackend: 'keil-headless',
autoBindUvoptx: true
},
ioInterface: {
uart: [
{
id: 'uart/1',
scope: 'external',
port: 'COM4',
baudRate: 921600,
encoding: 'utf8',
description: 'ConStart debug USART1 via CMSIS-DAP CDC UART (verified COM4)'
}
]
},
cloudApiUrl: 'http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667',
gatewaySessionId: 'gws_DESKTOP-1MHOD9I',
resourceId: 'res_windows_host',
capabilityId: 'cap_windows_cmd_exec',
hostWorkspaceRoot: 'F:\\Work\\ConStart\\projects\\71-00075-11',
hostCli: 'node tools\\device-host-cli.mjs',
...overrides
};
}
function profileTemplate(podId) {
if (podId === 'device-pod-71-00075-11') return conStart710007511Profile();
throw new Error(`unsupported profile template: ${podId}`);
}
function createProfile(parsed) {
const subcommand = parsed._[0] || '';
if (subcommand !== 'create') throw new Error(`unsupported profile command: ${subcommand || '(empty)'}`);
const podId = parsed.podId || parsed._[1];
if (!podId) throw new Error('profile create requires --pod-id <devicePodId>');
const profile = profileTemplate(podId);
const file = parsed.output || path.join(PROFILE_DIR, `${profile.devicePodId}.json`);
if (fs.existsSync(file) && parsed.force !== true) throw new Error(`profile already exists: ${file}; pass --force to overwrite`);
writeJsonFile(file, profile);
const raw = fs.readFileSync(file, 'utf8');
print({ ok: true, action: 'profile.create', status: 'succeeded', profilePath: file, profileHash: sha256(raw), devicePodId: profile.devicePodId, targetId: profile.target.id, route: routeSummary(profile), next: [
`node /app/tools/tran.mjs ${profile.gatewaySessionId}:/f/Work/ConStart/projects/71-00075-11 upload /app/skills/device-pod-cli/assets/device-host-cli.mjs tools/device-host-cli.mjs`,
`node /app/tools/tran.mjs ${profile.gatewaySessionId}:/f/Work/ConStart/projects/71-00075-11 upload ${file} .device-pod/${profile.devicePodId}.json`,
`node /app/tools/tran.mjs ${profile.gatewaySessionId}:/f/Work/ConStart/projects/71-00075-11 cmd -- node tools\\device-host-cli.mjs --profile .device-pod\\${profile.devicePodId}.json --pod-id ${profile.devicePodId} health`
] });
}
function listProfiles() {
const entries = fs.existsSync(PROFILE_DIR)
? fs.readdirSync(PROFILE_DIR).filter((name) => name.endsWith('.json')).sort()
: [];
const items = entries.map((name) => {
const file = path.join(PROFILE_DIR, name);
try {
const raw = fs.readFileSync(file, 'utf8');
const profile = JSON.parse(raw);
const devicePodId = profile.devicePodId || profile.podId || path.basename(name, '.json');
const targetId = profile.targetId || profile.target?.id || profile.deviceTarget?.targetId || profile.deviceTarget?.id || null;
return { ok: true, devicePodId, targetId, profilePath: file, profileHash: sha256(raw), gatewaySessionId: profile.gatewaySessionId || null, hostWorkspaceRoot: profile.hostWorkspaceRoot || profile.workspaceRoot || null };
} catch (error) {
return { ok: false, devicePodId: path.basename(name, '.json'), profilePath: file, error: error instanceof Error ? error.message : String(error) };
}
});
print({ ok: true, action: 'profile.list', status: 'succeeded', profileDir: PROFILE_DIR, count: items.length, items });
}
function showProfile(parsed) {
const profile = readProfile(parsed.podId || parsed._[1]);
print({ ok: true, action: 'profile.show', status: 'succeeded', ...profileSummary(profile), route: routeSummary(profile), projectWorkspace: profile.projectWorkspace || null, debugInterface: profile.debugInterface || null, ioInterface: profile.ioInterface || null, hostProfilePath: hostProfilePath(profile) });
}
function handleProfileCommand(parsed) {
const subcommand = parsed._[0] || 'list';
if (subcommand === 'list') return listProfiles();
if (subcommand === 'show') return showProfile(parsed);
if (subcommand === 'create') return createProfile(parsed);
throw new Error(`unsupported profile command: ${subcommand}`);
}
const CONTROL_OPTION_KEYS = new Set([
'approved',
'reason',
@@ -228,15 +335,36 @@ function compactDispatch(dispatch) {
return { status: dispatch.status || dispatch.dispatchStatus, exitCode: dispatch.exitCode, timedOut: dispatch.timedOut, durationMs: dispatch.durationMs, stdoutTruncated: dispatch.stdoutTruncated, stderrTruncated: dispatch.stderrTruncated, stderr: dispatch.stderr || '', auditId: dispatch.auditId, evidenceId: dispatch.evidenceId };
}
function hostJsonSummary(hostJson) {
if (!hostJson || typeof hostJson !== 'object') return {};
const data = hostJson.data && typeof hostJson.data === 'object' ? hostJson.data : {};
const result = hostJson.result && typeof hostJson.result === 'object'
? hostJson.result
: data.result && typeof data.result === 'object'
? data.result
: {};
const jobId = data.jobId || hostJson.jobId || hostJson.job_id || result.jobId || result.job_id || null;
const status = data.status || hostJson.status || result.status || null;
return {
hostJobId: jobId,
hostStatus: status,
hostSuccess: typeof hostJson.ok === 'boolean' ? hostJson.ok : result.success,
hostResult: result && Object.keys(result).length > 0 ? result : undefined
};
}
function help() {
print({ ok: true, action: 'help', scope: 'HWLAB internal code agents only; not for external UniDesk workspaces.', usage: [
'node tools/device-pod-cli.mjs health',
'node tools/device-pod-cli.mjs device-pod-71-freq:workspace:/ ls',
'node tools/device-pod-cli.mjs device-pod-71-freq:workspace:/firmware cat main.c',
'node tools/device-pod-cli.mjs device-pod-71-freq:workspace:/ apply-patch --approved --reason "DEV edit" < patch.diff',
'node tools/device-pod-cli.mjs device-pod-71-freq:workspace:/ build start|status <jobId>',
'node tools/device-pod-cli.mjs device-pod-71-freq:debug-probe chip-id|status|download|reset|launch-flash --approved --reason "DEV smoke"',
'node tools/device-pod-cli.mjs device-pod-71-freq:io-probe:/uart/1 ports|read|read-after-launch-flash|write --approved --reason "DEV io"'
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile list',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile show --pod-id device-pod-71-freq',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile create --pod-id device-pod-71-00075-11 --force',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs health --pod-id device-pod-71-freq',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:workspace:/ ls',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:workspace:/ apply-patch --approved --reason "DEV edit" < patch.diff',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:workspace:/ build start',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:workspace:/ build status <jobId>',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:debug-probe download start --approved --reason "DEV smoke"',
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs device-pod-71-freq:io-probe:/uart/1 read --duration-ms 5000'
] });
}
@@ -245,6 +373,7 @@ async function main() {
const selector = parseTarget(rawTarget);
if (selector.kind === 'help') return help();
const parsed = parseOptions(argv);
if (selector.kind === 'profile') return handleProfileCommand(parsed);
const profile = readProfile(selector.podId || parsed.podId);
if (selector.kind === 'health') return print({ ok: true, action: 'health', status: 'succeeded', ...profileSummary(profile), route: routeSummary(profile) });
const built = await buildHostCommand(selector, parsed);
@@ -253,7 +382,7 @@ async function main() {
if (parsed.dryRun) return print({ ok: true, action: 'device-pod.plan', status: 'planned', selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile) });
const result = await invoke(profile, command, parsed);
const ok = result.status === 'succeeded';
print({ ok, action: 'device-pod.invoke', status: result.status, selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile), traceId: result.traceId, operationId: result.operationId, requestId: result.requestId, httpStatus: result.httpStatus, dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch), hostJson: result.hostJson, evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId }, gatewayBody: parsed.full ? result.gatewayBody : undefined }, ok ? 0 : 1);
print({ ok, action: 'device-pod.invoke', status: result.status, selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile), traceId: result.traceId, operationId: result.operationId, requestId: result.requestId, httpStatus: result.httpStatus, ...hostJsonSummary(result.hostJson), dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch), hostJson: result.hostJson, evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId }, gatewayBody: parsed.full ? result.gatewayBody : undefined }, ok ? 0 : 1);
}
main().catch((error) => fail('device-pod-cli', error));