Merge pull request #2700 from pikasTech/feat/hwpod-temporal-split
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

HWPOD 独立 L1 运行面与 D601-WIN 恢复配置
This commit is contained in:
Lyon
2026-07-21 02:00:03 +08:00
committed by GitHub
8 changed files with 288 additions and 14 deletions
+48 -1
View File
@@ -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]
+41
View File
@@ -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
+3
View File
@@ -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
@@ -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 验收契约
- L0spec、topology、workflow contract 和 typed error 测试通过;默认路径不产生外部 mutation。
- L1API、worker、Web 可分别重启;worker 重启后未完成 workflow 可继续;Web 通过固定入口完成首屏、资源列表和 operation 状态交互。
- L1D601 节点注册后,HWPOD topology 同时显示 node online、spec available 和 capability match;节点断开后显示 node offline,不得伪造硬件成功。
- L1Cloud API 代理、独立 HWPOD API 和 CLI 对同一 operation 返回相同的 operation identity、状态和 blocker code。
+49 -4
View File
@@ -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
+43 -7
View File
@@ -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<string, unknown> {
usage: [
"bun scripts/cli.ts hwlab nodes native-development workbench api|worker|web start|stop|restart|status|logs --node <node> --lane <lane>",
"bun scripts/cli.ts hwlab nodes native-development workbench cli <health|session|turn|events ...> --node <node> --lane <lane>",
"bun scripts/cli.ts hwlab nodes native-development hwpod api|worker|web start|stop|restart|status|logs --node <node> --lane <lane>",
"bun scripts/cli.ts hwlab nodes native-development caserun api|web start|stop|restart|status|logs --node <node> --lane <lane>",
],
contract: "L1 API/Web 用户入口只输出 owning YAML 解析出的固定 HTTPS 域名;IP、port 和 bind/probe 地址不作为用户入口。",
@@ -32,7 +33,11 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
const native = spec.nativeDevelopment?.[application];
if (native === undefined) throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.${application} is required`);
const env = application === "workbench" ? workbenchEnvironment(spec, native as HwlabRuntimeNativeDevelopmentSpec["workbench"]) : hwlabCaseRunNativeDevelopmentEnvironment(native as NonNullable<HwlabRuntimeNativeDevelopmentSpec["caserun"]>);
const env = application === "workbench"
? workbenchEnvironment(spec, native as HwlabRuntimeNativeDevelopmentSpec["workbench"])
: application === "hwpod"
? hwlabHwpodNativeDevelopmentEnvironment(native as NonNullable<HwlabRuntimeNativeDevelopmentSpec["hwpod"]>)
: hwlabCaseRunNativeDevelopmentEnvironment(native as NonNullable<HwlabRuntimeNativeDevelopmentSpec["caserun"]>);
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<string,
const publicEndpoint = service === "api"
? { url: native.publicExposure.publicBaseUrl }
: service === "web"
? { url: `${native.publicExposure.publicBaseUrl}/${application === "workbench" ? "workbench" : "caserun"}` }
? { url: application === "hwpod" ? native.publicExposure.publicBaseUrl : `${native.publicExposure.publicBaseUrl}/${application === "workbench" ? "workbench" : "caserun"}` }
: null;
return {
ok: result.exitCode === 0 && payload.ok !== false,
@@ -186,13 +191,13 @@ function parsePayload(stdout: string): Record<string, unknown> {
}
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<HwlabRunti
CASERUN_WEB_PORT: String(native.web.port),
};
}
function hwlabHwpodNativeDevelopmentEnvironment(native: NonNullable<HwlabRuntimeNativeDevelopmentSpec["hwpod"]>): 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}`,
};
}
+2 -2
View File
@@ -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}`);
+54
View File
@@ -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<HwlabRuntimeNativeDevelopmentSpec["hwpod"]> {
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<HwlabRuntimeNativeDevelopmentSpec["tasktree"]> {
const raw = asRecord(value, path);
const api = asRecord(raw.api, `${path}.api`);