Files
pikasTech-unidesk/scripts/src/hwlab-node-lanes.ts
T
2026-07-15 05:15:19 +02:00

2482 lines
122 KiB
TypeScript

// SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel.
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-26-p9-multi-web-probe-sentinel.
// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.
// Responsibility: YAML source-of-truth parsing for HWLAB node/lane Workbench observability.
import { readFileSync } from "node:fs";
import { rootPath } from "./config";
import { materializeYamlComposition } from "./yaml-composition";
export type HwlabRuntimeLane = string;
export interface HwlabRuntimeNodeSpec {
readonly id: string;
readonly route: string;
readonly kubeRoute: string;
readonly sourceWorkspace: string;
readonly gitopsRoot: string;
readonly networkProfileId: string;
readonly downloadProfileId: string;
}
export interface HwlabProxySpec {
readonly http: string;
readonly https: string;
readonly all: string;
readonly socks5?: string;
readonly noProxy: readonly string[];
}
export interface HwlabNetworkProfileSpec {
readonly id: string;
readonly proxy: HwlabProxySpec;
readonly imageBuildProxy: HwlabProxySpec;
readonly dockerBuildProxy: HwlabProxySpec;
}
export interface HwlabDownloadProfileSpec {
readonly id: string;
readonly git: {
readonly proxyMode: "inherit" | "direct" | "none";
readonly retries: number;
readonly timeoutSeconds: number;
};
readonly npm: {
readonly registry: string;
readonly retries: number;
readonly fetchTimeoutSeconds: number;
};
readonly pip: {
readonly indexUrl: string;
readonly retries: number;
readonly timeoutSeconds: number;
};
readonly docker: {
readonly registryMirrors: readonly string[];
readonly pullRetries: number;
};
readonly curl: {
readonly retries: number;
readonly connectTimeoutSeconds: number;
readonly maxTimeSeconds: number;
};
}
export interface HwlabRuntimeExternalPostgresComponentSpec {
readonly secretName: string;
readonly secretKey: string;
readonly sourceRef: string;
readonly envKey: string;
readonly role: string;
readonly authnKey?: string;
readonly schema?: string;
}
export interface HwlabRuntimeExternalPostgresRuntimeAccessSpec {
readonly routeName?: string;
readonly endpointAddress: string;
readonly port: number;
}
export interface HwlabRuntimeExternalPostgresMigrationLedgerSpec {
readonly identity: string;
readonly table: string;
}
export interface HwlabRuntimeExternalPostgresSpec {
readonly provider: string;
readonly configRef: string;
readonly serviceName: string;
readonly endpointAddress: string;
readonly port: number;
readonly runtimeAccess?: HwlabRuntimeExternalPostgresRuntimeAccessSpec;
readonly sslmode: "require";
readonly database: string;
readonly migrationLedger: HwlabRuntimeExternalPostgresMigrationLedgerSpec;
readonly cloudApi: HwlabRuntimeExternalPostgresComponentSpec;
readonly openfga: HwlabRuntimeExternalPostgresComponentSpec;
}
export interface HwlabRuntimePostgresStoreSpec {
readonly mode?: "local-k3s" | "platform-service";
readonly secretName?: string;
readonly statefulSet?: string;
readonly serviceName?: string;
readonly adminUser?: string;
readonly adminPasswordSourceRef?: string;
readonly adminPasswordSourceKey?: string;
readonly cloudApi?: HwlabRuntimePostgresStoreComponentSpec;
readonly openfga?: HwlabRuntimePostgresStoreComponentSpec;
readonly poolMax: number;
readonly connectionTimeoutMs?: number;
readonly queryRetryMaxAttempts?: number;
readonly queryRetryInitialDelayMs?: number;
readonly queryRetryMaxDelayMs?: number;
}
export interface HwlabRuntimePostgresStoreComponentSpec {
readonly secretName: string;
readonly secretKey: string;
readonly database: string;
readonly role: string;
readonly authnKey?: string;
readonly postgresPasswordKey?: string;
}
export interface HwlabRuntimeStoreSpec {
readonly postgres?: HwlabRuntimePostgresStoreSpec;
}
export interface HwlabRuntimeWebProbeServiceOriginSpec {
readonly mode: "k8s-service-cluster-ip";
readonly serviceName: string;
readonly namespace: string;
readonly port: number;
readonly scheme: "http" | "https";
readonly browserProxyMode?: "auto" | "direct";
}
export interface HwlabRuntimeWebProbePublicOriginSpec {
readonly mode: "public";
readonly baseUrl?: string;
readonly browserProxyMode?: "auto" | "direct";
}
export type HwlabRuntimeWebProbeOriginSpec = HwlabRuntimeWebProbeServiceOriginSpec | HwlabRuntimeWebProbePublicOriginSpec;
export type HwlabRuntimeWebProbeOriginName = "internal" | "public";
export interface HwlabRuntimeWebProbeOriginsSpec {
readonly internal: HwlabRuntimeWebProbeServiceOriginSpec;
readonly public: HwlabRuntimeWebProbePublicOriginSpec;
}
export interface HwlabRuntimeWebProbeSpec {
readonly browserProxyMode?: "auto" | "direct";
readonly playwrightBrowsersPath?: string;
readonly defaultOrigin?: HwlabRuntimeWebProbeOriginName;
readonly resourcePolicy?: HwlabRuntimeWebProbeResourcePolicySpec;
readonly origins?: HwlabRuntimeWebProbeOriginsSpec;
readonly authLogin?: HwlabRuntimeWebProbeAuthLoginSpec;
readonly alertThresholds?: HwlabRuntimeWebProbeAlertThresholdsSpec;
readonly browserFreezePolicy?: HwlabRuntimeWebProbeBrowserFreezePolicySpec;
readonly projectManagement?: HwlabRuntimeWebProbeProjectManagementSpec;
readonly realtimeFanoutProfiles?: Readonly<Record<string, HwlabRuntimeWebProbeRealtimeFanoutProfileSpec>>;
readonly workbenchKafkaDebugReplay?: HwlabRuntimeWebProbeWorkbenchKafkaDebugReplaySpec;
readonly workbenchTraceReadability?: HwlabRuntimeWebProbeWorkbenchTraceReadabilitySpec;
}
export interface HwlabRuntimeWebProbeMemoryStartGuardSpec {
readonly metric: "MemAvailable";
readonly source: "/proc/meminfo";
readonly swapExcluded: true;
readonly thresholdBytes: number;
readonly manualComparator: "less-than-or-equal";
readonly sentinelComparator: "less-than";
readonly snapshotTimeoutSeconds: number;
readonly launchLockPath: string;
readonly launchLockTimeoutSeconds: number;
readonly launchReadyTimeoutSeconds: number;
readonly launchReadyPollMilliseconds: number;
}
export interface HwlabRuntimeWebProbeObserverLifecycleSpec {
readonly maxRunSeconds: number;
readonly maxSamples: number;
readonly maxScreenshots: number;
readonly maxPerformanceEntries: number;
readonly maxBrowserProcessHistoryEntries: number;
readonly maxBrowserFreezeSignalHistoryEntries: number;
readonly maxRealtimeEventEntries: number;
readonly maxRealtimeEventBodyBytes: number;
readonly maxDomEvidenceRows: number;
readonly maxProcessScanEntries: number;
readonly maxPerformanceCaptureSeconds: number;
readonly maxResponseBodyBytes: number;
readonly maxWebPerformanceBodyBytes: number;
readonly maxJsonlQueueEntries: number;
readonly maxJsonlQueueBytes: number;
readonly maxJsonlLineBytes: number;
readonly jsonlFlushTimeoutMs: number;
readonly maxResponseBodyInFlight: number;
readonly maxPassiveListenerTasks: number;
readonly passiveTaskFlushTimeoutMs: number;
readonly closeStepTimeoutMs: number;
readonly maxProcessReadConcurrency: number;
readonly maxRealtimeTotalEntries: number;
readonly maxRealtimeTotalBodyBytes: number;
}
export interface HwlabRuntimeWebProbeResourcePolicySpec {
readonly memoryStartGuard: HwlabRuntimeWebProbeMemoryStartGuardSpec;
readonly observerLifecycle: HwlabRuntimeWebProbeObserverLifecycleSpec;
readonly browserEvidence: HwlabRuntimeWebProbeBrowserEvidenceSpec;
}
export interface HwlabRuntimeWebProbeBrowserEvidenceSpec {
readonly screenshotMode: "viewport";
readonly maxNetworkRequestEntries: number;
readonly maxNetworkRequestBytes: number;
readonly maxNetworkFailureEntries: number;
readonly maxNetworkFailureBytes: number;
}
export interface HwlabRuntimeWebProbeWorkbenchKafkaDebugReplaySpec {
readonly enabled: boolean;
readonly topic: string;
readonly groupPrefix: string;
readonly completionTimeoutMs: number;
}
export interface HwlabRuntimeWebProbeWorkbenchTraceReadabilitySpec {
readonly enabled: boolean;
readonly runningCardTimeoutMs: number;
readonly disclosureActionTimeoutMs: number;
readonly disclosureOpenTimeoutMs: number;
readonly runningReadableTimeoutMs: number;
readonly terminalTimeoutMs: number;
readonly domStableQuietMs: number;
readonly domStableTimeoutMs: number;
readonly pollIntervalMs: number;
}
export interface HwlabRuntimeWebProbeRealtimeFanoutProfileSpec {
readonly subscriberCount: 2;
readonly debugStreams: readonly ("stdio" | "agentrun" | "hwlab")[];
readonly productEventsPath: string;
readonly debugEventsPath: string;
readonly productReadyEvent: string;
readonly debugReadyEvent: string;
readonly businessEvent: string;
readonly fromBeginning: false;
readonly barrierTimeoutMs: number;
readonly terminalTimeoutMs: number;
readonly terminalQuietMs: number;
readonly navigationMaxAttempts: number;
readonly navigationRetryDelayMs: number;
readonly reconnectQuietMs: number;
readonly expectedProductSse: Readonly<{
deliverySemantics: string;
liveOnly: boolean;
replay: boolean;
replaySupported: boolean;
lossPossible: boolean;
refreshHandoff: boolean;
replayPriorTraceOnReconnect: boolean;
}>;
readonly forbiddenRequestPaths: readonly string[];
readonly requiredEventTypes: Readonly<{
agentrun: readonly string[];
hwlab: readonly string[];
}>;
readonly conditionalEventTypePairs: Readonly<Record<string, Readonly<{
agentrun: string;
hwlab: string;
}>>>;
}
export interface HwlabRuntimeWebProbeAuthLoginSpec {
readonly maxAttempts: number;
readonly requestTimeoutMs: number;
readonly initialDelayMs: number;
readonly maxDelayMs: number;
}
export interface HwlabRuntimeWebProbeBrowserFreezeMemoryPolicySpec {
readonly totalRssBlockerMb: number;
readonly processRssBlockerMb: number;
readonly growthBlockerMb: number;
}
export interface HwlabRuntimeWebProbeBrowserFreezeResponsivenessPolicySpec {
readonly latencyBlockerMs: number;
readonly eventBlockerCount: number;
}
export interface HwlabRuntimeWebProbeBrowserFreezeCdpPolicySpec {
readonly metricsTimeoutBlockerCount: number;
}
export interface HwlabRuntimeWebProbeBrowserFreezeKillPolicySpec {
readonly enabled: boolean;
readonly gracefulSignal: "SIGTERM";
readonly forceSignal: "SIGKILL";
readonly graceMs: number;
readonly pollIntervalMs: number;
readonly exitCode: number;
}
export interface HwlabRuntimeWebProbeBrowserFreezePolicySpec {
readonly enabled: boolean;
readonly blockerWindowMs: number;
readonly memory: HwlabRuntimeWebProbeBrowserFreezeMemoryPolicySpec;
readonly responsiveness: HwlabRuntimeWebProbeBrowserFreezeResponsivenessPolicySpec;
readonly cdp: HwlabRuntimeWebProbeBrowserFreezeCdpPolicySpec;
readonly kill: HwlabRuntimeWebProbeBrowserFreezeKillPolicySpec;
}
export type HwlabRuntimeWebProbeSentinelConfigRefKey = "runtime" | "scenarios" | "promptSet" | "reportViews" | "publicExposure" | "cicd" | "secrets";
export const HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS = ["runtime", "scenarios", "promptSet", "reportViews", "publicExposure", "cicd", "secrets"] as const satisfies readonly HwlabRuntimeWebProbeSentinelConfigRefKey[];
export interface HwlabRuntimeWebProbeSentinelSpec {
readonly enabled: boolean;
readonly configRefs: Record<HwlabRuntimeWebProbeSentinelConfigRefKey, string>;
}
export interface HwlabRuntimeWebProbeSentinelRegistryItemSpec {
readonly id: string;
readonly enabled: boolean;
readonly configRef: string;
}
export interface HwlabRuntimeWebProbeMonitorRootSpec {
readonly enabled: boolean;
readonly sentinelId: string;
readonly publicBaseUrl: string;
readonly routePrefix: "/";
readonly caddyManagedBlockOwner: string;
}
export interface HwlabRuntimeWebProbeAlertThresholdsSpec {
readonly sameOriginApiSlowMs: number;
readonly partialApiSlowMs: number;
readonly longLivedStreamOpenSlowMs: number;
readonly visibleLoadingSlowMs: number;
readonly turnTimingSampleSlackSeconds: number;
readonly turnElapsedSevereTimeoutSeconds: number;
readonly domEvaluateTimeoutRedCount: number;
readonly domEvaluateTimeoutRedWindowMs: number;
readonly screenshotTimeoutRedCount: number;
readonly pageErrorRedCount: number;
readonly longTaskRedMs: number;
readonly longAnimationFrameRedMs: number;
readonly eventLoopGapRedMs: number;
readonly browserProcessSampleIntervalMs: number;
readonly requestRateBucketMs: number;
readonly requestRateTotalRedPerMinute: number;
readonly requestRatePageRedPerMinute: number;
readonly requestRateApiPathRedPerMinute: number;
readonly browserTotalRssRedMb: number;
readonly browserProcessRssRedMb: number;
readonly browserRssGrowthRedMb: number;
readonly browserRssGrowthWindowMs: number;
readonly playwrightResponsivenessRedMs: number;
readonly playwrightResponsivenessTimeoutRedCount: number;
readonly cdpMetricsTimeoutRedCount: number;
readonly uncommandedStateChangeCommandWindowMs: number;
readonly scrollJumpCommandWindowMs: number;
readonly scrollJumpFromY: number;
readonly scrollJumpToY: number;
readonly sessionRailFallbackRatio: number;
}
export interface HwlabRuntimeWebProbeProjectManagementSpec {
readonly enabled: boolean;
readonly targetPaths: readonly string[];
readonly readinessSelectors: readonly string[];
readonly naturalApiPathPrefixes: readonly string[];
readonly commandAllowlist: readonly string[];
readonly launchRoute: string;
readonly slowApiBudgetMs: number;
}
export interface HwlabRuntimeBuildkitSpec {
readonly sidecarImage: string;
readonly sourceImage: string;
}
export interface HwlabRuntimeObservabilitySpec {
readonly prometheusOperator: boolean;
readonly traceExplorerUrlTemplate?: string;
readonly metricsEndpoint?: HwlabRuntimeObservabilityMetricsEndpointSpec;
readonly workbench?: HwlabRuntimeObservabilityWorkbenchSpec;
readonly webProbe?: HwlabRuntimeObservabilityWebProbeSpec;
readonly recordingRules: readonly HwlabRuntimeObservabilityRecordingRuleSpec[];
readonly warningAlerts: readonly HwlabRuntimeObservabilityWarningAlertSpec[];
}
export interface HwlabRuntimeObservabilityWebProbeSpec {
readonly sentinel?: HwlabRuntimeWebProbeSentinelSpec;
readonly sentinels?: readonly HwlabRuntimeWebProbeSentinelRegistryItemSpec[];
readonly monitor?: HwlabRuntimeWebProbeMonitorSpec;
readonly monitorRoot?: HwlabRuntimeWebProbeMonitorRootSpec;
}
export interface HwlabRuntimeWebProbeMonitorSpec {
readonly configRef: string;
}
export interface HwlabRuntimeObservabilityMetricsEndpointSpec {
readonly serviceName: string;
readonly containerName: string;
readonly port: number;
readonly scheme: "http";
readonly path: string;
readonly scrapeMode: "pod-loopback";
readonly publicRawMetrics: "denied";
}
export interface HwlabRuntimeObservabilityWorkbenchSpec {
readonly enabled: boolean;
readonly summaryPath: string;
readonly lowSampleThreshold: number;
readonly metricPrefixes: readonly string[];
readonly requiredSeries: readonly string[];
readonly backendLabelDenylist: readonly string[];
readonly maxUnknownEventLines: number;
}
export interface HwlabRuntimeObservabilityRecordingRuleSpec {
readonly id: string;
readonly metric: string;
readonly sourceMetric: string;
readonly quantile: number;
readonly window: string;
readonly minSamples: number;
readonly groupBy: readonly string[];
readonly matchLabels: Record<string, string>;
}
export interface HwlabRuntimeObservabilityWarningAlertSpec {
readonly id: string;
readonly ruleId: string;
readonly severity: "warning";
readonly thresholdSeconds: number;
readonly minSamples: number;
readonly for: string;
readonly matchLabels: Record<string, string>;
}
export interface HwlabRuntimeImageRewriteSpec {
readonly source: string;
readonly target: string;
}
export interface HwlabRuntimeImageBuildSpec {
readonly id: string;
readonly kind: "moonbridge" | "opencode-git";
readonly target: string;
readonly sourceImage?: string;
readonly sourceRepo?: string;
readonly sourceRef?: string;
readonly builderImage?: string;
readonly goProxy?: string;
readonly dockerNetworkMode: "default" | "host";
}
export interface HwlabRuntimeSourceAuthoritySpec {
readonly mode: "gitMirrorSnapshot" | "giteaSnapshot";
readonly resolver: "k8s-git-mirror" | "gitea-mirror";
readonly giteaMirrorRepoKey?: string;
readonly allowHostGit: false;
readonly allowHostWorkspace: false;
readonly allowGithubDirectInPipeline: false;
}
export interface HwlabRuntimeSourceSnapshotSpec {
readonly stageRefPrefix: string;
readonly missingObjectPolicy: "fail-fast";
readonly refreshPolicy: "sync-before-snapshot" | "gitea-controlled-snapshot";
}
export interface HwlabRuntimePublicExposureFrpcProxySpec {
readonly name: string;
readonly localIP: string;
readonly localPort: number;
readonly remotePort: number;
}
export interface HwlabRuntimePublicExposureFrpcExtraProxySpec extends HwlabRuntimePublicExposureFrpcProxySpec {
readonly id: string;
readonly hostname?: string;
readonly publicBaseUrl?: string;
readonly cloudWebEnvName?: string;
}
export interface HwlabRuntimePk01PublicExposureSpec {
readonly enabled: boolean;
readonly mode: "pk01-caddy-frp";
readonly publicBaseUrl: string;
readonly hostname: string;
readonly expectedA: string;
readonly serverAddr: string;
readonly serverPort: number;
readonly tokenSourceRef: string;
readonly tokenSourceKey: string;
readonly secretName: string;
readonly secretKey: string;
readonly tokenKey: string;
readonly caddyRoute: string;
readonly caddyConfigPath: string;
readonly caddyServiceName: string;
readonly caddyEmail: string;
readonly caddyTls: "auto" | "internal";
readonly responseHeaderTimeoutSeconds: number;
readonly webProxy: HwlabRuntimePublicExposureFrpcProxySpec;
readonly apiProxy: HwlabRuntimePublicExposureFrpcProxySpec;
readonly extraProxies: readonly HwlabRuntimePublicExposureFrpcExtraProxySpec[];
}
export interface HwlabRuntimeNodePortPublicExposureSpec {
readonly enabled: boolean;
readonly mode: "node-port-http";
readonly publicBaseUrl: string;
readonly publicAddress: string;
readonly serviceName: string;
readonly selector: Record<string, string>;
readonly port: number;
readonly targetPort: number;
readonly nodePort: number;
}
export type HwlabRuntimePublicExposureSpec = HwlabRuntimePk01PublicExposureSpec | HwlabRuntimeNodePortPublicExposureSpec;
export interface HwlabRuntimeBootstrapAdminSpec {
readonly username: string;
readonly displayName: string;
readonly usernameSourceRef?: string;
readonly usernameSourceKey?: string;
readonly usernameSourceLine?: number;
readonly passwordSourceRef: string;
readonly passwordSourceKey: string;
readonly passwordSourceLine?: number;
readonly passwordHashTransform: "hwlab-sha256";
readonly secretName: string;
readonly secretKey: string;
readonly rolloutDeployment: string;
}
export interface HwlabRuntimeCodeAgentProviderSpec {
readonly secretName: string;
readonly sourceRef: string;
readonly openaiSourceKey?: string;
readonly opencodeSourceKey?: string;
}
export interface HwlabRuntimeKafkaShadowProducerSpec {
readonly enabled: boolean;
readonly mode: "shadow-produce-only";
readonly consumeEnabled: false;
readonly configRef: string;
readonly bootstrapServers: string;
readonly commandTopic: string;
readonly clientId: string;
}
export interface HwlabRuntimeKafkaEventBridgeSpec {
readonly enabled: boolean;
readonly transactionalProjector?: {
readonly heartbeatIntervalMs: number;
};
readonly projectionOutboxRelay?: {
readonly intervalMs: number;
readonly batchSize: number;
readonly leaseMs: number;
readonly sendTimeoutMs: number;
readonly retryBackoffMs: number;
};
readonly projectionRealtime?: {
readonly outboxTailBatchSize: number;
readonly sseHeartbeatMs: number;
readonly sseDrainTimeoutMs: number;
};
readonly healthThresholds: {
readonly consumerLagWarning: number;
readonly outboxBacklogWarning: number;
readonly retryCountWarning: number;
readonly failedInboxWarning: number;
readonly dlqWarning: number;
};
readonly configRef: string;
readonly bootstrapServers: string;
readonly stdioTopic: string;
readonly agentRunEventTopic: string;
readonly hwlabEventTopic: string;
readonly clientId: string;
readonly transactionalProjectorConsumerGroupId: string;
readonly hwlabEventConsumerGroupId: string;
}
export interface HwlabRuntimeCodeAgentRuntimeSpec {
readonly enabled: boolean;
readonly adapter: string;
readonly controlPlaneTarget: {
readonly node: string;
readonly lane: string;
};
readonly managerUrl: string;
readonly apiKeySecretName: string;
readonly apiKeySecretKey: string;
readonly runnerNamespace: string;
readonly secretNamespace: string;
readonly repoUrlFrom: "runtimeGitReadUrl";
readonly providerIdFrom: "runtimeNodeId";
readonly defaultProviderProfile: string;
readonly codexStdioSupervisor: "repo-owned";
readonly kafkaShadowProducer?: HwlabRuntimeKafkaShadowProducerSpec;
readonly kafkaEventBridge?: HwlabRuntimeKafkaEventBridgeSpec;
}
export interface HwlabRuntimeSourceWorkspaceSpec {
readonly git?: HwlabRuntimeSourceWorkspaceGitSpec;
readonly requiredCommands: readonly string[];
readonly requiredFiles: readonly string[];
readonly hostDependencies?: HwlabRuntimeSourceWorkspaceHostDependenciesSpec;
readonly install: {
readonly executor: "k3s-job" | "host";
readonly dependencyCommand: string;
readonly browserCommand: string;
readonly stateDir?: string;
readonly timeoutSeconds: number;
};
}
export interface HwlabRuntimeSourceWorkspaceGitSpec {
readonly remoteName: string;
readonly remoteUrl: string;
readonly identityTarget?: string;
readonly identityId?: string;
readonly proxyEnvPath?: string;
readonly verifyRemote: boolean;
readonly requireUpToDate: boolean;
}
export interface HwlabRuntimeSourceWorkspaceHostDependenciesSpec {
readonly checkCommands: readonly string[];
readonly stateDir: string;
readonly install: {
readonly command: string;
readonly timeoutSeconds: number;
};
}
export interface HwlabRuntimePipelineProvenanceSpec {
readonly configRef: string;
readonly renderer: "hwlab-runtime-lane";
readonly mode: "remote-pipeline-annotation";
readonly maxInlineScriptBytes: number;
}
export interface HwlabRuntimeLaneSpec {
readonly lane: HwlabRuntimeLane;
readonly environment?: "development" | "production";
readonly nodeId: string;
readonly nodeRoute: string;
readonly nodeKubeRoute: string;
readonly gitopsRoot: string;
readonly minor: number;
readonly version: string;
readonly sourceBranch: string;
readonly sourceAuthority?: HwlabRuntimeSourceAuthoritySpec;
readonly sourceSnapshot?: HwlabRuntimeSourceSnapshotSpec;
readonly workspace: string;
readonly cicdRepo: string;
readonly cicdRepoLock: string;
readonly app: string;
readonly pipeline: string;
readonly pipelineRunPrefix: string;
readonly serviceAccountName: string;
readonly controlPlaneFieldManager: string;
readonly pipelineProvenance?: HwlabRuntimePipelineProvenanceSpec;
readonly gitUrl: string;
readonly gitReadUrl: string;
readonly gitWriteUrl: string;
readonly argoRepoUrl: string;
readonly gitopsBranch: string;
readonly catalogPath: string;
readonly runtimePath: string;
readonly runtimeNamespace: string;
readonly runtimeRenderDir: string;
readonly tektonDir: string;
readonly argoApplicationFile: string;
readonly registryPrefix: string;
readonly baseImage: string;
readonly baseImageSource?: string;
readonly serviceIds: readonly string[];
readonly publicWebUrl: string;
readonly publicApiUrl: string;
readonly stepEnv: Record<string, string>;
readonly buildkit?: HwlabRuntimeBuildkitSpec;
readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec;
readonly codeAgentProvider?: HwlabRuntimeCodeAgentProviderSpec;
readonly codeAgentRuntime?: HwlabRuntimeCodeAgentRuntimeSpec;
readonly sourceWorkspace?: HwlabRuntimeSourceWorkspaceSpec;
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
readonly runtimeStore?: HwlabRuntimeStoreSpec;
readonly webProbe?: HwlabRuntimeWebProbeSpec;
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
readonly observability: HwlabRuntimeObservabilitySpec;
readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[];
readonly runtimeImageBuilds: readonly HwlabRuntimeImageBuildSpec[];
readonly networkProfileId: string;
readonly downloadProfileId: string;
readonly networkProfile: HwlabNetworkProfileSpec;
readonly downloadProfile: HwlabDownloadProfileSpec;
}
export function hwlabRuntimeActiveExternalPostgres(spec: HwlabRuntimeLaneSpec): HwlabRuntimeExternalPostgresSpec | undefined {
return spec.runtimeStore?.postgres?.mode === "platform-service" ? spec.externalPostgres : undefined;
}
export function hwlabRuntimeSourceSnapshotStageRefPrefix(spec: HwlabRuntimeLaneSpec): string {
if (spec.sourceSnapshot === undefined) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH} lanes.${spec.lane}.sourceSnapshot is required for runtime source snapshot authority`);
return spec.sourceSnapshot.stageRefPrefix.replaceAll("{branch}", spec.sourceBranch);
}
export function hwlabRuntimeSourceSnapshotRef(spec: HwlabRuntimeLaneSpec, sourceCommit: string): string {
if (!/^[0-9a-f]{40}$/iu.test(sourceCommit)) throw new Error(`sourceCommit must be a 40-hex git SHA for source snapshot ref: ${sourceCommit}`);
return `${hwlabRuntimeSourceSnapshotStageRefPrefix(spec).replace(/\/+$/u, "")}/${sourceCommit.toLowerCase()}`;
}
export const HWLAB_NODE_LANE_CONFIG_PATH = "config/hwlab-node-lanes.yaml";
interface HwlabLaneConfig {
readonly id: HwlabRuntimeLane;
readonly environment?: "development" | "production";
readonly node: string;
readonly activeTarget?: string;
readonly minor: number;
readonly version: string;
readonly sourceBranch: string;
readonly sourceAuthority?: HwlabRuntimeSourceAuthoritySpec;
readonly sourceSnapshot?: HwlabRuntimeSourceSnapshotSpec;
readonly workspace: string;
readonly cicdRepo: string;
readonly cicdRepoLock: string;
readonly app: string;
readonly pipeline: string;
readonly pipelineRunPrefix: string;
readonly serviceAccountName: string;
readonly controlPlaneFieldManager: string;
readonly pipelineProvenance?: HwlabRuntimePipelineProvenanceSpec;
readonly git: { readonly url: string; readonly readUrl: string; readonly writeUrl: string };
readonly argo: { readonly repoURL?: string };
readonly gitopsBranch: string;
readonly catalogPath: string;
readonly runtime: { readonly path: string; readonly namespace: string; readonly renderDir: string };
readonly tektonDir: string;
readonly argoApplicationFile: string;
readonly registryPrefix: string;
readonly baseImage: string;
readonly baseImageSource?: string;
readonly serviceIds: readonly string[];
readonly public: { readonly webUrl: string; readonly apiUrl: string };
readonly stepEnv: Record<string, string>;
readonly buildkit?: HwlabRuntimeBuildkitSpec;
readonly bootstrapAdmin?: HwlabRuntimeBootstrapAdminSpec;
readonly codeAgentProvider?: HwlabRuntimeCodeAgentProviderSpec;
readonly codeAgentRuntime?: HwlabRuntimeCodeAgentRuntimeSpec;
readonly sourceWorkspace?: HwlabRuntimeSourceWorkspaceSpec;
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
readonly runtimeStore?: HwlabRuntimeStoreSpec;
readonly webProbe?: HwlabRuntimeWebProbeSpec;
readonly publicExposure: HwlabRuntimePublicExposureSpec | null;
readonly observability: HwlabRuntimeObservabilitySpec;
readonly runtimeImageRewrites: readonly HwlabRuntimeImageRewriteSpec[];
readonly runtimeImageBuilds: readonly HwlabRuntimeImageBuildSpec[];
}
interface HwlabNodeLaneConfig {
readonly defaultTarget: {
readonly node: string;
readonly lane: HwlabRuntimeLane;
};
readonly requiredNoProxy: readonly string[];
readonly nodes: Record<string, HwlabRuntimeNodeSpec>;
readonly lanes: Record<HwlabRuntimeLane, HwlabLaneConfig>;
readonly laneTargets: Partial<Record<HwlabRuntimeLane, Record<string, HwlabLaneConfig>>>;
readonly networkProfiles: Record<string, HwlabNetworkProfileSpec>;
readonly downloadProfiles: Record<string, HwlabDownloadProfileSpec>;
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
return value as Record<string, unknown>;
}
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
const value = obj[key];
if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
return value;
}
function enumStringField<T extends string>(obj: Record<string, unknown>, key: string, path: string, allowed: readonly T[]): T {
const value = stringField(obj, key, path);
if (!allowed.includes(value as T)) throw new Error(`${path}.${key} must be one of ${allowed.join(", ")}`);
return value as T;
}
function numberField(obj: Record<string, unknown>, key: string, path: string): number {
const value = obj[key];
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new Error(`${path}.${key} must be a positive integer`);
return value;
}
function stringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] {
const value = obj[key];
if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.length === 0)) {
throw new Error(`${path}.${key} must be an array of non-empty strings`);
}
return [...value] as string[];
}
function nonEmptyStringArrayField(obj: Record<string, unknown>, key: string, path: string): string[] {
const values = stringArrayField(obj, key, path);
if (values.length === 0) throw new Error(`${path}.${key} must contain at least one string`);
return values;
}
function optionalStringField(obj: Record<string, unknown>, key: string, path: string): string | undefined {
const value = obj[key];
if (value === undefined) return undefined;
if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
return value;
}
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
const value = obj[key];
if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
return value;
}
function literalTrueField(obj: Record<string, unknown>, key: string, path: string): true {
if (obj[key] !== true) throw new Error(`${path}.${key} must be true`);
return true;
}
function nonNegativeIntegerField(obj: Record<string, unknown>, key: string, path: string): number {
const value = obj[key];
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) throw new Error(`${path}.${key} must be a non-negative integer`);
return value;
}
function positiveNumberField(obj: Record<string, unknown>, key: string, path: string): number {
const value = obj[key];
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new Error(`${path}.${key} must be a positive number`);
return value;
}
function boundedIntegerField(obj: Record<string, unknown>, key: string, path: string, min: number, max: number): number {
const value = obj[key];
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
throw new Error(`${path}.${key} must be an integer between ${min} and ${max}`);
}
return value;
}
function sortedRecordEntries(value: unknown, path: string): Array<[string, Record<string, unknown>]> {
return Object.entries(asRecord(value, path)).map(([key, item]) => [key, asRecord(item, `${path}.${key}`)]);
}
function unique(values: readonly string[]): string[] {
return [...new Set(values)];
}
function mergeOptionalRecord(base: unknown, override: unknown): Record<string, unknown> | undefined {
if (base === undefined && override === undefined) return undefined;
const baseRecord = base === undefined ? {} : asRecord(base, "base");
const overrideRecord = override === undefined ? {} : asRecord(override, "override");
return { ...baseRecord, ...overrideRecord };
}
function optionalStringRecord(value: unknown, path: string): Record<string, string> {
if (value === undefined) return {};
const raw = asRecord(value, path);
return Object.fromEntries(Object.entries(raw).map(([key, item]) => {
if (typeof item !== "string" || item.length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
return [key, item];
}));
}
function proxyConfig(raw: Record<string, unknown>, id: string, key: "proxy" | "imageBuildProxy" | "dockerBuildProxy", requiredNoProxy: readonly string[]): HwlabProxySpec {
const proxy = asRecord(raw[key], `networkProfiles.${id}.${key}`);
return {
http: stringField(proxy, "http", `networkProfiles.${id}.${key}`),
https: stringField(proxy, "https", `networkProfiles.${id}.${key}`),
all: stringField(proxy, "all", `networkProfiles.${id}.${key}`),
socks5: optionalStringField(proxy, "socks5", `networkProfiles.${id}.${key}`),
noProxy: unique([...stringArrayField(proxy, "noProxy", `networkProfiles.${id}.${key}`), ...requiredNoProxy]),
};
}
function networkProfileConfig(id: string, raw: Record<string, unknown>, requiredNoProxy: readonly string[]): HwlabNetworkProfileSpec {
const imageBuildProxy = raw.imageBuildProxy === undefined
? proxyConfig(raw, id, "dockerBuildProxy", requiredNoProxy)
: proxyConfig(raw, id, "imageBuildProxy", requiredNoProxy);
return {
id,
proxy: proxyConfig(raw, id, "proxy", requiredNoProxy),
imageBuildProxy,
dockerBuildProxy: raw.dockerBuildProxy === undefined ? imageBuildProxy : proxyConfig(raw, id, "dockerBuildProxy", requiredNoProxy),
};
}
function downloadProfileConfig(id: string, raw: Record<string, unknown>): HwlabDownloadProfileSpec {
const git = asRecord(raw.git, `downloadProfiles.${id}.git`);
const npm = asRecord(raw.npm, `downloadProfiles.${id}.npm`);
const pip = asRecord(raw.pip, `downloadProfiles.${id}.pip`);
const docker = asRecord(raw.docker, `downloadProfiles.${id}.docker`);
const curl = asRecord(raw.curl, `downloadProfiles.${id}.curl`);
const proxyMode = stringField(git, "proxyMode", `downloadProfiles.${id}.git`);
if (proxyMode !== "inherit" && proxyMode !== "direct" && proxyMode !== "none") {
throw new Error(`downloadProfiles.${id}.git.proxyMode must be inherit, direct, or none`);
}
return {
id,
git: {
proxyMode,
retries: numberField(git, "retries", `downloadProfiles.${id}.git`),
timeoutSeconds: numberField(git, "timeoutSeconds", `downloadProfiles.${id}.git`),
},
npm: {
registry: stringField(npm, "registry", `downloadProfiles.${id}.npm`),
retries: numberField(npm, "retries", `downloadProfiles.${id}.npm`),
fetchTimeoutSeconds: numberField(npm, "fetchTimeoutSeconds", `downloadProfiles.${id}.npm`),
},
pip: {
indexUrl: stringField(pip, "indexUrl", `downloadProfiles.${id}.pip`),
retries: numberField(pip, "retries", `downloadProfiles.${id}.pip`),
timeoutSeconds: numberField(pip, "timeoutSeconds", `downloadProfiles.${id}.pip`),
},
docker: {
registryMirrors: stringArrayField(docker, "registryMirrors", `downloadProfiles.${id}.docker`),
pullRetries: numberField(docker, "pullRetries", `downloadProfiles.${id}.docker`),
},
curl: {
retries: numberField(curl, "retries", `downloadProfiles.${id}.curl`),
connectTimeoutSeconds: numberField(curl, "connectTimeoutSeconds", `downloadProfiles.${id}.curl`),
maxTimeSeconds: numberField(curl, "maxTimeSeconds", `downloadProfiles.${id}.curl`),
},
};
}
function nodeConfig(id: string, raw: Record<string, unknown>): HwlabRuntimeNodeSpec {
return {
id,
route: stringField(raw, "route", `nodes.${id}`),
kubeRoute: stringField(raw, "kubeRoute", `nodes.${id}`),
sourceWorkspace: stringField(raw, "sourceWorkspace", `nodes.${id}`),
gitopsRoot: stringField(raw, "gitopsRoot", `nodes.${id}`),
networkProfileId: stringField(raw, "networkProfile", `nodes.${id}`),
downloadProfileId: stringField(raw, "downloadProfile", `nodes.${id}`),
};
}
function assertYamlDeclaredLaneId(id: string, path: string): asserts id is HwlabRuntimeLane {
if (!/^[A-Za-z0-9._-]+$/u.test(id)) throw new Error(`${path} has an unsupported lane id; lane ids must be YAML-declared simple ids`);
}
function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLaneConfig {
const git = asRecord(raw.git, `lanes.${id}.git`);
const argo = raw.argo === undefined ? {} : asRecord(raw.argo, `lanes.${id}.argo`);
const runtime = asRecord(raw.runtime, `lanes.${id}.runtime`);
const publicUrls = asRecord(raw.public, `lanes.${id}.public`);
const minor = numberField(raw, "minor", `lanes.${id}`);
const version = stringField(raw, "version", `lanes.${id}`);
if (version !== `v0.${minor}`) throw new Error(`lanes.${id}.version must equal v0.${minor}`);
if (minor >= 3 && raw.sourceAuthority === undefined) throw new Error(`lanes.${id}.sourceAuthority is required for HWLAB runtime v0.3+`);
if (minor >= 3 && raw.sourceSnapshot === undefined) throw new Error(`lanes.${id}.sourceSnapshot is required for HWLAB runtime v0.3+`);
return {
id,
environment: raw.environment === undefined ? undefined : enumStringField(raw, "environment", `lanes.${id}`, ["development", "production"]),
node: stringField(raw, "node", `lanes.${id}`),
activeTarget: optionalStringField(raw, "activeTarget", `lanes.${id}`),
minor,
version,
sourceBranch: stringField(raw, "sourceBranch", `lanes.${id}`),
sourceAuthority: sourceAuthorityConfig(raw.sourceAuthority, `lanes.${id}.sourceAuthority`),
sourceSnapshot: sourceSnapshotConfig(raw.sourceSnapshot, `lanes.${id}.sourceSnapshot`),
workspace: stringField(raw, "workspace", `lanes.${id}`),
cicdRepo: stringField(raw, "cicdRepo", `lanes.${id}`),
cicdRepoLock: stringField(raw, "cicdRepoLock", `lanes.${id}`),
app: stringField(raw, "app", `lanes.${id}`),
pipeline: stringField(raw, "pipeline", `lanes.${id}`),
pipelineRunPrefix: stringField(raw, "pipelineRunPrefix", `lanes.${id}`),
serviceAccountName: stringField(raw, "serviceAccountName", `lanes.${id}`),
controlPlaneFieldManager: stringField(raw, "controlPlaneFieldManager", `lanes.${id}`),
pipelineProvenance: pipelineProvenanceConfig(raw.pipelineProvenance, `lanes.${id}.pipelineProvenance`),
git: {
url: stringField(git, "url", `lanes.${id}.git`),
readUrl: stringField(git, "readUrl", `lanes.${id}.git`),
writeUrl: stringField(git, "writeUrl", `lanes.${id}.git`),
},
argo: {
repoURL: optionalStringField(argo, "repoURL", `lanes.${id}.argo`),
},
gitopsBranch: stringField(raw, "gitopsBranch", `lanes.${id}`),
catalogPath: stringField(raw, "catalogPath", `lanes.${id}`),
runtime: {
path: stringField(runtime, "path", `lanes.${id}.runtime`),
namespace: stringField(runtime, "namespace", `lanes.${id}.runtime`),
renderDir: stringField(runtime, "renderDir", `lanes.${id}.runtime`),
},
tektonDir: stringField(raw, "tektonDir", `lanes.${id}`),
argoApplicationFile: stringField(raw, "argoApplicationFile", `lanes.${id}`),
registryPrefix: stringField(raw, "registryPrefix", `lanes.${id}`),
baseImage: stringField(raw, "baseImage", `lanes.${id}`),
baseImageSource: optionalStringField(raw, "baseImageSource", `lanes.${id}`),
serviceIds: stringArrayField(raw, "serviceIds", `lanes.${id}`),
public: {
webUrl: stringField(publicUrls, "webUrl", `lanes.${id}.public`),
apiUrl: stringField(publicUrls, "apiUrl", `lanes.${id}.public`),
},
stepEnv: optionalStringRecord(raw.stepEnv, `lanes.${id}.stepEnv`),
buildkit: buildkitConfig(raw.buildkit, `lanes.${id}.buildkit`),
bootstrapAdmin: bootstrapAdminConfig(raw.bootstrapAdmin, `lanes.${id}.bootstrapAdmin`),
codeAgentProvider: codeAgentProviderConfig(raw.codeAgentProvider, `lanes.${id}.codeAgentProvider`),
codeAgentRuntime: codeAgentRuntimeConfig(raw.codeAgentRuntime, `lanes.${id}.codeAgentRuntime`),
sourceWorkspace: sourceWorkspaceConfig(raw.sourceWorkspace, `lanes.${id}.sourceWorkspace`),
externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`),
runtimeStore: runtimeStoreConfig(raw.runtimeStore, `lanes.${id}.runtimeStore`),
webProbe: webProbeConfig(raw.webProbe, `lanes.${id}.webProbe`),
publicExposure: publicExposureConfig(raw.publicExposure, `lanes.${id}.publicExposure`),
observability: observabilityConfig(raw.observability, `lanes.${id}.observability`),
runtimeImageRewrites: runtimeImageRewritesConfig(raw.runtimeImageRewrites, `lanes.${id}.runtimeImageRewrites`),
runtimeImageBuilds: runtimeImageBuildsConfig(raw.runtimeImageBuilds, `lanes.${id}.runtimeImageBuilds`),
};
}
function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<string, unknown>, targetRaw: Record<string, unknown>): HwlabLaneConfig {
const expectedConfigRef = `${HWLAB_NODE_LANE_CONFIG_PATH}#lanes.${id}.targets.${nodeId}`;
parseHwlabRuntimePipelineProvenance(targetRaw.pipelineProvenance, `lanes.${id}.targets.${nodeId}.pipelineProvenance`, expectedConfigRef);
const merged: Record<string, unknown> = {
...baseRaw,
...targetRaw,
node: nodeId,
git: mergeOptionalRecord(baseRaw.git, targetRaw.git),
argo: mergeOptionalRecord(baseRaw.argo, targetRaw.argo),
runtime: mergeOptionalRecord(baseRaw.runtime, targetRaw.runtime),
public: mergeOptionalRecord(baseRaw.public, targetRaw.public),
stepEnv: mergeOptionalRecord(baseRaw.stepEnv, targetRaw.stepEnv) ?? {},
buildkit: mergeOptionalRecord(baseRaw.buildkit, targetRaw.buildkit),
bootstrapAdmin: mergeOptionalRecord(baseRaw.bootstrapAdmin, targetRaw.bootstrapAdmin),
codeAgentProvider: mergeOptionalRecord(baseRaw.codeAgentProvider, targetRaw.codeAgentProvider),
codeAgentRuntime: mergeOptionalRecord(baseRaw.codeAgentRuntime, targetRaw.codeAgentRuntime),
sourceAuthority: mergeOptionalRecord(baseRaw.sourceAuthority, targetRaw.sourceAuthority),
sourceSnapshot: mergeOptionalRecord(baseRaw.sourceSnapshot, targetRaw.sourceSnapshot),
sourceWorkspace: mergeOptionalRecord(baseRaw.sourceWorkspace, targetRaw.sourceWorkspace),
externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres),
runtimeStore: mergeOptionalRecord(baseRaw.runtimeStore, targetRaw.runtimeStore),
webProbe: mergeOptionalRecord(baseRaw.webProbe, targetRaw.webProbe),
publicExposure: mergeOptionalRecord(baseRaw.publicExposure, targetRaw.publicExposure),
observability: mergeOptionalRecord(baseRaw.observability, targetRaw.observability),
};
delete merged.targets;
delete merged.activeTarget;
const config = laneConfig(id, merged);
return config;
}
function pipelineProvenanceConfig(value: unknown, path: string): HwlabRuntimePipelineProvenanceSpec | undefined {
if (value === undefined) return undefined;
return parseHwlabRuntimePipelineProvenance(value, path);
}
export function parseHwlabRuntimePipelineProvenance(
value: unknown,
path: string,
expectedConfigRef?: string,
): HwlabRuntimePipelineProvenanceSpec {
if (value === undefined) throw new Error(`${path} must be declared by the target owner`);
const raw = asRecord(value, path);
const configRef = stringField(raw, "configRef", path);
if (!/^config\/hwlab-node-lanes\.yaml#lanes\.[A-Za-z0-9._-]+\.targets\.[A-Za-z0-9._-]+$/u.test(configRef)) {
throw new Error(`${path}.configRef must select a target owner in ${HWLAB_NODE_LANE_CONFIG_PATH}`);
}
if (expectedConfigRef !== undefined && configRef !== expectedConfigRef) {
throw new Error(`${path}.configRef must equal ${expectedConfigRef}`);
}
return {
configRef,
renderer: enumStringField(raw, "renderer", path, ["hwlab-runtime-lane"]),
mode: enumStringField(raw, "mode", path, ["remote-pipeline-annotation"]),
maxInlineScriptBytes: boundedIntegerField(raw, "maxInlineScriptBytes", path, 1024, 131072),
};
}
function sourceAuthorityConfig(value: unknown, path: string): HwlabRuntimeSourceAuthoritySpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const mode = enumStringField(raw, "mode", path, ["gitMirrorSnapshot", "giteaSnapshot"]);
const resolver = enumStringField(raw, "resolver", path, ["k8s-git-mirror", "gitea-mirror"]);
if (mode === "gitMirrorSnapshot" && resolver !== "k8s-git-mirror") throw new Error(`${path}.resolver must be k8s-git-mirror when mode is gitMirrorSnapshot`);
if (mode === "giteaSnapshot" && resolver !== "gitea-mirror") throw new Error(`${path}.resolver must be gitea-mirror when mode is giteaSnapshot`);
const giteaMirrorRepoKey = optionalStringField(raw, "giteaMirrorRepoKey", path);
if (mode === "giteaSnapshot" && giteaMirrorRepoKey === undefined) throw new Error(`${path}.giteaMirrorRepoKey is required when mode is giteaSnapshot`);
if (giteaMirrorRepoKey !== undefined && !/^[A-Za-z0-9._-]+$/u.test(giteaMirrorRepoKey)) throw new Error(`${path}.giteaMirrorRepoKey must be a simple Gitea mirror repo key`);
return {
mode,
resolver,
...(giteaMirrorRepoKey === undefined ? {} : { giteaMirrorRepoKey }),
allowHostGit: falseBooleanField(raw, "allowHostGit", path),
allowHostWorkspace: falseBooleanField(raw, "allowHostWorkspace", path),
allowGithubDirectInPipeline: falseBooleanField(raw, "allowGithubDirectInPipeline", path),
};
}
function sourceSnapshotConfig(value: unknown, path: string): HwlabRuntimeSourceSnapshotSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const stageRefPrefix = stringField(raw, "stageRefPrefix", path);
if (!stageRefPrefix.startsWith("refs/")) throw new Error(`${path}.stageRefPrefix must start with refs/`);
if (stageRefPrefix.includes("..") || /\s/u.test(stageRefPrefix)) throw new Error(`${path}.stageRefPrefix must not contain whitespace or ..`);
if (!stageRefPrefix.includes("{branch}")) throw new Error(`${path}.stageRefPrefix must include {branch}`);
return {
stageRefPrefix,
missingObjectPolicy: enumStringField(raw, "missingObjectPolicy", path, ["fail-fast"]),
refreshPolicy: enumStringField(raw, "refreshPolicy", path, ["sync-before-snapshot", "gitea-controlled-snapshot"]),
};
}
function falseBooleanField(obj: Record<string, unknown>, key: string, path: string): false {
const value = booleanField(obj, key, path);
if (value !== false) throw new Error(`${path}.${key} must be false`);
return false;
}
function buildkitConfig(value: unknown, path: string): HwlabRuntimeBuildkitSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
return {
sidecarImage: stringField(raw, "sidecarImage", path),
sourceImage: stringField(raw, "sourceImage", path),
};
}
function sourceRefField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[A-Za-z0-9_./-]+$/u.test(value)) throw new Error(`${path}.${key} has an unsupported format`);
return value;
}
function secretKeyField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!/^[A-Za-z0-9_.-]+$/u.test(value)) throw new Error(`${path}.${key} has an unsupported format`);
return value;
}
function bootstrapAdminConfig(value: unknown, path: string): HwlabRuntimeBootstrapAdminSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const transform = stringField(raw, "passwordHashTransform", path);
if (transform !== "hwlab-sha256") throw new Error(`${path}.passwordHashTransform must be hwlab-sha256`);
const rollout = asRecord(raw.rollout, `${path}.rollout`);
const usernameSourceRef = raw.usernameSourceRef === undefined ? undefined : sourceRefField(raw, "usernameSourceRef", path);
const usernameSourceKey = raw.usernameSourceKey === undefined ? undefined : secretKeyField(raw, "usernameSourceKey", path);
const usernameSourceLine = raw.usernameSourceLine === undefined ? undefined : numberField(raw, "usernameSourceLine", path);
const passwordSourceLine = raw.passwordSourceLine === undefined ? undefined : numberField(raw, "passwordSourceLine", path);
return {
username: stringField(raw, "username", path),
displayName: stringField(raw, "displayName", path),
...(usernameSourceRef === undefined ? {} : { usernameSourceRef }),
...(usernameSourceKey === undefined ? {} : { usernameSourceKey }),
...(usernameSourceLine === undefined ? {} : { usernameSourceLine }),
passwordSourceRef: sourceRefField(raw, "passwordSourceRef", path),
passwordSourceKey: secretKeyField(raw, "passwordSourceKey", path),
...(passwordSourceLine === undefined ? {} : { passwordSourceLine }),
passwordHashTransform: transform,
secretName: stringField(raw, "secretName", path),
secretKey: secretKeyField(raw, "secretKey", path),
rolloutDeployment: stringField(rollout, "deployment", `${path}.rollout`),
};
}
function codeAgentProviderConfig(value: unknown, path: string): HwlabRuntimeCodeAgentProviderSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const openaiSourceKey = optionalStringField(raw, "openaiSourceKey", path);
const opencodeSourceKey = optionalStringField(raw, "opencodeSourceKey", path);
if (openaiSourceKey === undefined && opencodeSourceKey === undefined) {
throw new Error(`${path} must declare at least one of openaiSourceKey or opencodeSourceKey`);
}
return {
secretName: stringField(raw, "secretName", path),
sourceRef: sourceRefField(raw, "sourceRef", path),
...(openaiSourceKey === undefined ? {} : { openaiSourceKey: secretKeyField(raw, "openaiSourceKey", path) }),
...(opencodeSourceKey === undefined ? {} : { opencodeSourceKey: secretKeyField(raw, "opencodeSourceKey", path) }),
};
}
function codeAgentRuntimeConfig(value: unknown, path: string): HwlabRuntimeCodeAgentRuntimeSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const managerUrl = stringField(raw, "managerUrl", path);
try {
const parsed = new URL(managerUrl);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
} catch {
throw new Error(`${path}.managerUrl must be an http(s) URL`);
}
const controlPlaneTarget = asRecord(raw.controlPlaneTarget, `${path}.controlPlaneTarget`);
return {
enabled: booleanField(raw, "enabled", path),
adapter: stringField(raw, "adapter", path),
controlPlaneTarget: {
node: stringField(controlPlaneTarget, "node", `${path}.controlPlaneTarget`),
lane: stringField(controlPlaneTarget, "lane", `${path}.controlPlaneTarget`),
},
managerUrl,
apiKeySecretName: stringField(raw, "apiKeySecretName", path),
apiKeySecretKey: secretKeyField(raw, "apiKeySecretKey", path),
runnerNamespace: stringField(raw, "runnerNamespace", path),
secretNamespace: stringField(raw, "secretNamespace", path),
repoUrlFrom: enumStringField(raw, "repoUrlFrom", path, ["runtimeGitReadUrl"]),
providerIdFrom: enumStringField(raw, "providerIdFrom", path, ["runtimeNodeId"]),
defaultProviderProfile: stringField(raw, "defaultProviderProfile", path),
codexStdioSupervisor: enumStringField(raw, "codexStdioSupervisor", path, ["repo-owned"]),
...(raw.kafkaShadowProducer === undefined
? {}
: { kafkaShadowProducer: codeAgentKafkaShadowProducerConfig(raw.kafkaShadowProducer, `${path}.kafkaShadowProducer`) }),
...(raw.kafkaEventBridge === undefined
? {}
: { kafkaEventBridge: codeAgentKafkaEventBridgeConfig(raw.kafkaEventBridge, `${path}.kafkaEventBridge`) }),
};
}
function codeAgentKafkaShadowProducerConfig(value: unknown, path: string): HwlabRuntimeKafkaShadowProducerSpec {
const raw = asRecord(value, path);
const mode = enumStringField(raw, "mode", path, ["shadow-produce-only"]);
const consumeEnabled = booleanField(raw, "consumeEnabled", path);
if (consumeEnabled !== false) throw new Error(`${path}.consumeEnabled must be false during shadow-produce-only stage`);
return {
enabled: booleanField(raw, "enabled", path),
mode,
consumeEnabled,
configRef: stringField(raw, "configRef", path),
bootstrapServers: stringField(raw, "bootstrapServers", path),
commandTopic: stringField(raw, "commandTopic", path),
clientId: stringField(raw, "clientId", path),
};
}
function codeAgentKafkaEventBridgeConfig(value: unknown, path: string): HwlabRuntimeKafkaEventBridgeSpec {
const raw = asRecord(value, path);
if (raw.refreshReplay !== undefined) throw new Error(`${path}.refreshReplay was removed with the fixed projection realtime architecture`);
const enabled = booleanField(raw, "enabled", path);
const transactionalProjectorConfig = raw.transactionalProjector === undefined
? undefined
: codeAgentKafkaTransactionalProjectorConfig(raw.transactionalProjector, `${path}.transactionalProjector`);
const projectionOutboxRelayConfig = raw.projectionOutboxRelay === undefined
? undefined
: codeAgentKafkaProjectionOutboxRelayConfig(raw.projectionOutboxRelay, `${path}.projectionOutboxRelay`);
const projectionRealtimeConfig = raw.projectionRealtime === undefined
? undefined
: codeAgentKafkaProjectionRealtimeConfig(raw.projectionRealtime, `${path}.projectionRealtime`);
if (transactionalProjectorConfig === undefined) throw new Error(`${path}.transactionalProjector is required by the fixed transactional projection architecture`);
if (projectionOutboxRelayConfig === undefined) throw new Error(`${path}.projectionOutboxRelay is required by the fixed transactional projection architecture`);
if (projectionRealtimeConfig === undefined) throw new Error(`${path}.projectionRealtime is required by the fixed transactional projection architecture`);
const transactionalProjectorConsumerGroupId = stringField(raw, "transactionalProjectorConsumerGroupId", path);
const hwlabEventConsumerGroupId = stringField(raw, "hwlabEventConsumerGroupId", path);
if (transactionalProjectorConsumerGroupId === hwlabEventConsumerGroupId) {
throw new Error(`${path} projector and realtime consumer group ids must be distinct`);
}
return {
enabled,
transactionalProjector: transactionalProjectorConfig,
projectionOutboxRelay: projectionOutboxRelayConfig,
projectionRealtime: projectionRealtimeConfig,
healthThresholds: codeAgentKafkaHealthThresholdsConfig(raw.healthThresholds, `${path}.healthThresholds`),
configRef: stringField(raw, "configRef", path),
bootstrapServers: stringField(raw, "bootstrapServers", path),
stdioTopic: stringField(raw, "stdioTopic", path),
agentRunEventTopic: stringField(raw, "agentRunEventTopic", path),
hwlabEventTopic: stringField(raw, "hwlabEventTopic", path),
clientId: stringField(raw, "clientId", path),
transactionalProjectorConsumerGroupId,
hwlabEventConsumerGroupId,
};
}
function codeAgentKafkaTransactionalProjectorConfig(value: unknown, path: string): NonNullable<HwlabRuntimeKafkaEventBridgeSpec["transactionalProjector"]> {
const raw = asRecord(value, path);
return { heartbeatIntervalMs: numberField(raw, "heartbeatIntervalMs", path) };
}
function codeAgentKafkaProjectionOutboxRelayConfig(value: unknown, path: string): NonNullable<HwlabRuntimeKafkaEventBridgeSpec["projectionOutboxRelay"]> {
const raw = asRecord(value, path);
return {
intervalMs: numberField(raw, "intervalMs", path),
batchSize: numberField(raw, "batchSize", path),
leaseMs: numberField(raw, "leaseMs", path),
sendTimeoutMs: numberField(raw, "sendTimeoutMs", path),
retryBackoffMs: numberField(raw, "retryBackoffMs", path),
};
}
function codeAgentKafkaProjectionRealtimeConfig(value: unknown, path: string): NonNullable<HwlabRuntimeKafkaEventBridgeSpec["projectionRealtime"]> {
const raw = asRecord(value, path);
return {
outboxTailBatchSize: numberField(raw, "outboxTailBatchSize", path),
sseHeartbeatMs: numberField(raw, "sseHeartbeatMs", path),
sseDrainTimeoutMs: numberField(raw, "sseDrainTimeoutMs", path),
};
}
function codeAgentKafkaHealthThresholdsConfig(value: unknown, path: string): HwlabRuntimeKafkaEventBridgeSpec["healthThresholds"] {
const raw = asRecord(value, path);
return {
consumerLagWarning: nonNegativeIntegerField(raw, "consumerLagWarning", path),
outboxBacklogWarning: nonNegativeIntegerField(raw, "outboxBacklogWarning", path),
retryCountWarning: nonNegativeIntegerField(raw, "retryCountWarning", path),
failedInboxWarning: nonNegativeIntegerField(raw, "failedInboxWarning", path),
dlqWarning: nonNegativeIntegerField(raw, "dlqWarning", path),
};
}
function commandNameField(value: string, path: string): string {
if (!/^[A-Za-z0-9._+-]+$/u.test(value)) throw new Error(`${path} must be a simple command name`);
return value;
}
function relativeWorkspacePathField(value: string, path: string): string {
if (value.startsWith("/") || value.includes("\0") || value.split("/").some((part) => part === "..")) {
throw new Error(`${path} must be a relative workspace path without ..`);
}
return value;
}
function absoluteHostPathField(value: string, path: string): string {
const parts = value.slice(1).split("/");
if (!value.startsWith("/") || value.includes("\0") || parts.some((part) => part === ".." || part === "")) {
throw new Error(`${path} must be an absolute host path without empty components or ..`);
}
return value;
}
function gitRemoteNameField(value: string, path: string): string {
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path} must be a git remote name`);
return value;
}
function sourceWorkspaceGitConfig(value: unknown, path: string): HwlabRuntimeSourceWorkspaceGitSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
return {
remoteName: gitRemoteNameField(stringField(raw, "remoteName", path), `${path}.remoteName`),
remoteUrl: stringField(raw, "remoteUrl", path),
...(raw.identityTarget === undefined ? {} : { identityTarget: stringField(raw, "identityTarget", path) }),
...(raw.identityId === undefined ? {} : { identityId: stringField(raw, "identityId", path) }),
...(raw.proxyEnvPath === undefined ? {} : { proxyEnvPath: absoluteHostPathField(stringField(raw, "proxyEnvPath", path), `${path}.proxyEnvPath`) }),
verifyRemote: booleanField(raw, "verifyRemote", path),
requireUpToDate: booleanField(raw, "requireUpToDate", path),
};
}
function sourceWorkspaceConfig(value: unknown, path: string): HwlabRuntimeSourceWorkspaceSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const install = asRecord(raw.install, `${path}.install`);
return {
git: sourceWorkspaceGitConfig(raw.git, `${path}.git`),
requiredCommands: stringArrayField(raw, "requiredCommands", path)
.map((item, index) => commandNameField(item, `${path}.requiredCommands[${index}]`)),
requiredFiles: stringArrayField(raw, "requiredFiles", path)
.map((item, index) => relativeWorkspacePathField(item, `${path}.requiredFiles[${index}]`)),
hostDependencies: sourceWorkspaceHostDependenciesConfig(raw.hostDependencies, `${path}.hostDependencies`),
install: {
executor: enumStringField(install, "executor", `${path}.install`, ["k3s-job", "host"]),
dependencyCommand: stringField(install, "dependencyCommand", `${path}.install`),
browserCommand: stringField(install, "browserCommand", `${path}.install`),
...(install.stateDir === undefined ? {} : { stateDir: absoluteHostPathField(stringField(install, "stateDir", `${path}.install`), `${path}.install.stateDir`) }),
timeoutSeconds: numberField(install, "timeoutSeconds", `${path}.install`),
},
};
}
function sourceWorkspaceHostDependenciesConfig(value: unknown, path: string): HwlabRuntimeSourceWorkspaceHostDependenciesSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const install = asRecord(raw.install, `${path}.install`);
return {
checkCommands: stringArrayField(raw, "checkCommands", path)
.map((item, index) => commandNameField(item, `${path}.checkCommands[${index}]`)),
stateDir: absoluteHostPathField(stringField(raw, "stateDir", path), `${path}.stateDir`),
install: {
command: stringField(install, "command", `${path}.install`),
timeoutSeconds: numberField(install, "timeoutSeconds", `${path}.install`),
},
};
}
function externalPostgresComponentConfig(value: unknown, path: string): HwlabRuntimeExternalPostgresComponentSpec {
const raw = asRecord(value, path);
return {
secretName: stringField(raw, "secretName", path),
secretKey: stringField(raw, "secretKey", path),
sourceRef: stringField(raw, "sourceRef", path),
envKey: stringField(raw, "envKey", path),
role: stringField(raw, "role", path),
authnKey: optionalStringField(raw, "authnKey", path),
schema: optionalStringField(raw, "schema", path),
};
}
function externalPostgresRuntimeAccessConfig(value: unknown, path: string): HwlabRuntimeExternalPostgresRuntimeAccessSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
return {
routeName: optionalStringField(raw, "routeName", path),
endpointAddress: stringField(raw, "endpointAddress", path),
port: numberField(raw, "port", path),
};
}
function externalPostgresMigrationLedgerConfig(value: unknown, path: string): HwlabRuntimeExternalPostgresMigrationLedgerSpec {
const raw = asRecord(value, path);
return {
identity: stringField(raw, "identity", path),
table: stringField(raw, "table", path),
};
}
function externalPostgresConfig(value: unknown, path: string): HwlabRuntimeExternalPostgresSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const sslmode = stringField(raw, "sslmode", path);
if (sslmode !== "require") throw new Error(`${path}.sslmode must be require`);
return {
provider: stringField(raw, "provider", path),
configRef: stringField(raw, "configRef", path),
serviceName: stringField(raw, "serviceName", path),
endpointAddress: stringField(raw, "endpointAddress", path),
port: numberField(raw, "port", path),
...(raw.runtimeAccess === undefined ? {} : { runtimeAccess: externalPostgresRuntimeAccessConfig(raw.runtimeAccess, `${path}.runtimeAccess`) }),
sslmode,
database: stringField(raw, "database", path),
migrationLedger: externalPostgresMigrationLedgerConfig(raw.migrationLedger, `${path}.migrationLedger`),
cloudApi: externalPostgresComponentConfig(raw.cloudApi, `${path}.cloudApi`),
openfga: externalPostgresComponentConfig(raw.openfga, `${path}.openfga`),
};
}
function runtimeStoreConfig(value: unknown, path: string): HwlabRuntimeStoreSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const postgres = raw.postgres === undefined ? undefined : asRecord(raw.postgres, `${path}.postgres`);
return {
...(postgres === undefined
? {}
: {
postgres: {
...(postgres.mode === undefined ? {} : { mode: postgresModeField(postgres, `${path}.postgres`) }),
...(postgres.secretName === undefined ? {} : { secretName: stringField(postgres, "secretName", `${path}.postgres`) }),
...(postgres.statefulSet === undefined ? {} : { statefulSet: stringField(postgres, "statefulSet", `${path}.postgres`) }),
...(postgres.serviceName === undefined ? {} : { serviceName: stringField(postgres, "serviceName", `${path}.postgres`) }),
...(postgres.adminUser === undefined ? {} : { adminUser: stringField(postgres, "adminUser", `${path}.postgres`) }),
...(postgres.adminPasswordSourceRef === undefined ? {} : { adminPasswordSourceRef: stringField(postgres, "adminPasswordSourceRef", `${path}.postgres`) }),
...(postgres.adminPasswordSourceKey === undefined ? {} : { adminPasswordSourceKey: secretKeyField(postgres, "adminPasswordSourceKey", `${path}.postgres`) }),
...(postgres.cloudApi === undefined ? {} : { cloudApi: runtimeStorePostgresComponentConfig(postgres.cloudApi, `${path}.postgres.cloudApi`) }),
...(postgres.openfga === undefined ? {} : { openfga: runtimeStorePostgresComponentConfig(postgres.openfga, `${path}.postgres.openfga`) }),
poolMax: numberField(postgres, "poolMax", `${path}.postgres`),
...(postgres.connectionTimeoutMs === undefined
? {}
: { connectionTimeoutMs: numberField(postgres, "connectionTimeoutMs", `${path}.postgres`) }),
...(postgres.queryRetryMaxAttempts === undefined
? {}
: { queryRetryMaxAttempts: numberField(postgres, "queryRetryMaxAttempts", `${path}.postgres`) }),
...(postgres.queryRetryInitialDelayMs === undefined
? {}
: { queryRetryInitialDelayMs: numberField(postgres, "queryRetryInitialDelayMs", `${path}.postgres`) }),
...(postgres.queryRetryMaxDelayMs === undefined
? {}
: { queryRetryMaxDelayMs: numberField(postgres, "queryRetryMaxDelayMs", `${path}.postgres`) }),
},
}),
};
}
function postgresModeField(raw: Record<string, unknown>, path: string): "local-k3s" | "platform-service" {
const mode = stringField(raw, "mode", path);
if (mode !== "local-k3s" && mode !== "platform-service") throw new Error(`${path}.mode must be local-k3s or platform-service`);
return mode;
}
function runtimeStorePostgresComponentConfig(value: unknown, path: string): HwlabRuntimePostgresStoreComponentSpec {
const raw = asRecord(value, path);
return {
secretName: stringField(raw, "secretName", path),
secretKey: secretKeyField(raw, "secretKey", path),
database: stringField(raw, "database", path),
role: stringField(raw, "role", path),
authnKey: optionalStringField(raw, "authnKey", path),
postgresPasswordKey: optionalStringField(raw, "postgresPasswordKey", path),
};
}
function webProbeConfig(value: unknown, path: string): HwlabRuntimeWebProbeSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const rawBrowserProxyMode = optionalStringField(raw, "browserProxyMode", path);
let browserProxyMode: "auto" | "direct" | undefined;
if (rawBrowserProxyMode !== undefined) {
if (rawBrowserProxyMode !== "auto" && rawBrowserProxyMode !== "direct") {
throw new Error(`${path}.browserProxyMode must be auto or direct`);
}
browserProxyMode = rawBrowserProxyMode;
}
const playwrightBrowsersPath = optionalStringField(raw, "playwrightBrowsersPath", path);
const defaultOriginRaw = optionalStringField(raw, "defaultOrigin", path);
let defaultOrigin: HwlabRuntimeWebProbeOriginName | undefined;
if (defaultOriginRaw !== undefined) {
if (defaultOriginRaw !== "internal" && defaultOriginRaw !== "public") {
throw new Error(`${path}.defaultOrigin must be internal or public`);
}
defaultOrigin = defaultOriginRaw;
}
const origins = raw.origins === undefined ? undefined : webProbeOriginsConfig(raw.origins, `${path}.origins`);
if (defaultOrigin !== undefined && origins === undefined) {
throw new Error(`${path}.origins must declare internal and public when defaultOrigin is set`);
}
return {
...(browserProxyMode === undefined ? {} : { browserProxyMode }),
...(playwrightBrowsersPath === undefined ? {} : { playwrightBrowsersPath }),
...(defaultOrigin === undefined ? {} : { defaultOrigin }),
...(raw.resourcePolicy === undefined ? {} : { resourcePolicy: webProbeResourcePolicyConfig(raw.resourcePolicy, `${path}.resourcePolicy`) }),
...(origins === undefined ? {} : { origins }),
...(raw.authLogin === undefined ? {} : { authLogin: webProbeAuthLoginConfig(raw.authLogin, `${path}.authLogin`) }),
...(raw.alertThresholds === undefined ? {} : { alertThresholds: webProbeAlertThresholdsConfig(raw.alertThresholds, `${path}.alertThresholds`) }),
...(raw.browserFreezePolicy === undefined ? {} : { browserFreezePolicy: webProbeBrowserFreezePolicyConfig(raw.browserFreezePolicy, `${path}.browserFreezePolicy`) }),
...(raw.projectManagement === undefined ? {} : { projectManagement: webProbeProjectManagementConfig(raw.projectManagement, `${path}.projectManagement`) }),
...(raw.realtimeFanoutProfiles === undefined ? {} : { realtimeFanoutProfiles: webProbeRealtimeFanoutProfilesConfig(raw.realtimeFanoutProfiles, `${path}.realtimeFanoutProfiles`) }),
...(raw.workbenchKafkaDebugReplay === undefined
? {}
: { workbenchKafkaDebugReplay: webProbeWorkbenchKafkaDebugReplayConfig(raw.workbenchKafkaDebugReplay, `${path}.workbenchKafkaDebugReplay`) }),
...(raw.workbenchTraceReadability === undefined
? {}
: { workbenchTraceReadability: webProbeWorkbenchTraceReadabilityConfig(raw.workbenchTraceReadability, `${path}.workbenchTraceReadability`) }),
};
}
function webProbeResourcePolicyConfig(value: unknown, path: string): HwlabRuntimeWebProbeResourcePolicySpec {
const raw = asRecord(value, path);
const memory = asRecord(raw.memoryStartGuard, `${path}.memoryStartGuard`);
const lifecycle = asRecord(raw.observerLifecycle, `${path}.observerLifecycle`);
const browserEvidence = asRecord(raw.browserEvidence, `${path}.browserEvidence`);
return {
memoryStartGuard: {
metric: enumStringField(memory, "metric", `${path}.memoryStartGuard`, ["MemAvailable"] as const),
source: enumStringField(memory, "source", `${path}.memoryStartGuard`, ["/proc/meminfo"] as const),
swapExcluded: literalTrueField(memory, "swapExcluded", `${path}.memoryStartGuard`),
thresholdBytes: boundedIntegerField(memory, "thresholdBytes", `${path}.memoryStartGuard`, 1, Number.MAX_SAFE_INTEGER),
manualComparator: enumStringField(memory, "manualComparator", `${path}.memoryStartGuard`, ["less-than-or-equal"] as const),
sentinelComparator: enumStringField(memory, "sentinelComparator", `${path}.memoryStartGuard`, ["less-than"] as const),
snapshotTimeoutSeconds: boundedIntegerField(memory, "snapshotTimeoutSeconds", `${path}.memoryStartGuard`, 5, 120),
launchLockPath: absoluteHostPathField(stringField(memory, "launchLockPath", `${path}.memoryStartGuard`), `${path}.memoryStartGuard.launchLockPath`),
launchLockTimeoutSeconds: boundedIntegerField(memory, "launchLockTimeoutSeconds", `${path}.memoryStartGuard`, 1, 60),
launchReadyTimeoutSeconds: boundedIntegerField(memory, "launchReadyTimeoutSeconds", `${path}.memoryStartGuard`, 1, 30),
launchReadyPollMilliseconds: boundedIntegerField(memory, "launchReadyPollMilliseconds", `${path}.memoryStartGuard`, 50, 1000),
},
observerLifecycle: {
maxRunSeconds: boundedIntegerField(lifecycle, "maxRunSeconds", `${path}.observerLifecycle`, 1, 86_400),
maxSamples: boundedIntegerField(lifecycle, "maxSamples", `${path}.observerLifecycle`, 1, 10_000_000),
maxScreenshots: boundedIntegerField(lifecycle, "maxScreenshots", `${path}.observerLifecycle`, 1, 10_000),
maxPerformanceEntries: boundedIntegerField(lifecycle, "maxPerformanceEntries", `${path}.observerLifecycle`, 1, 100_000),
maxBrowserProcessHistoryEntries: boundedIntegerField(lifecycle, "maxBrowserProcessHistoryEntries", `${path}.observerLifecycle`, 1, 100_000),
maxBrowserFreezeSignalHistoryEntries: boundedIntegerField(lifecycle, "maxBrowserFreezeSignalHistoryEntries", `${path}.observerLifecycle`, 1, 100_000),
maxRealtimeEventEntries: boundedIntegerField(lifecycle, "maxRealtimeEventEntries", `${path}.observerLifecycle`, 1, 100_000),
maxRealtimeEventBodyBytes: boundedIntegerField(lifecycle, "maxRealtimeEventBodyBytes", `${path}.observerLifecycle`, 1, 16 * 1024 * 1024),
maxDomEvidenceRows: boundedIntegerField(lifecycle, "maxDomEvidenceRows", `${path}.observerLifecycle`, 1, 100_000),
maxProcessScanEntries: boundedIntegerField(lifecycle, "maxProcessScanEntries", `${path}.observerLifecycle`, 1, 1_000_000),
maxPerformanceCaptureSeconds: boundedIntegerField(lifecycle, "maxPerformanceCaptureSeconds", `${path}.observerLifecycle`, 1, 600),
maxResponseBodyBytes: boundedIntegerField(lifecycle, "maxResponseBodyBytes", `${path}.observerLifecycle`, 1, 16 * 1024 * 1024),
maxWebPerformanceBodyBytes: boundedIntegerField(lifecycle, "maxWebPerformanceBodyBytes", `${path}.observerLifecycle`, 1, 16 * 1024 * 1024),
maxJsonlQueueEntries: boundedIntegerField(lifecycle, "maxJsonlQueueEntries", `${path}.observerLifecycle`, 1, 100_000),
maxJsonlQueueBytes: boundedIntegerField(lifecycle, "maxJsonlQueueBytes", `${path}.observerLifecycle`, 1024, 64 * 1024 * 1024),
maxJsonlLineBytes: boundedIntegerField(lifecycle, "maxJsonlLineBytes", `${path}.observerLifecycle`, 1024, 16 * 1024 * 1024),
jsonlFlushTimeoutMs: boundedIntegerField(lifecycle, "jsonlFlushTimeoutMs", `${path}.observerLifecycle`, 100, 60_000),
maxResponseBodyInFlight: boundedIntegerField(lifecycle, "maxResponseBodyInFlight", `${path}.observerLifecycle`, 1, 64),
maxPassiveListenerTasks: boundedIntegerField(lifecycle, "maxPassiveListenerTasks", `${path}.observerLifecycle`, 1, 10_000),
passiveTaskFlushTimeoutMs: boundedIntegerField(lifecycle, "passiveTaskFlushTimeoutMs", `${path}.observerLifecycle`, 100, 60_000),
closeStepTimeoutMs: boundedIntegerField(lifecycle, "closeStepTimeoutMs", `${path}.observerLifecycle`, 100, 60_000),
maxProcessReadConcurrency: boundedIntegerField(lifecycle, "maxProcessReadConcurrency", `${path}.observerLifecycle`, 1, 256),
maxRealtimeTotalEntries: boundedIntegerField(lifecycle, "maxRealtimeTotalEntries", `${path}.observerLifecycle`, 1, 100_000),
maxRealtimeTotalBodyBytes: boundedIntegerField(lifecycle, "maxRealtimeTotalBodyBytes", `${path}.observerLifecycle`, 1024, 64 * 1024 * 1024),
},
browserEvidence: {
screenshotMode: enumStringField(browserEvidence, "screenshotMode", `${path}.browserEvidence`, ["viewport"] as const),
maxNetworkRequestEntries: boundedIntegerField(browserEvidence, "maxNetworkRequestEntries", `${path}.browserEvidence`, 1, 100_000),
maxNetworkRequestBytes: boundedIntegerField(browserEvidence, "maxNetworkRequestBytes", `${path}.browserEvidence`, 1024, 64 * 1024 * 1024),
maxNetworkFailureEntries: boundedIntegerField(browserEvidence, "maxNetworkFailureEntries", `${path}.browserEvidence`, 1, 100_000),
maxNetworkFailureBytes: boundedIntegerField(browserEvidence, "maxNetworkFailureBytes", `${path}.browserEvidence`, 1024, 64 * 1024 * 1024),
},
};
}
function webProbeWorkbenchKafkaDebugReplayConfig(value: unknown, path: string): HwlabRuntimeWebProbeWorkbenchKafkaDebugReplaySpec {
const raw = asRecord(value, path);
const topic = stringField(raw, "topic", path);
const groupPrefix = stringField(raw, "groupPrefix", path);
const completionTimeoutMs = numberField(raw, "completionTimeoutMs", path);
if (!/^[A-Za-z0-9._-]+$/u.test(topic)) throw new Error(`${path}.topic must use Kafka topic identifier characters`);
if (!/^[A-Za-z0-9._-]+$/u.test(groupPrefix)) throw new Error(`${path}.groupPrefix must use Kafka consumer group identifier characters`);
if (!/(?:^|[.-])debug(?:[.-]|$)/u.test(topic)) throw new Error(`${path}.topic must identify an isolated debug topic`);
if (!/(?:^|[.-])debug(?:[.-]|$)/u.test(groupPrefix)) throw new Error(`${path}.groupPrefix must identify an isolated debug consumer group`);
if (!Number.isInteger(completionTimeoutMs) || completionTimeoutMs < 1000 || completionTimeoutMs > 120000) {
throw new Error(`${path}.completionTimeoutMs must be an integer between 1000 and 120000`);
}
return {
enabled: booleanField(raw, "enabled", path),
topic,
groupPrefix,
completionTimeoutMs,
};
}
function webProbeWorkbenchTraceReadabilityConfig(value: unknown, path: string): HwlabRuntimeWebProbeWorkbenchTraceReadabilitySpec {
const raw = asRecord(value, path);
return {
enabled: booleanField(raw, "enabled", path),
runningCardTimeoutMs: numberField(raw, "runningCardTimeoutMs", path),
disclosureActionTimeoutMs: numberField(raw, "disclosureActionTimeoutMs", path),
disclosureOpenTimeoutMs: numberField(raw, "disclosureOpenTimeoutMs", path),
runningReadableTimeoutMs: numberField(raw, "runningReadableTimeoutMs", path),
terminalTimeoutMs: numberField(raw, "terminalTimeoutMs", path),
domStableQuietMs: numberField(raw, "domStableQuietMs", path),
domStableTimeoutMs: numberField(raw, "domStableTimeoutMs", path),
pollIntervalMs: numberField(raw, "pollIntervalMs", path),
};
}
function webProbeOriginsConfig(value: unknown, path: string): HwlabRuntimeWebProbeOriginsSpec {
const raw = asRecord(value, path);
for (const key of Object.keys(raw)) {
if (key !== "internal" && key !== "public") throw new Error(`${path}.${key} is not a supported web-probe origin`);
}
const internal = webProbeOriginConfig(raw.internal, `${path}.internal`);
const publicOrigin = webProbeOriginConfig(raw.public, `${path}.public`);
if (internal.mode !== "k8s-service-cluster-ip") throw new Error(`${path}.internal.mode must be k8s-service-cluster-ip`);
if (publicOrigin.mode !== "public") throw new Error(`${path}.public.mode must be public`);
return { internal, public: publicOrigin };
}
function webProbeRealtimeFanoutProfilesConfig(value: unknown, path: string): Readonly<Record<string, HwlabRuntimeWebProbeRealtimeFanoutProfileSpec>> {
const raw = asRecord(value, path);
const entries = Object.entries(raw).map(([id, item]) => {
if (!/^[A-Za-z0-9_.-]+$/u.test(id)) throw new Error(`${path} profile id ${id} must use safe identifier characters`);
return [id, webProbeRealtimeFanoutProfileConfig(item, `${path}.${id}`)] as const;
});
if (entries.length === 0) throw new Error(`${path} must declare at least one profile`);
return Object.fromEntries(entries);
}
function webProbeRealtimeFanoutProfileConfig(value: unknown, path: string): HwlabRuntimeWebProbeRealtimeFanoutProfileSpec {
const raw = asRecord(value, path);
const subscriberCount = numberField(raw, "subscriberCount", path);
if (subscriberCount !== 2) throw new Error(`${path}.subscriberCount must be 2`);
const debugStreams = nonEmptyStringArrayField(raw, "debugStreams", path);
if (debugStreams.length !== 3 || new Set(debugStreams).size !== 3 || debugStreams.some((stream) => !["stdio", "agentrun", "hwlab"].includes(stream))) {
throw new Error(`${path}.debugStreams must contain stdio, agentrun, and hwlab exactly once`);
}
const fromBeginning = booleanField(raw, "fromBeginning", path);
if (fromBeginning !== false) throw new Error(`${path}.fromBeginning must be false for turn-scoped debug streams`);
const expectedProductSseRaw = asRecord(raw.expectedProductSse, `${path}.expectedProductSse`);
const expectedProductSse = {
deliverySemantics: stringField(expectedProductSseRaw, "deliverySemantics", `${path}.expectedProductSse`),
liveOnly: booleanField(expectedProductSseRaw, "liveOnly", `${path}.expectedProductSse`),
replay: booleanField(expectedProductSseRaw, "replay", `${path}.expectedProductSse`),
replaySupported: booleanField(expectedProductSseRaw, "replaySupported", `${path}.expectedProductSse`),
lossPossible: booleanField(expectedProductSseRaw, "lossPossible", `${path}.expectedProductSse`),
refreshHandoff: booleanField(expectedProductSseRaw, "refreshHandoff", `${path}.expectedProductSse`),
replayPriorTraceOnReconnect: booleanField(expectedProductSseRaw, "replayPriorTraceOnReconnect", `${path}.expectedProductSse`),
};
if (expectedProductSse.refreshHandoff && (!expectedProductSse.replay || !expectedProductSse.replaySupported || expectedProductSse.liveOnly)) {
throw new Error(`${path}.expectedProductSse refresh handoff requires replay=true, replaySupported=true, and liveOnly=false`);
}
if (expectedProductSse.replayPriorTraceOnReconnect && !expectedProductSse.replay) {
throw new Error(`${path}.expectedProductSse replayPriorTraceOnReconnect requires replay=true`);
}
const requiredEventTypes = asRecord(raw.requiredEventTypes, `${path}.requiredEventTypes`);
const forbiddenRequestPaths = nonEmptyStringArrayField(raw, "forbiddenRequestPaths", path);
if (new Set(forbiddenRequestPaths).size !== forbiddenRequestPaths.length) throw new Error(`${path}.forbiddenRequestPaths must not contain duplicates`);
for (const requestPath of forbiddenRequestPaths) {
if (!requestPath.startsWith("/")) throw new Error(`${path}.forbiddenRequestPaths entries must be absolute paths`);
}
const productEventsPath = stringField(raw, "productEventsPath", path);
const debugEventsPath = stringField(raw, "debugEventsPath", path);
for (const [field, routePath] of [["productEventsPath", productEventsPath], ["debugEventsPath", debugEventsPath]] as const) {
if (!routePath.startsWith("/")) throw new Error(`${path}.${field} must be an absolute path`);
}
const agentrunEventTypes = nonEmptyStringArrayField(requiredEventTypes, "agentrun", `${path}.requiredEventTypes`);
const hwlabEventTypes = nonEmptyStringArrayField(requiredEventTypes, "hwlab", `${path}.requiredEventTypes`);
if (new Set(agentrunEventTypes).size !== agentrunEventTypes.length) throw new Error(`${path}.requiredEventTypes.agentrun must not contain duplicates`);
if (new Set(hwlabEventTypes).size !== hwlabEventTypes.length) throw new Error(`${path}.requiredEventTypes.hwlab must not contain duplicates`);
const conditionalEventTypePairsRaw = asRecord(raw.conditionalEventTypePairs, `${path}.conditionalEventTypePairs`);
if (Object.keys(conditionalEventTypePairsRaw).length === 0) throw new Error(`${path}.conditionalEventTypePairs must contain at least one pair`);
const conditionalEventTypePairs = Object.fromEntries(Object.entries(conditionalEventTypePairsRaw).map(([id, value]) => {
if (!/^[a-z][a-z0-9-]*$/u.test(id)) throw new Error(`${path}.conditionalEventTypePairs.${id} must use a stable lowercase id`);
const pairPath = `${path}.conditionalEventTypePairs.${id}`;
const pair = asRecord(value, pairPath);
const unknownFields = Object.keys(pair).filter((field) => field !== "agentrun" && field !== "hwlab");
if (unknownFields.length > 0) throw new Error(`${pairPath}.${unknownFields[0]} is not allowed`);
const agentrun = stringField(pair, "agentrun", pairPath);
const hwlab = stringField(pair, "hwlab", pairPath);
if (agentrunEventTypes.includes(agentrun)) throw new Error(`${pairPath}.agentrun duplicates a required event type`);
if (hwlabEventTypes.includes(hwlab)) throw new Error(`${pairPath}.hwlab duplicates a required event type`);
return [id, { agentrun, hwlab }];
}));
const conditionalAgentrunTypes = Object.values(conditionalEventTypePairs).map((pair) => pair.agentrun);
const conditionalHwlabTypes = Object.values(conditionalEventTypePairs).map((pair) => pair.hwlab);
if (new Set(conditionalAgentrunTypes).size !== conditionalAgentrunTypes.length) throw new Error(`${path}.conditionalEventTypePairs agentrun types must not contain duplicates`);
if (new Set(conditionalHwlabTypes).size !== conditionalHwlabTypes.length) throw new Error(`${path}.conditionalEventTypePairs hwlab types must not contain duplicates`);
return {
subscriberCount: 2,
debugStreams: debugStreams as ("stdio" | "agentrun" | "hwlab")[],
productEventsPath,
debugEventsPath,
productReadyEvent: stringField(raw, "productReadyEvent", path),
debugReadyEvent: stringField(raw, "debugReadyEvent", path),
businessEvent: stringField(raw, "businessEvent", path),
fromBeginning: false,
barrierTimeoutMs: boundedIntegerField(raw, "barrierTimeoutMs", path, 1_000, 120_000),
terminalTimeoutMs: boundedIntegerField(raw, "terminalTimeoutMs", path, 1_000, 600_000),
terminalQuietMs: boundedIntegerField(raw, "terminalQuietMs", path, 250, 10_000),
navigationMaxAttempts: boundedIntegerField(raw, "navigationMaxAttempts", path, 1, 10),
navigationRetryDelayMs: boundedIntegerField(raw, "navigationRetryDelayMs", path, 100, 10_000),
reconnectQuietMs: boundedIntegerField(raw, "reconnectQuietMs", path, 250, 30_000),
expectedProductSse,
forbiddenRequestPaths,
requiredEventTypes: {
agentrun: agentrunEventTypes,
hwlab: hwlabEventTypes,
},
conditionalEventTypePairs,
};
}
function webProbeAuthLoginConfig(value: unknown, path: string): HwlabRuntimeWebProbeAuthLoginSpec {
const raw = asRecord(value, path);
const maxAttempts = positiveNumberField(raw, "maxAttempts", path);
if (!Number.isInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 20) throw new Error(`${path}.maxAttempts must be an integer between 1 and 20`);
return {
maxAttempts,
requestTimeoutMs: boundedIntegerField(raw, "requestTimeoutMs", path, 1000, 120000),
initialDelayMs: boundedIntegerField(raw, "initialDelayMs", path, 0, 60000),
maxDelayMs: boundedIntegerField(raw, "maxDelayMs", path, 0, 120000),
};
}
function webProbeBrowserFreezePolicyConfig(value: unknown, path: string): HwlabRuntimeWebProbeBrowserFreezePolicySpec {
const raw = asRecord(value, path);
const memory = asRecord(raw.memory, `${path}.memory`);
const responsiveness = asRecord(raw.responsiveness, `${path}.responsiveness`);
const cdp = asRecord(raw.cdp, `${path}.cdp`);
const kill = asRecord(raw.kill, `${path}.kill`);
return {
enabled: booleanField(raw, "enabled", path),
blockerWindowMs: positiveNumberField(raw, "blockerWindowMs", path),
memory: {
totalRssBlockerMb: positiveNumberField(memory, "totalRssBlockerMb", `${path}.memory`),
processRssBlockerMb: positiveNumberField(memory, "processRssBlockerMb", `${path}.memory`),
growthBlockerMb: positiveNumberField(memory, "growthBlockerMb", `${path}.memory`),
},
responsiveness: {
latencyBlockerMs: positiveNumberField(responsiveness, "latencyBlockerMs", `${path}.responsiveness`),
eventBlockerCount: positiveNumberField(responsiveness, "eventBlockerCount", `${path}.responsiveness`),
},
cdp: {
metricsTimeoutBlockerCount: positiveNumberField(cdp, "metricsTimeoutBlockerCount", `${path}.cdp`),
},
kill: {
enabled: booleanField(kill, "enabled", `${path}.kill`),
gracefulSignal: enumStringField(kill, "gracefulSignal", `${path}.kill`, ["SIGTERM"] as const),
forceSignal: enumStringField(kill, "forceSignal", `${path}.kill`, ["SIGKILL"] as const),
graceMs: boundedIntegerField(kill, "graceMs", `${path}.kill`, 1, 120000),
pollIntervalMs: boundedIntegerField(kill, "pollIntervalMs", `${path}.kill`, 1, 10000),
exitCode: boundedIntegerField(kill, "exitCode", `${path}.kill`, 1, 125),
},
};
}
function webProbeSentinelConfig(value: unknown, path: string): HwlabRuntimeWebProbeSentinelSpec {
const raw = asRecord(value, path);
const allowed = new Set(["enabled", "configRefs"]);
for (const key of Object.keys(raw)) {
if (!allowed.has(key)) throw new Error(`${path}.${key} is not allowed; root sentinel YAML may only contain enabled/configRefs`);
}
const refs = asRecord(raw.configRefs, `${path}.configRefs`);
const allowedRefKeys = new Set(HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS);
for (const key of Object.keys(refs)) {
if (!allowedRefKeys.has(key as HwlabRuntimeWebProbeSentinelConfigRefKey)) throw new Error(`${path}.configRefs.${key} is not a supported sentinel configRef`);
}
const configRefs = Object.fromEntries(HWLAB_WEB_PROBE_SENTINEL_CONFIG_REF_KEYS.map((key) => {
const ref = stringField(refs, key, `${path}.configRefs`);
validateConfigRef(ref, `${path}.configRefs.${key}`);
return [key, ref];
})) as Record<HwlabRuntimeWebProbeSentinelConfigRefKey, string>;
return {
enabled: booleanField(raw, "enabled", path),
configRefs,
};
}
function webProbeSentinelRegistryItemConfig(value: unknown, path: string): HwlabRuntimeWebProbeSentinelRegistryItemSpec {
const raw = asRecord(value, path);
const allowed = new Set(["id", "enabled", "configRef"]);
for (const key of Object.keys(raw)) {
if (!allowed.has(key)) throw new Error(`${path}.${key} is not allowed; sentinel registry items may only contain id/enabled/configRef`);
}
const id = stringField(raw, "id", path);
if (!/^[a-z0-9][a-z0-9-]{1,80}$/u.test(id)) throw new Error(`${path}.id must be a stable lowercase sentinel id`);
const configRef = stringField(raw, "configRef", path);
validateConfigRef(configRef, `${path}.configRef`);
return {
id,
enabled: booleanField(raw, "enabled", path),
configRef,
};
}
function webProbeSentinelRegistryConfig(value: unknown, path: string): readonly HwlabRuntimeWebProbeSentinelRegistryItemSpec[] {
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
if (value.length === 0) throw new Error(`${path} must contain at least one sentinel`);
const items = value.map((item, index) => webProbeSentinelRegistryItemConfig(item, `${path}[${index}]`));
const ids = new Set<string>();
for (const item of items) {
if (ids.has(item.id)) throw new Error(`${path} contains duplicate sentinel id ${item.id}`);
ids.add(item.id);
}
return items;
}
function validateConfigRef(ref: string, path: string): void {
const [file, fragment, extra] = ref.split("#");
if (extra !== undefined || file === undefined || fragment === undefined || file.length === 0 || fragment.length === 0) {
throw new Error(`${path} must use path/to/file.yaml#object.path syntax`);
}
if (file.startsWith("/") || file.includes("\0") || file.includes("..") || !file.startsWith("config/") || !file.endsWith(".yaml")) {
throw new Error(`${path} must reference a repo-relative config/*.yaml file without ..`);
}
if (!/^[A-Za-z0-9_.\-[\]${}]+$/u.test(fragment)) {
throw new Error(`${path} has an unsupported YAML path fragment`);
}
}
function webProbeProjectManagementConfig(value: unknown, path: string): HwlabRuntimeWebProbeProjectManagementSpec {
const raw = asRecord(value, path);
const targetPaths = nonEmptyStringArrayField(raw, "targetPaths", path);
const readinessSelectors = nonEmptyStringArrayField(raw, "readinessSelectors", path);
const naturalApiPathPrefixes = nonEmptyStringArrayField(raw, "naturalApiPathPrefixes", path);
const commandAllowlist = nonEmptyStringArrayField(raw, "commandAllowlist", path);
const launchRoute = stringField(raw, "launchRoute", path);
if (!launchRoute.startsWith("/")) throw new Error(`${path}.launchRoute must be an absolute path`);
for (const [field, values] of Object.entries({ targetPaths, naturalApiPathPrefixes })) {
for (const item of values) {
if (!item.startsWith("/")) throw new Error(`${path}.${field} entries must be absolute paths; got ${item}`);
}
}
return {
enabled: booleanField(raw, "enabled", path),
targetPaths,
readinessSelectors,
naturalApiPathPrefixes,
commandAllowlist,
launchRoute,
slowApiBudgetMs: positiveNumberField(raw, "slowApiBudgetMs", path),
};
}
function webProbeAlertThresholdsConfig(value: unknown, path: string): HwlabRuntimeWebProbeAlertThresholdsSpec {
const raw = asRecord(value, path);
const sessionRailFallbackRatio = positiveNumberField(raw, "sessionRailFallbackRatio", path);
if (sessionRailFallbackRatio > 1) throw new Error(`${path}.sessionRailFallbackRatio must be <= 1`);
return {
sameOriginApiSlowMs: positiveNumberField(raw, "sameOriginApiSlowMs", path),
partialApiSlowMs: positiveNumberField(raw, "partialApiSlowMs", path),
longLivedStreamOpenSlowMs: positiveNumberField(raw, "longLivedStreamOpenSlowMs", path),
visibleLoadingSlowMs: positiveNumberField(raw, "visibleLoadingSlowMs", path),
turnTimingSampleSlackSeconds: positiveNumberField(raw, "turnTimingSampleSlackSeconds", path),
turnElapsedSevereTimeoutSeconds: positiveNumberField(raw, "turnElapsedSevereTimeoutSeconds", path),
domEvaluateTimeoutRedCount: positiveNumberField(raw, "domEvaluateTimeoutRedCount", path),
domEvaluateTimeoutRedWindowMs: positiveNumberField(raw, "domEvaluateTimeoutRedWindowMs", path),
screenshotTimeoutRedCount: positiveNumberField(raw, "screenshotTimeoutRedCount", path),
pageErrorRedCount: positiveNumberField(raw, "pageErrorRedCount", path),
longTaskRedMs: positiveNumberField(raw, "longTaskRedMs", path),
longAnimationFrameRedMs: positiveNumberField(raw, "longAnimationFrameRedMs", path),
eventLoopGapRedMs: positiveNumberField(raw, "eventLoopGapRedMs", path),
browserProcessSampleIntervalMs: positiveNumberField(raw, "browserProcessSampleIntervalMs", path),
requestRateBucketMs: positiveNumberField(raw, "requestRateBucketMs", path),
requestRateTotalRedPerMinute: positiveNumberField(raw, "requestRateTotalRedPerMinute", path),
requestRatePageRedPerMinute: positiveNumberField(raw, "requestRatePageRedPerMinute", path),
requestRateApiPathRedPerMinute: positiveNumberField(raw, "requestRateApiPathRedPerMinute", path),
browserTotalRssRedMb: positiveNumberField(raw, "browserTotalRssRedMb", path),
browserProcessRssRedMb: positiveNumberField(raw, "browserProcessRssRedMb", path),
browserRssGrowthRedMb: positiveNumberField(raw, "browserRssGrowthRedMb", path),
browserRssGrowthWindowMs: positiveNumberField(raw, "browserRssGrowthWindowMs", path),
playwrightResponsivenessRedMs: positiveNumberField(raw, "playwrightResponsivenessRedMs", path),
playwrightResponsivenessTimeoutRedCount: positiveNumberField(raw, "playwrightResponsivenessTimeoutRedCount", path),
cdpMetricsTimeoutRedCount: positiveNumberField(raw, "cdpMetricsTimeoutRedCount", path),
uncommandedStateChangeCommandWindowMs: positiveNumberField(raw, "uncommandedStateChangeCommandWindowMs", path),
scrollJumpCommandWindowMs: positiveNumberField(raw, "scrollJumpCommandWindowMs", path),
scrollJumpFromY: positiveNumberField(raw, "scrollJumpFromY", path),
scrollJumpToY: positiveNumberField(raw, "scrollJumpToY", path),
sessionRailFallbackRatio,
};
}
function webProbeOriginConfig(value: unknown, path: string): HwlabRuntimeWebProbeOriginSpec {
const raw = asRecord(value, path);
const mode = stringField(raw, "mode", path);
const browserProxyModeRaw = optionalStringField(raw, "browserProxyMode", path);
let browserProxyMode: "auto" | "direct" | undefined;
if (browserProxyModeRaw !== undefined) {
if (browserProxyModeRaw !== "auto" && browserProxyModeRaw !== "direct") {
throw new Error(`${path}.browserProxyMode must be auto or direct`);
}
browserProxyMode = browserProxyModeRaw;
}
if (mode === "public") {
return {
mode,
...(raw.baseUrl === undefined ? {} : { baseUrl: stringField(raw, "baseUrl", path).replace(/\/+$/u, "") }),
...(browserProxyMode === undefined ? {} : { browserProxyMode }),
};
}
if (mode !== "k8s-service-cluster-ip") throw new Error(`${path}.mode must be public or k8s-service-cluster-ip`);
const scheme = stringField(raw, "scheme", path);
if (scheme !== "http" && scheme !== "https") throw new Error(`${path}.scheme must be http or https`);
return {
mode,
serviceName: stringField(raw, "serviceName", path),
namespace: stringField(raw, "namespace", path),
port: numberField(raw, "port", path),
scheme,
...(browserProxyMode === undefined ? {} : { browserProxyMode }),
};
}
function publicExposureProxyConfig(value: unknown, path: string): HwlabRuntimePublicExposureFrpcProxySpec {
const raw = asRecord(value, path);
return {
name: stringField(raw, "name", path),
localIP: stringField(raw, "localIP", path),
localPort: numberField(raw, "localPort", path),
remotePort: numberField(raw, "remotePort", path),
};
}
function publicExposureExtraProxyConfig(value: unknown, path: string): HwlabRuntimePublicExposureFrpcExtraProxySpec {
const raw = asRecord(value, path);
const publicBaseUrl = optionalStringField(raw, "publicBaseUrl", path)?.replace(/\/+$/u, "");
const hostname = optionalStringField(raw, "hostname", path);
if (publicBaseUrl !== undefined) {
validatePublicExposureOrigin(publicBaseUrl, `${path}.publicBaseUrl`);
if (hostname !== undefined && new URL(publicBaseUrl).hostname !== hostname) throw new Error(`${path}.publicBaseUrl hostname must match ${path}.hostname`);
}
const cloudWebEnvName = optionalStringField(raw, "cloudWebEnvName", path);
if (cloudWebEnvName !== undefined && !/^HWLAB_CLOUD_WEB_[A-Z0-9_]+$/u.test(cloudWebEnvName)) throw new Error(`${path}.cloudWebEnvName must be a HWLAB_CLOUD_WEB_* env name`);
if (cloudWebEnvName !== undefined && publicBaseUrl === undefined) throw new Error(`${path}.cloudWebEnvName requires ${path}.publicBaseUrl`);
return {
id: stringField(raw, "id", path),
...publicExposureProxyConfig(value, path),
...(hostname === undefined ? {} : { hostname }),
...(publicBaseUrl === undefined ? {} : { publicBaseUrl }),
...(cloudWebEnvName === undefined ? {} : { cloudWebEnvName }),
};
}
function publicExposureExtraProxiesConfig(value: unknown, path: string): HwlabRuntimePublicExposureFrpcExtraProxySpec[] {
if (value === undefined) return [];
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
return value.map((item, index) => publicExposureExtraProxyConfig(item, `${path}[${index}]`));
}
function publicExposureConfig(value: unknown, path: string): HwlabRuntimePublicExposureSpec | null {
if (value === undefined) return null;
const raw = asRecord(value, path);
const mode = stringField(raw, "mode", path);
const publicBaseUrl = stringField(raw, "publicBaseUrl", path).replace(/\/+$/u, "");
if (mode === "node-port-http") {
const service = asRecord(raw.service, `${path}.service`);
const selector = optionalStringRecord(service.selector, `${path}.service.selector`);
if (Object.keys(selector).length === 0) throw new Error(`${path}.service.selector must declare at least one label`);
const publicAddress = stringField(raw, "publicAddress", path);
const nodePort = numberField(service, "nodePort", `${path}.service`);
validateNodePortPublicExposureOrigin(publicBaseUrl, publicAddress, nodePort, `${path}.publicBaseUrl`);
return {
enabled: true,
mode,
publicBaseUrl,
publicAddress,
serviceName: stringField(service, "name", `${path}.service`),
selector,
port: numberField(service, "port", `${path}.service`),
targetPort: numberField(service, "targetPort", `${path}.service`),
nodePort,
};
}
if (mode !== "pk01-caddy-frp") throw new Error(`${path}.mode must be pk01-caddy-frp or node-port-http`);
const frpc = asRecord(raw.frpc, `${path}.frpc`);
const caddy = asRecord(raw.caddy, `${path}.caddy`);
const caddyTls = optionalStringField(caddy, "tls", `${path}.caddy`) ?? "auto";
if (caddyTls !== "auto" && caddyTls !== "internal") throw new Error(`${path}.caddy.tls must be auto or internal`);
return {
enabled: true,
mode,
publicBaseUrl,
hostname: stringField(raw, "hostname", path),
expectedA: stringField(raw, "expectedA", path),
serverAddr: stringField(frpc, "serverAddr", `${path}.frpc`),
serverPort: numberField(frpc, "serverPort", `${path}.frpc`),
tokenSourceRef: stringField(frpc, "tokenSourceRef", `${path}.frpc`),
tokenSourceKey: stringField(frpc, "tokenSourceKey", `${path}.frpc`),
secretName: stringField(frpc, "secretName", `${path}.frpc`),
secretKey: optionalStringField(frpc, "secretKey", `${path}.frpc`) ?? "frpc.toml",
tokenKey: optionalStringField(frpc, "tokenKey", `${path}.frpc`) ?? "token",
caddyRoute: stringField(caddy, "route", `${path}.caddy`),
caddyConfigPath: stringField(caddy, "configPath", `${path}.caddy`),
caddyServiceName: stringField(caddy, "serviceName", `${path}.caddy`),
caddyEmail: stringField(caddy, "email", `${path}.caddy`),
caddyTls,
responseHeaderTimeoutSeconds: numberField(caddy, "responseHeaderTimeoutSeconds", `${path}.caddy`),
webProxy: publicExposureProxyConfig(frpc.webProxy, `${path}.frpc.webProxy`),
apiProxy: publicExposureProxyConfig(frpc.apiProxy, `${path}.frpc.apiProxy`),
extraProxies: publicExposureExtraProxiesConfig(frpc.extraProxies, `${path}.frpc.extraProxies`),
};
}
function validateNodePortPublicExposureOrigin(publicBaseUrl: string, publicAddress: string, nodePort: number, path: string): void {
try {
const parsed = new URL(publicBaseUrl);
if (parsed.protocol !== "http:") throw new Error("must use http");
if (parsed.hostname !== publicAddress) throw new Error(`hostname must match publicAddress ${publicAddress}`);
if (Number(parsed.port) !== nodePort) throw new Error(`port must match service.nodePort ${nodePort}`);
if (parsed.pathname !== "/" || parsed.search.length > 0 || parsed.hash.length > 0) throw new Error("must point to the public origin root");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`${path} must be an http NodePort origin root URL: ${message}`);
}
}
function validatePublicExposureOrigin(publicBaseUrl: string, path: string): void {
try {
const parsed = new URL(publicBaseUrl);
if (parsed.protocol !== "https:") throw new Error("must use https");
if (parsed.pathname !== "/" || parsed.search.length > 0 || parsed.hash.length > 0) throw new Error("must point to the public origin root");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`${path} must be an https origin root URL: ${message}`);
}
}
function observabilityConfig(value: unknown, path: string): HwlabRuntimeObservabilitySpec {
const raw = asRecord(value, path);
const recordingRules = observabilityRecordingRulesConfig(raw.recordingRules, `${path}.recordingRules`);
const warningAlerts = observabilityWarningAlertsConfig(raw.warningAlerts, `${path}.warningAlerts`);
const recordingRuleIds = new Set(recordingRules.map((rule) => rule.id));
for (const alert of warningAlerts) {
if (!recordingRuleIds.has(alert.ruleId)) throw new Error(`${path}.warningAlerts.${alert.id}.ruleId must reference a recordingRules id`);
}
const traceExplorerUrlTemplate = optionalStringField(raw, "traceExplorerUrlTemplate", path);
if (traceExplorerUrlTemplate !== undefined) validateTraceExplorerUrlTemplate(traceExplorerUrlTemplate, `${path}.traceExplorerUrlTemplate`);
return {
prometheusOperator: booleanField(raw, "prometheusOperator", path),
traceExplorerUrlTemplate,
metricsEndpoint: observabilityMetricsEndpointConfig(raw.metricsEndpoint, `${path}.metricsEndpoint`),
workbench: observabilityWorkbenchConfig(raw.workbench, `${path}.workbench`),
webProbe: observabilityWebProbeConfig(raw.webProbe, `${path}.webProbe`),
recordingRules,
warningAlerts,
};
}
function observabilityWebProbeConfig(value: unknown, path: string): HwlabRuntimeObservabilityWebProbeSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const allowed = new Set(["sentinel", "sentinels", "monitor", "monitorRoot"]);
for (const key of Object.keys(raw)) {
if (!allowed.has(key)) throw new Error(`${path}.${key} is not allowed; observability.webProbe currently only owns sentinel/sentinels/monitor/monitorRoot`);
}
if (raw.sentinel !== undefined && raw.sentinels !== undefined) throw new Error(`${path} may declare sentinel or sentinels, not both`);
const sentinel = raw.sentinel === undefined ? undefined : webProbeSentinelConfig(raw.sentinel, `${path}.sentinel`);
const sentinels = raw.sentinels === undefined ? undefined : webProbeSentinelRegistryConfig(raw.sentinels, `${path}.sentinels`);
const monitor = raw.monitor === undefined ? undefined : webProbeMonitorConfig(raw.monitor, `${path}.monitor`);
const monitorRoot = raw.monitorRoot === undefined ? undefined : webProbeMonitorRootConfig(raw.monitorRoot, `${path}.monitorRoot`);
if (monitorRoot !== undefined) {
if (sentinels !== undefined && !sentinels.some((item) => item.id === monitorRoot.sentinelId)) {
throw new Error(`${path}.monitorRoot.sentinelId must reference one entry from ${path}.sentinels`);
}
if (sentinel !== undefined && monitorRoot.sentinelId !== "workbench-dsflash-go-tool-call-10x") {
throw new Error(`${path}.monitorRoot.sentinelId must be workbench-dsflash-go-tool-call-10x for legacy sentinel config`);
}
}
return {
...(sentinel === undefined ? {} : { sentinel }),
...(sentinels === undefined ? {} : { sentinels }),
...(monitor === undefined ? {} : { monitor }),
...(monitorRoot === undefined ? {} : { monitorRoot }),
};
}
function webProbeMonitorConfig(value: unknown, path: string): HwlabRuntimeWebProbeMonitorSpec {
const raw = asRecord(value, path);
const allowed = new Set(["configRef"]);
for (const key of Object.keys(raw)) if (!allowed.has(key)) throw new Error(`${path}.${key} is not allowed; monitor may only contain configRef`);
const configRef = stringField(raw, "configRef", path);
if (!configRef.startsWith("config/") || !configRef.includes("#")) throw new Error(`${path}.configRef must be a config YAML selector`);
return { configRef };
}
function webProbeMonitorRootConfig(value: unknown, path: string): HwlabRuntimeWebProbeMonitorRootSpec {
const raw = asRecord(value, path);
const allowed = new Set(["enabled", "sentinelId", "publicBaseUrl", "routePrefix", "caddyManagedBlockOwner"]);
for (const key of Object.keys(raw)) {
if (!allowed.has(key)) throw new Error(`${path}.${key} is not allowed; monitorRoot may only contain enabled/sentinelId/publicBaseUrl/routePrefix/caddyManagedBlockOwner`);
}
const sentinelId = stringField(raw, "sentinelId", path);
if (!/^[a-z0-9][a-z0-9-]{1,80}$/u.test(sentinelId)) throw new Error(`${path}.sentinelId must be a stable lowercase sentinel id`);
const publicBaseUrl = stringField(raw, "publicBaseUrl", path).replace(/\/+$/u, "");
try {
const parsed = new URL(publicBaseUrl);
if (parsed.protocol !== "https:") throw new Error("must use https");
if (parsed.pathname !== "/" || parsed.search.length > 0 || parsed.hash.length > 0) throw new Error("must point to the public origin root");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`${path}.publicBaseUrl must be an https origin root URL: ${message}`);
}
const routePrefix = stringField(raw, "routePrefix", path);
if (routePrefix !== "/") throw new Error(`${path}.routePrefix must be / for the monitor root switch`);
const caddyManagedBlockOwner = stringField(raw, "caddyManagedBlockOwner", path);
if (!/^[a-z0-9][a-z0-9-]{1,100}$/u.test(caddyManagedBlockOwner)) throw new Error(`${path}.caddyManagedBlockOwner must be a stable lowercase owner id`);
return {
enabled: booleanField(raw, "enabled", path),
sentinelId,
publicBaseUrl,
routePrefix,
caddyManagedBlockOwner,
};
}
function validateTraceExplorerUrlTemplate(template: string, path: string): void {
if (!template.includes("{trace_id}")) throw new Error(`${path} must include {trace_id}`);
if (template.startsWith("//")) throw new Error(`${path} must not be protocol-relative`);
const placeholders = Array.from(template.matchAll(/\{([A-Za-z0-9_:-]+)\}/gu), (match) => match[0]);
if (placeholders.length === 0 || placeholders.some((placeholder) => placeholder !== "{trace_id}")) {
throw new Error(`${path} may only use the {trace_id} placeholder`);
}
try {
const url = new URL(template.replaceAll("{trace_id}", "trc_template_probe"), "https://hwlab.local");
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("unsupported protocol");
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`${path} must be an http(s) or same-origin relative URL template: ${message}`);
}
}
function observabilityMetricsEndpointConfig(value: unknown, path: string): HwlabRuntimeObservabilityMetricsEndpointSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
const scheme = stringField(raw, "scheme", path);
if (scheme !== "http") throw new Error(`${path}.scheme must be http`);
const scrapeMode = stringField(raw, "scrapeMode", path);
if (scrapeMode !== "pod-loopback") throw new Error(`${path}.scrapeMode must be pod-loopback`);
const publicRawMetrics = stringField(raw, "publicRawMetrics", path);
if (publicRawMetrics !== "denied") throw new Error(`${path}.publicRawMetrics must be denied`);
return {
serviceName: stringField(raw, "serviceName", path),
containerName: stringField(raw, "containerName", path),
port: numberField(raw, "port", path),
scheme,
path: stringField(raw, "path", path),
scrapeMode,
publicRawMetrics,
};
}
function observabilityWorkbenchConfig(value: unknown, path: string): HwlabRuntimeObservabilityWorkbenchSpec | undefined {
if (value === undefined) return undefined;
const raw = asRecord(value, path);
return {
enabled: booleanField(raw, "enabled", path),
summaryPath: stringField(raw, "summaryPath", path),
lowSampleThreshold: numberField(raw, "lowSampleThreshold", path),
metricPrefixes: stringArrayField(raw, "metricPrefixes", path),
requiredSeries: stringArrayField(raw, "requiredSeries", path),
backendLabelDenylist: stringArrayField(raw, "backendLabelDenylist", path),
maxUnknownEventLines: nonNegativeIntegerField(raw, "maxUnknownEventLines", path),
};
}
function observabilityRecordingRulesConfig(value: unknown, path: string): HwlabRuntimeObservabilityRecordingRuleSpec[] {
if (value === undefined) return [];
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
return value.map((item, index) => {
const itemPath = `${path}[${index}]`;
const raw = asRecord(item, itemPath);
const quantile = positiveNumberField(raw, "quantile", itemPath);
if (quantile >= 1) throw new Error(`${itemPath}.quantile must be less than 1`);
return {
id: stringField(raw, "id", itemPath),
metric: stringField(raw, "metric", itemPath),
sourceMetric: stringField(raw, "sourceMetric", itemPath),
quantile,
window: stringField(raw, "window", itemPath),
minSamples: numberField(raw, "minSamples", itemPath),
groupBy: stringArrayField(raw, "groupBy", itemPath),
matchLabels: optionalStringRecord(raw.matchLabels, `${itemPath}.matchLabels`),
};
});
}
function observabilityWarningAlertsConfig(value: unknown, path: string): HwlabRuntimeObservabilityWarningAlertSpec[] {
if (value === undefined) return [];
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
return value.map((item, index) => {
const itemPath = `${path}[${index}]`;
const raw = asRecord(item, itemPath);
const severity = stringField(raw, "severity", itemPath);
if (severity !== "warning") throw new Error(`${itemPath}.severity must be warning`);
return {
id: stringField(raw, "id", itemPath),
ruleId: stringField(raw, "ruleId", itemPath),
severity,
thresholdSeconds: positiveNumberField(raw, "thresholdSeconds", itemPath),
minSamples: numberField(raw, "minSamples", itemPath),
for: stringField(raw, "for", itemPath),
matchLabels: optionalStringRecord(raw.matchLabels, `${itemPath}.matchLabels`),
};
});
}
function runtimeImageRewritesConfig(value: unknown, path: string): HwlabRuntimeImageRewriteSpec[] {
if (value === undefined) return [];
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
return value.map((item, index) => {
const raw = asRecord(item, `${path}[${index}]`);
return {
source: stringField(raw, "source", `${path}[${index}]`),
target: stringField(raw, "target", `${path}[${index}]`),
};
});
}
function runtimeImageBuildsConfig(value: unknown, path: string): HwlabRuntimeImageBuildSpec[] {
if (value === undefined) return [];
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
return value.map((item, index) => {
const itemPath = `${path}[${index}]`;
const raw = asRecord(item, itemPath);
const kind = stringField(raw, "kind", itemPath);
if (kind !== "moonbridge" && kind !== "opencode-git") throw new Error(`${itemPath}.kind must be moonbridge or opencode-git`);
if (kind === "opencode-git") {
return {
id: stringField(raw, "id", itemPath),
kind,
target: stringField(raw, "target", itemPath),
sourceImage: stringField(raw, "sourceImage", itemPath),
dockerNetworkMode: enumStringField(raw, "dockerNetworkMode", itemPath, ["default", "host"]),
};
}
return {
id: stringField(raw, "id", itemPath),
kind,
target: stringField(raw, "target", itemPath),
sourceRepo: stringField(raw, "sourceRepo", itemPath),
sourceRef: stringField(raw, "sourceRef", itemPath),
builderImage: stringField(raw, "builderImage", itemPath),
goProxy: stringField(raw, "goProxy", itemPath),
dockerNetworkMode: enumStringField(raw, "dockerNetworkMode", itemPath, ["default", "host"]),
};
});
}
function readHwlabNodeLaneConfig(): HwlabNodeLaneConfig {
const path = rootPath(HWLAB_NODE_LANE_CONFIG_PATH);
const raw = readFileSync(path, "utf8");
const parsed = asRecord(
materializeYamlComposition(Bun.YAML.parse(raw) as unknown, { label: HWLAB_NODE_LANE_CONFIG_PATH }).value,
HWLAB_NODE_LANE_CONFIG_PATH,
);
validateConfigEnvelope(parsed);
const defaultTarget = parseDefaultTarget(asRecord(parsed.defaults, `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`));
const requiredNoProxy = stringArrayField(parsed, "requiredNoProxy", HWLAB_NODE_LANE_CONFIG_PATH);
for (const required of ["hyueapi.com", ".hyueapi.com"]) {
if (!requiredNoProxy.includes(required)) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.requiredNoProxy must include ${required}`);
}
const nodes = Object.fromEntries(sortedRecordEntries(parsed.nodes, "nodes").map(([id, item]) => [id, nodeConfig(id, item)]));
const networkProfiles = Object.fromEntries(
sortedRecordEntries(parsed.networkProfiles, "networkProfiles").map(([id, item]) => [id, networkProfileConfig(id, item, requiredNoProxy)]),
);
const downloadProfiles = Object.fromEntries(
sortedRecordEntries(parsed.downloadProfiles, "downloadProfiles").map(([id, item]) => [id, downloadProfileConfig(id, item)]),
);
const laneEntries = sortedRecordEntries(parsed.lanes, "lanes");
const lanes = Object.fromEntries(laneEntries.map(([id, item]) => {
assertYamlDeclaredLaneId(id, `lanes.${id}`);
return [id, laneConfig(id, item)];
})) as Record<string, HwlabLaneConfig>;
const laneTargets: Partial<Record<string, Record<string, HwlabLaneConfig>>> = {};
for (const [id, item] of laneEntries) {
assertYamlDeclaredLaneId(id, `lanes.${id}`);
if (item.targets === undefined) continue;
laneTargets[id] = Object.fromEntries(sortedRecordEntries(item.targets, `lanes.${id}.targets`).map(([nodeId, target]) => [
nodeId,
laneTargetConfig(id, nodeId, item, target),
]));
}
for (const node of Object.values(nodes)) {
if (networkProfiles[node.networkProfileId] === undefined) throw new Error(`nodes.${node.id}.networkProfile references missing profile ${node.networkProfileId}`);
if (downloadProfiles[node.downloadProfileId] === undefined) throw new Error(`nodes.${node.id}.downloadProfile references missing profile ${node.downloadProfileId}`);
}
for (const lane of [...Object.values(lanes), ...Object.values(laneTargets).flatMap((targets) => Object.values(targets))]) {
if (nodes[lane.node] === undefined) throw new Error(`lanes.${lane.id}.node references missing node ${lane.node}`);
}
if (nodes[defaultTarget.node] === undefined) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.defaults.node references missing node ${defaultTarget.node}`);
const defaultLane = laneTargets[defaultTarget.lane]?.[defaultTarget.node] ?? lanes[defaultTarget.lane];
if (defaultLane === undefined || defaultLane.node !== defaultTarget.node) {
throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.defaults must reference a declared lane target`);
}
for (const lane of Object.values(lanes)) {
if (lane.activeTarget === undefined) continue;
if (nodes[lane.activeTarget] === undefined) {
throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.lanes.${lane.id}.activeTarget references missing node ${lane.activeTarget}`);
}
const target = laneTargets[lane.id]?.[lane.activeTarget];
if (target === undefined && lane.node !== lane.activeTarget) {
throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.lanes.${lane.id}.activeTarget must reference lanes.${lane.id}.targets.${lane.activeTarget}`);
}
}
return { defaultTarget, requiredNoProxy, nodes, lanes, laneTargets, networkProfiles, downloadProfiles };
}
function validateConfigEnvelope(parsed: Record<string, unknown>): void {
if (parsed.version !== 1) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.version must be 1`);
if (parsed.kind !== "HwlabNodeLaneConfig") throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.kind must be HwlabNodeLaneConfig`);
const metadata = asRecord(parsed.metadata, `${HWLAB_NODE_LANE_CONFIG_PATH}.metadata`);
if (stringField(metadata, "name", `${HWLAB_NODE_LANE_CONFIG_PATH}.metadata`) !== "hwlab-node-lanes") {
throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.metadata.name must be hwlab-node-lanes`);
}
}
function parseDefaultTarget(raw: Record<string, unknown>): { readonly node: string; readonly lane: HwlabRuntimeLane } {
const lane = stringField(raw, "lane", `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`);
assertYamlDeclaredLaneId(lane, `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults.lane`);
return {
node: stringField(raw, "node", `${HWLAB_NODE_LANE_CONFIG_PATH}.defaults`),
lane,
};
}
const HWLAB_NODE_LANE_CONFIG = readHwlabNodeLaneConfig();
function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
const node = HWLAB_NODE_LANE_CONFIG.nodes[config.node];
const networkProfile = HWLAB_NODE_LANE_CONFIG.networkProfiles[node.networkProfileId];
const downloadProfile = HWLAB_NODE_LANE_CONFIG.downloadProfiles[node.downloadProfileId];
if (config.publicExposure?.mode === "node-port-http") {
if (config.environment === undefined) throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.lanes.${config.id}.environment is required for node-port-http public exposure`);
if (config.public.webUrl !== config.publicExposure.publicBaseUrl || config.public.apiUrl !== config.publicExposure.publicBaseUrl) {
throw new Error(`${HWLAB_NODE_LANE_CONFIG_PATH}.lanes.${config.id}.public webUrl/apiUrl must match publicExposure.publicBaseUrl for node-port-http`);
}
}
return {
lane: config.id,
...(config.environment === undefined ? {} : { environment: config.environment }),
nodeId: node.id,
nodeRoute: node.route,
nodeKubeRoute: node.kubeRoute,
gitopsRoot: node.gitopsRoot,
minor: config.minor,
version: config.version,
sourceBranch: config.sourceBranch,
...(config.sourceAuthority === undefined ? {} : { sourceAuthority: config.sourceAuthority }),
...(config.sourceSnapshot === undefined ? {} : { sourceSnapshot: config.sourceSnapshot }),
workspace: config.workspace,
cicdRepo: config.cicdRepo,
cicdRepoLock: config.cicdRepoLock,
app: config.app,
pipeline: config.pipeline,
pipelineRunPrefix: config.pipelineRunPrefix,
serviceAccountName: config.serviceAccountName,
controlPlaneFieldManager: config.controlPlaneFieldManager,
...(config.pipelineProvenance === undefined ? {} : { pipelineProvenance: config.pipelineProvenance }),
gitUrl: config.git.url,
gitReadUrl: config.git.readUrl,
gitWriteUrl: config.git.writeUrl,
argoRepoUrl: config.argo.repoURL ?? config.git.readUrl,
gitopsBranch: config.gitopsBranch,
catalogPath: config.catalogPath,
runtimePath: config.runtime.path,
runtimeNamespace: config.runtime.namespace,
runtimeRenderDir: config.runtime.renderDir,
tektonDir: config.tektonDir,
argoApplicationFile: config.argoApplicationFile,
registryPrefix: config.registryPrefix,
baseImage: config.baseImage,
...(config.baseImageSource === undefined ? {} : { baseImageSource: config.baseImageSource }),
serviceIds: config.serviceIds,
publicWebUrl: config.public.webUrl,
publicApiUrl: config.public.apiUrl,
stepEnv: config.stepEnv,
...(config.buildkit === undefined ? {} : { buildkit: config.buildkit }),
...(config.bootstrapAdmin === undefined ? {} : { bootstrapAdmin: config.bootstrapAdmin }),
...(config.codeAgentProvider === undefined ? {} : { codeAgentProvider: config.codeAgentProvider }),
...(config.codeAgentRuntime === undefined ? {} : { codeAgentRuntime: config.codeAgentRuntime }),
...(config.sourceWorkspace === undefined ? {} : { sourceWorkspace: config.sourceWorkspace }),
...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }),
...(config.runtimeStore === undefined ? {} : { runtimeStore: config.runtimeStore }),
...(config.webProbe === undefined ? {} : { webProbe: config.webProbe }),
publicExposure: config.publicExposure,
observability: config.observability,
runtimeImageRewrites: config.runtimeImageRewrites,
runtimeImageBuilds: config.runtimeImageBuilds,
networkProfileId: networkProfile.id,
downloadProfileId: downloadProfile.id,
networkProfile,
downloadProfile,
};
}
const RUNTIME_LANE_SPECS = Object.fromEntries(
Object.values(HWLAB_NODE_LANE_CONFIG.lanes).map((config) => [config.id, buildRuntimeLaneSpec(config)]),
) as Record<string, HwlabRuntimeLaneSpec>;
export function isHwlabRuntimeLane(value: string): value is HwlabRuntimeLane {
return Object.prototype.hasOwnProperty.call(RUNTIME_LANE_SPECS, value);
}
export function hwlabRuntimeLaneSpec(lane: HwlabRuntimeLane): HwlabRuntimeLaneSpec {
return RUNTIME_LANE_SPECS[lane];
}
export function hwlabRuntimeLaneSpecForNode(lane: HwlabRuntimeLane, nodeId: string): HwlabRuntimeLaneSpec {
const targetSpec = HWLAB_NODE_LANE_CONFIG.laneTargets[lane]?.[nodeId];
if (targetSpec !== undefined) return buildRuntimeLaneSpec(targetSpec);
const defaultSpec = RUNTIME_LANE_SPECS[lane];
if (defaultSpec.nodeId === nodeId) return defaultSpec;
const knownNodes = unique([defaultSpec.nodeId, ...Object.keys(HWLAB_NODE_LANE_CONFIG.laneTargets[lane] ?? {})]).join(", ");
throw new Error(`lane ${lane} has no target for node ${nodeId}; known nodes: ${knownNodes}`);
}
export function hwlabRuntimeLaneIds(): HwlabRuntimeLane[] {
return Object.keys(RUNTIME_LANE_SPECS) as HwlabRuntimeLane[];
}
export function hwlabRuntimeNodeIds(): string[] {
return Object.keys(HWLAB_NODE_LANE_CONFIG.nodes);
}
export function hwlabDefaultRuntimeTarget(): { readonly node: string; readonly lane: HwlabRuntimeLane } {
return { ...HWLAB_NODE_LANE_CONFIG.defaultTarget };
}
export function hwlabRuntimeActiveTarget(lane: HwlabRuntimeLane): { readonly node: string; readonly lane: HwlabRuntimeLane } {
const config = HWLAB_NODE_LANE_CONFIG.lanes[lane];
return {
node: config.activeTarget ?? config.node,
lane,
};
}
export function hwlabRuntimeActiveLaneSpec(lane: HwlabRuntimeLane): HwlabRuntimeLaneSpec {
const target = hwlabRuntimeActiveTarget(lane);
return hwlabRuntimeLaneSpecForNode(target.lane, target.node);
}
export function hwlabRuntimeLaneConfigPath(): string {
return HWLAB_NODE_LANE_CONFIG_PATH;
}
export function hwlabRequiredNoProxyEntries(): string[] {
return [...HWLAB_NODE_LANE_CONFIG.requiredNoProxy];
}