// SPEC: PJ2026-01010305 71FREQ预装 draft-2026-06-26-71freq-v03-hwpod-preinstall. // Responsibility: Redacted YAML configRef graph and plan/status rendering for D601 71-FREQ HWPOD preinstall. import { createHash } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { repoRoot, rootPath } from "./config"; import { runCommand, type CommandResult } from "./command"; import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "./hwlab-node-lanes"; import { assertLane, assertNodeId, requiredOption } from "./hwlab-node/utils"; import { assertKnownOptions } from "./hwlab-node/web-probe-observe"; import type { RenderedCliResult } from "./output"; type HwpodPreinstallAction = "plan" | "status" | "gateway-plan" | "gateway-status" | "gateway-apply"; type HwpodPreinstallConfigRefKey = "preinstall" | "projectManagementSource" | "gatewayProfile"; interface HwpodPreinstallOptions { readonly action: HwpodPreinstallAction; readonly node: string; readonly lane: string; readonly dryRun: boolean; readonly confirm: boolean; } interface HwpodPreinstallRoot { readonly present: boolean; readonly enabled: boolean; readonly rootPath: string; readonly configRefs: Partial>; readonly error: string | null; } interface HwpodPreinstallConfigPlan { readonly ok: boolean; readonly command: string; readonly status: "ready" | "blocked" | "disabled"; readonly node: string; readonly lane: string; readonly rootPath: string; readonly enabled: boolean; readonly refs: readonly HwpodPreinstallConfigRefStatus[]; readonly conflicts: readonly string[]; readonly artifacts: readonly HwpodPreinstallArtifactRow[]; readonly next: Record; readonly valuesRedacted: true; } interface HwpodGatewayPlan { readonly ok: boolean; readonly command: string; readonly status: "ready" | "blocked" | "applied"; readonly node: string; readonly lane: string; readonly runtimeRoot: string; readonly profile: HwpodGatewayProfileSummary | null; readonly files: readonly HwpodGatewayFilePlan[]; readonly legacyRetire: readonly string[]; readonly blockers: readonly string[]; readonly remoteStatus: HwpodGatewayRemoteStatus | null; readonly remoteApply: HwpodGatewayApplyResult | null; readonly next: Record; readonly valuesRedacted: true; } interface HwpodGatewayProfileSummary { readonly hwlabNode: string; readonly lane: string; readonly cloudUrl: string; readonly websocketUrl: string; readonly hwpodId: string; readonly nodeId: string; readonly gatewayId: string; readonly sessionId: string; readonly resourceId: string; readonly capabilityId: string; readonly taskName: string; readonly periodicTaskName: string; readonly runtimeRoot: string; readonly bunPath: string; readonly runKeyName: string; readonly startCommand: string; readonly statusCommand: string; readonly processPattern: string; readonly reconnectBaseMs: number; readonly reconnectMaxMs: number; } interface HwpodGatewayFilePlan { readonly path: string; readonly kind: "source" | "runner"; readonly bytes: number; readonly sha256: string; } interface HwpodGatewayRemoteStatus { readonly ok: boolean; readonly status: string; readonly rootExists: boolean; readonly expectedProcessCount: number; readonly legacyProcessCount: number; readonly taskStates: Record; readonly fileStates: Record; readonly legacyProcessCommands: readonly string[]; readonly valuesRedacted?: boolean; } interface HwpodGatewayApplyResult { readonly ok: boolean; readonly status: string; readonly writtenFiles: readonly string[]; readonly stoppedLegacyPids: readonly number[]; readonly taskName: string; readonly periodicTaskName: string; readonly runTaskExitCode: number | null; readonly failureDetail?: string; readonly valuesRedacted?: boolean; } interface HwpodPreinstallConfigRefStatus { readonly key: HwpodPreinstallConfigRefKey; readonly ref: string; readonly file: string; readonly path: string; readonly present: boolean; readonly targetPresent: boolean; readonly targetKind: "object" | "array" | "scalar" | "null" | "missing"; readonly sha256: string | null; readonly byteCount: number | null; readonly missingFields: readonly string[]; readonly conflicts: readonly string[]; readonly summary: string; readonly error: string | null; } interface InternalConfigRefStatus extends HwpodPreinstallConfigRefStatus { readonly target: unknown; } interface RequiredTargetShape { readonly kind: "object"; readonly requiredPaths: readonly string[]; } interface HwpodPreinstallArtifactRow { readonly kind: string; readonly name: string; readonly target: string; readonly detail: string; } const CONFIG_REF_KEYS = ["preinstall", "projectManagementSource", "gatewayProfile"] as const satisfies readonly HwpodPreinstallConfigRefKey[]; const REQUIRED_TARGET_SHAPES: Record = { preinstall: { kind: "object", requiredPaths: [ "hwpodId", "sourceRef.spec", "metadataRef", "targetDevice.board", "targetDevice.mcu", "nodeBinding.hwlabNode", "nodeBinding.lane", "nodeBinding.nodeId", "workspaceRootRef", "projectRoot", "toolchain.name", "toolchain.keilProject", "toolchain.keilTarget", "toolchain.keilCliPath", "debugProbe.type", "debugProbe.probeUid", "uart.id", "uart.port", "uart.baudRate", "boardComm.endpoints[0].id", "ioProbe.uart.id", "runtimeMount.namespace", "runtimeMount.configMapName", "runtimeMount.mountPath", "runtimeMount.envKey", "runtimeMount.rolloutTarget.name", "specDocument.metadata.name", "specDocument.spec.workspace.keilProject", "metadataSidecar.contractVersion", ], }, projectManagementSource: { kind: "object", requiredPaths: [ "sourceId", "sourceKind", "projectId", "hwpodId", "nodeId", "workspaceRootRef", "mdtodoRootRef", "focusFiles[0]", "hwpodNodeOpsUrlConfigRef", "runtimeEnv.envKey", "runtimeEnv.rolloutTarget.name", ], }, gatewayProfile: { kind: "object", requiredPaths: [ "cloudUrl", "node", "lane", "gatewayId", "sessionId", "resourceId", "capabilityId", "hwpodId", "nodeId", "nodeOps.serviceUrl", "nodeOps.publicUrl", "nodeOps.websocketUrl", "managedRun.mode", "managedRun.runtimeRoot", "managedRun.bunPath", "managedRun.taskName", "managedRun.periodicTaskName", "managedRun.runKeyName", "managedRun.startCommand", "managedRun.statusCommand", "managedRun.processPattern", "managedRun.reconnectBaseMs", "managedRun.reconnectMaxMs", "secretRefs[0].sourceRef", ], }, }; export function hwlabNodeHwpodPreinstallHelp(): Record { return { ok: true, command: "hwlab nodes hwpod-preinstall", description: "Render the YAML-first HWPOD preinstall configRef graph for a selected node/lane without mutating runtime.", configPath: hwlabRuntimeLaneConfigPath(), examples: [ "bun scripts/cli.ts hwlab nodes hwpod-preinstall plan --node D601 --lane v03 --dry-run", "bun scripts/cli.ts hwlab nodes hwpod-preinstall status --node D601 --lane v03", "bun scripts/cli.ts hwlab nodes hwpod-preinstall gateway-plan --node D601 --lane v03 --dry-run", "bun scripts/cli.ts hwlab nodes hwpod-preinstall gateway-status --node D601 --lane v03", "bun scripts/cli.ts hwlab nodes hwpod-preinstall gateway-apply --node D601 --lane v03 --dry-run", ], actions: { plan: "Validate D601/v03 hwpodPreinstall configRefs, cross-check ids, and render runtime artifact targets.", status: "Render the same redacted graph for the selected target as a read-only status view.", "gateway-plan": "Render the v0.3+ Windows HWPOD node/gateway runner files and legacy-retire policy without touching runtime.", "gateway-status": "Read D601 Windows task/process/file state and classify old v0.2 direct-url runners as legacy drift.", "gateway-apply": "Sync v0.3 source tools, overwrite the Windows runner from YAML, stop legacy v0.2 runners, and recreate managed tasks.", }, notes: [ "Root hwpodPreinstall must be declared under the selected lane target; this command does not use global defaults.", "Secret values, full YAML objects, host file content, and raw Markdown are not printed.", "v0.2 is not a compatibility target for this feature; gateway commands only support v0.3+ selected lanes.", "Runtime apply/rollout is a later P3 step; plan/status are P2 visibility and friction-removal commands.", ], }; } export async function runHwlabNodeHwpodPreinstallCommand(args: string[]): Promise> { if (args.length === 0 || args.includes("--help") || args.includes("-h") || args[0] === "help") return hwlabNodeHwpodPreinstallHelp(); const options = parseHwpodPreinstallOptions(args); const spec = hwlabRuntimeLaneSpecForNode(options.lane, options.node); if (options.action === "plan" || options.action === "status") { return withHwpodPreinstallRendered(hwpodPreinstallConfigPlan(spec, options.action, options.dryRun)); } return withHwpodGatewayRendered(await hwpodGatewayPlan(spec, options)); } function parseHwpodPreinstallOptions(args: string[]): HwpodPreinstallOptions { const [actionRaw] = args; if ( actionRaw !== "plan" && actionRaw !== "status" && actionRaw !== "gateway-plan" && actionRaw !== "gateway-status" && actionRaw !== "gateway-apply" ) { throw new Error("hwpod-preinstall usage: hwpod-preinstall plan|status|gateway-plan|gateway-status|gateway-apply --node NODE --lane vNN [--dry-run|--confirm]"); } assertKnownOptions(args, new Set(["--node", "--lane"]), new Set(["--dry-run", "--confirm", "--full"])); const node = requiredOption(args, "--node"); assertNodeId(node); const lane = requiredOption(args, "--lane"); assertLane(lane); if (!isHwlabRuntimeLane(lane)) throw new Error(`hwpod-preinstall only supports HWLAB runtime lanes, got ${lane}`); const confirm = args.includes("--confirm"); const dryRun = args.includes("--dry-run"); if (confirm && dryRun) throw new Error("hwpod-preinstall accepts only one of --confirm or --dry-run"); if (actionRaw === "gateway-apply" && !confirm && !dryRun) throw new Error("gateway-apply requires --dry-run or --confirm"); if (actionRaw !== "gateway-apply" && confirm) throw new Error(`${actionRaw} is read-only and does not accept --confirm`); return { action: actionRaw, node, lane, dryRun, confirm }; } function hwpodPreinstallConfigPlan(spec: HwlabRuntimeLaneSpec, action: HwpodPreinstallAction, dryRun: boolean): HwpodPreinstallConfigPlan { const root = readHwpodPreinstallRoot(spec); const command = `hwlab nodes hwpod-preinstall ${action} --node ${spec.nodeId} --lane ${spec.lane}${dryRun ? " --dry-run" : ""}`; const refs = root.present && root.enabled ? CONFIG_REF_KEYS.map((key) => readConfigRef(key, root.configRefs[key] ?? "")) : []; const rootConflicts = root.error === null ? [] : [root.error]; const conflicts = root.enabled ? [...rootConflicts, ...crossReferenceConflicts(spec, refs)] : rootConflicts; const refBlocked = refs.some((ref) => !ref.present || !ref.targetPresent || ref.missingFields.length > 0 || ref.conflicts.length > 0 || ref.error !== null); const ok = root.present && root.enabled && !refBlocked && conflicts.length === 0; return { ok, command, status: root.enabled ? ok ? "ready" : "blocked" : "disabled", node: spec.nodeId, lane: spec.lane, rootPath: root.rootPath, enabled: root.enabled, refs: refs.map(stripInternalTarget), conflicts, artifacts: runtimeArtifactRows(refs), next: nextCommands(spec.nodeId, spec.lane), valuesRedacted: true, }; } function withHwpodPreinstallRendered(value: HwpodPreinstallConfigPlan): RenderedCliResult { return { ok: value.ok, command: value.command, contentType: "text/plain", renderedText: renderHwpodPreinstallPlan(value), }; } function withHwpodGatewayRendered(value: HwpodGatewayPlan): RenderedCliResult { return { ok: value.ok, command: value.command, contentType: "text/plain", renderedText: renderHwpodGatewayPlan(value), }; } async function hwpodGatewayPlan(spec: HwlabRuntimeLaneSpec, options: HwpodPreinstallOptions): Promise { const root = readHwpodPreinstallRoot(spec); const refs = root.present && root.enabled ? CONFIG_REF_KEYS.map((key) => readConfigRef(key, root.configRefs[key] ?? "")) : []; const graph = hwpodPreinstallConfigPlan(spec, "plan", true); const gateway = recordTarget(new Map(refs.map((ref) => [ref.key, ref])).get("gatewayProfile")); const profile = gatewayProfileSummary(gateway); const unsupportedLegacyLane = spec.lane === "v02" || spec.version === "v0.2"; const blockers = [ ...(graph.ok ? [] : ["preinstall configRef graph is not ready; run hwpod-preinstall plan first"]), ...(profile === null ? ["gatewayProfile configRef target is missing or invalid"] : []), ...(unsupportedLegacyLane ? ["v0.2 is legacy-retire for 71-FREQ HWPOD gateway; no compatibility or fallback is retained"] : []), ]; const files = profile === null ? [] : gatewayRunnerFiles(profile).map((file) => ({ path: file.path, kind: "runner" as const, bytes: Buffer.byteLength(file.content, "utf8"), sha256: sha256Fingerprint(file.content), })); let remoteStatus: HwpodGatewayRemoteStatus | null = null; let remoteApply: HwpodGatewayApplyResult | null = null; if (profile !== null && !unsupportedLegacyLane && (options.action === "gateway-status" || options.action === "gateway-apply")) { remoteStatus = readGatewayRemoteStatus(spec, profile); } if (profile !== null && !unsupportedLegacyLane && options.action === "gateway-apply" && options.confirm && blockers.length === 0) { const sourceFiles = readGatewaySourceFiles(spec); remoteApply = applyGatewayRuntime(spec, profile, sourceFiles, gatewayRunnerFiles(profile)); remoteStatus = readGatewayRemoteStatus(spec, profile); } const remoteOk = remoteStatus === null || remoteStatus.ok; const applyOk = remoteApply === null || remoteApply.ok; const ok = blockers.length === 0 && remoteOk && applyOk; return { ok, command: `hwlab nodes hwpod-preinstall ${options.action} --node ${spec.nodeId} --lane ${spec.lane}${options.dryRun ? " --dry-run" : ""}${options.confirm ? " --confirm" : ""}`, status: remoteApply?.ok ? "applied" : ok ? "ready" : "blocked", node: spec.nodeId, lane: spec.lane, runtimeRoot: profile?.runtimeRoot ?? "-", profile, files, legacyRetire: [ "v0.2 direct-url hwpod-node is not a compatibility target", "apply stops hwpod-node.ts connect processes for the selected node before starting the YAML-rendered v0.3 runner", "old .device-pod profiles and old cloud URLs are drift evidence only", ], blockers, remoteStatus, remoteApply, next: { plan: `bun scripts/cli.ts hwlab nodes hwpod-preinstall gateway-plan --node ${spec.nodeId} --lane ${spec.lane} --dry-run`, status: `bun scripts/cli.ts hwlab nodes hwpod-preinstall gateway-status --node ${spec.nodeId} --lane ${spec.lane}`, apply: `bun scripts/cli.ts hwlab nodes hwpod-preinstall gateway-apply --node ${spec.nodeId} --lane ${spec.lane} --confirm`, }, valuesRedacted: true, }; } function readHwpodPreinstallRoot(spec: HwlabRuntimeLaneSpec): HwpodPreinstallRoot { const rootFragment = `lanes.${spec.lane}.targets.${spec.nodeId}.hwpodPreinstall`; const root = `config/hwlab-node-lanes.yaml#${rootFragment}`; try { const text = readFileSync(rootPath(hwlabRuntimeLaneConfigPath()), "utf8"); const doc = Bun.YAML.parse(text) as unknown; const target = valueAtPath(doc, rootFragment); if (!isRecord(target)) { return { present: false, enabled: false, rootPath: root, configRefs: {}, error: `${root} must be declared on the selected node/lane target` }; } const enabled = target.enabled === true; const refs = isRecord(target.configRefs) ? target.configRefs : {}; const configRefs: Partial> = {}; for (const key of CONFIG_REF_KEYS) { const value = refs[key]; if (typeof value === "string" && value.length > 0) configRefs[key] = value; } const missingKeys = CONFIG_REF_KEYS.filter((key) => configRefs[key] === undefined); return { present: true, enabled, rootPath: root, configRefs, error: missingKeys.length === 0 ? null : `${root}.configRefs missing ${missingKeys.join(",")}`, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return { present: false, enabled: false, rootPath: root, configRefs: {}, error: message }; } } function readConfigRef(key: HwpodPreinstallConfigRefKey, ref: string): InternalConfigRefStatus { const parsed = parseConfigRef(ref); if (parsed.error !== null) return emptyRefStatus(key, ref, parsed.file, parsed.path, parsed.error); const absPath = rootPath(parsed.file); if (!existsSync(absPath)) return emptyRefStatus(key, ref, parsed.file, parsed.path, `${parsed.file} does not exist`); try { const text = readFileSync(absPath, "utf8"); const sha256 = `sha256:${createHash("sha256").update(text).digest("hex")}`; const doc = Bun.YAML.parse(text) as unknown; const target = valueAtPath(doc, parsed.path); const targetKind = target === undefined ? "missing" : targetKindOf(target); const missingFields = target === undefined ? ["target"] : missingFieldsForTarget(key, target); return { key, ref, file: parsed.file, path: parsed.path, present: true, targetPresent: target !== undefined, targetKind, sha256, byteCount: Buffer.byteLength(text), missingFields, conflicts: [], summary: summarizeTarget(key, target), error: null, target, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); return emptyRefStatus(key, ref, parsed.file, parsed.path, message); } } function parseConfigRef(ref: string): { readonly file: string; readonly path: string; readonly error: string | null } { const [file, path, extra] = ref.split("#"); if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) { return { file: file ?? "", path: path ?? "", error: "configRef must use path/to/file.yaml#object.path" }; } if (file.startsWith("/") || file.includes("\0") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) { return { file, path, error: "configRef file must be repo-relative config/*.yaml without .." }; } return { file, path, error: null }; } function emptyRefStatus(key: HwpodPreinstallConfigRefKey, ref: string, file: string, path: string, error: string): InternalConfigRefStatus { return { key, ref, file, path, present: false, targetPresent: false, targetKind: "missing", sha256: null, byteCount: null, missingFields: ["target"], conflicts: [], summary: "-", error, target: undefined, }; } function missingFieldsForTarget(key: HwpodPreinstallConfigRefKey, target: unknown): string[] { const shape = REQUIRED_TARGET_SHAPES[key]; if (!isRecord(target)) return [`expected ${shape.kind}`]; return shape.requiredPaths.filter((path) => valueAtPath(target, path) === undefined); } function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly InternalConfigRefStatus[]): string[] { const byKey = new Map(refs.map((ref) => [ref.key, ref])); const preinstall = recordTarget(byKey.get("preinstall")); const pm = recordTarget(byKey.get("projectManagementSource")); const gateway = recordTarget(byKey.get("gatewayProfile")); const conflicts: string[] = []; if (preinstall !== null) { requireEquals(conflicts, byKey.get("preinstall"), "nodeBinding.hwlabNode", spec.nodeId, `selected node ${spec.nodeId}`); requireEquals(conflicts, byKey.get("preinstall"), "nodeBinding.lane", spec.lane, `selected lane ${spec.lane}`); requireEquals(conflicts, byKey.get("preinstall"), "runtimeMount.namespace", spec.runtimeNamespace, `selected namespace ${spec.runtimeNamespace}`); requireSelfEquals(conflicts, byKey.get("preinstall"), "hwpodId", "specDocument.metadata.name"); requireSelfEquals(conflicts, byKey.get("preinstall"), "workspaceRootRef", "specDocument.spec.workspace.path"); requireSelfEquals(conflicts, byKey.get("preinstall"), "toolchain.keilProject", "specDocument.spec.workspace.keilProject"); requireSelfEquals(conflicts, byKey.get("preinstall"), "toolchain.keilTarget", "specDocument.spec.workspace.keilTarget"); requireSelfEquals(conflicts, byKey.get("preinstall"), "nodeBinding.nodeId", "specDocument.spec.nodeBinding.nodeId"); } const hwpodId = stringAt(preinstall, "hwpodId"); const nodeId = stringAt(preinstall, "nodeBinding.nodeId"); const workspaceRootRef = stringAt(preinstall, "workspaceRootRef"); if (pm !== null) { requireEquals(conflicts, byKey.get("projectManagementSource"), "sourceKind", "hwpod-workspace", "required sourceKind hwpod-workspace"); if (hwpodId !== null) requireEquals(conflicts, byKey.get("projectManagementSource"), "hwpodId", hwpodId, `preinstall hwpodId ${hwpodId}`); if (nodeId !== null) requireEquals(conflicts, byKey.get("projectManagementSource"), "nodeId", nodeId, `preinstall nodeId ${nodeId}`); if (workspaceRootRef !== null) requireEquals(conflicts, byKey.get("projectManagementSource"), "workspaceRootRef", workspaceRootRef, "preinstall workspaceRootRef"); } if (gateway !== null) { requireEquals(conflicts, byKey.get("gatewayProfile"), "node", spec.nodeId, `selected node ${spec.nodeId}`); requireEquals(conflicts, byKey.get("gatewayProfile"), "lane", spec.lane, `selected lane ${spec.lane}`); if (hwpodId !== null) { requireEquals(conflicts, byKey.get("gatewayProfile"), "hwpodId", hwpodId, `preinstall hwpodId ${hwpodId}`); requireEquals(conflicts, byKey.get("gatewayProfile"), "resourceId", hwpodId, `preinstall hwpodId ${hwpodId}`); } if (nodeId !== null) requireEquals(conflicts, byKey.get("gatewayProfile"), "nodeId", nodeId, `preinstall nodeId ${nodeId}`); } const pmOpsRef = stringAt(pm, "hwpodNodeOpsUrlConfigRef"); const gatewayRef = byKey.get("gatewayProfile"); if (pmOpsRef !== null && gatewayRef !== undefined) { const expected = `${gatewayRef.file}#${gatewayRef.path}.nodeOps.serviceUrl`; if (pmOpsRef !== expected) { conflicts.push(`${byKey.get("projectManagementSource")?.file}#${byKey.get("projectManagementSource")?.path}.hwpodNodeOpsUrlConfigRef=${pmOpsRef} does not match gateway nodeOps serviceUrl ref ${expected}`); } } return conflicts; } function runtimeArtifactRows(refs: readonly InternalConfigRefStatus[]): HwpodPreinstallArtifactRow[] { const byKey = new Map(refs.map((ref) => [ref.key, ref])); const preinstall = recordTarget(byKey.get("preinstall")); const pm = recordTarget(byKey.get("projectManagementSource")); const gateway = recordTarget(byKey.get("gatewayProfile")); return [ ...(preinstall === null ? [] : [ { kind: "ConfigMap", name: textAt(preinstall, "runtimeMount.configMapName"), target: textAt(preinstall, "runtimeMount.namespace"), detail: `mount=${textAt(preinstall, "runtimeMount.mountPath")} env=${textAt(preinstall, "runtimeMount.envKey")} rollout=${textAt(preinstall, "runtimeMount.rolloutTarget.name")}`, }, { kind: "HWPOD", name: textAt(preinstall, "hwpodId"), target: textAt(preinstall, "nodeBinding.nodeId"), detail: `keil=${textAt(preinstall, "toolchain.keilTarget")} uart=${textAt(preinstall, "uart.port")}/${textAt(preinstall, "uart.baudRate")}`, }, ]), ...(pm === null ? [] : [ { kind: "PM source", name: textAt(pm, "sourceId"), target: textAt(pm, "projectId"), detail: `root=${textAt(pm, "mdtodoRootRef")} env=${textAt(pm, "runtimeEnv.envKey")} focusFiles=${arrayAt(pm, "focusFiles").length}`, }, ]), ...(gateway === null ? [] : [ { kind: "Gateway", name: textAt(gateway, "gatewayId"), target: textAt(gateway, "resourceId"), detail: `mode=${textAt(gateway, "managedRun.mode")} status=${lastPathSegment(textAt(gateway, "managedRun.statusCommand"))}`, }, ]), ]; } function requireEquals(conflicts: string[], ref: InternalConfigRefStatus | undefined, path: string, expected: string, expectedLabel: string): void { if (ref === undefined) return; const actual = stringAt(ref.target, path); if (actual !== null && actual !== expected) { conflicts.push(`${ref.file}#${ref.path}.${path}=${actual} does not match ${expectedLabel}`); } } function requireSelfEquals(conflicts: string[], ref: InternalConfigRefStatus | undefined, firstPath: string, secondPath: string): void { if (ref === undefined) return; const first = stringAt(ref.target, firstPath); const second = stringAt(ref.target, secondPath); if (first !== null && second !== null && first !== second) { conflicts.push(`${ref.file}#${ref.path}.${firstPath}=${first} does not match ${secondPath}=${second}`); } } function stripInternalTarget(ref: InternalConfigRefStatus): HwpodPreinstallConfigRefStatus { return { key: ref.key, ref: ref.ref, file: ref.file, path: ref.path, present: ref.present, targetPresent: ref.targetPresent, targetKind: ref.targetKind, sha256: ref.sha256, byteCount: ref.byteCount, missingFields: ref.missingFields, conflicts: ref.conflicts, summary: ref.summary, error: ref.error, }; } function summarizeTarget(key: HwpodPreinstallConfigRefKey, target: unknown): string { if (target === undefined) return "target=missing"; if (!isRecord(target)) return `kind=${targetKindOf(target)}`; if (key === "preinstall") { return `hwpod=${textAt(target, "hwpodId")} keil=${textAt(target, "toolchain.keilTarget")} uart=${textAt(target, "uart.port")}/${textAt(target, "uart.baudRate")} board=${textAt(target, "targetDevice.board")}`; } if (key === "projectManagementSource") { return `source=${textAt(target, "sourceId")} project=${textAt(target, "projectId")} root=${textAt(target, "mdtodoRootRef")} focusFiles=${arrayAt(target, "focusFiles").length}`; } if (key === "gatewayProfile") { return `gateway=${textAt(target, "gatewayId")} resource=${textAt(target, "resourceId")} mode=${textAt(target, "managedRun.mode")} cloud=${textAt(target, "cloudUrl")}`; } return `keys=${Object.keys(target).length}`; } function gatewayProfileSummary(gateway: Record | null): HwpodGatewayProfileSummary | null { if (gateway === null) return null; const hwlabNode = stringAt(gateway, "node"); const lane = stringAt(gateway, "lane"); const cloudUrl = stringAt(gateway, "cloudUrl"); const websocketUrl = stringAt(gateway, "nodeOps.websocketUrl"); const hwpodId = stringAt(gateway, "hwpodId"); const nodeId = stringAt(gateway, "nodeId"); const gatewayId = stringAt(gateway, "gatewayId"); const sessionId = stringAt(gateway, "sessionId"); const resourceId = stringAt(gateway, "resourceId"); const capabilityId = stringAt(gateway, "capabilityId"); const runtimeRoot = stringAt(gateway, "managedRun.runtimeRoot"); const bunPath = stringAt(gateway, "managedRun.bunPath"); const taskName = stringAt(gateway, "managedRun.taskName"); const periodicTaskName = stringAt(gateway, "managedRun.periodicTaskName"); const runKeyName = stringAt(gateway, "managedRun.runKeyName"); const startCommand = stringAt(gateway, "managedRun.startCommand"); const statusCommand = stringAt(gateway, "managedRun.statusCommand"); const processPattern = stringAt(gateway, "managedRun.processPattern"); const reconnectBaseMs = numberAt(gateway, "managedRun.reconnectBaseMs"); const reconnectMaxMs = numberAt(gateway, "managedRun.reconnectMaxMs"); if ( hwlabNode === null || lane === null || cloudUrl === null || websocketUrl === null || hwpodId === null || nodeId === null || gatewayId === null || sessionId === null || resourceId === null || capabilityId === null || runtimeRoot === null || bunPath === null || taskName === null || periodicTaskName === null || runKeyName === null || startCommand === null || statusCommand === null || processPattern === null || reconnectBaseMs === null || reconnectMaxMs === null ) { return null; } return { hwlabNode, lane, cloudUrl, websocketUrl, hwpodId, nodeId, gatewayId, sessionId, resourceId, capabilityId, runtimeRoot, bunPath, taskName, periodicTaskName, runKeyName, startCommand, statusCommand, processPattern, reconnectBaseMs, reconnectMaxMs, }; } function gatewayRunnerFiles(profile: HwpodGatewayProfileSummary): { readonly path: string; readonly content: string }[] { return [ { path: "run-node-ws-detached.cmd", content: gatewayRunDetachedCmd(profile) }, { path: "start-node-ws.cmd", content: gatewayStartCmd(profile) }, { path: "status-node-ws.cmd", content: gatewayStatusCmd(profile) }, { path: "install-node-ws.cmd", content: gatewayInstallCmd(profile) }, { path: "run-node-ws.vbs", content: gatewayRunVbs(profile) }, { path: "ensure-node-ws.vbs", content: gatewayEnsureVbs(profile) }, ]; } function readGatewayRemoteStatus(spec: HwlabRuntimeLaneSpec, profile: HwpodGatewayProfileSummary): HwpodGatewayRemoteStatus { const result = runTrans(spec, "win", ["ps"], gatewayRemoteStatusScript(profile), 30_000); if (result.exitCode !== 0) { return { ok: false, status: "capture-failed", rootExists: false, expectedProcessCount: 0, legacyProcessCount: 0, taskStates: {}, fileStates: {}, legacyProcessCommands: [compactResultTail(result)], valuesRedacted: true, }; } const parsed = parseJsonFromStdout(result.stdout); if (parsed === null) { return { ok: false, status: "invalid-status-json", rootExists: false, expectedProcessCount: 0, legacyProcessCount: 0, taskStates: {}, fileStates: {}, legacyProcessCommands: [short(result.stdout, 500)], valuesRedacted: true, }; } return { ok: parsed.ok === true, status: typeof parsed.status === "string" ? parsed.status : "unknown", rootExists: parsed.rootExists === true, expectedProcessCount: numberFromUnknown(parsed.expectedProcessCount), legacyProcessCount: numberFromUnknown(parsed.legacyProcessCount), taskStates: stringRecord(parsed.taskStates), fileStates: stringRecord(parsed.fileStates), legacyProcessCommands: stringArray(parsed.legacyProcessCommands).map((item) => short(item, 180)), valuesRedacted: true, }; } function readGatewaySourceFiles(spec: HwlabRuntimeLaneSpec): { readonly path: string; readonly base64: string; readonly sha256: string; readonly bytes: number }[] { const result = runTrans(spec, spec.workspace, ["sh", "--", gatewaySourceBundleScript()], "", 30_000); if (result.exitCode !== 0) throw new Error(`failed to read HWLAB v0.3 hwpod-node source files from ${spec.nodeId}:${spec.workspace}: ${compactResultTail(result)}`); const parsed = parseJsonFromStdout(result.stdout); if (parsed === null || parsed.ok !== true || !Array.isArray(parsed.files)) throw new Error(`invalid hwpod-node source bundle output: ${short(result.stdout, 500)}`); return parsed.files.map((item) => { if (!isRecord(item)) throw new Error("invalid source file record"); const path = stringAt(item, "path"); const base64 = stringAt(item, "base64"); const sha256 = stringAt(item, "sha256"); const bytes = numberFromUnknown(item.bytes); if (path === null || base64 === null || sha256 === null || bytes <= 0) throw new Error("invalid source file fields"); return { path, base64, sha256, bytes }; }); } function applyGatewayRuntime( spec: HwlabRuntimeLaneSpec, profile: HwpodGatewayProfileSummary, sourceFiles: readonly { readonly path: string; readonly base64: string; readonly sha256: string; readonly bytes: number }[], runnerFiles: readonly { readonly path: string; readonly content: string }[], ): HwpodGatewayApplyResult { const payload = { root: profile.runtimeRoot, profile, sourceFiles, runnerFiles: runnerFiles.map((file) => ({ path: file.path, base64: Buffer.from(file.content, "utf8").toString("base64"), sha256: sha256Fingerprint(file.content), bytes: Buffer.byteLength(file.content, "utf8"), })), }; const result = runTrans(spec, "win", ["ps"], gatewayApplyScript(payload), 60_000); if (result.exitCode !== 0) { return { ok: false, status: "apply-failed", writtenFiles: [], stoppedLegacyPids: [], taskName: profile.taskName, periodicTaskName: profile.periodicTaskName, runTaskExitCode: null, failureDetail: compactResultTail(result), valuesRedacted: true, }; } const parsed = parseJsonFromStdout(result.stdout); if (parsed === null) { return { ok: false, status: "invalid-apply-json", writtenFiles: [], stoppedLegacyPids: [], taskName: profile.taskName, periodicTaskName: profile.periodicTaskName, runTaskExitCode: null, failureDetail: short(result.stdout, 500), valuesRedacted: true, }; } return { ok: parsed.ok === true, status: typeof parsed.status === "string" ? parsed.status : "unknown", writtenFiles: stringArray(parsed.writtenFiles), stoppedLegacyPids: numberArray(parsed.stoppedLegacyPids), taskName: profile.taskName, periodicTaskName: profile.periodicTaskName, runTaskExitCode: typeof parsed.runTaskExitCode === "number" ? parsed.runTaskExitCode : null, failureDetail: typeof parsed.failureDetail === "string" && parsed.failureDetail.length > 0 ? short(parsed.failureDetail, 500) : undefined, valuesRedacted: true, }; } function renderHwpodGatewayPlan(value: HwpodGatewayPlan): string { const profileRows = value.profile === null ? [["profile", "missing"]] : [ ["cloudUrl", value.profile.cloudUrl], ["websocketUrl", value.profile.websocketUrl], ["hwpodId", value.profile.hwpodId], ["nodeId", value.profile.nodeId], ["resourceId", value.profile.resourceId], ["capabilityId", value.profile.capabilityId], ["taskName", value.profile.taskName], ["periodicTaskName", value.profile.periodicTaskName], ["runKeyName", value.profile.runKeyName], ["bunPath", value.profile.bunPath], ["reconnectBaseMs", value.profile.reconnectBaseMs], ["reconnectMaxMs", value.profile.reconnectMaxMs], ["runtimeRoot", value.runtimeRoot], ]; const remoteRows = value.remoteStatus === null ? [["remote", "not-read"]] : [ ["status", value.remoteStatus.status], ["rootExists", value.remoteStatus.rootExists], ["expectedProcessCount", value.remoteStatus.expectedProcessCount], ["legacyProcessCount", value.remoteStatus.legacyProcessCount], ["taskStates", Object.entries(value.remoteStatus.taskStates).map(([key, state]) => `${key}=${state}`).join(",") || "-"], ["fileStates", Object.entries(value.remoteStatus.fileStates).map(([key, state]) => `${key}=${state}`).join(",") || "-"], ]; const applyRows = value.remoteApply === null ? [["apply", "not-run"]] : [ ["status", value.remoteApply.status], ["writtenFiles", value.remoteApply.writtenFiles.length], ["stoppedLegacyPids", value.remoteApply.stoppedLegacyPids.join(",") || "-"], ["runTaskExitCode", value.remoteApply.runTaskExitCode ?? "-"], ["failureDetail", value.remoteApply.failureDetail ?? "-"], ]; return [ `hwlab nodes hwpod-preinstall ${commandAction(value.command)} (${value.status})`, "", hwpodTable(["NODE", "LANE", "OK", "RUNTIME"], [[value.node, value.lane, value.ok, value.runtimeRoot]]), "", "PROFILE", hwpodTable(["FIELD", "VALUE"], profileRows.map(([key, item]) => [key, short(String(item), 140)])), "", "RUNNER FILES", value.files.length === 0 ? " none" : hwpodTable(["PATH", "KIND", "BYTES", "HASH"], value.files.map((file) => [file.path, file.kind, file.bytes, `${file.sha256.slice(0, 19)}...`])), "", "REMOTE STATUS", hwpodTable(["FIELD", "VALUE"], remoteRows.map(([key, item]) => [key, short(String(item), 180)])), "", "REMOTE APPLY", hwpodTable(["FIELD", "VALUE"], applyRows.map(([key, item]) => [key, short(String(item), 180)])), "", "LEGACY RETIRE", ...value.legacyRetire.map((item) => ` - ${item}`), ...(value.remoteStatus !== null && value.remoteStatus.legacyProcessCommands.length > 0 ? [" legacyProcessCommands:", ...value.remoteStatus.legacyProcessCommands.map((item) => ` ${item}`)] : []), ...(value.blockers.length === 0 ? [] : ["", "BLOCKERS", ...value.blockers.map((item) => ` - ${item}`)]), "", "NEXT", ` plan: ${value.next.plan}`, ` status: ${value.next.status}`, ` apply: ${value.next.apply}`, "DISCLOSURE", " valuesRedacted=true; secret values, raw logs, full process dumps, and complete file content are not printed.", ].join("\n"); } function renderHwpodPreinstallPlan(value: HwpodPreinstallConfigPlan): string { const refSummary = value.refs.length === 0 ? ["CONFIG REFS", " none (root disabled or not declared)"] : [ hwpodTable( ["KEY", "PRESENT", "TARGET", "TYPE", "HASH", "MISSING", "SUMMARY"], value.refs.map((ref) => [ ref.key, ref.present, ref.targetPresent, ref.targetKind, ref.sha256 === null ? "-" : `${ref.sha256.slice(0, 19)}...`, ref.missingFields.length === 0 ? "-" : short(ref.missingFields.join(","), 52), short(ref.summary, 120), ]), ), "", hwpodTable( ["KEY", "FILE", "PATH", "BYTES"], value.refs.map((ref) => [ref.key, ref.file, ref.path, ref.byteCount ?? "-"]), ), ]; const artifactSummary = value.artifacts.length === 0 ? ["ARTIFACTS", " none"] : [ hwpodTable( ["KIND", "NAME", "TARGET", "DETAIL"], value.artifacts.map((item) => [item.kind, item.name, item.target, short(item.detail, 120)]), ), ]; const blocked = value.ok ? [] : [ "", "Blocked detail:", hwpodTable(["KIND", "VALUE"], [ ...value.conflicts.map((item) => ["conflict", short(item, 150)]), ...value.refs.flatMap((ref) => [ ...(ref.error === null ? [] : [[`${ref.key}.error`, short(ref.error, 150)]]), ...(ref.missingFields.length === 0 ? [] : [[`${ref.key}.missing`, short(ref.missingFields.join(","), 150)]]), ...(ref.conflicts.length === 0 ? [] : [[`${ref.key}.conflict`, short(ref.conflicts.join(" | "), 150)]]), ]), ]), ]; return [ `hwlab nodes hwpod-preinstall ${commandAction(value.command)} (${value.status})`, "", hwpodTable(["NODE", "LANE", "ENABLED", "OK", "ROOT"], [[value.node, value.lane, value.enabled, value.ok, value.rootPath]]), "", ...refSummary, "", ...artifactSummary, ...blocked, "", "NEXT", ` plan: ${value.next.plan}`, ` status: ${value.next.status}`, ` deploy: ${value.next.deploy}`, "DISCLOSURE", " valuesRedacted=true; secret values, raw Markdown, host file content, and full YAML objects are not printed.", ].join("\n"); } function nextCommands(node: string, lane: string): Record { return { plan: `bun scripts/cli.ts hwlab nodes hwpod-preinstall plan --node ${node} --lane ${lane} --dry-run`, status: `bun scripts/cli.ts hwlab nodes hwpod-preinstall status --node ${node} --lane ${lane}`, deploy: `P3: apply the rendered ConfigMap/env/gateway profile through controlled node/lane rollout for ${node}/${lane}`, }; } function valueAtPath(value: unknown, path: string): unknown { let current: unknown = value; for (const segment of path.split(".")) { if (segment.length === 0) return undefined; const match = /^(?:([A-Za-z0-9_-]+))?(?:\[(\d+)\])?$/u.exec(segment); if (match === null) return undefined; if (match[1] !== undefined) { if (!isRecord(current)) return undefined; current = current[match[1]]; } if (match[2] !== undefined) { if (!Array.isArray(current)) return undefined; current = current[Number(match[2])]; } } return current; } function stringAt(value: unknown, path: string): string | null { const found = valueAtPath(value, path); return typeof found === "string" && found.length > 0 ? found : null; } function numberAt(value: unknown, path: string): number | null { const found = valueAtPath(value, path); return typeof found === "number" && Number.isFinite(found) ? found : null; } function textAt(value: unknown, path: string): string { const found = valueAtPath(value, path); if (typeof found === "string") return found; if (typeof found === "number" || typeof found === "boolean") return String(found); return "-"; } function arrayAt(value: unknown, path: string): unknown[] { const found = valueAtPath(value, path); return Array.isArray(found) ? found : []; } function recordTarget(ref: InternalConfigRefStatus | undefined): Record | null { return ref !== undefined && isRecord(ref.target) ? ref.target : null; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function targetKindOf(value: unknown): "object" | "array" | "scalar" | "null" { if (value === null) return "null"; if (Array.isArray(value)) return "array"; if (isRecord(value)) return "object"; return "scalar"; } function commandAction(command: string): string { for (const action of ["gateway-apply", "gateway-status", "gateway-plan", "status", "plan"]) { if (command.includes(` ${action} `) || command.endsWith(` ${action}`)) return action; } return "plan"; } function hwpodTable(headers: string[], rows: unknown[][]): string { const normalized = [headers, ...rows.map((row) => row.map((cell) => hwpodText(cell)))]; const widths = headers.map((_, index) => Math.max(...normalized.map((row) => hwpodText(row[index] ?? "").length))); return normalized.map((row) => row.map((cell, index) => hwpodText(cell).padEnd(widths[index])).join(" ").trimEnd()).join("\n"); } function hwpodText(value: unknown): string { if (value === null || value === undefined || value === "") return "-"; if (typeof value === "boolean") return value ? "true" : "false"; return String(value).replace(/\s+/gu, " ").trim(); } function short(value: string, maxLength: number): string { if (value.length <= maxLength) return value; if (maxLength <= 1) return value.slice(0, maxLength); return `${value.slice(0, maxLength - 1)}~`; } function lastPathSegment(value: string): string { const normalized = value.replace(/\\/gu, "/"); const segment = normalized.split("/").filter(Boolean).pop(); return segment ?? value; } function sha256Fingerprint(value: string | Buffer): string { return `sha256:${createHash("sha256").update(value).digest("hex")}`; } function runTrans(spec: HwlabRuntimeLaneSpec, planeOrWorkspace: string, args: string[], input: string, timeoutMs: number): CommandResult { const route = planeOrWorkspace === "win" ? `${spec.nodeId}:win` : `${spec.nodeId}:${planeOrWorkspace}`; return runCommand(["trans", route, ...args], repoRoot, { input, timeoutMs }); } function gatewaySourceBundleScript(): string { const evalScript = [ "const fs=require('fs');", "const crypto=require('crypto');", "const paths=['tools/hwpod-node.ts','tools/src/hwpod-node-lib.ts','tools/src/hwpod-node-ops-contract.ts'];", "const files=paths.map((path)=>{const buf=fs.readFileSync(path);return {path,bytes:buf.length,sha256:'sha256:'+crypto.createHash('sha256').update(buf).digest('hex'),base64:buf.toString('base64')}});", "console.log(JSON.stringify({ok:true,files,valuesRedacted:true}));", ].join(""); return `bun --eval ${shQuote(evalScript)}`; } function gatewayRemoteStatusScript(profile: HwpodGatewayProfileSummary): string { return ` $ErrorActionPreference = 'Stop' $Root = ${psQuote(profile.runtimeRoot)} $CloudUrl = ${psQuote(profile.cloudUrl)} $WsUrl = ${psQuote(profile.websocketUrl)} $NodeId = ${psQuote(profile.nodeId)} $TaskName = ${psQuote(profile.taskName)} $PeriodicTaskName = ${psQuote(profile.periodicTaskName)} $Files = @('tools\\hwpod-node.ts','tools\\src\\hwpod-node-lib.ts','tools\\src\\hwpod-node-ops-contract.ts','run-node-ws-detached.cmd','start-node-ws.cmd','status-node-ws.cmd','install-node-ws.cmd','run-node-ws.vbs','ensure-node-ws.vbs') function TaskState([string]$Name) { $task = Get-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue if ($null -eq $task) { return 'missing' } return [string]$task.State } $fileStates = [ordered]@{} foreach ($rel in $Files) { $path = Join-Path $Root $rel if (Test-Path $path) { $hash = (Get-FileHash -Algorithm SHA256 $path).Hash.ToLowerInvariant() $fileStates[$rel] = 'sha256:' + $hash } else { $fileStates[$rel] = 'missing' } } $processes = @(Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { ($_.CommandLine -match 'hwpod-node\\.ts\\s+connect') -and ($_.CommandLine -match [regex]::Escape($NodeId)) }) $expected = @($processes | Where-Object { ($_.CommandLine -match [regex]::Escape($CloudUrl)) -and ($_.CommandLine -match [regex]::Escape($WsUrl)) }) $legacy = @($processes | Where-Object { -not (($_.CommandLine -match [regex]::Escape($CloudUrl)) -and ($_.CommandLine -match [regex]::Escape($WsUrl))) }) $taskStates = [ordered]@{ $TaskName = TaskState $TaskName $PeriodicTaskName = TaskState $PeriodicTaskName } $rootExists = Test-Path $Root $filesReady = -not ($fileStates.Values | Where-Object { $_ -eq 'missing' } | Select-Object -First 1) $tasksReady = -not ($taskStates.Values | Where-Object { $_ -eq 'missing' } | Select-Object -First 1) $ok = $rootExists -and $filesReady -and $tasksReady -and ($expected.Count -gt 0) -and ($legacy.Count -eq 0) $result = [ordered]@{ ok = [bool]$ok status = $(if ($ok) { 'ready' } elseif ($legacy.Count -gt 0) { 'legacy-drift' } else { 'blocked' }) rootExists = [bool]$rootExists expectedProcessCount = [int]$expected.Count legacyProcessCount = [int]$legacy.Count taskStates = $taskStates fileStates = $fileStates legacyProcessCommands = @($legacy | ForEach-Object { $_.CommandLine }) valuesRedacted = $true } $result | ConvertTo-Json -Compress -Depth 8 `.trim(); } function gatewayApplyScript(payload: Record): string { const payloadB64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64"); return ` $ErrorActionPreference = 'Stop' $payloadJson = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String(${psQuote(payloadB64)})) $payload = $payloadJson | ConvertFrom-Json $Root = [string]$payload.root $CloudUrl = [string]$payload.profile.cloudUrl $WsUrl = [string]$payload.profile.websocketUrl $NodeId = [string]$payload.profile.nodeId $TaskName = [string]$payload.profile.taskName $PeriodicTaskName = [string]$payload.profile.periodicTaskName $RunKeyName = [string]$payload.profile.runKeyName $RunVbs = Join-Path $Root 'run-node-ws.vbs' $EnsureVbs = Join-Path $Root 'ensure-node-ws.vbs' $RunTask = 'wscript.exe //B //Nologo "' + $RunVbs + '"' $EnsureTask = 'wscript.exe //B //Nologo "' + $EnsureVbs + '"' New-Item -ItemType Directory -Force -Path $Root, (Join-Path $Root 'logs'), (Join-Path $Root '.state'), (Join-Path $Root 'tools'), (Join-Path $Root 'tools\\src') | Out-Null function RunNativeCapture([scriptblock]$Block) { $previous = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $output = @(& $Block 2>&1) $exitCode = $global:LASTEXITCODE return [pscustomobject]@{ exitCode = [int]$exitCode output = (($output | ForEach-Object { [string]$_ }) -join "\`n") } } finally { $ErrorActionPreference = $previous } } function EndTaskIfPresent([string]$Name) { $task = Get-ScheduledTask -TaskName $Name -ErrorAction SilentlyContinue if ($null -eq $task) { return } if ([string]$task.State -eq 'Running') { $null = RunNativeCapture { schtasks /End /TN $Name } Start-Sleep -Seconds 2 } } function WritePayloadFile($item) { $target = Join-Path $Root ([string]$item.path) $parent = Split-Path -Parent $target if (-not [string]::IsNullOrWhiteSpace($parent)) { New-Item -ItemType Directory -Force -Path $parent | Out-Null } $bytes = [System.Convert]::FromBase64String([string]$item.base64) [System.IO.File]::WriteAllBytes($target, $bytes) $actual = 'sha256:' + (Get-FileHash -Algorithm SHA256 $target).Hash.ToLowerInvariant() if ($actual -ne [string]$item.sha256) { throw "sha mismatch for $target expected=$($item.sha256) actual=$actual" } return [string]$item.path } $stopped = @() EndTaskIfPresent $PeriodicTaskName EndTaskIfPresent $TaskName $processes = @(Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { ($_.CommandLine -match 'hwpod-node\\.ts\\s+connect') -and ($_.CommandLine -match [regex]::Escape($NodeId)) }) foreach ($proc in $processes) { $stopped += [int]$proc.ProcessId Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue } $written = @() foreach ($item in @($payload.sourceFiles)) { $written += WritePayloadFile $item } foreach ($item in @($payload.runnerFiles)) { $written += WritePayloadFile $item } $runCreate = RunNativeCapture { schtasks /Create /TN $TaskName /SC ONCE /ST 00:00 /TR $RunTask /F } if ($runCreate.exitCode -ne 0) { throw "run task create failed: $($runCreate.output)" } $periodicCreate = RunNativeCapture { schtasks /Create /TN $PeriodicTaskName /SC MINUTE /MO 5 /TR $EnsureTask /F } if ($periodicCreate.exitCode -ne 0) { throw "periodic task create failed: $($periodicCreate.output)" } $runKey = RunNativeCapture { reg add HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v $RunKeyName /t REG_SZ /d $EnsureTask /f } if ($runKey.exitCode -ne 0) { throw "HKCU Run update failed: $($runKey.output)" } $runOut = RunNativeCapture { schtasks /Run /TN $TaskName } $runExit = $runOut.exitCode Start-Sleep -Seconds 3 $expected = @(Get-CimInstance Win32_Process -Filter "Name='bun.exe'" | Where-Object { ($_.CommandLine -match 'hwpod-node\\.ts\\s+connect') -and ($_.CommandLine -match [regex]::Escape($NodeId)) -and ($_.CommandLine -match [regex]::Escape($CloudUrl)) -and ($_.CommandLine -match [regex]::Escape($WsUrl)) }) $result = [ordered]@{ ok = [bool](($runExit -eq 0) -and ($expected.Count -gt 0)) status = $(if (($runExit -eq 0) -and ($expected.Count -gt 0)) { 'applied' } else { 'started-pending' }) writtenFiles = $written stoppedLegacyPids = $stopped taskName = $TaskName periodicTaskName = $PeriodicTaskName runTaskExitCode = $runExit failureDetail = $(if (($runExit -eq 0) -and ($expected.Count -gt 0)) { $null } else { "runOutput=$($runOut.output)" }) valuesRedacted = $true } $result | ConvertTo-Json -Compress -Depth 8 `.trim(); } function gatewayRunDetachedCmd(profile: HwpodGatewayProfileSummary): string { return crlf([ "@echo off", "setlocal EnableExtensions", "chcp 65001 >nul", `set "ROOT=${profile.runtimeRoot}"`, `set "HWLAB_GATEWAY_ID=${profile.gatewayId}"`, `set "HWLAB_GATEWAY_SESSION_ID=${profile.sessionId}"`, `set "HWLAB_GATEWAY_RESOURCE_ID=${profile.resourceId}"`, `set "HWLAB_GATEWAY_CMD_CAPABILITY_ID=${profile.capabilityId}"`, `set "HWLAB_HWPOD_ID=${profile.hwpodId}"`, `set "HWLAB_HWPOD_NODE_ID=${profile.nodeId}"`, `set "HWLAB_HWPOD_NODE_WS_URL=${profile.websocketUrl}"`, `set "HWLAB_HWPOD_NODE_CLOUD_URL=${profile.cloudUrl}"`, `set "BUN_EXE=${profile.bunPath}"`, "cd /d \"%ROOT%\" || exit /b 2", "if not exist logs mkdir logs", "if not exist .state mkdir .state", "echo {\"ok\":true,\"action\":\"hwpod-node.ws.v03.runner\",\"status\":\"starting\"}>>\"%ROOT%\\logs\\hwpod-node-ws.lifecycle.log\"", `"${"%BUN_EXE%"}" tools\\hwpod-node.ts connect --ws-url ${profile.websocketUrl} --cloud-url ${profile.cloudUrl} --node-id ${profile.nodeId} --reconnect-base-ms ${profile.reconnectBaseMs} --reconnect-max-ms ${profile.reconnectMaxMs} 1>>"%ROOT%\\logs\\hwpod-node-ws.stdout.log" 2>>"%ROOT%\\logs\\hwpod-node-ws.stderr.log"`, "set \"EXIT_CODE=%ERRORLEVEL%\"", "echo {\"ok\":false,\"action\":\"hwpod-node.ws.v03.runner\",\"status\":\"exited\",\"exitCode\":%EXIT_CODE%}>>\"%ROOT%\\logs\\hwpod-node-ws.lifecycle.log\"", "exit /b %EXIT_CODE%", ]); } function gatewayStartCmd(profile: HwpodGatewayProfileSummary): string { return crlf([ "@echo off", "setlocal EnableExtensions", "chcp 65001 >nul", `set "ROOT=${profile.runtimeRoot}"`, `set "TASK_NAME=${profile.taskName}"`, "cd /d \"%ROOT%\" || (", " echo {\"ok\":false,\"action\":\"hwpod-node.ws.v03.start\",\"status\":\"blocked\",\"message\":\"runtime root not found\"}", " exit /b 2", ")", "schtasks /Run /TN \"%TASK_NAME%\" > .state\\hwpod-node-ws.schtasks-run.txt 2>&1", "set \"RUN_EXIT=%ERRORLEVEL%\"", "ping -n 4 127.0.0.1 >nul", `echo {"ok":true,"action":"hwpod-node.ws.v03.start","status":"submitted","taskName":"${jsonEscape(profile.taskName)}","cloudUrl":"${jsonEscape(profile.cloudUrl)}","nodeId":"${jsonEscape(profile.nodeId)}","runExit":%RUN_EXIT%}`, "exit /b %RUN_EXIT%", ]); } function gatewayStatusCmd(profile: HwpodGatewayProfileSummary): string { return crlf([ "@echo off", "setlocal EnableExtensions", "chcp 65001 >nul", `echo {"ok":true,"action":"hwpod-node.ws.v03.status","root":"${jsonEscape(profile.runtimeRoot)}","cloudUrl":"${jsonEscape(profile.cloudUrl)}","websocketUrl":"${jsonEscape(profile.websocketUrl)}","nodeId":"${jsonEscape(profile.nodeId)}","statusCommand":"bun scripts/cli.ts hwlab nodes hwpod-preinstall gateway-status --node ${jsonEscape(profile.hwlabNode)} --lane ${jsonEscape(profile.lane)}","valuesRedacted":true}`, ]); } function gatewayInstallCmd(profile: HwpodGatewayProfileSummary): string { return crlf([ "@echo off", "setlocal EnableExtensions", "chcp 65001 >nul", `set "ROOT=${profile.runtimeRoot}"`, `set "TASK_NAME=${profile.taskName}"`, `set "PERIODIC_TASK_NAME=${profile.periodicTaskName}"`, `set "RUN_KEY_NAME=${profile.runKeyName}"`, "set \"RUN_TASK=wscript.exe //B //Nologo %ROOT%\\run-node-ws.vbs\"", "set \"ENSURE_TASK=wscript.exe //B //Nologo %ROOT%\\ensure-node-ws.vbs\"", "cd /d \"%ROOT%\" || exit /b 2", "schtasks /Create /TN \"%TASK_NAME%\" /SC ONCE /ST 00:00 /TR \"%RUN_TASK%\" /F || exit /b 2", "schtasks /Create /TN \"%PERIODIC_TASK_NAME%\" /SC MINUTE /MO 5 /TR \"%ENSURE_TASK%\" /F || exit /b 2", "reg add HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /v \"%RUN_KEY_NAME%\" /t REG_SZ /d \"%ENSURE_TASK%\" /f || exit /b 2", `echo {"ok":true,"action":"hwpod-node.ws.v03.install","status":"installed","taskName":"${jsonEscape(profile.taskName)}","periodicTaskName":"${jsonEscape(profile.periodicTaskName)}","valuesRedacted":true}`, "exit /b 0", ]); } function gatewayRunVbs(profile: HwpodGatewayProfileSummary): string { return crlf([ "Set sh = CreateObject(\"WScript.Shell\")", `sh.CurrentDirectory = "${vbsEscape(profile.runtimeRoot)}"`, `sh.Run "cmd.exe /c ""${vbsEscape(`${profile.runtimeRoot}\\run-node-ws-detached.cmd`)}""", 0, False`, ]); } function gatewayEnsureVbs(profile: HwpodGatewayProfileSummary): string { return crlf([ "Set sh = CreateObject(\"WScript.Shell\")", `sh.CurrentDirectory = "${vbsEscape(profile.runtimeRoot)}"`, `sh.Run "cmd.exe /c ""${vbsEscape(`${profile.runtimeRoot}\\start-node-ws.cmd`)}""", 0, False`, ]); } function parseJsonFromStdout(stdout: string): Record | null { const start = stdout.indexOf("{"); const end = stdout.lastIndexOf("}"); if (start === -1 || end <= start) return null; try { const parsed = JSON.parse(stdout.slice(start, end + 1)) as unknown; return isRecord(parsed) ? parsed : null; } catch { return null; } } function compactResultTail(result: CommandResult): string { return short(`exit=${result.exitCode} timedOut=${result.timedOut} stdout=${result.stdout.slice(-400)} stderr=${result.stderr.slice(-400)}`, 900); } function numberFromUnknown(value: unknown): number { return typeof value === "number" && Number.isFinite(value) ? value : 0; } function numberArray(value: unknown): number[] { return Array.isArray(value) ? value.filter((item): item is number => typeof item === "number" && Number.isFinite(item)) : []; } function stringArray(value: unknown): string[] { return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []; } function stringRecord(value: unknown): Record { if (!isRecord(value)) return {}; const result: Record = {}; for (const [key, item] of Object.entries(value)) result[key] = typeof item === "string" ? item : String(item); return result; } function shQuote(value: string): string { return `'${value.replaceAll("'", "'\"'\"'")}'`; } function psQuote(value: string): string { return `'${value.replace(/'/gu, "''")}'`; } function crlf(lines: readonly string[]): string { return `${lines.join("\r\n")}\r\n`; } function jsonEscape(value: string): string { return value.replace(/\\/gu, "\\\\").replace(/"/gu, "\\\""); } function vbsEscape(value: string): string { return value.replace(/"/gu, "\"\""); }