From 40984789ff0cf5c3b77bc146565745c29ad596b6 Mon Sep 17 00:00:00 2001 From: Code Queue Review Date: Sat, 23 May 2026 06:08:35 +0000 Subject: [PATCH] fix: surface DB runtime readiness layers --- deploy/artifact-catalog.dev.json | 80 +++++++++---------- deploy/deploy.json | 42 +++++----- deploy/k8s/base/workloads.yaml | 40 +++++----- internal/cloud/db-contract.mjs | 130 ++++++++++++++++++++++++++++++- internal/cloud/server.mjs | 11 ++- internal/cloud/server.test.mjs | 20 +++++ 6 files changed, 237 insertions(+), 86 deletions(-) diff --git a/deploy/artifact-catalog.dev.json b/deploy/artifact-catalog.dev.json index 9d9048ea..ad94e989 100644 --- a/deploy/artifact-catalog.dev.json +++ b/deploy/artifact-catalog.dev.json @@ -5,7 +5,7 @@ "profile": "dev", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", - "commitId": "42338c0", + "commitId": "b3fe1d5", "artifactState": "contract-skeleton", "publish": { "ciPublished": false, @@ -66,9 +66,9 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -82,9 +82,9 @@ }, { "serviceId": "hwlab-cloud-web", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -98,9 +98,9 @@ }, { "serviceId": "hwlab-agent-mgr", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -114,9 +114,9 @@ }, { "serviceId": "hwlab-agent-worker", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -130,9 +130,9 @@ }, { "serviceId": "hwlab-gateway", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -146,9 +146,9 @@ }, { "serviceId": "hwlab-gateway-simu", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -162,9 +162,9 @@ }, { "serviceId": "hwlab-box-simu", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -178,9 +178,9 @@ }, { "serviceId": "hwlab-patch-panel", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -194,9 +194,9 @@ }, { "serviceId": "hwlab-router", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-router:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-router:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -210,9 +210,9 @@ }, { "serviceId": "hwlab-tunnel-client", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -226,9 +226,9 @@ }, { "serviceId": "hwlab-edge-proxy", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -242,9 +242,9 @@ }, { "serviceId": "hwlab-cli", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", @@ -258,9 +258,9 @@ }, { "serviceId": "hwlab-agent-skills", - "commitId": "42338c0", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:42338c0", - "imageTag": "42338c0", + "commitId": "b3fe1d5", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:b3fe1d5", + "imageTag": "b3fe1d5", "digest": "not_published", "publishState": "skeleton-only", "profile": "dev", diff --git a/deploy/deploy.json b/deploy/deploy.json index 32c8bbbe..20b09634 100644 --- a/deploy/deploy.json +++ b/deploy/deploy.json @@ -1,7 +1,7 @@ { "manifestVersion": "v1", "environment": "dev", - "commitId": "42338c0", + "commitId": "b3fe1d5", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", "health": { @@ -181,7 +181,7 @@ "services": [ { "serviceId": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -189,9 +189,9 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667", - "HWLAB_COMMIT_ID": "42338c0", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:42338c0", - "HWLAB_IMAGE_TAG": "42338c0", + "HWLAB_COMMIT_ID": "b3fe1d5", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b3fe1d5", + "HWLAB_IMAGE_TAG": "b3fe1d5", "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy", "HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url", "HWLAB_CLOUD_DB_SSL_MODE": "disable", @@ -210,7 +210,7 @@ }, { "serviceId": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -218,14 +218,14 @@ "env": { "HWLAB_ENVIRONMENT": "dev", "HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", - "HWLAB_COMMIT_ID": "42338c0", - "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:42338c0", - "HWLAB_IMAGE_TAG": "42338c0" + "HWLAB_COMMIT_ID": "b3fe1d5", + "HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b3fe1d5", + "HWLAB_IMAGE_TAG": "b3fe1d5" } }, { "serviceId": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -237,7 +237,7 @@ }, { "serviceId": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -249,7 +249,7 @@ }, { "serviceId": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -261,7 +261,7 @@ }, { "serviceId": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -281,7 +281,7 @@ }, { "serviceId": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -300,7 +300,7 @@ }, { "serviceId": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -321,7 +321,7 @@ }, { "serviceId": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-router:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -333,7 +333,7 @@ }, { "serviceId": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -347,7 +347,7 @@ }, { "serviceId": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -359,7 +359,7 @@ }, { "serviceId": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", @@ -370,13 +370,13 @@ }, { "serviceId": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:b3fe1d5", "namespace": "hwlab-dev", "healthPath": "/health/live", "profile": "dev", "replicas": 1, "env": { - "HWLAB_SKILLS_COMMIT_ID": "42338c0" + "HWLAB_SKILLS_COMMIT_ID": "b3fe1d5" } } ], diff --git a/deploy/k8s/base/workloads.yaml b/deploy/k8s/base/workloads.yaml index 0c839894..882c72a8 100644 --- a/deploy/k8s/base/workloads.yaml +++ b/deploy/k8s/base/workloads.yaml @@ -31,7 +31,7 @@ "containers": [ { "name": "hwlab-cloud-api", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b3fe1d5", "ports": [ { "name": "http", @@ -49,15 +49,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "42338c0" + "value": "b3fe1d5" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:42338c0" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-api:b3fe1d5" }, { "name": "HWLAB_IMAGE_TAG", - "value": "42338c0" + "value": "b3fe1d5" }, { "name": "HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN", @@ -175,7 +175,7 @@ "containers": [ { "name": "hwlab-cloud-web", - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b3fe1d5", "ports": [ { "name": "http", @@ -189,15 +189,15 @@ }, { "name": "HWLAB_COMMIT_ID", - "value": "42338c0" + "value": "b3fe1d5" }, { "name": "HWLAB_IMAGE", - "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:42338c0" + "value": "127.0.0.1:5000/hwlab/hwlab-cloud-web:b3fe1d5" }, { "name": "HWLAB_IMAGE_TAG", - "value": "42338c0" + "value": "b3fe1d5" } ], "readinessProbe": { @@ -247,7 +247,7 @@ "containers": [ { "name": "hwlab-agent-mgr", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:b3fe1d5", "ports": [ { "name": "http", @@ -303,7 +303,7 @@ "containers": [ { "name": "hwlab-agent-worker", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:b3fe1d5", "env": [ { "name": "HWLAB_AGENT_SESSION_MODE", @@ -346,7 +346,7 @@ "containers": [ { "name": "hwlab-gateway", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:b3fe1d5", "ports": [ { "name": "http", @@ -407,7 +407,7 @@ "containers": [ { "name": "hwlab-gateway-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:b3fe1d5", "ports": [ { "name": "http", @@ -480,7 +480,7 @@ "containers": [ { "name": "hwlab-box-simu", - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:b3fe1d5", "ports": [ { "name": "http", @@ -548,7 +548,7 @@ "containers": [ { "name": "hwlab-patch-panel", - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:b3fe1d5", "ports": [ { "name": "http", @@ -612,7 +612,7 @@ "containers": [ { "name": "hwlab-router", - "image": "127.0.0.1:5000/hwlab/hwlab-router:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-router:b3fe1d5", "ports": [ { "name": "http", @@ -672,7 +672,7 @@ "containers": [ { "name": "hwlab-tunnel-client", - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:b3fe1d5", "ports": [ { "name": "http", @@ -744,7 +744,7 @@ "containers": [ { "name": "hwlab-edge-proxy", - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:b3fe1d5", "ports": [ { "name": "http", @@ -805,7 +805,7 @@ "containers": [ { "name": "hwlab-cli", - "image": "127.0.0.1:5000/hwlab/hwlab-cli:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:b3fe1d5", "env": [ { "name": "HWLAB_CLI_ENDPOINT", @@ -847,7 +847,7 @@ "containers": [ { "name": "hwlab-agent-skills", - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:42338c0", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:b3fe1d5", "ports": [ { "name": "http", @@ -857,7 +857,7 @@ "env": [ { "name": "HWLAB_SKILLS_COMMIT_ID", - "value": "42338c0" + "value": "b3fe1d5" } ], "readinessProbe": { diff --git a/internal/cloud/db-contract.mjs b/internal/cloud/db-contract.mjs index 4ca84966..9d9a094d 100644 --- a/internal/cloud/db-contract.mjs +++ b/internal/cloud/db-contract.mjs @@ -1,5 +1,14 @@ import net from "node:net"; +import { + RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED, + RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED, + RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED, + RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED, + RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED, + RUNTIME_DURABILITY_REQUIRED_EVIDENCE, + RUNTIME_STORE_KIND_POSTGRES +} from "../db/runtime-store.mjs"; import { ENVIRONMENT_DEV } from "../protocol/index.mjs"; export const DEV_DB_ENV_CONTRACT = Object.freeze({ @@ -36,8 +45,11 @@ export const DEV_DB_ENV_CONTRACT = Object.freeze({ readinessLayers: Object.freeze([ "dns", "tcp", + "ssl", "auth", - "schema" + "schema", + "migration", + "durability" ]), nonSecretDefaults: Object.freeze({ HWLAB_CLOUD_DB_SSL_MODE: "disable" @@ -199,12 +211,36 @@ export function summarizeDbContract(db = buildDbHealthContract()) { optionalPublicDnsAlias: summarizeOptionalPublicDnsAlias(db.optionalPublicDnsAlias ?? db.endpoint?.optionalPublicDnsAlias), connection: summarizeConnection(db.connection), redaction: summarizeRedaction(db.redaction), - readinessLayers: buildReadinessLayers(db.connection), + readinessLayers: db.readinessLayers ?? buildReadinessLayers(db.connection), blocker: db.blocker ?? null, fixtureEvidence: false }; } +export function applyRuntimeDbReadinessLayers(db = buildDbHealthContract(), runtime = {}) { + const baseLayers = buildReadinessLayers(db.connection); + if (!db?.ready || db?.connectionResult !== "connected") { + return { + ...db, + readinessLayers: baseLayers, + runtimeReadiness: buildDbRuntimeReadinessSummary(runtime, { dbLiveReady: false }) + }; + } + + return { + ...db, + readinessLayers: { + ...baseLayers, + ssl: layerFromRuntimeGate(runtime, "ssl"), + auth: layerFromRuntimeGate(runtime, "auth"), + schema: layerFromRuntimeGate(runtime, "schema"), + migration: layerFromRuntimeGate(runtime, "migration"), + durability: layerFromRuntimeGate(runtime, "durability") + }, + runtimeReadiness: buildDbRuntimeReadinessSummary(runtime, { dbLiveReady: true }) + }; +} + function hasEnvValue(env, name) { return typeof env?.[name] === "string" && env[name].trim().length > 0; } @@ -494,6 +530,10 @@ function buildReadinessLayers(connection = {}) { status: tcpReady ? "pass" : "not_proven", result }, + ssl: { + status: "not_proven", + result: connected ? "tcp_connected_ssl_not_checked" : result + }, auth: { status: "not_proven", result: connected ? "tcp_connected_auth_not_checked" : result @@ -501,10 +541,96 @@ function buildReadinessLayers(connection = {}) { schema: { status: "not_proven", result: connected ? "tcp_connected_schema_not_checked" : result + }, + migration: { + status: "not_proven", + result: connected ? "tcp_connected_migration_not_checked" : result + }, + durability: { + status: "not_proven", + result: connected ? "tcp_connected_durability_not_checked" : result } }; } +function layerFromRuntimeGate(runtime = {}, gateName) { + if (!isPostgresRuntime(runtime)) { + return { + status: "not_proven", + result: "runtime_adapter_not_postgres" + }; + } + + const gate = runtime.gates?.[gateName]; + if (gate?.ready === true || gate?.status === "ready") { + return { + status: "pass", + result: `runtime_${gateName}_ready`, + blocker: null + }; + } + + if (gate?.status === "blocked" || runtimeGateBlockedAt(runtime, gateName)) { + return { + status: "blocked", + result: runtimeLayerResult(runtime, gateName), + blocker: gate?.blocker ?? runtime.blocker ?? null + }; + } + + return { + status: "not_proven", + result: `runtime_${gateName}_not_checked`, + blocker: null + }; +} + +function isPostgresRuntime(runtime = {}) { + return runtime?.adapter === RUNTIME_STORE_KIND_POSTGRES || runtime?.durableRequested === true; +} + +function runtimeGateBlockedAt(runtime = {}, gateName) { + const blocker = runtime?.blocker; + if (gateName === "ssl") return blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED; + if (gateName === "auth") return blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED; + if (gateName === "schema") return blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED; + if (gateName === "migration") return blocker === RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED; + if (gateName === "durability") return blocker === RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED; + return false; +} + +function runtimeLayerResult(runtime = {}, gateName) { + const queryResult = runtime?.connection?.queryResult; + if (typeof queryResult === "string" && queryResult.length > 0) return queryResult; + if (gateName === "ssl") return "ssl_negotiation_blocked"; + if (gateName === "auth") return "auth_blocked"; + if (gateName === "schema") return "schema_blocked"; + if (gateName === "migration") return "migration_blocked"; + if (gateName === "durability") return "query_blocked"; + return "runtime_query_blocked"; +} + +function buildDbRuntimeReadinessSummary(runtime = {}, { dbLiveReady } = {}) { + const contract = runtime?.durabilityContract ?? {}; + return { + adapter: runtime?.adapter ?? "unknown", + durable: Boolean(runtime?.durable), + durableRequested: Boolean(runtime?.durableRequested || runtime?.adapter === RUNTIME_STORE_KIND_POSTGRES), + ready: runtime?.ready === true, + status: runtime?.status ?? "unknown", + blocker: runtime?.blocker ?? null, + blockedLayer: contract.blockedLayer ?? null, + queryAttempted: Boolean(runtime?.connection?.queryAttempted), + queryResult: runtime?.connection?.queryResult ?? (dbLiveReady ? "not_checked" : "db_live_not_ready"), + dbLiveEvidenceObserved: Boolean(dbLiveReady), + dbLiveEvidenceIsDurabilityEvidence: false, + requiredEvidence: contract.requiredEvidence ?? RUNTIME_DURABILITY_REQUIRED_EVIDENCE, + secretMaterialRead: false, + valuesRedacted: true, + endpointRedacted: true + }; +} + function summarizeConnection(connection) { if (!connection) { return { diff --git a/internal/cloud/server.mjs b/internal/cloud/server.mjs index 05e92e0a..cf9cfb87 100644 --- a/internal/cloud/server.mjs +++ b/internal/cloud/server.mjs @@ -13,7 +13,10 @@ import { handleJsonRpcRequest } from "./json-rpc.mjs"; import { describeCodeAgentAvailability, handleCodeAgentChat } from "./code-agent-chat.mjs"; -import { buildDbRuntimeReadiness } from "./db-contract.mjs"; +import { + applyRuntimeDbReadinessLayers, + buildDbRuntimeReadiness +} from "./db-contract.mjs"; import { buildCloudApiReadiness } from "./health-contract.mjs"; import { M3_IO_CONTROL_ROUTE, @@ -48,9 +51,10 @@ export async function buildHealthPayload(options = {}) { const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_GIT_SHA || "unknown"; const imageReference = process.env.HWLAB_IMAGE || "unknown"; const imageTag = process.env.HWLAB_IMAGE_TAG || commitId.slice(0, 7) || "unknown"; - const db = await buildDbRuntimeReadiness(env, options.dbProbe); + const dbProbe = await buildDbRuntimeReadiness(env, options.dbProbe); const codeAgent = describeCodeAgentAvailability(env, options); const runtime = await runtimeReadiness(options.runtimeStore ?? createConfiguredCloudRuntimeStore({ ...options, env })); + const db = applyRuntimeDbReadinessLayers(dbProbe, runtime); const readiness = buildCloudApiReadiness({ db, codeAgent, runtime }); return { @@ -160,9 +164,10 @@ async function handleRpcHttpRequest(request, response, options) { async function handleRestAdapter(request, response, url, options) { if (request.method === "GET" && url.pathname === "/v1") { - const db = await buildDbRuntimeReadiness(options.env ?? process.env, options.dbProbe); + const dbProbe = await buildDbRuntimeReadiness(options.env ?? process.env, options.dbProbe); const codeAgent = describeCodeAgentAvailability(options.env ?? process.env, options); const runtime = await runtimeReadiness(options.runtimeStore); + const db = applyRuntimeDbReadinessLayers(dbProbe, runtime); const readiness = buildCloudApiReadiness({ db, codeAgent, runtime }); sendJson(response, 200, { serviceId: CLOUD_API_SERVICE_ID, diff --git a/internal/cloud/server.test.mjs b/internal/cloud/server.test.mjs index 8a2fa295..fc756340 100644 --- a/internal/cloud/server.test.mjs +++ b/internal/cloud/server.test.mjs @@ -515,6 +515,15 @@ test("cloud api health contract distinguishes durable SSL, auth, schema, migrati assert.equal(payload.runtime.gates.migration.status, testCase.gates.migration); assert.equal(payload.runtime.gates.durability.status, testCase.gates.durability); assert.equal(payload.runtime.durabilityContract.blockedLayer, testCase.blockedLayer); + assert.equal(payload.db.runtimeReadiness.blockedLayer, testCase.blockedLayer); + assert.equal(payload.db.runtimeReadiness.queryResult, testCase.queryResult); + assert.equal(payload.db.runtimeReadiness.dbLiveEvidenceIsDurabilityEvidence, false); + assert.equal(payload.db.readinessLayers.ssl.status, layerStatus(testCase.gates.ssl)); + assert.equal(payload.db.readinessLayers.auth.status, layerStatus(testCase.gates.auth)); + assert.equal(payload.db.readinessLayers.schema.status, layerStatus(testCase.gates.schema)); + assert.equal(payload.db.readinessLayers.migration.status, layerStatus(testCase.gates.migration)); + assert.equal(payload.db.readinessLayers.durability.status, layerStatus(testCase.gates.durability)); + assert.equal(payload.db.readinessLayers[layerForBlockedCase(testCase.blockedLayer)].result, testCase.queryResult); assert.equal(payload.readiness.durability.blockedLayer, testCase.blockedLayer); assert.equal(payload.readiness.durability.queryResult, testCase.queryResult); assert.equal(payload.readiness.durability.secretMaterialRead, false); @@ -652,6 +661,17 @@ function durableBlockedRuntime({ blocker, blockedLayer, queryResult, gates }) { }; } +function layerStatus(gateStatus) { + if (gateStatus === "ready") return "pass"; + if (gateStatus === "blocked") return "blocked"; + return "not_proven"; +} + +function layerForBlockedCase(blockedLayer) { + if (blockedLayer === "durability_query") return "durability"; + return blockedLayer; +} + test("cloud api /v1 describes Code Agent provider blocker without leaking secret values", async () => { const server = createCloudApiServer({ env: {