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:
+1
-1
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 最小预装能力。
|
||||
|
||||
## 四要素
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user