fix: expose hwpod in code queue runners
This commit is contained in:
@@ -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",
|
||||
|
||||
Executable
+57
@@ -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
|
||||
Reference in New Issue
Block a user