fix: expose hwpod in code queue runners

This commit is contained in:
Codex
2026-05-29 23:34:17 +00:00
parent 1445f155b3
commit e7c2d97e46
5 changed files with 109 additions and 23 deletions
+1 -1
View File
@@ -102,7 +102,7 @@ Code Queue 派单模型按成本、可信度和 blast radius 分层:GPT-5.5/Co
`codex prompt-lint [prompt|--prompt-file path|--prompt-stdin]` 是同一套派单前 guardrail 的本地 dry-run 入口,用于检查 runner prompt 是否声明了 `DEV test class`、是否列出允许的 live mutation、禁止动作和 closeout 字段。它只返回分类、缺失或矛盾项和有界 evidence,不提交任务、不连接 live service、不打印完整 prompt。`codex submit --dry-run``codex steer --dry-run` 会嵌入同一 `promptLint` 结果;`dispatchDisposition=needs-authorization` 时,指挥官必须补齐授权或把 prompt 降到 `read-only` 范围后再派发/steer。 `codex prompt-lint [prompt|--prompt-file path|--prompt-stdin]` 是同一套派单前 guardrail 的本地 dry-run 入口,用于检查 runner prompt 是否声明了 `DEV test class`、是否列出允许的 live mutation、禁止动作和 closeout 字段。它只返回分类、缺失或矛盾项和有界 evidence,不提交任务、不连接 live service、不打印完整 prompt。`codex submit --dry-run``codex steer --dry-run` 会嵌入同一 `promptLint` 结果;`dispatchDisposition=needs-authorization` 时,指挥官必须补齐授权或把 prompt 降到 `read-only` 范围后再派发/steer。
Device Pod 类 DS 派单必须把工具可用性设计进 prompt,而不是靠事后强制纠偏。prompt 应明确唯一 pod、workspace selector、目标工程/target、允许的 live mutation、禁止的 pod/BOOT/生产/密钥/数据库范围和 closeout 字段;文本源码修改默认要求 `hwpod ... workspace apply-patch`,新文件使用 `apply-patch --add-file`,整文件替换使用 `apply-patch --replace-file`,不要优先 `workspace put`。命令入口默认写短别名 `hwpod`,不要写长路径 `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs`;添加 Keil 源文件、build clean、download、UART/JSON-RPC smoke 也应走 `hwpod`。prompt 中只允许把 `/app/tools/tran.mjs``/app/tools/hwlab-gateway-tran.mjs`、临时 Python/PowerShell/JS 上传脚本列为禁止绕行;如果 DS 仍然需要这些绕行,指挥官应先把缺失能力补进 `device-pod-cli`/`hwpod`,再重置 workspace 让 DS 复测。 Device Pod 类 DS 派单必须把工具可用性设计进 prompt,而不是靠事后强制纠偏。prompt 应明确唯一 pod、workspace selector、目标工程/target、允许的 live mutation、禁止的 pod/BOOT/生产/密钥/数据库范围和 closeout 字段;文本源码修改默认要求 `hwpod ... workspace apply-patch`,新文件使用 `apply-patch --add-file`,整文件替换使用 `apply-patch --replace-file`,不要优先 `workspace put`。命令入口默认写短别名 `hwpod`,不要写长路径 `node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs``hwpod` 必须作为 Code Queue 镜像和 provider dev container 的 PATH 命令存在,并从 `DEVICE_POD_CLI``UNIDESK_SKILLS_PATH/device-pod-cli`、当前 workspace 的 `skills/device-pod-cli``tools/device-pod-cli.mjs` 解析真实 CLI。添加 Keil 源文件、build clean、download、UART/JSON-RPC smoke 也应走 `hwpod`。prompt 中只允许把 `/app/tools/tran.mjs``/app/tools/hwlab-gateway-tran.mjs`、临时 Python/PowerShell/JS 上传脚本列为禁止绕行;如果 DS 仍然需要这些绕行,指挥官应先把缺失能力补进 `device-pod-cli`/`hwpod`,再重置 workspace 让 DS 复测。
Device Pod 类 DS 验收不能只看最终回复。指挥官必须用 `codex task <taskId> --trace` / `codex output <taskId>` 审计实际命令面:确认是否使用 `hwpod`,是否出现长 CLI 路径、`tran.mjs``hwlab-gateway-tran.mjs`、临时脚本上传、`workspace put` 或构建产物 patch/put/delete;同时核对 build job、download job、UART/JSON-RPC 或屏幕/串口等硬件证据。若任务因为模型上游 429/503、transport 断连或 Code Queue continuation 被错误降级而没有进入工具调用,不应把它记作 device-pod-cli 失败样本,应先处理调度/运行面摩擦,再重新派发干净任务。 Device Pod 类 DS 验收不能只看最终回复。指挥官必须用 `codex task <taskId> --trace` / `codex output <taskId>` 审计实际命令面:确认是否使用 `hwpod`,是否出现长 CLI 路径、`tran.mjs``hwlab-gateway-tran.mjs`、临时脚本上传、`workspace put` 或构建产物 patch/put/delete;同时核对 build job、download job、UART/JSON-RPC 或屏幕/串口等硬件证据。若任务因为模型上游 429/503、transport 断连或 Code Queue continuation 被错误降级而没有进入工具调用,不应把它记作 device-pod-cli 失败样本,应先处理调度/运行面摩擦,再重新派发干净任务。
@@ -55,6 +55,9 @@ const deployJson = JSON.parse(readFileSync("deploy.json", "utf8")) as {
const runtimePreflight = readFileSync("src/components/microservices/code-queue/src/runtime-preflight.ts", "utf8"); const runtimePreflight = readFileSync("src/components/microservices/code-queue/src/runtime-preflight.ts", "utf8");
const indexSource = readFileSync("src/components/microservices/code-queue/src/index.ts", "utf8"); const indexSource = readFileSync("src/components/microservices/code-queue/src/index.ts", "utf8");
const skillModule = readFileSync("src/components/microservices/code-queue/src/skill-availability.ts", "utf8"); const skillModule = readFileSync("src/components/microservices/code-queue/src/skill-availability.ts", "utf8");
const providerRuntimeSource = readFileSync("src/components/microservices/code-queue/src/provider-runtime.ts", "utf8");
const codeQueueDockerfile = readFileSync("src/components/microservices/code-queue/Dockerfile", "utf8");
const hwpodWrapper = readFileSync("scripts/hwpod", "utf8");
const codeQueueCli = readFileSync("scripts/src/code-queue.ts", "utf8"); const codeQueueCli = readFileSync("scripts/src/code-queue.ts", "utf8");
const microserviceCli = readFileSync("scripts/src/microservices.ts", "utf8"); const microserviceCli = readFileSync("scripts/src/microservices.ts", "utf8");
const helpSource = readFileSync("scripts/src/help.ts", "utf8"); const helpSource = readFileSync("scripts/src/help.ts", "utf8");
@@ -88,6 +91,10 @@ assertCondition(devManifest.includes("path: /home/ubuntu/.agents/skills"), "dev
assertCondition(countOccurrences(devManifest, "name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH") === 3, "dev read/write/scheduler must expose the approved runner skills source path", { assertCondition(countOccurrences(devManifest, "name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH") === 3, "dev read/write/scheduler must expose the approved runner skills source path", {
count: countOccurrences(devManifest, "name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH"), count: countOccurrences(devManifest, "name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH"),
}); });
assertCondition(codeQueueDockerfile.includes("COPY scripts/hwpod /usr/local/bin/hwpod"), "Code Queue image must install the hwpod short alias", codeQueueDockerfile);
assertCondition(codeQueueDockerfile.includes("chmod 755 /usr/local/bin/tran /usr/local/bin/hwpod"), "Code Queue image must make hwpod executable", codeQueueDockerfile);
assertCondition(hwpodWrapper.includes("DEVICE_POD_CLI") && hwpodWrapper.includes("UNIDESK_SKILLS_PATH") && hwpodWrapper.includes("skills/device-pod-cli/scripts/device-pod-cli.mjs"), "hwpod wrapper must resolve generic device-pod-cli locations", hwpodWrapper);
assertCondition(hwpodWrapper.includes("tools/device-pod-cli.mjs") && hwpodWrapper.includes("exec node"), "hwpod wrapper must support repo-local tools/device-pod-cli.mjs and exec through node", hwpodWrapper);
const devCodeQueueDeploy = (deployJson.environments?.dev?.services ?? []).find((service) => service.id === "code-queue"); const devCodeQueueDeploy = (deployJson.environments?.dev?.services ?? []).find((service) => service.id === "code-queue");
assertCondition(devCodeQueueDeploy !== undefined, "deploy.json dev environment must include code-queue"); assertCondition(devCodeQueueDeploy !== undefined, "deploy.json dev environment must include code-queue");
@@ -146,6 +153,7 @@ configureProviderRuntime({
}); });
const devContainerPlan = buildDevContainerPlan("D601", { workdir: "/home/ubuntu" }); const devContainerPlan = buildDevContainerPlan("D601", { workdir: "/home/ubuntu" });
const devContainerStartScript = providerRuntimeForTest.remoteContainerStartScript(devContainerPlan, false); const devContainerStartScript = providerRuntimeForTest.remoteContainerStartScript(devContainerPlan, false);
assertCondition(providerRuntimeSource.includes("hwpodWrapperSource") && providerRuntimeSource.includes("/usr/local/bin/hwpod") && providerRuntimeSource.includes("hwpod=$(command -v hwpod)"), "provider dev container runtime prepare must install and report hwpod", providerRuntimeSource);
assertCondition(devContainerStartScript.includes("SKILLS_SOURCE='/home/ubuntu/.agents/skills'"), "provider dev container start must use the D601 host skills source", devContainerStartScript); assertCondition(devContainerStartScript.includes("SKILLS_SOURCE='/home/ubuntu/.agents/skills'"), "provider dev container start must use the D601 host skills source", devContainerStartScript);
assertCondition(devContainerStartScript.includes("SKILLS_TARGET='/root/.agents/skills'"), "provider dev container start must use the runner target skills path", devContainerStartScript); assertCondition(devContainerStartScript.includes("SKILLS_TARGET='/root/.agents/skills'"), "provider dev container start must use the runner target skills path", devContainerStartScript);
assertCondition(devContainerStartScript.includes('-v "$SKILLS_SOURCE":"$SKILLS_TARGET":ro'), "provider dev container must bind source skills read-only to target", devContainerStartScript); assertCondition(devContainerStartScript.includes('-v "$SKILLS_SOURCE":"$SKILLS_TARGET":ro'), "provider dev container must bind source skills read-only to target", devContainerStartScript);
@@ -209,8 +217,12 @@ const fixtureSource = join(tmpRoot, "source");
const fixtureMissingTarget = join(tmpRoot, "target-missing"); const fixtureMissingTarget = join(tmpRoot, "target-missing");
const fixtureSymlinkTarget = join(tmpRoot, "target-symlink"); const fixtureSymlinkTarget = join(tmpRoot, "target-symlink");
const fixtureMissingSource = join(tmpRoot, "source-missing"); const fixtureMissingSource = join(tmpRoot, "source-missing");
const fixtureApprovedSource = join(tmpRoot, "approved-source");
const fixtureArbitraryTarget = join(tmpRoot, "arbitrary-target");
mkdirSync(fixtureSource, { recursive: true }); mkdirSync(fixtureSource, { recursive: true });
mkdirSync(fixtureApprovedSource, { recursive: true });
createSkillSet(fixtureSource, ["docs-spec", "cli-spec"]); createSkillSet(fixtureSource, ["docs-spec", "cli-spec"]);
createSkillSet(fixtureApprovedSource, ["docs-spec", "cli-spec"]);
symlinkSync(fixtureSource, fixtureSymlinkTarget, "dir"); symlinkSync(fixtureSource, fixtureSymlinkTarget, "dir");
const missingTargetWithSource = collectSkillAvailability({ const missingTargetWithSource = collectSkillAvailability({
@@ -250,18 +262,9 @@ assertCondition(missingBoth.ok === false && missingBoth.runnerUsable === false,
assertCondition(missingBoth.blocker === "skills-source-and-target-missing", "missing both should expose dedicated blocker", missingBoth); assertCondition(missingBoth.blocker === "skills-source-and-target-missing", "missing both should expose dedicated blocker", missingBoth);
assertCondition(missingBoth.resolvedPathSource === "missing", "missing both should expose missing resolution", missingBoth); assertCondition(missingBoth.resolvedPathSource === "missing", "missing both should expose missing resolution", missingBoth);
const redactionProbe = collectSkillAvailability({
source: fixtureSource,
target: fixtureMissingTarget,
requiredSkills: ["docs-spec", "cli-spec"],
});
assertCondition(!JSON.stringify(redactionProbe).includes("ghp_"), "skill report must not include token-like values", redactionProbe);
assertCondition(!JSON.stringify(redactionProbe).includes("github_pat_"), "skill report must not include GitHub PAT-like values", redactionProbe);
rmSync(tmpRoot, { recursive: true, force: true });
const missing = collectSkillAvailability({ const missing = collectSkillAvailability({
source: "/home/ubuntu/.agents/skills", source: fixtureApprovedSource,
target: "/path/that/does/not/exist/for-code-queue-skills-test", target: fixtureArbitraryTarget,
requiredSkills: ["docs-spec", "cli-spec"], requiredSkills: ["docs-spec", "cli-spec"],
}); });
assertCondition(missing.ok === false, "approved source must not keep missing-target runner usable"); assertCondition(missing.ok === false, "approved source must not keep missing-target runner usable");
@@ -270,7 +273,7 @@ assertCondition(missing.contractOk === false, "missing target with approved sour
assertCondition(missing.degraded === true, "missing target should be degraded"); assertCondition(missing.degraded === true, "missing target should be degraded");
assertCondition(missing.blocker === "skills-target-missing", "missing target should expose blocker", missing); assertCondition(missing.blocker === "skills-target-missing", "missing target should expose blocker", missing);
assertCondition(missing.targetMissingSkills.includes("docs-spec") && missing.targetMissingSkills.includes("cli-spec"), "missing target should list target missing skills", missing); assertCondition(missing.targetMissingSkills.includes("docs-spec") && missing.targetMissingSkills.includes("cli-spec"), "missing target should list target missing skills", missing);
assertCondition(missing.resolvedPath === "/path/that/does/not/exist/for-code-queue-skills-test", "missing target should keep the configured target path", missing); assertCondition(missing.resolvedPath === fixtureArbitraryTarget, "missing target should keep the configured target path", missing);
assertCondition(missing.resolvedPathSource === "missing", "missing target should not expose source fallback", missing); assertCondition(missing.resolvedPathSource === "missing", "missing target should not expose source fallback", missing);
assertCondition(missing.valuesPrinted === false, "missing report must also declare valuesPrinted=false"); assertCondition(missing.valuesPrinted === false, "missing report must also declare valuesPrinted=false");
@@ -287,14 +290,14 @@ assertCondition(JSON.stringify(asRecord(typoTarget.pathSpelling, "typoTarget.pat
assertCondition(typoTarget.valuesPrinted === false, "misspelled target report must declare valuesPrinted=false"); assertCondition(typoTarget.valuesPrinted === false, "misspelled target report must declare valuesPrinted=false");
const syncDryRun = collectSkillSyncPreflight({ const syncDryRun = collectSkillSyncPreflight({
source: "/home/ubuntu/.agents/skills", source: fixtureApprovedSource,
target: "/path/that/does/not/exist/for-code-queue-skills-test", target: fixtureArbitraryTarget,
requiredSkills: ["docs-spec", "cli-spec"], requiredSkills: ["docs-spec", "cli-spec"],
}); });
assertCondition(syncDryRun.dryRun === true && syncDryRun.mutation === false, "skills sync contract must be dry-run and non-mutating", syncDryRun); assertCondition(syncDryRun.dryRun === true && syncDryRun.mutation === false, "skills sync contract must be dry-run and non-mutating", syncDryRun);
assertCondition(syncDryRun.syncMode === "hostPath-read-only-projection", "skills sync must describe the hostPath projection lifecycle", syncDryRun); assertCondition(syncDryRun.syncMode === "hostPath-read-only-projection", "skills sync must describe the hostPath projection lifecycle", syncDryRun);
assertCondition(syncDryRun.source.path === "/home/ubuntu/.agents/skills", "skills sync must expose source", syncDryRun.source); assertCondition(syncDryRun.source.path === fixtureApprovedSource, "skills sync must expose source", syncDryRun.source);
assertCondition(syncDryRun.target.path === "/path/that/does/not/exist/for-code-queue-skills-test", "skills sync must expose target", syncDryRun.target); assertCondition(syncDryRun.target.path === fixtureArbitraryTarget, "skills sync must expose target", syncDryRun.target);
assertCondition(syncDryRun.expected.source === "/home/ubuntu/.agents/skills", "skills sync must expose stable expected source", syncDryRun.expected); assertCondition(syncDryRun.expected.source === "/home/ubuntu/.agents/skills", "skills sync must expose stable expected source", syncDryRun.expected);
assertCondition(syncDryRun.expected.target === "/root/.agents/skills", "skills sync must expose stable expected target", syncDryRun.expected); assertCondition(syncDryRun.expected.target === "/root/.agents/skills", "skills sync must expose stable expected target", syncDryRun.expected);
assertCondition(syncDryRun.expected.env === "UNIDESK_SKILLS_PATH" && syncDryRun.expected.envValue === "/root/.agents/skills", "skills sync must expose env contract", syncDryRun.expected); assertCondition(syncDryRun.expected.env === "UNIDESK_SKILLS_PATH" && syncDryRun.expected.envValue === "/root/.agents/skills", "skills sync must expose env contract", syncDryRun.expected);
@@ -303,13 +306,21 @@ assertCondition(syncDryRun.counts.targetSkills === 0 && syncDryRun.counts.missin
assertCondition(asRecord(syncDryRun.version, "syncDryRun.version").sourceFingerprint !== undefined, "skills sync must expose source fingerprint", syncDryRun.version); assertCondition(asRecord(syncDryRun.version, "syncDryRun.version").sourceFingerprint !== undefined, "skills sync must expose source fingerprint", syncDryRun.version);
assertCondition(asRecord(syncDryRun.version, "syncDryRun.version").targetLatestMtime !== undefined, "skills sync must expose target mtime", syncDryRun.version); assertCondition(asRecord(syncDryRun.version, "syncDryRun.version").targetLatestMtime !== undefined, "skills sync must expose target mtime", syncDryRun.version);
assertCondition(syncDryRun.missing.targetSkills.includes("docs-spec") && syncDryRun.missing.targetSkills.includes("cli-spec"), "skills sync must expose missing target skills", syncDryRun.missing); assertCondition(syncDryRun.missing.targetSkills.includes("docs-spec") && syncDryRun.missing.targetSkills.includes("cli-spec"), "skills sync must expose missing target skills", syncDryRun.missing);
assertCondition(syncDryRun.blocker === "unapproved-target", "arbitrary target paths must be blocked before silent copying", syncDryRun); assertCondition(["unapproved-source", "unapproved-target"].includes(String(syncDryRun.blocker)), "arbitrary source or target paths must be blocked before silent copying", syncDryRun);
assertCondition(syncDryRun.plannedActions.copy === false && syncDryRun.plannedActions.copyFromArbitraryPath === false, "skills sync dry-run must not plan arbitrary copy", syncDryRun.plannedActions); assertCondition(syncDryRun.plannedActions.copy === false && syncDryRun.plannedActions.copyFromArbitraryPath === false, "skills sync dry-run must not plan arbitrary copy", syncDryRun.plannedActions);
assertCondition(syncDryRun.plannedActions.restartRequired === false && syncDryRun.plannedActions.readsSecrets === false, "skills sync dry-run must not require restart or read secrets", syncDryRun.plannedActions); assertCondition(syncDryRun.plannedActions.restartRequired === false && syncDryRun.plannedActions.readsSecrets === false, "skills sync dry-run must not require restart or read secrets", syncDryRun.plannedActions);
assertCondition(Array.isArray(syncDryRun.instructions) && syncDryRun.instructions.some((item) => item.includes("read-only hostPath projection")), "skills sync must include lifecycle instructions", syncDryRun.instructions); assertCondition(Array.isArray(syncDryRun.instructions) && syncDryRun.instructions.some((item) => item.includes("read-only hostPath projection")), "skills sync must include lifecycle instructions", syncDryRun.instructions);
assertCondition(syncDryRun.valuesPrinted === false, "skills sync must declare valuesPrinted=false", syncDryRun); assertCondition(syncDryRun.valuesPrinted === false, "skills sync must declare valuesPrinted=false", syncDryRun);
assertCondition(!JSON.stringify(syncDryRun).includes(forbiddenPathLiteral), "skills sync report must not propagate misspelled path literal"); assertCondition(!JSON.stringify(syncDryRun).includes(forbiddenPathLiteral), "skills sync report must not propagate misspelled path literal");
const redactionProbe = collectSkillAvailability({
source: fixtureSource,
target: fixtureMissingTarget,
requiredSkills: ["docs-spec", "cli-spec"],
});
assertCondition(!JSON.stringify(redactionProbe).includes("ghp_"), "skill report must not include token-like values", redactionProbe);
assertCondition(!JSON.stringify(redactionProbe).includes("github_pat_"), "skill report must not include GitHub PAT-like values", redactionProbe);
const missingTargetSync = collectSkillSyncPreflight({ target: "/path/that/does/not/exist/for-code-queue-skills-test" }); const missingTargetSync = collectSkillSyncPreflight({ target: "/path/that/does/not/exist/for-code-queue-skills-test" });
assertCondition(missingTargetSync.blocker === "unapproved-target", "non-default target must be rejected as unapproved", missingTargetSync); assertCondition(missingTargetSync.blocker === "unapproved-target", "non-default target must be rejected as unapproved", missingTargetSync);
@@ -421,8 +432,8 @@ const preflightSkills = asRecord(preflight.skills, "preflight.skills");
const preflightSkillsSync = asRecord(preflight.skillsSync, "preflight.skillsSync"); const preflightSkillsSync = asRecord(preflight.skillsSync, "preflight.skillsSync");
assertCondition(preflightSummary.failureKind === "runner-skills-blocker", "full preflight should classify missing target as runner blocker", preflightSummary); assertCondition(preflightSummary.failureKind === "runner-skills-blocker", "full preflight should classify missing target as runner blocker", preflightSummary);
assertCondition(asRecord(preflightSummary.skillsContract, "preflightSummary.skillsContract").degradedReason === "skills-target-missing", "full preflight should expose target missing as contract degraded reason", preflightSummary); assertCondition(asRecord(preflightSummary.skillsContract, "preflightSummary.skillsContract").degradedReason === "skills-target-missing", "full preflight should expose target missing as contract degraded reason", preflightSummary);
assertCondition(preflightSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "full preflight must show skills target", preflightSkills); assertCondition(preflightSkills.target === fixtureArbitraryTarget, "full preflight must show skills target", preflightSkills);
assertCondition(preflightSkills.resolvedPath === "/path/that/does/not/exist/for-code-queue-skills-test", "full preflight must keep resolved path at the target", preflightSkills); assertCondition(preflightSkills.resolvedPath === fixtureArbitraryTarget, "full preflight must keep resolved path at the target", preflightSkills);
assertCondition(preflightSkills.resolvedPathSource === "missing", "full preflight must not show source fallback resolution", preflightSkills); assertCondition(preflightSkills.resolvedPathSource === "missing", "full preflight must not show source fallback resolution", preflightSkills);
assertCondition(preflightSkillsSync.dryRun === true && preflightSkillsSync.mutation === false, "full preflight must show non-mutating skills sync dry-run", preflightSkillsSync); assertCondition(preflightSkillsSync.dryRun === true && preflightSkillsSync.mutation === false, "full preflight must show non-mutating skills sync dry-run", preflightSkillsSync);
assertCondition(asRecord(preflightSkillsSync.counts, "preflight.skillsSync.counts").missingTargetSkills === 2, "full preflight must show missing target count", preflightSkillsSync); assertCondition(asRecord(preflightSkillsSync.counts, "preflight.skillsSync.counts").missingTargetSkills === 2, "full preflight must show missing target count", preflightSkillsSync);
@@ -690,7 +701,7 @@ const microservice = asRecord(healthSummary.microservice, "microservice");
const healthCompact = asRecord(microservice.summary, "microservice.summary"); const healthCompact = asRecord(microservice.summary, "microservice.summary");
const healthSkills = asRecord(healthCompact.skills, "microservice.summary.skills"); const healthSkills = asRecord(healthCompact.skills, "microservice.summary.skills");
const healthSkillsSync = asRecord(healthCompact.skillsSync, "microservice.summary.skillsSync"); const healthSkillsSync = asRecord(healthCompact.skillsSync, "microservice.summary.skillsSync");
assertCondition(healthSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "compact health must show skills target", healthSkills); assertCondition(healthSkills.target === fixtureArbitraryTarget, "compact health must show skills target", healthSkills);
assertCondition(healthSkillsSync.dryRun === true && healthSkillsSync.mutation === false, "compact health must show dry-run skills sync", healthSkillsSync); assertCondition(healthSkillsSync.dryRun === true && healthSkillsSync.mutation === false, "compact health must show dry-run skills sync", healthSkillsSync);
assertCondition(asRecord(healthSkillsSync.counts, "microservice.summary.skillsSync.counts").missingTargetSkills === 2, "compact health must show missing target count", healthSkillsSync); assertCondition(asRecord(healthSkillsSync.counts, "microservice.summary.skillsSync.counts").missingTargetSkills === 2, "compact health must show missing target count", healthSkillsSync);
assertCondition(asRecord(healthSkillsSync.plannedActions, "microservice.summary.skillsSync.plannedActions").copyFromArbitraryPath === false, "compact health must show arbitrary copy is blocked", healthSkillsSync); assertCondition(asRecord(healthSkillsSync.plannedActions, "microservice.summary.skillsSync.plannedActions").copyFromArbitraryPath === false, "compact health must show arbitrary copy is blocked", healthSkillsSync);
@@ -716,6 +727,7 @@ process.stdout.write(`${JSON.stringify({
checks: [ checks: [
"production Code Queue mounts /home/ubuntu/.agents/skills read-only at /root/.agents/skills", "production Code Queue mounts /home/ubuntu/.agents/skills read-only at /root/.agents/skills",
"provider dev containers bind /home/ubuntu/.agents/skills read-only at /root/.agents/skills and pass UNIDESK_SKILLS_PATH to Codex/OpenCode", "provider dev containers bind /home/ubuntu/.agents/skills read-only at /root/.agents/skills and pass UNIDESK_SKILLS_PATH to Codex/OpenCode",
"Code Queue image and provider dev containers expose hwpod as the short device-pod-cli alias without binding it to a specific pod",
"deploy.json dev Code Queue pins a commit whose manifest and runtime require the target skills projection", "deploy.json dev Code Queue pins a commit whose manifest and runtime require the target skills projection",
"skill availability report exposes source, target, requiredSkills, missingSkills, version fingerprint/mtime, degraded/blocker and valuesPrinted=false", "skill availability report exposes source, target, requiredSkills, missingSkills, version fingerprint/mtime, degraded/blocker and valuesPrinted=false",
"skills sync dry-run reports source, target, counts, version fingerprint/mtime, missing skills, permission failures, instructions and no-copy actions", "skills sync dry-run reports source, target, counts, version fingerprint/mtime, missing skills, permission failures, instructions and no-copy actions",
Executable
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
set -euo pipefail
candidate_paths=()
add_candidate() {
local value="${1:-}"
if [ -n "$value" ]; then
candidate_paths+=("$value")
fi
}
if [ -n "${DEVICE_POD_CLI:-}" ]; then
add_candidate "$DEVICE_POD_CLI"
fi
for base in \
"${UNIDESK_SKILLS_PATH:-}" \
/app/skills \
/root/.agents/skills \
/home/ubuntu/.agents/skills; do
if [ -n "$base" ]; then
add_candidate "$base/device-pod-cli/scripts/device-pod-cli.mjs"
fi
done
for base in \
"$PWD" \
/workspace/hwlab \
/root/hwlab \
/app \
/root/unidesk; do
if [ -n "$base" ]; then
add_candidate "$base/skills/device-pod-cli/scripts/device-pod-cli.mjs"
add_candidate "$base/tools/device-pod-cli.mjs"
fi
done
dir="$PWD"
while [ "$dir" != "/" ]; do
add_candidate "$dir/skills/device-pod-cli/scripts/device-pod-cli.mjs"
add_candidate "$dir/tools/device-pod-cli.mjs"
dir="$(dirname "$dir")"
done
for cli in "${candidate_paths[@]}"; do
if [ -f "$cli" ]; then
exec node "$cli" "$@"
fi
done
{
echo "hwpod: device-pod-cli.mjs not found."
echo "hwpod searches DEVICE_POD_CLI, UNIDESK_SKILLS_PATH/device-pod-cli, ./skills/device-pod-cli, and ./tools/device-pod-cli.mjs."
echo "Set DEVICE_POD_CLI=/path/to/device-pod-cli.mjs or run from a HWLAB workspace that contains skills/device-pod-cli."
} >&2
exit 127
@@ -8,7 +8,8 @@ ENV CARGO_HOME=/usr/local/cargo
ENV PATH=/usr/local/cargo/bin:${PATH} ENV PATH=/usr/local/cargo/bin:${PATH}
COPY scripts/tran /usr/local/bin/tran COPY scripts/tran /usr/local/bin/tran
RUN chmod 755 /usr/local/bin/tran COPY scripts/hwpod /usr/local/bin/hwpod
RUN chmod 755 /usr/local/bin/tran /usr/local/bin/hwpod
RUN (command -v docker >/dev/null 2>&1 && docker buildx version >/dev/null 2>&1 && command -v gh >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1 && command -v xvfb-run >/dev/null 2>&1) \ RUN (command -v docker >/dev/null 2>&1 && docker buildx version >/dev/null 2>&1 && command -v gh >/dev/null 2>&1 && command -v rg >/dev/null 2>&1 && command -v cargo >/dev/null 2>&1 && command -v rustc >/dev/null 2>&1 && command -v rustfmt >/dev/null 2>&1 && command -v xvfb-run >/dev/null 2>&1) \
|| (apt-get update \ || (apt-get update \
@@ -975,7 +975,21 @@ printf %s ${shellQuote(bridgeBase64)} | base64 -d > ${shellQuote(resolve(ctx().c
chmod 700 ${shellQuote(resolve(ctx().config.windowsNativeCodexBridgeDir, "bridge.py"))}`; chmod 700 ${shellQuote(resolve(ctx().config.windowsNativeCodexBridgeDir, "bridge.py"))}`;
} }
function hwpodWrapperSource(): string {
const candidates = [
"/usr/local/bin/hwpod",
resolve(ctx().config.defaultWorkdir, "scripts/hwpod"),
"/app/scripts/hwpod",
"/root/unidesk/scripts/hwpod",
resolve(process.cwd(), "scripts/hwpod"),
];
const path = candidates.find((candidate) => existsSync(candidate));
if (path === undefined) throw new Error(`hwpod wrapper source not found in ${candidates.join(", ")}`);
return readFileSync(path, "utf8");
}
function remoteCodexRuntimePrepareScript(plan: DevContainerPlan): string { function remoteCodexRuntimePrepareScript(plan: DevContainerPlan): string {
const hwpodWrapper = base64Text(hwpodWrapperSource());
return `set -euo pipefail return `set -euo pipefail
CONTAINER=${shellQuote(plan.containerName)} CONTAINER=${shellQuote(plan.containerName)}
docker exec -i "$CONTAINER" bash -s <<'INNER' docker exec -i "$CONTAINER" bash -s <<'INNER'
@@ -1008,8 +1022,10 @@ fi
if ! command -v opencode >/dev/null 2>&1; then if ! command -v opencode >/dev/null 2>&1; then
npm install -g --no-audit --no-fund --prefer-offline ${opencodeNpmPackage} npm install -g --no-audit --no-fund --prefer-offline ${opencodeNpmPackage}
fi fi
printf %s ${shellQuote(hwpodWrapper)} | base64 -d > /usr/local/bin/hwpod
chmod 755 /usr/local/bin/hwpod
mkdir -p "$WORKDIR" "$CODEX_HOME_DIR" "$OPENCODE_XDG_DIR/data" "$OPENCODE_XDG_DIR/config" "$OPENCODE_XDG_DIR/cache" "$OPENCODE_XDG_DIR/state" mkdir -p "$WORKDIR" "$CODEX_HOME_DIR" "$OPENCODE_XDG_DIR/data" "$OPENCODE_XDG_DIR/config" "$OPENCODE_XDG_DIR/cache" "$OPENCODE_XDG_DIR/state"
echo "code_agent_runtime_ready codex=$(command -v codex) opencode=$(command -v opencode) cwd=$WORKDIR home=$CODEX_HOME_DIR opencodeXdg=$OPENCODE_XDG_DIR" echo "code_agent_runtime_ready codex=$(command -v codex) opencode=$(command -v opencode) hwpod=$(command -v hwpod) cwd=$WORKDIR home=$CODEX_HOME_DIR opencodeXdg=$OPENCODE_XDG_DIR"
INNER`; INNER`;
} }