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
@@ -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 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 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 microserviceCli = readFileSync("scripts/src/microservices.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", {
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");
assertCondition(devCodeQueueDeploy !== undefined, "deploy.json dev environment must include code-queue");
@@ -146,6 +153,7 @@ configureProviderRuntime({
});
const devContainerPlan = buildDevContainerPlan("D601", { workdir: "/home/ubuntu" });
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_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);
@@ -209,8 +217,12 @@ const fixtureSource = join(tmpRoot, "source");
const fixtureMissingTarget = join(tmpRoot, "target-missing");
const fixtureSymlinkTarget = join(tmpRoot, "target-symlink");
const fixtureMissingSource = join(tmpRoot, "source-missing");
const fixtureApprovedSource = join(tmpRoot, "approved-source");
const fixtureArbitraryTarget = join(tmpRoot, "arbitrary-target");
mkdirSync(fixtureSource, { recursive: true });
mkdirSync(fixtureApprovedSource, { recursive: true });
createSkillSet(fixtureSource, ["docs-spec", "cli-spec"]);
createSkillSet(fixtureApprovedSource, ["docs-spec", "cli-spec"]);
symlinkSync(fixtureSource, fixtureSymlinkTarget, "dir");
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.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({
source: "/home/ubuntu/.agents/skills",
target: "/path/that/does/not/exist/for-code-queue-skills-test",
source: fixtureApprovedSource,
target: fixtureArbitraryTarget,
requiredSkills: ["docs-spec", "cli-spec"],
});
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.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.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.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");
const syncDryRun = collectSkillSyncPreflight({
source: "/home/ubuntu/.agents/skills",
target: "/path/that/does/not/exist/for-code-queue-skills-test",
source: fixtureApprovedSource,
target: fixtureArbitraryTarget,
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.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.target.path === "/path/that/does/not/exist/for-code-queue-skills-test", "skills sync must expose target", syncDryRun.target);
assertCondition(syncDryRun.source.path === fixtureApprovedSource, "skills sync must expose source", syncDryRun.source);
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.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);
@@ -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").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.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.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(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");
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" });
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");
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(preflightSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "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.target === fixtureArbitraryTarget, "full preflight must show skills 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(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);
@@ -690,7 +701,7 @@ const microservice = asRecord(healthSummary.microservice, "microservice");
const healthCompact = asRecord(microservice.summary, "microservice.summary");
const healthSkills = asRecord(healthCompact.skills, "microservice.summary.skills");
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(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);
@@ -716,6 +727,7 @@ process.stdout.write(`${JSON.stringify({
checks: [
"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",
"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",
"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",