From 984fa9e8356f153614b12d2448f1ddac32f30b90 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 10 Jul 2026 08:12:04 +0200 Subject: [PATCH] feat: add composable HWLAB realtime capabilities --- config/hwlab-node-lanes.yaml | 11 ++++- scripts/src/hwlab-node-lanes.ts | 31 +++++++++++-- scripts/src/hwlab-node/render.ts | 50 ++++++++++++++------ scripts/src/hwlab-node/web-probe.ts | 71 +++++++++++++++++++++++++++-- 4 files changed, 139 insertions(+), 24 deletions(-) diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index c7e6fad1..f8e0ba62 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -200,13 +200,20 @@ lanes: codexStdioSupervisor: repo-owned kafkaEventBridge: enabled: true - mode: agentrun-event-to-hwlab-event + features: + directPublish: true + liveKafkaSse: true + transactionalProjector: false + projectionOutboxRelay: false + projectionRealtime: false configRef: config/platform-infra/kafka.yaml#clients.hwlab-v03-cloud-api bootstrapServers: platform-infra-kafka-kafka-bootstrap.platform-infra.svc.cluster.local:9092 agentRunEventTopic: agentrun.event.v1 hwlabEventTopic: hwlab.event.v1 clientId: hwlab-v03-cloud-api - consumerGroupId: hwlab-v03-agentrun-event-bridge + directPublishConsumerGroupId: hwlab-v03-agentrun-event-direct-publish + transactionalProjectorConsumerGroupId: hwlab-v03-agentrun-event-projector + hwlabEventConsumerGroupId: hwlab-v03-workbench-live-sse publicExposure: extends: templates.hwlabV03.publicExposurePk01 frpc: diff --git a/scripts/src/hwlab-node-lanes.ts b/scripts/src/hwlab-node-lanes.ts index 643860bc..2fc1e707 100644 --- a/scripts/src/hwlab-node-lanes.ts +++ b/scripts/src/hwlab-node-lanes.ts @@ -418,13 +418,21 @@ export interface HwlabRuntimeKafkaShadowProducerSpec { export interface HwlabRuntimeKafkaEventBridgeSpec { readonly enabled: boolean; - readonly mode: "agentrun-event-to-hwlab-event"; + readonly features: { + readonly directPublish: boolean; + readonly liveKafkaSse: boolean; + readonly transactionalProjector: boolean; + readonly projectionOutboxRelay: boolean; + readonly projectionRealtime: boolean; + }; readonly configRef: string; readonly bootstrapServers: string; readonly agentRunEventTopic: string; readonly hwlabEventTopic: string; readonly clientId: string; - readonly consumerGroupId: string; + readonly directPublishConsumerGroupId: string; + readonly transactionalProjectorConsumerGroupId: string; + readonly hwlabEventConsumerGroupId: string; } export interface HwlabRuntimeCodeAgentRuntimeSpec { @@ -1024,15 +1032,30 @@ function codeAgentKafkaShadowProducerConfig(value: unknown, path: string): Hwlab function codeAgentKafkaEventBridgeConfig(value: unknown, path: string): HwlabRuntimeKafkaEventBridgeSpec { const raw = asRecord(value, path); + const features = asRecord(raw.features, `${path}.features`); + const directPublishConsumerGroupId = stringField(raw, "directPublishConsumerGroupId", path); + const transactionalProjectorConsumerGroupId = stringField(raw, "transactionalProjectorConsumerGroupId", path); + const hwlabEventConsumerGroupId = stringField(raw, "hwlabEventConsumerGroupId", path); + if (new Set([directPublishConsumerGroupId, transactionalProjectorConsumerGroupId, hwlabEventConsumerGroupId]).size !== 3) { + throw new Error(`${path} consumer group ids must be distinct so enabled capabilities receive independent event streams`); + } return { enabled: booleanField(raw, "enabled", path), - mode: enumStringField(raw, "mode", path, ["agentrun-event-to-hwlab-event"]), + features: { + directPublish: booleanField(features, "directPublish", `${path}.features`), + liveKafkaSse: booleanField(features, "liveKafkaSse", `${path}.features`), + transactionalProjector: booleanField(features, "transactionalProjector", `${path}.features`), + projectionOutboxRelay: booleanField(features, "projectionOutboxRelay", `${path}.features`), + projectionRealtime: booleanField(features, "projectionRealtime", `${path}.features`), + }, configRef: stringField(raw, "configRef", path), bootstrapServers: stringField(raw, "bootstrapServers", path), agentRunEventTopic: stringField(raw, "agentRunEventTopic", path), hwlabEventTopic: stringField(raw, "hwlabEventTopic", path), clientId: stringField(raw, "clientId", path), - consumerGroupId: stringField(raw, "consumerGroupId", path), + directPublishConsumerGroupId, + transactionalProjectorConsumerGroupId, + hwlabEventConsumerGroupId, }; } diff --git a/scripts/src/hwlab-node/render.ts b/scripts/src/hwlab-node/render.ts index 744c591e..3405bfae 100644 --- a/scripts/src/hwlab-node/render.ts +++ b/scripts/src/hwlab-node/render.ts @@ -1751,10 +1751,15 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { "function isEnvReuseContainer(container) { return envValue(container, 'HWLAB_RUNTIME_MODE') === 'env-reuse-git-mirror-checkout' || envValue(container, 'HWLAB_BOOT_SH') !== undefined || envValue(container, 'HWLAB_BOOT_COMMIT') !== undefined; }", "function workloadName(item) { return item && item.metadata && item.metadata.labels && item.metadata.labels['app.kubernetes.io/name'] ? String(item.metadata.labels['app.kubernetes.io/name']) : String(item && item.metadata && item.metadata.name || ''); }", "function expectedPublicEndpoint(item) { return workloadName(item) === 'hwlab-cloud-web' ? overlay.publicWebUrl : overlay.publicApiUrl; }", - "function publicExposureCloudWebEnvEntries() {", + "function cloudWebRuntimeEnvEntries() {", " const exposure = overlay.publicExposure;", - " if (!exposure || !Array.isArray(exposure.extraProxies)) return [];", - " return exposure.extraProxies.filter((proxy) => proxy && proxy.cloudWebEnvName && proxy.publicBaseUrl).map((proxy) => ({ name: String(proxy.cloudWebEnvName), value: String(proxy.publicBaseUrl) }));", + " const entries = !exposure || !Array.isArray(exposure.extraProxies) ? [] : exposure.extraProxies.filter((proxy) => proxy && proxy.cloudWebEnvName && proxy.publicBaseUrl).map((proxy) => ({ name: String(proxy.cloudWebEnvName), value: String(proxy.publicBaseUrl) }));", + " const kafka = overlay.codeAgentRuntime && overlay.codeAgentRuntime.kafkaEventBridge;", + " if (kafka) {", + " entries.push({ name: 'HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED', value: String(kafka.enabled && kafka.features.liveKafkaSse) });", + " entries.push({ name: 'HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED', value: String(kafka.enabled && kafka.features.projectionRealtime) });", + " }", + " return entries;", "}", "function startupProbeFrom(probe) {", " const next = JSON.parse(JSON.stringify(probe));", @@ -1821,7 +1826,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { " let cloudWebRuntimeChanged = false;", " const pg = overlay.externalPostgres;", " const codeAgentRuntime = overlay.codeAgentRuntime;", - " const cloudWebEnvEntries = publicExposureCloudWebEnvEntries();", + " const cloudWebEnvEntries = cloudWebRuntimeEnvEntries();", " for (const group of ['containers', 'initContainers']) {", " for (const container of Array.isArray(podSpec[group]) ? podSpec[group] : []) {", " if (!isObject(container)) continue;", @@ -1851,12 +1856,19 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { " if (codeAgentRuntime.kafkaEventBridge) {", " const kafka = codeAgentRuntime.kafkaEventBridge;", " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_ENABLED', String(kafka.enabled)) || codeAgentRuntimeChanged;", - " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED', String(kafka.enabled)) || codeAgentRuntimeChanged;", + " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED', String(kafka.enabled && (kafka.features.directPublish || kafka.features.transactionalProjector))) || codeAgentRuntimeChanged;", " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_BOOTSTRAP_SERVERS', String(kafka.bootstrapServers)) || codeAgentRuntimeChanged;", " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC', String(kafka.agentRunEventTopic)) || codeAgentRuntimeChanged;", " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_EVENT_TOPIC', String(kafka.hwlabEventTopic)) || codeAgentRuntimeChanged;", " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_CLIENT_ID', String(kafka.clientId)) || codeAgentRuntimeChanged;", - " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID', String(kafka.consumerGroupId)) || codeAgentRuntimeChanged;", + " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID', String(kafka.directPublishConsumerGroupId)) || codeAgentRuntimeChanged;", + " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_PROJECTOR_GROUP_ID', String(kafka.transactionalProjectorConsumerGroupId)) || codeAgentRuntimeChanged;", + " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID', String(kafka.hwlabEventConsumerGroupId)) || codeAgentRuntimeChanged;", + " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED', String(kafka.enabled && kafka.features.directPublish)) || codeAgentRuntimeChanged;", + " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED', String(kafka.enabled && kafka.features.liveKafkaSse)) || codeAgentRuntimeChanged;", + " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED', String(kafka.enabled && kafka.features.transactionalProjector)) || codeAgentRuntimeChanged;", + " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED', String(kafka.enabled && kafka.features.projectionOutboxRelay)) || codeAgentRuntimeChanged;", + " codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED', String(kafka.enabled && kafka.features.projectionRealtime)) || codeAgentRuntimeChanged;", " }", " }", " }", @@ -2178,10 +2190,15 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { "function isEnvReuseContainer(container) { return envValue(container, 'HWLAB_RUNTIME_MODE') === 'env-reuse-git-mirror-checkout' || envValue(container, 'HWLAB_BOOT_SH') !== undefined || envValue(container, 'HWLAB_BOOT_COMMIT') !== undefined; }", "function workloadName(item) { return item && item.metadata && item.metadata.labels && item.metadata.labels['app.kubernetes.io/name'] ? String(item.metadata.labels['app.kubernetes.io/name']) : String(item && item.metadata && item.metadata.name || ''); }", "function expectedPublicEndpoint(item) { return workloadName(item) === 'hwlab-cloud-web' ? overlay.publicWebUrl : overlay.publicApiUrl; }", - "function publicExposureCloudWebEnvEntries() {", + "function cloudWebRuntimeEnvEntries() {", " const exposure = overlay.publicExposure;", - " if (!exposure || !Array.isArray(exposure.extraProxies)) return [];", - " return exposure.extraProxies.filter((proxy) => proxy && proxy.cloudWebEnvName && proxy.publicBaseUrl).map((proxy) => ({ name: String(proxy.cloudWebEnvName), value: String(proxy.publicBaseUrl) }));", + " const entries = !exposure || !Array.isArray(exposure.extraProxies) ? [] : exposure.extraProxies.filter((proxy) => proxy && proxy.cloudWebEnvName && proxy.publicBaseUrl).map((proxy) => ({ name: String(proxy.cloudWebEnvName), value: String(proxy.publicBaseUrl) }));", + " const kafka = overlay.codeAgentRuntime && overlay.codeAgentRuntime.kafkaEventBridge;", + " if (kafka) {", + " entries.push({ name: 'HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED', value: String(kafka.enabled && kafka.features.liveKafkaSse) });", + " entries.push({ name: 'HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED', value: String(kafka.enabled && kafka.features.projectionRealtime) });", + " }", + " return entries;", "}", "function workloadRef(item, file, container) { return { file, kind: item && item.kind, name: item && item.metadata && item.metadata.name, container: container && container.name }; }", "function workloadChecks() {", @@ -2196,7 +2213,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { " const wrongCloudWebRuntimeEnvs = [];", " const rewriteSources = new Set((overlay.runtimeImageRewrites || []).map((item) => item && item.source).filter(Boolean));", " const codeAgentRuntime = overlay.codeAgentRuntime;", - " const cloudWebEnvEntries = publicExposureCloudWebEnvEntries();", + " const cloudWebEnvEntries = cloudWebRuntimeEnvEntries();", " function checkCloudWebRuntimeValue(item, file, container, envName, expected) {", " const value = envValue(container, envName);", " if (value !== expected) wrongCloudWebRuntimeEnvs.push({ ...workloadRef(item, file, container), envName, expected, value: value ?? null });", @@ -2250,12 +2267,19 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { " if (codeAgentRuntime.kafkaEventBridge) {", " const kafka = codeAgentRuntime.kafkaEventBridge;", " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_ENABLED', String(kafka.enabled));", - " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED', String(kafka.enabled));", + " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED', String(kafka.enabled && (kafka.features.directPublish || kafka.features.transactionalProjector)));", " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_BOOTSTRAP_SERVERS', String(kafka.bootstrapServers));", " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC', String(kafka.agentRunEventTopic));", " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_EVENT_TOPIC', String(kafka.hwlabEventTopic));", " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_CLIENT_ID', String(kafka.clientId));", - " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID', String(kafka.consumerGroupId));", + " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID', String(kafka.directPublishConsumerGroupId));", + " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_PROJECTOR_GROUP_ID', String(kafka.transactionalProjectorConsumerGroupId));", + " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID', String(kafka.hwlabEventConsumerGroupId));", + " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED', String(kafka.enabled && kafka.features.directPublish));", + " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED', String(kafka.enabled && kafka.features.liveKafkaSse));", + " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED', String(kafka.enabled && kafka.features.transactionalProjector));", + " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED', String(kafka.enabled && kafka.features.projectionOutboxRelay));", + " checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED', String(kafka.enabled && kafka.features.projectionRealtime));", " }", " }", " }", @@ -2289,7 +2313,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] { "if (workloadCheck.wrongCodeAgentRuntimeEnvs.length > 0) fail('code-agent-runtime-env-mismatch', { refs: workloadCheck.wrongCodeAgentRuntimeEnvs.slice(0, 12), count: workloadCheck.wrongCodeAgentRuntimeEnvs.length });", "if (overlay.codeAgentRuntime && overlay.codeAgentRuntime.enabled) checks.push('code-agent-runtime-env');", "if (workloadCheck.wrongCloudWebRuntimeEnvs.length > 0) fail('cloud-web-runtime-env-mismatch', { refs: workloadCheck.wrongCloudWebRuntimeEnvs.slice(0, 12), count: workloadCheck.wrongCloudWebRuntimeEnvs.length });", - "if (publicExposureCloudWebEnvEntries().length > 0) checks.push('cloud-web-runtime-env');", + "if (cloudWebRuntimeEnvEntries().length > 0) checks.push('cloud-web-runtime-env');", "const pg = overlay.externalPostgres;", "if (pg && pg.serviceName) {", " const access = pg.runtimeAccess || { endpointAddress: pg.endpointAddress, port: pg.port };", diff --git a/scripts/src/hwlab-node/web-probe.ts b/scripts/src/hwlab-node/web-probe.ts index 9172b9cb..1315a6c2 100644 --- a/scripts/src/hwlab-node/web-probe.ts +++ b/scripts/src/hwlab-node/web-probe.ts @@ -1478,13 +1478,15 @@ export function nodeRuntimeCodeAgentRuntimeStatus(spec: HwlabRuntimeLaneSpec, na const managerDeploymentReady = managerDeployment.exitCode === 0 && (managerReadyReplicas ?? 0) >= Math.max(1, managerDesiredReplicas ?? 1); const apiKey = secretKeyStatus(spec, runtime.apiKeySecretName, runtime.apiKeySecretKey); const cloudApiEnv = nodeRuntimeCodeAgentCloudApiEnvStatus(spec, runtime); + const cloudWebEnv = nodeRuntimeCodeAgentCloudWebEnvStatus(spec, runtime); const ready = runnerNamespace.exitCode === 0 && secretNamespace.exitCode === 0 && managerNamespace.exitCode === 0 && managerService.exitCode === 0 && managerDeploymentReady && apiKey.present === true - && cloudApiEnv.ready === true; + && cloudApiEnv.ready === true + && cloudWebEnv.ready === true; const degradedReason = ready ? undefined : runnerNamespace.exitCode !== 0 @@ -1499,7 +1501,9 @@ export function nodeRuntimeCodeAgentRuntimeStatus(spec: HwlabRuntimeLaneSpec, na ? "agentrun-manager-deployment-not-ready" : apiKey.present !== true ? "agentrun-api-key-secret-missing" - : "hwlab-cloud-api-code-agent-env-mismatch"; + : cloudApiEnv.ready !== true + ? "hwlab-cloud-api-code-agent-env-mismatch" + : "hwlab-cloud-web-realtime-env-mismatch"; return { required: true, enabled: true, @@ -1530,6 +1534,7 @@ export function nodeRuntimeCodeAgentRuntimeStatus(spec: HwlabRuntimeLaneSpec, na }, apiKey, cloudApiEnv, + cloudWebEnv, repoUrlFrom: runtime.repoUrlFrom, providerIdFrom: runtime.providerIdFrom, valuesPrinted: false, @@ -1620,19 +1625,75 @@ function nodeRuntimeCodeAgentCloudApiEnvStatus(spec: HwlabRuntimeLaneSpec, runti const kafkaEventBridge = runtime.kafkaEventBridge; if (kafkaEventBridge !== undefined) { expectValue("HWLAB_KAFKA_ENABLED", String(kafkaEventBridge.enabled)); - expectValue("HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED", String(kafkaEventBridge.enabled)); + expectValue("HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED", String(kafkaEventBridge.enabled && (kafkaEventBridge.features.directPublish || kafkaEventBridge.features.transactionalProjector))); expectValue("HWLAB_KAFKA_BOOTSTRAP_SERVERS", kafkaEventBridge.bootstrapServers); expectValue("HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC", kafkaEventBridge.agentRunEventTopic); expectValue("HWLAB_KAFKA_EVENT_TOPIC", kafkaEventBridge.hwlabEventTopic); expectValue("HWLAB_KAFKA_CLIENT_ID", kafkaEventBridge.clientId); - expectValue("HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID", kafkaEventBridge.consumerGroupId); + expectValue("HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID", kafkaEventBridge.directPublishConsumerGroupId); + expectValue("HWLAB_KAFKA_PROJECTOR_GROUP_ID", kafkaEventBridge.transactionalProjectorConsumerGroupId); + expectValue("HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID", kafkaEventBridge.hwlabEventConsumerGroupId); + expectValue("HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.directPublish)); + expectValue("HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.liveKafkaSse)); + expectValue("HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.transactionalProjector)); + expectValue("HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.projectionOutboxRelay)); + expectValue("HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.projectionRealtime)); } return { ready: mismatches.length === 0, deploymentExists: true, deploymentName: "hwlab-cloud-api", containerName: "hwlab-cloud-api", - checkedEnvCount: 9 + (kafkaShadowProducer === undefined ? 0 : 5) + (kafkaEventBridge === undefined ? 0 : 7), + checkedEnvCount: 9 + (kafkaShadowProducer === undefined ? 0 : 5) + (kafkaEventBridge === undefined ? 0 : 14), + envCount: envByName.size, + mismatches, + result: compactCodeAgentDeploymentProbeResult(envProbe), + valuesPrinted: false, + }; +} + +function nodeRuntimeCodeAgentCloudWebEnvStatus(spec: HwlabRuntimeLaneSpec, runtime: NonNullable): Record { + const kafkaEventBridge = runtime.kafkaEventBridge; + if (kafkaEventBridge === undefined) return { required: false, ready: true, checkedEnvCount: 0, valuesPrinted: false }; + const envProbe = runNodeK3sArgs(spec, [ + "kubectl", + "-n", + spec.runtimeNamespace, + "get", + "deployment", + "hwlab-cloud-web", + "-o", + 'jsonpath={range .spec.template.spec.containers[?(@.name=="hwlab-cloud-web")].env[*]}{.name}{"\\t"}{.value}{"\\t"}{.valueFrom.secretKeyRef.name}{"\\t"}{.valueFrom.secretKeyRef.key}{"\\n"}{end}', + ], 60); + if (envProbe.exitCode !== 0) { + return { + required: true, + ready: false, + deploymentExists: false, + mismatches: [{ name: "hwlab-cloud-web", reason: "deployment-missing" }], + result: compactCodeAgentDeploymentProbeResult(envProbe), + valuesPrinted: false, + }; + } + const envByName = parseCodeAgentEnvProbe(envProbe.stdout); + const mismatches: Array> = []; + const expectValue = (name: string, expected: string): void => { + const item = envByName.get(name); + if (item === undefined) { + mismatches.push({ name, expected, present: false, kind: "value" }); + return; + } + if (item.value !== expected) mismatches.push({ name, expected, value: item.value, present: true, kind: "value" }); + }; + expectValue("HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.liveKafkaSse)); + expectValue("HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.projectionRealtime)); + return { + required: true, + ready: mismatches.length === 0, + deploymentExists: true, + deploymentName: "hwlab-cloud-web", + containerName: "hwlab-cloud-web", + checkedEnvCount: 2, envCount: envByName.size, mismatches, result: compactCodeAgentDeploymentProbeResult(envProbe),