// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. plan module for scripts/src/hwlab-node-impl.ts. // Moved mechanically from scripts/src/hwlab-node-impl.ts:440-567 for #903. // SPEC: PJ2026-01060505 Workbench Performance draft-2026-06-17-p0. // SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel. // Responsibility: YAML-first node/lane operations, including Workbench observability control commands. import { createHash, randomBytes } from "node:crypto"; import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { repoRoot, rootPath, type Config } from "../config"; import { runCommand, type CommandResult } from "../command"; import { startJob } from "../jobs"; import { classifySshTcpPoolFailure } from "../ssh"; import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane"; import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes"; import { nodeWebProbeScriptRunnerSource } from "../hwlab-node-web-probe-runner-source"; import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source"; import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source"; import { nodeWebObserveCollectViewNodeScript, parseNodeWebProbeObserveCollectView, type NodeWebProbeObserveCollectView } from "../hwlab-node-web-observe-collect"; import { withWebObserveCollectRendered, withWebObserveCommandRendered, withWebObserveStatusRendered } from "../hwlab-node-web-observe-render"; import { buildWebObserveWrapperForObserveOptions, webObserveWrapperStateDirFromStatus } from "../hwlab-node-web-observe-wrapper"; import { renderWebObserveWrapperContract } from "../hwlab-node-web-observe-wrapper-render"; import { runWebProbeSentinelCommand, type WebProbeSentinelOptions } from "../hwlab-node-web-sentinel-cicd"; import { hwlabNodeHelp, hwlabNodeObservabilityHelp, hwlabNodeWebProbeHelp } from "../hwlab-node-help"; import { compactWebProbeResult, compactWebProbeScriptResult } from "../hwlab-node-web-probe-summary"; import { nodeObservabilityRecordingRuleExpression, nodeObservabilityRecordingRuleSummaries, nodeObservabilityWarningAlertExpression, nodeObservabilityWarningAlertSummaries } from "../hwlab-node-observability-promql"; import { runDelegatedHwlabNodeCommand, type DelegatedNodeDomain } from "../hwlab-node-transport"; import type { RenderedCliResult } from "../output"; import { NODE_RUNTIME_CICD_WAIT_WARNING_SECONDS } from "./entry"; import { publicExposureSummary } from "./public-exposure"; import { assertNodeId, positiveIntegerOption, requiredOption } from "./utils"; import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes"; export function parseNodeScopedDelegatedOptions(domain: DelegatedNodeDomain, args: string[]): { domain: DelegatedNodeDomain; action: string; runtimeImageAction: string | null; node: string; lane: HwlabRuntimeLane; confirm: boolean; dryRun: boolean; wait: boolean; rerun: boolean; allowLiveDbRead: boolean; timeoutSeconds: number; originalArgs: string[]; spec: HwlabRuntimeLaneSpec; } { const [actionRaw] = args; if (typeof actionRaw !== "string" || actionRaw.startsWith("--")) throw new Error(`${domain} usage: ${domain} ACTION --node NODE --lane vNN [--dry-run|--confirm]`); const runtimeImageAction = actionRaw === "runtime-image" && typeof args[1] === "string" && !args[1].startsWith("--") ? args[1] : null; const node = requiredOption(args, "--node"); assertNodeId(node); const laneRaw = requiredOption(args, "--lane"); if (!isHwlabRuntimeLane(laneRaw)) throw new Error(`--lane must be one of v02, v03; got ${laneRaw}`); const spec = hwlabRuntimeLaneSpecForNode(laneRaw, node); const confirm = args.includes("--confirm"); const dryRun = args.includes("--dry-run"); if (confirm && dryRun) throw new Error(`${domain} accepts only one of --confirm or --dry-run`); return { domain, action: actionRaw, runtimeImageAction, node, lane: laneRaw, confirm, dryRun, wait: args.includes("--wait"), rerun: args.includes("--rerun"), allowLiveDbRead: args.includes("--allow-live-db-read"), timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", NODE_RUNTIME_CICD_WAIT_WARNING_SECONDS, 3600), originalArgs: [...args], spec, }; } export function nodeRuntimeLocalPostgresExpectedAbsent(spec: HwlabRuntimeLaneSpec): boolean { return spec.runtimeStore?.postgres?.mode === "platform-service"; } export function nodeRuntimeExpected(spec: HwlabRuntimeLaneSpec): Record { const activeExternalPostgres = hwlabRuntimeActiveExternalPostgres(spec); return { configPath: hwlabRuntimeLaneConfigPath(), node: spec.nodeId, nodeRoute: spec.nodeRoute, nodeKubeRoute: spec.nodeKubeRoute, lane: spec.lane, sourceBranch: spec.sourceBranch, workspace: spec.workspace, cicdRepo: spec.cicdRepo, git: { sourceUrl: spec.gitUrl, readUrl: spec.gitReadUrl, writeUrl: spec.gitWriteUrl, }, argo: { repoURL: spec.argoRepoUrl, }, gitopsBranch: spec.gitopsBranch, catalogPath: spec.catalogPath, runtimePath: spec.runtimePath, runtimeNamespace: spec.runtimeNamespace, renderDir: spec.runtimeRenderDir, pipeline: spec.pipeline, pipelineRunPrefix: spec.pipelineRunPrefix, serviceAccount: spec.serviceAccountName, argoApplication: spec.app, registryPrefix: spec.registryPrefix, baseImage: { image: spec.baseImage, sourceImage: spec.baseImageSource ?? null, }, serviceIds: spec.serviceIds, buildkit: spec.buildkit === undefined ? null : { sidecarImage: spec.buildkit.sidecarImage, sourceImage: spec.buildkit.sourceImage, }, dockerBuildProxy: { http: spec.networkProfile.dockerBuildProxy.http, https: spec.networkProfile.dockerBuildProxy.https, all: spec.networkProfile.dockerBuildProxy.all, noProxy: spec.networkProfile.dockerBuildProxy.noProxy, }, stepEnv: spec.stepEnv, public: { webUrl: spec.publicWebUrl, apiUrl: spec.publicApiUrl, }, webProbe: spec.webProbe === undefined ? null : { browserProxyMode: spec.webProbe.browserProxyMode ?? null, defaultOrigin: spec.webProbe.defaultOrigin ?? null, }, bootstrapAdmin: spec.bootstrapAdmin === undefined ? null : { username: spec.bootstrapAdmin.username, displayName: spec.bootstrapAdmin.displayName, passwordSourceRef: spec.bootstrapAdmin.passwordSourceRef, passwordSourceKey: spec.bootstrapAdmin.passwordSourceKey, passwordHashTransform: spec.bootstrapAdmin.passwordHashTransform, secretName: spec.bootstrapAdmin.secretName, secretKey: spec.bootstrapAdmin.secretKey, rolloutDeployment: spec.bootstrapAdmin.rolloutDeployment, valuesPrinted: false, }, codeAgentRuntime: spec.codeAgentRuntime === undefined ? null : { enabled: spec.codeAgentRuntime.enabled, adapter: spec.codeAgentRuntime.adapter, managerUrl: spec.codeAgentRuntime.managerUrl, apiKeySecretName: spec.codeAgentRuntime.apiKeySecretName, apiKeySecretKey: spec.codeAgentRuntime.apiKeySecretKey, runnerNamespace: spec.codeAgentRuntime.runnerNamespace, secretNamespace: spec.codeAgentRuntime.secretNamespace, repoUrlFrom: spec.codeAgentRuntime.repoUrlFrom, repoUrl: spec.gitReadUrl, providerIdFrom: spec.codeAgentRuntime.providerIdFrom, providerId: spec.nodeId, defaultProviderProfile: spec.codeAgentRuntime.defaultProviderProfile, codexStdioSupervisor: spec.codeAgentRuntime.codexStdioSupervisor, kafkaShadowProducer: spec.codeAgentRuntime.kafkaShadowProducer ?? null, valuesPrinted: false, }, sourceWorkspace: spec.sourceWorkspace === undefined ? null : { requiredCommands: spec.sourceWorkspace.requiredCommands, requiredFiles: spec.sourceWorkspace.requiredFiles, install: spec.sourceWorkspace.install, }, publicExposure: spec.publicExposure === null ? null : publicExposureSummary(spec.publicExposure), runtimeStore: spec.runtimeStore ?? null, downloadProfile: { id: spec.downloadProfileId, git: spec.downloadProfile.git, npm: spec.downloadProfile.npm, }, observability: spec.observability, runtimeImageRewrites: spec.runtimeImageRewrites, runtimeImageBuilds: spec.runtimeImageBuilds, externalPostgres: spec.externalPostgres === undefined ? null : { active: activeExternalPostgres !== undefined, provider: spec.externalPostgres.provider, configRef: spec.externalPostgres.configRef, serviceName: spec.externalPostgres.serviceName, endpointAddress: spec.externalPostgres.endpointAddress, port: spec.externalPostgres.port, runtimeAccess: spec.externalPostgres.runtimeAccess ?? null, sslmode: spec.externalPostgres.sslmode, database: spec.externalPostgres.database, cloudApi: { secretName: spec.externalPostgres.cloudApi.secretName, secretKey: spec.externalPostgres.cloudApi.secretKey, sourceRef: spec.externalPostgres.cloudApi.sourceRef, envKey: spec.externalPostgres.cloudApi.envKey, role: spec.externalPostgres.cloudApi.role, }, openfga: { secretName: spec.externalPostgres.openfga.secretName, secretKey: spec.externalPostgres.openfga.secretKey, sourceRef: spec.externalPostgres.openfga.sourceRef, envKey: spec.externalPostgres.openfga.envKey, authnKey: spec.externalPostgres.openfga.authnKey ?? null, role: spec.externalPostgres.openfga.role, schema: spec.externalPostgres.openfga.schema ?? null, }, valuesPrinted: false, }, localPostgres: { shouldRender: spec.runtimeStore?.postgres?.mode !== "platform-service", expectedAbsent: spec.runtimeStore?.postgres?.mode === "platform-service", }, }; }