diff --git a/config/hwlab-hwpod-nodes.yaml b/config/hwlab-hwpod-nodes.yaml index 9779468c..83d67635 100644 --- a/config/hwlab-hwpod-nodes.yaml +++ b/config/hwlab-hwpod-nodes.yaml @@ -24,6 +24,54 @@ cloud: sourceKey: HWLAB_API_KEY targets: + D601-WIN: + enabled: true + lane: v03 + providerId: D601-WIN + windowsRoute: D601-WIN:win/f/Work/ConStart + nodeId: node-d601-f103-v2 + hwpodId: constart-71freq-c + interactiveUser: liang + python: + probeLauncherPath: "C:\\Windows\\py.exe" + guiLauncherPath: "C:\\Windows\\pyw.exe" + launcherArgs: [] + expectedVersionPrefix: "3.13.14" + runtime: + root: "C:\\Users\\liang\\hwlab-node" + scriptPath: "C:\\Users\\liang\\hwlab-node\\hwlab-node.py" + configPath: "C:\\Users\\liang\\.hwlab\\config.json" + credentialPath: "C:\\Users\\liang\\.hwlab\\hwpod-node.token" + logPath: "C:\\Users\\liang\\.hwlab\\logs\\hwlab-node.log" + artifact: + metadataUrl: https://lab-dev.hwpod.com/v1/hwlab-node/update + channel: stable + platform: windows + workspace: + allowedRoots: + - "F:\\Work\\ConStart" + defaultRoot: "F:\\Work\\ConStart" + desktopConfig: + serverUrl: https://lab-dev.hwpod.com + autoConnect: true + requireCredential: true + updateEnabled: true + updateAutoApplyWhenIdle: false + startup: + mode: hkcu-run + runKeyName: hwlab-node-d601-win + initialLaunch: interactive-process + processPattern: hwlab-node.py + legacyTaskNames: + - HWLAB-HWPOD-Node-WS + - hwpod-node-ws-periodic + legacyRunKeyNames: + - hwpod-node-ws + legacyProcessPatterns: + - tools\hwpod-node.ts connect + - run-node-ws-detached.cmd + projectManagementSourceRef: config/hwlab-project-management/constart-71freq-mdtodo.yaml#projectManagement.sources[0] + G14-WSL: enabled: true lane: v03 @@ -64,4 +112,3 @@ targets: initialLaunch: interactive-process processPattern: hwlab-node.py projectManagementSourceRef: config/hwlab-project-management/constar-workspace-mdtodo.yaml#projectManagement.sources[0] - diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index e58a96d6..b4ddd6a0 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -63,6 +63,8 @@ lanes: - hwlab-project-management - hwlab-harnessrl-api - hwlab-harnessrl-worker + - hwlab-hwpod-api + - hwlab-hwpod-worker - hwlab-cloud-web - hwlab-gateway - hwlab-edge-proxy @@ -209,6 +211,39 @@ lanes: traceTimeline: autoExpandRunning: false autoCollapseTerminal: false + hwpod: + runtimeApiUrl: https://lab-dev.hwpod.com + runtimeApiAuth: + sourceRef: hwlab/nc01-v03-admin.env + sourceKey: HWLAB_API_KEY + publicExposure: + publicBaseUrl: https://lab-dev-hwpod.hwpod.com + hostname: lab-dev-hwpod.hwpod.com + expectedA: 152.53.229.148 + upstream: http://127.0.0.1:5175 + healthPath: /health/live + stateDir: .state/hwpod-native/services + temporal: + address: 10.43.145.243:7233 + namespace: unidesk + taskQueue: hwlab-v03-hwpod-native-l1 + activity: + startToCloseTimeoutMs: 900000 + retryInitialIntervalMs: 1000 + retryMaximumIntervalMs: 10000 + retryMaximumAttempts: 1 + api: + bindHost: 0.0.0.0 + probeHost: 127.0.0.1 + port: 6681 + worker: + bindHost: 127.0.0.1 + probeHost: 127.0.0.1 + healthPort: 6682 + web: + bindHost: 0.0.0.0 + probeHost: 127.0.0.1 + port: 5175 caserun: publicExposure: publicBaseUrl: https://lab-dev-caserun.hwpod.com @@ -225,6 +260,12 @@ lanes: bindHost: 0.0.0.0 probeHost: 127.0.0.1 port: 5173 + hwpodPreinstall: + enabled: true + configRefs: + preinstall: config/hwlab-hwpod-preinstalls/constart-71freq-c.yaml#hwpodPreinstall + projectManagementSource: config/hwlab-project-management/constart-71freq-mdtodo.yaml#projectManagement.sources[0] + gatewayProfile: config/hwlab-gateway/constart-71freq.yaml#gateway.profile sourceAuthority: extends: templates.hwlabV03.sourceAuthorityGiteaSnapshot giteaMirrorRepoKey: hwlab-nc01-v03 diff --git a/config/platform-infra/public-edge.yaml b/config/platform-infra/public-edge.yaml index 7aa70d78..f0f10f3d 100644 --- a/config/platform-infra/public-edge.yaml +++ b/config/platform-infra/public-edge.yaml @@ -93,6 +93,9 @@ targets: - id: hwlab-workbench-native configRef: config/hwlab-node-lanes.yaml path: lanes.v03.targets.NC01.nativeDevelopment.workbench.publicExposure + - id: hwlab-hwpod-native + configRef: config/hwlab-node-lanes.yaml + path: lanes.v03.targets.NC01.nativeDevelopment.hwpod.publicExposure - id: agentrun-native configRef: config/agentrun.yaml path: nativeDevelopment.manager.publicExposure diff --git a/project-management/PJ2026-01/specs/PJ2026-010103-hwpod-service.md b/project-management/PJ2026-01/specs/PJ2026-010103-hwpod-service.md index 23f9ad1e..ddca73ed 100644 --- a/project-management/PJ2026-01/specs/PJ2026-010103-hwpod-service.md +++ b/project-management/PJ2026-01/specs/PJ2026-010103-hwpod-service.md @@ -178,3 +178,51 @@ HWPOD服务应提供有界 typed topology read model,统一表达 owning 配 该投影必须分别表达 service ready、node online、HWPOD available、leased/busy 和 capability mismatch。列表接口只返回有界摘要并支持服务端筛选、排序和 cursor;完整 spec document、日志和诊断通过实体详情或专用查询渐进披露。 Cloud API 可以代理并映射主体、ACL、错误与 correlation,但不得在浏览器或 Cloud API 临时拼装第二份 topology authority。若连接规模要求抽取独立 HWPOD 服务,registry、topology、operation 和诊断事实必须随 owner 一次迁移。 + +## 7. 独立服务架构与运行等级 + +HWPOD 服务应作为独立的可组合应用交付,分为 Temporal worker、API 后端和 Web 前端三个可独立启动、停止、检查和扩缩的运行单元。 + +### 7.1 目标架构 + +```mermaid +flowchart LR + W[HWPOD Web] --> A[HWPOD API] + C[HWPOD CLI / Cloud API] --> A + A --> T[Temporal namespace unidesk] + T --> WK[HWPOD Worker] + WK --> R[Registry / Operation ledger] + WK --> N[主动出站 HWPOD Node] + N --> H[真实硬件与工作区] +``` + +- API 只负责 HTTP envelope、鉴权、correlation、查询投影和 workflow 提交。 +- Worker 负责 HWPOD workflow、节点路由、operation 状态推进和结果归属。 +- Web 只依赖 HWPOD API 合同,不复制 registry、topology 或 operation authority。 +- Cloud API 可以代理 HWPOD API,但不得在 Cloud API 内保留第二份 HWPOD 业务 authority。 +- HWPOD Node 通过主动出站连接注册到服务端入口;节点不得依赖用户侧公网入站端口。 + +### 7.2 L0/L1 合同 + +- L0 必须能在不启动 API、worker、Temporal、Web 或 Kubernetes workload 的情况下,直接验证 spec 解析、topology 投影、workflow 输入和错误分类。 +- L1 必须能按 owning YAML 独立启动 API、worker 和 Web,并用同一 HWPOD API 合同完成健康、spec、topology、operation 和 workflow smoke。 +- L1 Web 的首选入口为 owning YAML 声明的 HTTPS origin;公共入口不可用时,只能使用同一 YAML 声明的固定公网 HTTP 降级入口。 +- L1 API、worker 和 Web 必须分别暴露健康/就绪检查;worker 不得创建公网业务入口。 +- L1 Web 必须支持 HWPOD 资源列表、节点状态、operation 状态和明确 blocker 展示,且页面刷新后仍通过 API 恢复同一投影。 + +### 7.3 稳定接口 + +- `GET /health/live` 和 `GET /health/ready`:分别返回进程存活和 HWPOD/Temporal 依赖就绪状态。 +- `GET /v1/hwpod/specs`:返回有界 HWPOD spec 摘要和可用性。 +- `GET /v1/hwpod/topology`:返回 node、HWPOD、能力、占用、最近 operation 和 blocker 投影。 +- `POST /v1/hwpod/operations`:提交带有 `operationId`、`hwpodId`、`nodeId`、调用方和租约摘要的 workflow。 +- `GET /v1/hwpod/operations/{operationId}`:读取同一 operation 的 durable 状态和结果摘要。 + +所有接口必须保持 `valuesPrinted=false`,不得返回 token、Authorization、Secret 值或可复用节点凭据。 + +### 7.4 验收契约 + +- L0:spec、topology、workflow contract 和 typed error 测试通过;默认路径不产生外部 mutation。 +- L1:API、worker、Web 可分别重启;worker 重启后未完成 workflow 可继续;Web 通过固定入口完成首屏、资源列表和 operation 状态交互。 +- L1:D601 节点注册后,HWPOD topology 同时显示 node online、spec available 和 capability match;节点断开后显示 node offline,不得伪造硬件成功。 +- L1:Cloud API 代理、独立 HWPOD API 和 CLI 对同一 operation 返回相同的 operation identity、状态和 blocker code。 diff --git a/scripts/src/hwlab-hwpod-node.ts b/scripts/src/hwlab-hwpod-node.ts index e2303b3c..2ea3695c 100644 --- a/scripts/src/hwlab-hwpod-node.ts +++ b/scripts/src/hwlab-hwpod-node.ts @@ -73,7 +73,15 @@ interface HwpodNodeSpec { updateEnabled: boolean; updateAutoApplyWhenIdle: boolean; }; - startup: { mode: string; runKeyName: string; initialLaunch: string; processPattern: string }; + startup: { + mode: string; + runKeyName: string; + initialLaunch: string; + processPattern: string; + legacyTaskNames: string[]; + legacyRunKeyNames: string[]; + legacyProcessPatterns: string[]; + }; sourceRef: string; source: SourceSpec; } @@ -253,6 +261,9 @@ function readSpec(node: string, lane: string): HwpodNodeSpec { runKeyName: text(startup.runKeyName, `targets.${node}.startup.runKeyName`), initialLaunch: text(startup.initialLaunch, `targets.${node}.startup.initialLaunch`), processPattern: text(startup.processPattern, `targets.${node}.startup.processPattern`), + legacyTaskNames: startup.legacyTaskNames === undefined ? [] : texts(startup.legacyTaskNames, `targets.${node}.startup.legacyTaskNames`), + legacyRunKeyNames: startup.legacyRunKeyNames === undefined ? [] : texts(startup.legacyRunKeyNames, `targets.${node}.startup.legacyRunKeyNames`), + legacyProcessPatterns: startup.legacyProcessPatterns === undefined ? [] : texts(startup.legacyProcessPatterns, `targets.${node}.startup.legacyProcessPatterns`), }, sourceRef, source, @@ -285,7 +296,7 @@ function validateCrossRefs(spec: HwpodNodeSpec): void { if (!spec.workspace.allowedRoots.includes(spec.workspace.defaultRoot)) conflicts.push("workspace.defaultRoot 不在 allowedRoots 中"); if (spec.source.workspaceRootRef !== spec.workspace.defaultRoot) conflicts.push("source.workspaceRootRef 与 workspace.defaultRoot 不一致"); if (spec.startup.mode !== "hkcu-run") conflicts.push(`startup.mode 仅支持 hkcu-run,实际为 ${spec.startup.mode}`); - if (!spec.windowsRoute.endsWith(":win")) conflicts.push("windowsRoute 必须指向 :win plane"); + if (!/:win(?:\/[a-zA-Z](?:\/.*)?)?$/u.test(spec.windowsRoute)) conflicts.push("windowsRoute 必须指向 :win plane,并可附带显式 Windows cwd"); if (conflicts.length > 0) throw new Error(`HWPOD 节点配置冲突:${conflicts.join(";")}`); } @@ -448,6 +459,9 @@ function applyDesktop(spec: HwpodNodeSpec, artifact: ArtifactBundle, credential: launcherArgs: spec.python.launcherArgs, expectedVersionPrefix: spec.python.expectedVersionPrefix, runKeyName: spec.startup.runKeyName, + legacyTaskNames: spec.startup.legacyTaskNames, + legacyRunKeyNames: spec.startup.legacyRunKeyNames, + legacyProcessPatterns: spec.startup.legacyProcessPatterns, artifactBase64: artifact.content.toString("base64"), artifactSha256: artifact.sha256, configBase64: Buffer.from(`${JSON.stringify(config, null, 2)}\n`, "utf8").toString("base64"), @@ -570,11 +584,31 @@ $logPath = [string]$payload.logPath $probe = [string]$payload.probeLauncherPath $gui = [string]$payload.guiLauncherPath $launcherArgs = @($payload.launcherArgs | ForEach-Object { [string]$_ }) +$legacyTaskNames = @($payload.legacyTaskNames | ForEach-Object { [string]$_ }) +$legacyRunKeyNames = @($payload.legacyRunKeyNames | ForEach-Object { [string]$_ }) +$legacyProcessPatterns = @($payload.legacyProcessPatterns | ForEach-Object { [string]$_ }) if (-not (Test-Path $probe)) { throw "Python probe launcher missing: $probe" } if (-not (Test-Path $gui)) { throw "Python GUI launcher missing: $gui" } $versionOut = @(& $probe @launcherArgs -c "import sys;import tkinter;print(sys.version);print(sys.executable);print(tkinter.TkVersion)" 2>&1) if ($LASTEXITCODE -ne 0 -or (($versionOut -join "\`n") -notlike "*$($payload.expectedVersionPrefix)*")) { throw "Python version mismatch: $($versionOut -join ' | ')" } New-Item -ItemType Directory -Force -Path $root, (Split-Path -Parent $configPath), (Split-Path -Parent $credentialPath), (Split-Path -Parent $logPath) | Out-Null +$legacyStopped = @() +foreach ($taskName in $legacyTaskNames) { + Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + Disable-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue | Out-Null +} +foreach ($runKey in $legacyRunKeyNames) { + Remove-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' -Name $runKey -ErrorAction SilentlyContinue +} +$legacy = @(Get-CimInstance Win32_Process | Where-Object { + $commandLine = [string]$_.CommandLine + $legacyProcessPatterns | Where-Object { $commandLine.Contains($_) } | Select-Object -First 1 +}) +foreach ($proc in $legacy) { + Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue + $legacyStopped += [int]$proc.ProcessId +} +Start-Sleep -Milliseconds 500 $old = @(Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and $_.CommandLine.Contains($scriptPath) }) foreach ($proc in $old) { Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue } Start-Sleep -Milliseconds 500 @@ -597,9 +631,15 @@ Start-Sleep -Seconds 3 $processes = @(Get-CimInstance Win32_Process | Where-Object { $_.Name -like 'python*.exe' -and $_.CommandLine -and $_.CommandLine.Contains($scriptPath) }) $explorerSessions = @(Get-Process explorer -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SessionId -Unique) $interactive = @($processes | Where-Object { $explorerSessions -contains $_.SessionId }) +$legacyTasksStillEnabled = @($legacyTaskNames | Where-Object { $task = Get-ScheduledTask -TaskName $_ -ErrorAction SilentlyContinue; $task -and $task.Settings.Enabled }) +$legacyProcessesRemaining = @(Get-CimInstance Win32_Process | Where-Object { + $commandLine = [string]$_.CommandLine + $legacyProcessPatterns | Where-Object { $commandLine.Contains($_) } | Select-Object -First 1 +}) +$ready = [bool](($processes.Count -eq 1) -and ($interactive.Count -eq 1) -and ($legacyTasksStillEnabled.Count -eq 0) -and ($legacyProcessesRemaining.Count -eq 0)) $result = [ordered]@{ - ok = [bool](($processes.Count -eq 1) -and ($interactive.Count -eq 1)) - status = $(if (($processes.Count -eq 1) -and ($interactive.Count -eq 1)) { 'applied' } else { 'installed-not-interactive' }) + ok = $ready + status = $(if ($ready) { 'applied' } elseif (($processes.Count -eq 1) -and ($interactive.Count -eq 1)) { 'legacy-authority-not-retired' } else { 'installed-not-interactive' }) pythonSummary = (($versionOut | Select-Object -First 3) -join ' | ') scriptSha256 = 'sha256:' + $artifactActual configSha256 = 'sha256:' + $configActual @@ -609,6 +649,11 @@ $result = [ordered]@{ processIds = @($processes | ForEach-Object { [int]$_.ProcessId }) runKeyReady = $true stoppedProcessIds = @($old | ForEach-Object { [int]$_.ProcessId }) + retiredLegacyProcessIds = @($legacyStopped) + retiredLegacyTaskNames = @($legacyTaskNames) + retiredLegacyRunKeyNames = @($legacyRunKeyNames) + legacyTasksStillEnabled = @($legacyTasksStillEnabled) + legacyProcessIdsRemaining = @($legacyProcessesRemaining | ForEach-Object { [int]$_.ProcessId }) valuesRedacted = $true } $result | ConvertTo-Json -Compress -Depth 6 diff --git a/scripts/src/hwlab-native-development.ts b/scripts/src/hwlab-native-development.ts index ab02503b..7fce1a66 100644 --- a/scripts/src/hwlab-native-development.ts +++ b/scripts/src/hwlab-native-development.ts @@ -4,7 +4,7 @@ import { runCommand } from "./command"; import { readEnvSourceFile, requiredEnvValue } from "./secrets"; import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes"; -type Application = "workbench" | "caserun"; +type Application = "workbench" | "hwpod" | "caserun"; type Service = "api" | "worker" | "web"; type Action = "start" | "stop" | "restart" | "status" | "logs"; @@ -15,6 +15,7 @@ export function hwlabNativeDevelopmentHelp(): Record { usage: [ "bun scripts/cli.ts hwlab nodes native-development workbench api|worker|web start|stop|restart|status|logs --node --lane ", "bun scripts/cli.ts hwlab nodes native-development workbench cli --node --lane ", + "bun scripts/cli.ts hwlab nodes native-development hwpod api|worker|web start|stop|restart|status|logs --node --lane ", "bun scripts/cli.ts hwlab nodes native-development caserun api|web start|stop|restart|status|logs --node --lane ", ], contract: "L1 API/Web 用户入口只输出 owning YAML 解析出的固定 HTTPS 域名;IP、port 和 bind/probe 地址不作为用户入口。", @@ -32,7 +33,11 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record); + const env = application === "workbench" + ? workbenchEnvironment(spec, native as HwlabRuntimeNativeDevelopmentSpec["workbench"]) + : application === "hwpod" + ? hwlabHwpodNativeDevelopmentEnvironment(native as NonNullable) + : hwlabCaseRunNativeDevelopmentEnvironment(native as NonNullable); if (application === "workbench" && serviceRaw === "cli") { const commandArgs = withoutOptions(args.slice(2), ["--node", "--lane"]); if (commandArgs.length === 0) throw new Error("native-development workbench cli requires a Workbench command"); @@ -60,7 +65,7 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record { } function parseApplication(value: string | undefined): Application { - if (value === "workbench" || value === "caserun") return value; - throw new Error("native-development application must be workbench or caserun"); + if (value === "workbench" || value === "hwpod" || value === "caserun") return value; + throw new Error("native-development application must be workbench, hwpod, or caserun"); } function parseService(value: string | undefined, application: Application): Service { - if (value === "api" || value === "web" || (application === "workbench" && value === "worker")) return value; - throw new Error(`native-development ${application} service must be ${application === "workbench" ? "api, worker, or web" : "api or web"}`); + if (value === "api" || value === "web" || (application !== "caserun" && value === "worker")) return value; + throw new Error(`native-development ${application} service must be ${application === "caserun" ? "api or web" : "api, worker, or web"}`); } function parseAction(value: string | undefined): Action { @@ -236,3 +241,34 @@ function hwlabCaseRunNativeDevelopmentEnvironment(native: NonNullable): NodeJS.ProcessEnv { + const authorizationSource = readEnvSourceFile({ + root: "/root/.unidesk/.state/secrets", + sourceRef: native.runtimeApiAuth.sourceRef, + }); + const runtimeApiKey = requiredEnvValue(authorizationSource.values, native.runtimeApiAuth.sourceKey, native.runtimeApiAuth.sourceRef); + return { + HWPOD_NATIVE_SERVICE_STATE_DIR: native.stateDir, + HWPOD_PUBLIC_BASE_URL: native.publicExposure.publicBaseUrl, + HWPOD_TEMPORAL_ADDRESS: native.temporal.address, + HWPOD_TEMPORAL_NAMESPACE: native.temporal.namespace, + HWPOD_TEMPORAL_TASK_QUEUE: native.temporal.taskQueue, + HWPOD_ACTIVITY_START_TO_CLOSE_TIMEOUT_MS: String(native.temporal.activity.startToCloseTimeoutMs), + HWPOD_ACTIVITY_RETRY_INITIAL_INTERVAL_MS: String(native.temporal.activity.retryInitialIntervalMs), + HWPOD_ACTIVITY_RETRY_MAXIMUM_INTERVAL_MS: String(native.temporal.activity.retryMaximumIntervalMs), + HWPOD_ACTIVITY_RETRY_MAXIMUM_ATTEMPTS: String(native.temporal.activity.retryMaximumAttempts), + HWPOD_RUNTIME_API_URL: native.runtimeApiUrl, + HWPOD_RUNTIME_API_AUTHORIZATION: `Bearer ${runtimeApiKey}`, + HWPOD_API_BIND_HOST: native.api.bindHost, + HWPOD_API_PROBE_HOST: native.api.probeHost, + HWPOD_API_PORT: String(native.api.port), + HWPOD_WORKER_BIND_HOST: native.worker.bindHost, + HWPOD_WORKER_PROBE_HOST: native.worker.probeHost, + HWPOD_WORKER_HEALTH_PORT: String(native.worker.healthPort), + HWPOD_WEB_BIND_HOST: native.web.bindHost, + HWPOD_WEB_PROBE_HOST: native.web.probeHost, + HWPOD_WEB_PORT: String(native.web.port), + HWPOD_NATIVE_API_URL: `http://${native.api.probeHost}:${native.api.port}`, + }; +} diff --git a/scripts/src/hwlab-node-hwpod-preinstall.ts b/scripts/src/hwlab-node-hwpod-preinstall.ts index e9388c32..42da9c42 100644 --- a/scripts/src/hwlab-node-hwpod-preinstall.ts +++ b/scripts/src/hwlab-node-hwpod-preinstall.ts @@ -502,7 +502,6 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte 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"); @@ -514,6 +513,7 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte const hwpodId = stringAt(preinstall, "hwpodId"); const nodeId = stringAt(preinstall, "nodeBinding.nodeId"); + const hwlabNode = stringAt(preinstall, "nodeBinding.hwlabNode"); const workspaceRootRef = stringAt(preinstall, "workspaceRootRef"); if (pm !== null) { requireEquals(conflicts, byKey.get("projectManagementSource"), "sourceKind", "hwpod-workspace", "required sourceKind hwpod-workspace"); @@ -522,7 +522,7 @@ function crossReferenceConflicts(spec: HwlabRuntimeLaneSpec, refs: readonly Inte 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}`); + if (hwlabNode !== null) requireEquals(conflicts, byKey.get("gatewayProfile"), "node", hwlabNode, `preinstall hardware node ${hwlabNode}`); 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}`); diff --git a/scripts/src/hwlab-node-lanes.ts b/scripts/src/hwlab-node-lanes.ts index 5c4c8095..b5713edf 100644 --- a/scripts/src/hwlab-node-lanes.ts +++ b/scripts/src/hwlab-node-lanes.ts @@ -736,6 +736,26 @@ export interface HwlabRuntimeNativeDevelopmentSpec { }; }; }; + readonly hwpod?: { + readonly publicExposure: HwlabRuntimeNativePublicExposureSpec; + readonly stateDir: string; + readonly runtimeApiUrl: string; + readonly runtimeApiAuth: { readonly sourceRef: string; readonly sourceKey: string }; + readonly temporal: { + readonly address: string; + readonly namespace: string; + readonly taskQueue: string; + readonly activity: { + readonly startToCloseTimeoutMs: number; + readonly retryInitialIntervalMs: number; + readonly retryMaximumIntervalMs: number; + readonly retryMaximumAttempts: number; + }; + }; + readonly api: { readonly bindHost: string; readonly probeHost: string; readonly port: number }; + readonly worker: { readonly bindHost: string; readonly probeHost: string; readonly healthPort: number }; + readonly web: { readonly bindHost: string; readonly probeHost: string; readonly port: number }; + }; readonly caserun?: { readonly publicExposure: HwlabRuntimeNativePublicExposureSpec; readonly stateDir: string; @@ -1594,10 +1614,44 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati }, }, }, + ...(raw.hwpod === undefined ? {} : { hwpod: nativeHwpodConfig(raw.hwpod, `${path}.hwpod`) }), ...(raw.caserun === undefined ? {} : { caserun: nativeCaseRunConfig(raw.caserun, `${path}.caserun`) }), }; } +function nativeHwpodConfig(value: unknown, path: string): NonNullable { + const raw = asRecord(value, path); + const runtimeApiAuth = asRecord(raw.runtimeApiAuth, `${path}.runtimeApiAuth`); + const temporal = asRecord(raw.temporal, `${path}.temporal`); + const activity = asRecord(temporal.activity, `${path}.temporal.activity`); + const api = asRecord(raw.api, `${path}.api`); + const worker = asRecord(raw.worker, `${path}.worker`); + const web = asRecord(raw.web, `${path}.web`); + return { + publicExposure: nativePublicExposureConfig(raw.publicExposure, `${path}.publicExposure`), + stateDir: relativeWorkspacePathField(stringField(raw, "stateDir", path), `${path}.stateDir`), + runtimeApiUrl: httpUrlField(raw, "runtimeApiUrl", path), + runtimeApiAuth: { + sourceRef: stringField(runtimeApiAuth, "sourceRef", `${path}.runtimeApiAuth`), + sourceKey: stringField(runtimeApiAuth, "sourceKey", `${path}.runtimeApiAuth`), + }, + temporal: { + address: stringField(temporal, "address", `${path}.temporal`), + namespace: stringField(temporal, "namespace", `${path}.temporal`), + taskQueue: stringField(temporal, "taskQueue", `${path}.temporal`), + activity: { + startToCloseTimeoutMs: numberField(activity, "startToCloseTimeoutMs", `${path}.temporal.activity`), + retryInitialIntervalMs: numberField(activity, "retryInitialIntervalMs", `${path}.temporal.activity`), + retryMaximumIntervalMs: numberField(activity, "retryMaximumIntervalMs", `${path}.temporal.activity`), + retryMaximumAttempts: numberField(activity, "retryMaximumAttempts", `${path}.temporal.activity`), + }, + }, + api: { bindHost: stringField(api, "bindHost", `${path}.api`), probeHost: stringField(api, "probeHost", `${path}.api`), port: numberField(api, "port", `${path}.api`) }, + worker: { bindHost: stringField(worker, "bindHost", `${path}.worker`), probeHost: stringField(worker, "probeHost", `${path}.worker`), healthPort: numberField(worker, "healthPort", `${path}.worker`) }, + web: { bindHost: stringField(web, "bindHost", `${path}.web`), probeHost: stringField(web, "probeHost", `${path}.web`), port: numberField(web, "port", `${path}.web`) }, + }; +} + function nativeTaskTreeConfig(value: unknown, path: string): NonNullable { const raw = asRecord(value, path); const api = asRecord(raw.api, `${path}.api`);