From 357a68bb955fa81b1b6cd228a0993ca125b1d45b Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Fri, 22 May 2026 12:27:01 +0800 Subject: [PATCH] fix: layer dev DB readiness evidence Refs #49 Merged by commander after reviewing Code Queue task codex_1779422762669_1. Adds redacted DB readiness layering and refreshes DEV endpoint/report evidence. Follow-up: deploy/verify the new readiness probe in DEV via standard image CI/CD. --- deploy/README.md | 3 +- deploy/k8s/dev/health-contract.yaml | 2 +- docs/cloud-api-runtime.md | 22 +- docs/dev-edge-health.md | 30 +- docs/dev-gate-report.md | 7 +- .../dev-deploy-smoke/dev-deploy-smoke.json | 2 +- fixtures/mvp/evidence/mvp-runtime-plan.md | 2 +- fixtures/mvp/m5-e2e/dry-run-plan.json | 12 +- fixtures/mvp/runtime.json | 2 +- internal/cloud/db-contract.mjs | 318 +++++++++++++++++- internal/cloud/json-rpc.mjs | 10 +- internal/cloud/server.mjs | 10 +- internal/cloud/server.test.mjs | 31 +- .../m0-contract/deploy-manifest.dev.json | 4 +- .../examples/runtime-boundary/guard.dev.json | 2 +- protocol/mvp-e2e-contract.md | 2 +- reports/dev-gate/dev-edge-health.json | 256 +++++++++----- reports/dev-gate/dev-gate-report.example.json | 2 +- reports/dev-gate/dev-m3-hardware-loop.json | 16 +- reports/dev-gate/dev-m4-agent-loop.json | 8 +- .../dev-gate/dev-m5-gate-aggregator-v2.json | 26 +- reports/dev-gate/dev-m5-gate-aggregator-v2.md | 8 +- reports/dev-gate/dev-mvp-gate-report.json | 16 +- reports/dev-gate/dev-mvp-gate-report.md | 8 +- reports/dev-gate/dev-preflight-report.json | 4 +- scripts/cloud-api-runtime-smoke.mjs | 3 + scripts/m1-contract-smoke.mjs | 3 + scripts/src/dev-edge-health-smoke-lib.mjs | 219 +++++++++++- scripts/validate-dev-gate-report.mjs | 45 +++ 29 files changed, 877 insertions(+), 196 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index 76975faa..5f6debcb 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -16,7 +16,8 @@ future compatibility, but PROD deployment is not part of MVP acceptance. `HWLAB_CLOUD_DB_URL` from Secret reference `hwlab-cloud-api-dev-db/database-url` and non-secret `HWLAB_CLOUD_DB_SSL_MODE=require`. The repository records names only, never - secret values or a live DB connection string. + secret values or a live DB connection string; runtime health reports redacted + env injection and DB connection result classifiers. - `deploy/k8s/prod` is a disabled placeholder gate only. - `deploy/frp` describes the D601-to-master reverse link without secrets. - `deploy/master-edge` describes public edge ownership and health boundaries. diff --git a/deploy/k8s/dev/health-contract.yaml b/deploy/k8s/dev/health-contract.yaml index 17734c76..d28b453b 100644 --- a/deploy/k8s/dev/health-contract.yaml +++ b/deploy/k8s/dev/health-contract.yaml @@ -12,7 +12,7 @@ "data": { "endpoint": "http://74.48.78.17:16667", "cloud-api": "GET /health/live via hwlab-edge-proxy then hwlab-cloud-api:6667", - "cloud-api-db": "DEV DB config gate requires HWLAB_CLOUD_DB_URL from Secret hwlab-cloud-api-dev-db/database-url plus HWLAB_CLOUD_DB_SSL_MODE=require; health reports only env presence and redacted Secret reference names, not live DB evidence", + "cloud-api-db": "DEV DB config gate requires HWLAB_CLOUD_DB_URL from Secret hwlab-cloud-api-dev-db/database-url plus HWLAB_CLOUD_DB_SSL_MODE=require; health reports env presence, env injection, redacted connection attempt/result, and liveConnected without exposing secret values", "cloud-web": "GET /health/live on hwlab-cloud-web:8080; consumes cloud-api only", "agent": "hwlab-agent-mgr readiness gates worker template creation; hwlab-agent-worker is suspended until a session is scheduled", "sim": "hwlab-gateway-simu:7101 and hwlab-box-simu:7201 expose /health/live for DEV smoke", diff --git a/docs/cloud-api-runtime.md b/docs/cloud-api-runtime.md index 9314b8fc..f5018391 100644 --- a/docs/cloud-api-runtime.md +++ b/docs/cloud-api-runtime.md @@ -16,8 +16,9 @@ and verifies: - `GET /health` reports `status: "degraded"` when live DB persistence is not connected. -- The DB contract reports required env names and redacted Secret refs without - exposing secret values. +- The DB contract reports required env names, redacted Secret refs, env + injection, connection attempt status, connection result, and `liveConnected` + without exposing DB URL or secret values. - `gateway.session.register`, `box.resource.register`, `box.capability.report`, `hardware.invoke.shell`, `audit.event.query`, and `evidence.record.query` round-trip through the same runtime store. @@ -35,8 +36,19 @@ The current runtime store is process-local memory: This is intentional for L1. If `HWLAB_CLOUD_DB_URL` and `HWLAB_CLOUD_DB_SSL_MODE` are absent, health reports `db.status: "blocked"`. -If both env vars are present, health reports `db.status: "degraded"` and still -keeps `db.connected: false`, `db.ready: false`, and `db.connectionChecked: -false`; env presence is not live DB evidence. +If both env vars are present, cloud-api attempts a short read-only TCP +connection to the DB endpoint from the injected runtime env. The health payload +keeps all endpoint details redacted and reports only: + +- `db.configReady`: required env names are present. +- `db.connectionAttempted`: the runtime tried the live connection path. +- `db.connectionResult`: redacted classifier such as `connected`, `refused`, + `timeout`, `dns_error`, `invalid_url`, or `unsupported_protocol`. +- `db.liveConnected`: the TCP connection completed. +- `db.ready`: currently aliases `db.liveConnected`. + +Env presence alone is not treated as green. If the runtime has env injection +but `db.connectionAttempted` is false or `db.liveConnected` is false, the gate +must stay blocked/degraded with the reported blocker. Fixture: `fixtures/cloud-api-runtime/minimal-runtime.json`. diff --git a/docs/dev-edge-health.md b/docs/dev-edge-health.md index e3a2dbb4..e642af6b 100644 --- a/docs/dev-edge-health.md +++ b/docs/dev-edge-health.md @@ -25,8 +25,10 @@ forwarding checks. `db` in the cloud-api health payload is a redacted configuration gate. It reports required env names, presence, missing names, and Secret reference names -only. It must not expose a connection string, token, password, or claim fixture -output as live DB evidence. +only. Runtime health additionally reports redacted DB readiness layers: +`configReady`, `envInjected`, `connectionAttempted`, `connectionResult`, and +`liveConnected`. It must not expose a connection string, token, password, DB +host, username, or claim fixture output as live DB evidence. Required DEV DB config names: @@ -37,8 +39,10 @@ Required DEV DB config names: When the Secret/env is absent, the health JSON should keep `db.connected: false`, set `db.status: "blocked"`, and list the missing env names so gate reports can identify `cloud-api-db-health-gate`. When the env is -present but no live DB probe is connected, `db.status` remains `"degraded"` and -`db.ready` remains `false`; env presence alone is not live DB evidence. +present but no live DB probe is attempted, the gate remains blocked with +`connectionResult: "not_attempted"`. When a probe is attempted but +`liveConnected` is false, the gate remains blocked/degraded with the redacted +connection classifier. Env presence alone is not live DB evidence. ## Route Classification @@ -52,7 +56,8 @@ The smoke classifies failures into these blockers: - `dns_port_firewall_blocker`: public endpoint times out or cannot be resolved as a port/network path. - `app_health_blocker`: public HTTP reaches an app but health JSON is wrong, - missing evidence, or returns an HTTP error. + missing evidence, returns an HTTP error, or reports DB env injected without a + live DB connection. ## Machine-Readable Diagnosis @@ -69,17 +74,10 @@ Blocked `frp_blocker` and `edge_proxy_blocker` reports now include a ## Current Blocker The committed report at `reports/dev-gate/dev-edge-health.json` records a -read-only live run where: - -- public `74.48.78.17:16667` returned `ECONNREFUSED`; -- frps control `74.48.78.17:7000` returned `ECONNREFUSED`; -- tunnel health `74.48.78.17:7402` returned `ECONNREFUSED`; -- `kubectl` was not installed in this runner, so D601 k3s service state was not - observable from here. - -Classification: `frp_blocker`. The master frps endpoint is not reachable from -the runner, so the edge proxy and downstream app health cannot be proven through -the public DEV route yet. +read-only live run where public `http://74.48.78.17:16667/health/live` returned +HWLAB DEV health from `hwlab-cloud-api`, DB config was present, but runtime +health still reported no live DB connection attempt/result from the deployed +image. Classification: `app_health_blocker` with likely layer `cloud-api-db`. ## DEV FRP Contract diff --git a/docs/dev-gate-report.md b/docs/dev-gate-report.md index 380b6410..c1b41410 100644 --- a/docs/dev-gate-report.md +++ b/docs/dev-gate-report.md @@ -53,8 +53,11 @@ Every report must include: objects. - DB gate extensions may add redacted `db`, `cloudApiDb`, or check evidence objects. These must include only env names, presence/missing status, and - Secret reference names. They must not include connection strings, passwords, - tokens, or fixture output presented as live DB evidence. + Secret reference names plus redacted readiness layers such as `configReady`, + `envInjected`, `connectionAttempted`, `connectionResult`, and + `liveConnected`. They must not include connection strings, passwords, tokens, + DB hostnames from the secret URL, or fixture output presented as live DB + evidence. ## Validation diff --git a/fixtures/dev-deploy-smoke/dev-deploy-smoke.json b/fixtures/dev-deploy-smoke/dev-deploy-smoke.json index 36347ba8..4f0f8ab3 100644 --- a/fixtures/dev-deploy-smoke/dev-deploy-smoke.json +++ b/fixtures/dev-deploy-smoke/dev-deploy-smoke.json @@ -2,7 +2,7 @@ "smokeId": "m2-dev-deploy-smoke", "issue": "pikasTech/HWLAB#23", "environment": "dev", - "endpoint": "http://74.48.78.17:6667", + "endpoint": "http://74.48.78.17:16667", "defaultMode": "dry-run", "safetyGates": { "dryRunOnly": true, diff --git a/fixtures/mvp/evidence/mvp-runtime-plan.md b/fixtures/mvp/evidence/mvp-runtime-plan.md index 140aa9bf..f9d34027 100644 --- a/fixtures/mvp/evidence/mvp-runtime-plan.md +++ b/fixtures/mvp/evidence/mvp-runtime-plan.md @@ -3,6 +3,6 @@ This evidence fixture is a mock artifact used by the CLI and cloud web skeleton. - Environment: dev -- Endpoint: http://74.48.78.17:6667 +- Endpoint: http://74.48.78.17:16667 - Scope: browser/CLI/gateway through cloud-web/cloud-api - Status: dry-run only diff --git a/fixtures/mvp/m5-e2e/dry-run-plan.json b/fixtures/mvp/m5-e2e/dry-run-plan.json index 7e2d0d01..5aba8b26 100644 --- a/fixtures/mvp/m5-e2e/dry-run-plan.json +++ b/fixtures/mvp/m5-e2e/dry-run-plan.json @@ -5,7 +5,7 @@ "issue": "pikasTech/HWLAB#26", "mode": "dry-run", "environment": "dev", - "endpoint": "http://74.48.78.17:6667", + "endpoint": "http://74.48.78.17:16667", "safety": { "allowNetwork": false, "allowDeploy": false, @@ -248,7 +248,7 @@ "component": "DEV ingress", "status": "healthy", "deployEnv": "dev", - "endpoint": "http://74.48.78.17:6667/health", + "endpoint": "http://74.48.78.17:16667/health", "healthTimestamp": "2026-05-21T00:00:00.000Z", "observedBy": "fixture", "dryRun": true, @@ -263,7 +263,7 @@ "component": "master edge proxy", "status": "healthy", "deployEnv": "dev", - "endpoint": "http://74.48.78.17:6667", + "endpoint": "http://74.48.78.17:16667", "healthTimestamp": "2026-05-21T00:00:00.000Z", "observedBy": "fixture", "dryRun": true, @@ -324,7 +324,7 @@ "component": "Cloud Web", "status": "healthy", "deployEnv": "dev", - "endpoint": "http://74.48.78.17:6667", + "endpoint": "http://74.48.78.17:16667", "healthTimestamp": "2026-05-21T00:00:00.000Z", "observedBy": "fixture", "dryRun": true, @@ -441,7 +441,7 @@ "component": "CLI", "status": "healthy", "deployEnv": "dev", - "endpoint": "http://74.48.78.17:6667", + "endpoint": "http://74.48.78.17:16667", "healthTimestamp": "2026-05-21T00:00:00.000Z", "observedBy": "fixture", "dryRun": true, @@ -470,7 +470,7 @@ "projectId": "proj_m5-mvp-e2e", "serviceId": "hwlab-gateway", "gatewayId": "gtw_m5-dev-boundary", - "endpoint": "http://74.48.78.17:6667/gateway", + "endpoint": "http://74.48.78.17:16667/gateway", "status": "connected", "environment": "dev", "startedAt": "2026-05-21T00:00:00.000Z", diff --git a/fixtures/mvp/runtime.json b/fixtures/mvp/runtime.json index 46992bd6..b0061aa9 100644 --- a/fixtures/mvp/runtime.json +++ b/fixtures/mvp/runtime.json @@ -48,7 +48,7 @@ "worker cleanup" ], "endpoints": { - "dev": "http://74.48.78.17:6667" + "dev": "http://74.48.78.17:16667" }, "loops": [ { diff --git a/internal/cloud/db-contract.mjs b/internal/cloud/db-contract.mjs index 20021362..21f56a63 100644 --- a/internal/cloud/db-contract.mjs +++ b/internal/cloud/db-contract.mjs @@ -1,3 +1,5 @@ +import net from "node:net"; + import { ENVIRONMENT_DEV } from "../protocol/index.mjs"; export const DEV_DB_ENV_CONTRACT = Object.freeze({ @@ -25,6 +27,8 @@ export const DEV_DB_ENV_CONTRACT = Object.freeze({ }); const secretEnvNames = new Set(DEV_DB_ENV_CONTRACT.secretRefs.map((item) => item.env)); +const supportedDbProtocols = new Set(["postgres:", "postgresql:"]); +const defaultProbeTimeoutMs = 1200; export function buildDbHealthContract(env = process.env) { const fields = DEV_DB_ENV_CONTRACT.requiredEnv.map((name) => { @@ -39,6 +43,7 @@ export function buildDbHealthContract(env = process.env) { }); const missingEnv = fields.filter((field) => !field.present).map((field) => field.name); const configReady = missingEnv.length === 0; + const connection = buildNotAttemptedConnection(configReady, missingEnv); const connected = false; const connectionChecked = false; const status = configReady ? "degraded" : "blocked"; @@ -47,11 +52,14 @@ export function buildDbHealthContract(env = process.env) { contractVersion: DEV_DB_ENV_CONTRACT.contractVersion, environment: DEV_DB_ENV_CONTRACT.environment, connected, + liveConnected: connected, connectionChecked, + connectionAttempted: connection.attempted, + connectionResult: connection.result, configReady, ready: connected, status, - mode: configReady ? "configured_without_live_connection" : "not_configured", + mode: configReady ? "configured_without_live_connection_attempt" : "not_configured", fields, missingEnv, secretRefs: DEV_DB_ENV_CONTRACT.secretRefs.map(({ env: envName, secretName, secretKey }) => ({ @@ -59,8 +67,12 @@ export function buildDbHealthContract(env = process.env) { secretName, secretKey, present: hasEnvValue(env, envName), + envInjected: hasEnvValue(env, envName), + secretPresent: "not_observed_by_runtime", + secretKeyPresent: "not_observed_by_runtime", redacted: true })), + connection, safety: { devOnly: true, prodAllowed: DEV_DB_ENV_CONTRACT.forbidden.prodAllowed, @@ -68,10 +80,29 @@ export function buildDbHealthContract(env = process.env) { valuesRedacted: true, liveDbEvidence: false }, + blocker: configReady ? "live DB connection has not been attempted by this health contract" : missingEnvBlocker(missingEnv), evidence: configReady ? "env_presence_only_no_live_db" : "env_contract_blocked" }; } +export async function buildDbRuntimeReadiness(env = process.env, options = {}) { + const base = buildDbHealthContract(env); + if (!base.configReady) { + return base; + } + + if (probeDisabled(env, options)) { + return { + ...base, + blocker: "live DB probe is disabled for this runtime", + evidence: "env_presence_only_probe_disabled" + }; + } + + const connection = await probeDbConnection(env, options); + return applyConnectionResult(base, connection); +} + export function buildDbEnvManifestPlaceholder() { return { requiredEnv: [...DEV_DB_ENV_CONTRACT.requiredEnv], @@ -90,20 +121,40 @@ export function summarizeDbContract(db = buildDbHealthContract()) { ready: db.ready, configReady: db.configReady, connected: db.connected, + liveConnected: db.liveConnected ?? db.connected, connectionChecked: db.connectionChecked, + connectionAttempted: db.connectionAttempted ?? db.connectionChecked, + connectionResult: db.connectionResult ?? db.connection?.result ?? "unknown", requiredEnv: db.fields.map((field) => ({ name: field.name, present: field.present, - redacted: field.redacted + redacted: field.redacted, + source: field.source })), missingEnv: [...db.missingEnv], - secretRefs: db.secretRefs.map(({ env: envName, secretName, secretKey, present, redacted }) => ({ - env: envName, - secretName, - secretKey, - present, - redacted - })), + secretRefs: db.secretRefs.map( + ({ + env: envName, + secretName, + secretKey, + present, + envInjected, + secretPresent, + secretKeyPresent, + redacted + }) => ({ + env: envName, + secretName, + secretKey, + present, + envInjected: envInjected ?? present, + secretPresent: secretPresent ?? "not_observed", + secretKeyPresent: secretKeyPresent ?? "not_observed", + redacted + }) + ), + connection: summarizeConnection(db.connection), + blocker: db.blocker ?? null, liveDbEvidence: db.safety.liveDbEvidence, fixtureEvidence: false }; @@ -112,3 +163,252 @@ export function summarizeDbContract(db = buildDbHealthContract()) { function hasEnvValue(env, name) { return typeof env?.[name] === "string" && env[name].trim().length > 0; } + +function probeDisabled(env, options) { + return options.probe === false || env?.HWLAB_CLOUD_DB_PROBE_DISABLED === "1"; +} + +function buildNotAttemptedConnection(configReady, missingEnv) { + return { + attempted: false, + networkAttempted: false, + result: configReady ? "not_attempted" : "not_attempted_missing_env", + classification: configReady ? "configured_no_live_attempt" : "missing_runtime_env", + probeType: "tcp-connect", + endpointRedacted: true, + valueRedacted: true, + timeoutMs: null, + durationMs: 0, + errorCode: null, + missingEnv: [...missingEnv] + }; +} + +async function probeDbConnection(env, options) { + const timeoutMs = normalizeTimeoutMs(options.timeoutMs ?? env?.HWLAB_CLOUD_DB_PROBE_TIMEOUT_MS); + const parsed = parseDbTarget(env?.HWLAB_CLOUD_DB_URL); + if (!parsed.ok) { + return { + attempted: true, + networkAttempted: false, + result: parsed.result, + classification: parsed.classification, + probeType: "tcp-connect", + endpointRedacted: true, + valueRedacted: true, + timeoutMs, + durationMs: 0, + errorCode: parsed.errorCode, + missingEnv: [] + }; + } + + return tcpConnect({ + host: parsed.host, + port: parsed.port, + timeoutMs + }); +} + +function parseDbTarget(rawUrl) { + try { + const url = new URL(rawUrl); + if (!supportedDbProtocols.has(url.protocol)) { + return { + ok: false, + result: "unsupported_protocol", + classification: "db_url_unsupported_protocol", + errorCode: "UNSUPPORTED_PROTOCOL" + }; + } + if (!url.hostname) { + return { + ok: false, + result: "invalid_url", + classification: "db_url_missing_host", + errorCode: "MISSING_HOST" + }; + } + const port = url.port ? Number.parseInt(url.port, 10) : 5432; + if (!Number.isInteger(port) || port <= 0 || port > 65535) { + return { + ok: false, + result: "invalid_url", + classification: "db_url_invalid_port", + errorCode: "INVALID_PORT" + }; + } + return { + ok: true, + host: url.hostname, + port + }; + } catch { + return { + ok: false, + result: "invalid_url", + classification: "db_url_parse_error", + errorCode: "INVALID_URL" + }; + } +} + +function tcpConnect({ host, port, timeoutMs }) { + return new Promise((resolve) => { + const startedAt = Date.now(); + const socket = net.connect({ host, port, timeout: timeoutMs }); + let settled = false; + + function finish(result) { + if (settled) return; + settled = true; + socket.destroy(); + resolve({ + attempted: true, + networkAttempted: true, + probeType: "tcp-connect", + endpointRedacted: true, + valueRedacted: true, + timeoutMs, + durationMs: Date.now() - startedAt, + missingEnv: [], + ...result + }); + } + + socket.on("connect", () => { + finish({ + result: "connected", + classification: "tcp_connected", + errorCode: null + }); + }); + socket.on("timeout", () => { + finish({ + result: "timeout", + classification: "tcp_timeout", + errorCode: "ETIMEDOUT" + }); + }); + socket.on("error", (error) => { + finish(classifySocketError(error)); + }); + }); +} + +function classifySocketError(error) { + const code = typeof error?.code === "string" ? error.code : "UNKNOWN"; + if (code === "ECONNREFUSED") { + return { + result: "refused", + classification: "tcp_refused", + errorCode: code + }; + } + if (code === "ENOTFOUND" || code === "EAI_AGAIN") { + return { + result: "dns_error", + classification: "dns_resolution_failed", + errorCode: code + }; + } + if (code === "ETIMEDOUT") { + return { + result: "timeout", + classification: "tcp_timeout", + errorCode: code + }; + } + if (code === "EHOSTUNREACH" || code === "ENETUNREACH") { + return { + result: "network_unreachable", + classification: "network_unreachable", + errorCode: code + }; + } + return { + result: "error", + classification: "tcp_error", + errorCode: code + }; +} + +function applyConnectionResult(base, connection) { + const liveConnected = connection.result === "connected"; + return { + ...base, + connected: liveConnected, + liveConnected, + connectionChecked: connection.attempted, + connectionAttempted: connection.attempted, + connectionResult: connection.result, + ready: liveConnected, + status: liveConnected ? "ready" : "degraded", + mode: liveConnected ? "live_connection_ready" : "live_connection_blocked", + connection, + safety: { + ...base.safety, + liveDbEvidence: connection.attempted, + liveDbConnectedEvidence: liveConnected + }, + blocker: liveConnected ? null : blockerForConnection(connection), + evidence: liveConnected ? "live_db_tcp_connection_ready" : "live_db_tcp_connection_blocked" + }; +} + +function blockerForConnection(connection) { + if (connection.result === "invalid_url") { + return "DB URL is injected but cannot be parsed as a supported postgres URL"; + } + if (connection.result === "unsupported_protocol") { + return "DB URL is injected but does not use a supported postgres protocol"; + } + if (connection.result === "refused") { + return "DB endpoint refused a TCP connection from the cloud-api runtime"; + } + if (connection.result === "timeout") { + return "DB endpoint TCP connection timed out from the cloud-api runtime"; + } + if (connection.result === "dns_error") { + return "DB endpoint DNS resolution failed from the cloud-api runtime"; + } + if (connection.result === "network_unreachable") { + return "DB endpoint network is unreachable from the cloud-api runtime"; + } + return "DB endpoint TCP connection did not complete from the cloud-api runtime"; +} + +function missingEnvBlocker(missingEnv) { + return `DB runtime config is missing ${missingEnv.join(", ")}`; +} + +function normalizeTimeoutMs(value) { + const parsed = Number.parseInt(String(value ?? ""), 10); + if (!Number.isInteger(parsed) || parsed <= 0) { + return defaultProbeTimeoutMs; + } + return Math.min(parsed, 5000); +} + +function summarizeConnection(connection) { + if (!connection) { + return { + attempted: false, + result: "unknown", + endpointRedacted: true, + valueRedacted: true + }; + } + return { + attempted: Boolean(connection.attempted), + networkAttempted: Boolean(connection.networkAttempted), + result: connection.result, + classification: connection.classification, + probeType: connection.probeType, + endpointRedacted: true, + valueRedacted: true, + timeoutMs: connection.timeoutMs, + durationMs: connection.durationMs, + errorCode: connection.errorCode ?? null + }; +} diff --git a/internal/cloud/json-rpc.mjs b/internal/cloud/json-rpc.mjs index 4622bd2d..f2a786f8 100644 --- a/internal/cloud/json-rpc.mjs +++ b/internal/cloud/json-rpc.mjs @@ -14,7 +14,7 @@ import { createAuditRecord, deriveActorFromMeta } from "../audit/index.mjs"; -import { buildDbHealthContract } from "./db-contract.mjs"; +import { buildDbRuntimeReadiness } from "./db-contract.mjs"; import { createCloudRuntimeStore } from "../db/runtime-store.mjs"; export const SUPPORTED_RPC_METHODS = Object.freeze([ @@ -217,24 +217,24 @@ export function createResponseMeta(requestMeta = {}) { return meta; } -function handleSystemHealth(params, envelope, context) { +async function handleSystemHealth(params, envelope, context) { return { status: healthStatus(), serviceId: CLOUD_API_SERVICE_ID, environment: ENVIRONMENT_DEV, - db: buildDbHealthContract(), + db: await buildDbRuntimeReadiness(process.env, context.dbProbe), runtime: getRuntimeStore(context).summary() }; } -function handleAdapterDescribe(params, envelope, context) { +async function handleAdapterDescribe(params, envelope, context) { return { serviceId: CLOUD_API_SERVICE_ID, rpcEndpoint: "POST /rpc", restEndpoint: "POST /v1/rpc/{method}", methods: SUPPORTED_RPC_METHODS, auditFields: AUDIT_FIELD_NAMES, - db: buildDbHealthContract(), + db: await buildDbRuntimeReadiness(process.env, context.dbProbe), runtime: getRuntimeStore(context).summary() }; } diff --git a/internal/cloud/server.mjs b/internal/cloud/server.mjs index a76cac06..3907fa9b 100644 --- a/internal/cloud/server.mjs +++ b/internal/cloud/server.mjs @@ -12,7 +12,7 @@ import { createErrorEnvelope, handleJsonRpcRequest } from "./json-rpc.mjs"; -import { buildDbHealthContract } from "./db-contract.mjs"; +import { buildDbRuntimeReadiness } from "./db-contract.mjs"; import { createCloudRuntimeStore } from "../db/runtime-store.mjs"; const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024; @@ -34,12 +34,12 @@ export function createCloudApiServer(options = {}) { }); } -export function buildHealthPayload(options = {}) { +export async function buildHealthPayload(options = {}) { const serviceId = CLOUD_API_SERVICE_ID; 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 = buildDbHealthContract(); + const db = await buildDbRuntimeReadiness(process.env, options.dbProbe); return { serviceId, @@ -71,7 +71,7 @@ async function routeRequest(request, response, options) { const url = new URL(request.url || "/", "http://hwlab-cloud-api.local"); if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) { - sendJson(response, 200, buildHealthPayload(options)); + sendJson(response, 200, await buildHealthPayload(options)); return; } @@ -148,7 +148,7 @@ async function handleRestAdapter(request, response, url, options) { rpcBridge: "POST /v1/rpc/{method}", methods: SUPPORTED_RPC_METHODS, auditFields: AUDIT_FIELD_NAMES, - db: buildDbHealthContract(), + db: await buildDbRuntimeReadiness(process.env, options.dbProbe), runtime: options.runtimeStore.summary() }); return; diff --git a/internal/cloud/server.test.mjs b/internal/cloud/server.test.mjs index c7dadcd4..63707e5e 100644 --- a/internal/cloud/server.test.mjs +++ b/internal/cloud/server.test.mjs @@ -1,4 +1,5 @@ import assert from "node:assert/strict"; +import { createServer as createTcpServer } from "node:net"; import test from "node:test"; import { createCloudApiServer } from "./server.mjs"; @@ -26,6 +27,9 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => { assert.equal(healthPayload.service.id, "hwlab-cloud-api"); assert.equal(healthPayload.db.status, "blocked"); assert.equal(healthPayload.db.connected, false); + assert.equal(healthPayload.db.liveConnected, false); + assert.equal(healthPayload.db.connectionAttempted, false); + assert.equal(healthPayload.db.connectionResult, "not_attempted_missing_env"); assert.equal(healthPayload.runtime.durable, false); assert.equal(healthPayload.runtime.status, "degraded"); assert.deepEqual(healthPayload.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]); @@ -64,10 +68,13 @@ test("cloud api exposes /health, /health/live, and /live probes", async () => { } }); -test("cloud api health reports DB env presence without leaking values", async () => { +test("cloud api health reports DB env presence and live connection classification without leaking values", async () => { const originalUrl = process.env.HWLAB_CLOUD_DB_URL; const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE; - process.env.HWLAB_CLOUD_DB_URL = "postgres://user:password@db.example.invalid/hwlab"; + const fakeDb = createTcpServer((socket) => socket.end()); + await new Promise((resolve) => fakeDb.listen(0, "127.0.0.1", resolve)); + const dbPort = fakeDb.address().port; + process.env.HWLAB_CLOUD_DB_URL = `postgres://user:password@127.0.0.1:${dbPort}/hwlab`; process.env.HWLAB_CLOUD_DB_SSL_MODE = "require"; const server = createCloudApiServer(); @@ -78,16 +85,21 @@ test("cloud api health reports DB env presence without leaking values", async () const response = await fetch(`http://127.0.0.1:${port}/health`); const payload = await response.json(); assert.equal(payload.status, "degraded"); - assert.equal(payload.db.status, "degraded"); - assert.equal(payload.db.connected, false); - assert.equal(payload.db.connectionChecked, false); + assert.equal(payload.db.status, "ready"); + assert.equal(payload.db.connected, true); + assert.equal(payload.db.liveConnected, true); + assert.equal(payload.db.connectionChecked, true); + assert.equal(payload.db.connectionAttempted, true); + assert.equal(payload.db.connectionResult, "connected"); assert.equal(payload.db.configReady, true); - assert.equal(payload.db.ready, false); - assert.equal(payload.db.evidence, "env_presence_only_no_live_db"); + assert.equal(payload.db.ready, true); + assert.equal(payload.db.evidence, "live_db_tcp_connection_ready"); assert.deepEqual(payload.db.missingEnv, []); assert.equal(JSON.stringify(payload.db).includes("password"), false); - assert.equal(JSON.stringify(payload.db).includes("db.example.invalid"), false); + assert.equal(JSON.stringify(payload.db).includes("127.0.0.1"), false); + assert.equal(JSON.stringify(payload.db).includes(String(dbPort)), false); assert.equal(payload.db.fields.find((field) => field.name === "HWLAB_CLOUD_DB_URL").redacted, true); + assert.equal(payload.db.connection.endpointRedacted, true); } finally { if (originalUrl === undefined) { delete process.env.HWLAB_CLOUD_DB_URL; @@ -102,5 +114,8 @@ test("cloud api health reports DB env presence without leaking values", async () await new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }); + await new Promise((resolve, reject) => { + fakeDb.close((error) => (error ? reject(error) : resolve())); + }); } }); diff --git a/protocol/examples/m0-contract/deploy-manifest.dev.json b/protocol/examples/m0-contract/deploy-manifest.dev.json index 5f688ac1..755b7431 100644 --- a/protocol/examples/m0-contract/deploy-manifest.dev.json +++ b/protocol/examples/m0-contract/deploy-manifest.dev.json @@ -3,13 +3,13 @@ "environment": "dev", "commitId": "6509a35804ee97dd8d711bdde334c46ca37c55b9", "namespace": "hwlab-dev", - "endpoint": "http://74.48.78.17:6667", + "endpoint": "http://74.48.78.17:16667", "profiles": { "dev": { "name": "dev", "enabled": true, "namespace": "hwlab-dev", - "endpoint": "http://74.48.78.17:6667", + "endpoint": "http://74.48.78.17:16667", "notes": "M0 contract example only; not a deploy manifest." } }, diff --git a/protocol/examples/runtime-boundary/guard.dev.json b/protocol/examples/runtime-boundary/guard.dev.json index 90f2daef..f7c6de30 100644 --- a/protocol/examples/runtime-boundary/guard.dev.json +++ b/protocol/examples/runtime-boundary/guard.dev.json @@ -2,7 +2,7 @@ "guard": "hwlab-runtime-boundary", "schemaVersion": "v1", "environment": "dev", - "endpoint": "http://74.48.78.17:6667", + "endpoint": "http://74.48.78.17:16667", "namespace": "hwlab-dev", "scope": "static-runtime-boundary", "policy": { diff --git a/protocol/mvp-e2e-contract.md b/protocol/mvp-e2e-contract.md index e0426188..5eba9897 100644 --- a/protocol/mvp-e2e-contract.md +++ b/protocol/mvp-e2e-contract.md @@ -8,7 +8,7 @@ heavy e2e test suite. All MVP acceptance traffic targets: ```text -http://74.48.78.17:6667 +http://74.48.78.17:16667 ``` ## Minimal Flow diff --git a/reports/dev-gate/dev-edge-health.json b/reports/dev-gate/dev-edge-health.json index 1ce3addc..a430972e 100644 --- a/reports/dev-gate/dev-edge-health.json +++ b/reports/dev-gate/dev-edge-health.json @@ -4,12 +4,12 @@ "reportVersion": "v1", "issue": "pikasTech/HWLAB#36", "taskId": "dev-edge-health", - "commitId": "73b379f4441d", + "commitId": "cb35ada68606", "acceptanceLevel": "dev_edge_health", "devOnly": true, "prodDisabled": true, - "status": "pass", - "generatedAt": "2026-05-22T03:38:24.622Z", + "status": "blocked", + "generatedAt": "2026-05-22T04:22:14.774Z", "namespace": "hwlab-dev", "endpoint": "http://74.48.78.17:16667", "sourceContract": { @@ -51,18 +51,25 @@ "summary": "MVP dry-run remains the generic gate command; edge health uses a narrower route smoke." }, "devPreconditions": { - "status": "pass", + "status": "blocked", "requirements": [ "http://74.48.78.17:16667/health returns HWLAB DEV JSON with service, commit, and image evidence", "frps control on 74.48.78.17:7000 accepts the D601 frp client", "hwlab-dev hwlab-edge-proxy, hwlab-tunnel-client, hwlab-router, and hwlab-cloud-api are observable", "No PROD endpoint, secret read, restart, or UniDesk runtime substitute is used" ], - "summary": "DEV edge health preconditions are satisfied." + "summary": "cloud-api DB env is injected, but runtime health has not attempted a live DB connection" }, - "blockers": [], + "blockers": [ + { + "type": "runtime_blocker", + "scope": "cloud-api-db", + "status": "open", + "summary": "cloud-api DB env is injected, but runtime health has not attempted a live DB connection" + } + ], "edgeHealth": { - "generatedAt": "2026-05-22T03:38:24.622Z", + "generatedAt": "2026-05-22T04:22:14.774Z", "mode": "live-read-only", "endpoint": "http://74.48.78.17:16667", "safety": { @@ -123,17 +130,22 @@ "ready": false, "configReady": true, "connected": false, + "liveConnected": false, "connectionChecked": false, + "connectionAttempted": false, + "connectionResult": "manifest_only_not_attempted", "requiredEnv": [ { "name": "HWLAB_CLOUD_DB_URL", "present": true, - "redacted": true + "redacted": true, + "source": "k8s-secret-ref" }, { "name": "HWLAB_CLOUD_DB_SSL_MODE", "present": true, - "redacted": false + "redacted": false, + "source": "runtime-env" } ], "missingEnv": [], @@ -143,14 +155,32 @@ "secretName": "hwlab-cloud-api-dev-db", "secretKey": "database-url", "present": true, + "envInjected": false, + "secretPresent": "not_observed_by_manifest", + "secretKeyPresent": "declared", "redacted": true } ], + "connection": { + "attempted": false, + "networkAttempted": false, + "result": "manifest_only_not_attempted", + "classification": "manifest_only", + "probeType": "tcp-connect", + "endpointRedacted": true, + "valueRedacted": true, + "timeoutMs": null, + "durationMs": 0, + "errorCode": null + }, + "blocker": "Manifest declares DB env/Secret references but does not prove runtime env injection or live DB connection", "liveDbEvidence": false, "fixtureEvidence": false, + "manifestConfigReady": true, + "envInjected": false, "secretMaterialRead": false, "valuesRedacted": true, - "note": "Manifest-only DB env placeholder; this is not live DB evidence." + "note": "Manifest-only DB env placeholder; this is not runtime env injection or live DB evidence." } } }, @@ -159,19 +189,19 @@ "host": "74.48.78.17", "port": 16667, "status": "connected", - "durationMs": 205 + "durationMs": 212 }, { "host": "74.48.78.17", "port": 7000, "status": "connected", - "durationMs": 1231 + "durationMs": 1205 }, { "host": "74.48.78.17", "port": 7402, "status": "connected", - "durationMs": 199 + "durationMs": 232 } ], "publicHttp": [ @@ -184,7 +214,7 @@ "connection": "keep-alive", "content-length": "661", "content-type": "application/json; charset=utf-8", - "date": "Fri, 22 May 2026 03:38:23 GMT", + "date": "Fri, 22 May 2026 04:22:14 GMT", "keep-alive": "timeout=5" }, "json": { @@ -198,23 +228,23 @@ "livePath": "/health/live" }, "commit": { - "id": "73b379f4441d47a99dfd978a80fd43727410af51", + "id": "cb35ada6860653b27269b0a57991184118cbf4b1", "source": "runtime-env" }, "image": { - "reference": "ghcr.io/pikastech/hwlab-edge-proxy:73b379f", - "tag": "73b379f", + "reference": "ghcr.io/pikastech/hwlab-edge-proxy:cb35ada", + "tag": "cb35ada", "digest": "unknown" }, "endpoint": "http://74.48.78.17:16667", - "observedAt": "2026-05-22T03:38:23.861Z", + "observedAt": "2026-05-22T04:22:14.011Z", "details": { "upstream": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", "mode": "dev-edge-proxy" } }, - "bodyPreview": "{\n \"serviceId\": \"hwlab-edge-proxy\",\n \"environment\": \"dev\",\n \"status\": \"ok\",\n \"service\": {\n \"id\": \"hwlab-edge-proxy\",\n \"role\": \"public-dev-ingress\",\n \"healthPath\": \"/health\",\n \"livePath\": \"/health/live\"\n },\n \"commit\": {\n \"id\": \"73b379f4441d47a99dfd978a80fd43727410af51\",\n \"source\": \"runtime-env\"\n },\n \"image\": {\n \"reference\": \"ghcr.io/pikastech/hwlab-edge-proxy:73b379f\",\n \"tag\": \"73b379f\",\n \"digest\": \"unknown\"\n },\n \"endpoint\": \"http://74.48.78.17:16667\",\n \"obse", - "durationMs": 656 + "bodyPreview": "{\n \"serviceId\": \"hwlab-edge-proxy\",\n \"environment\": \"dev\",\n \"status\": \"ok\",\n \"service\": {\n \"id\": \"hwlab-edge-proxy\",\n \"role\": \"public-dev-ingress\",\n \"healthPath\": \"/health\",\n \"livePath\": \"/health/live\"\n },\n \"commit\": {\n \"id\": \"cb35ada6860653b27269b0a57991184118cbf4b1\",\n \"source\": \"runtime-env\"\n },\n \"image\": {\n \"reference\": \"ghcr.io/pikastech/hwlab-edge-proxy:cb35ada\",\n \"tag\": \"cb35ada\",\n \"digest\": \"unknown\"\n },\n \"endpoint\": \"http://74.48.78.17:16667\",\n \"obse", + "durationMs": 821 }, { "url": "http://74.48.78.17:16667/health/live", @@ -225,7 +255,7 @@ "cache-control": "no-store", "connection": "keep-alive", "content-type": "application/json; charset=utf-8", - "date": "Fri, 22 May 2026 03:38:23 GMT", + "date": "Fri, 22 May 2026 04:22:14 GMT", "keep-alive": "timeout=5", "transfer-encoding": "chunked" }, @@ -240,16 +270,16 @@ "livePath": "/health/live" }, "commit": { - "id": "73b379f", + "id": "cb35ada", "source": "runtime-env" }, "image": { - "reference": "127.0.0.1:5000/hwlab/hwlab-cloud-api:73b379f", - "tag": "73b379f", + "reference": "127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada", + "tag": "cb35ada", "digest": "unknown" }, "endpoint": "http://74.48.78.17:16667", - "observedAt": "2026-05-22T03:38:23.864Z", + "observedAt": "2026-05-22T04:22:14.013Z", "db": { "contractVersion": "v1", "environment": "dev", @@ -309,8 +339,8 @@ } } }, - "bodyPreview": "{\"serviceId\":\"hwlab-cloud-api\",\"environment\":\"dev\",\"status\":\"degraded\",\"service\":{\"id\":\"hwlab-cloud-api\",\"role\":\"cloud-api\",\"healthPath\":\"/health\",\"livePath\":\"/health/live\"},\"commit\":{\"id\":\"73b379f\",\"source\":\"runtime-env\"},\"image\":{\"reference\":\"127.0.0.1:5000/hwlab/hwlab-cloud-api:73b379f\",\"tag\":\"73b379f\",\"digest\":\"unknown\"},\"endpoint\":\"http://74.48.78.17:16667\",\"observedAt\":\"2026-05-22T03:38:23.864Z\",\"db\":{\"contractVersion\":\"v1\",\"environment\":\"dev\",\"connected\":false,\"connectionChecked\":false,\"c", - "durationMs": 651 + "bodyPreview": "{\"serviceId\":\"hwlab-cloud-api\",\"environment\":\"dev\",\"status\":\"degraded\",\"service\":{\"id\":\"hwlab-cloud-api\",\"role\":\"cloud-api\",\"healthPath\":\"/health\",\"livePath\":\"/health/live\"},\"commit\":{\"id\":\"cb35ada\",\"source\":\"runtime-env\"},\"image\":{\"reference\":\"127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada\",\"tag\":\"cb35ada\",\"digest\":\"unknown\"},\"endpoint\":\"http://74.48.78.17:16667\",\"observedAt\":\"2026-05-22T04:22:14.013Z\",\"db\":{\"contractVersion\":\"v1\",\"environment\":\"dev\",\"connected\":false,\"connectionChecked\":false,\"c", + "durationMs": 793 } ], "kubernetes": { @@ -318,7 +348,7 @@ "namespace": "hwlab-dev", "context": { "exitCode": 0, - "value": "default", + "value": "in-cluster", "stderr": "" }, "services": [ @@ -399,7 +429,7 @@ { "name": "hwlab-cloud-api", "readyAddresses": [ - "10.42.0.188" + "10.42.0.201" ], "notReadyAddresses": [], "ports": [ @@ -412,7 +442,7 @@ { "name": "hwlab-edge-proxy", "readyAddresses": [ - "10.42.0.191" + "10.42.0.203" ], "notReadyAddresses": [], "ports": [ @@ -425,7 +455,7 @@ { "name": "hwlab-tunnel-client", "readyAddresses": [ - "10.42.0.194" + "10.42.0.207" ], "notReadyAddresses": [], "ports": [ @@ -438,7 +468,7 @@ { "name": "hwlab-router", "readyAddresses": [ - "10.42.0.193" + "10.42.0.206" ], "notReadyAddresses": [], "ports": [ @@ -451,7 +481,7 @@ ], "pods": [ { - "name": "hwlab-agent-mgr-6b4976cb77-rjc2f", + "name": "hwlab-agent-mgr-75544c7747-5dzt9", "phase": "Running", "serviceId": "hwlab-agent-mgr", "containers": [ @@ -459,13 +489,13 @@ "name": "hwlab-agent-mgr", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-mgr@sha256:ae6ca76cd09a7d21e46e0076e12dea1843417a0d59d57b3115915c831e88ebef" + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-mgr@sha256:c16c733306a28ae9385087653b79f5bb2f28724d917a89bb296646f6a02c5318" } ] }, { - "name": "hwlab-agent-skills-76b5d4f4bb-8x55z", + "name": "hwlab-agent-skills-849d9976f4-2nsmw", "phase": "Running", "serviceId": "hwlab-agent-skills", "containers": [ @@ -473,13 +503,13 @@ "name": "hwlab-agent-skills", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-skills@sha256:5bdf9a4d17f8f4c7f3ecead99dcbd4de0ac346a2c28fef020b8853cf494df573" + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-skills@sha256:246fb7bb48597a7179aab62cb091e8213e47b71b6871ba94e6028032992e916c" } ] }, { - "name": "hwlab-box-simu-5d574d4675-jb6vb", + "name": "hwlab-box-simu-55688cbcc7-pftxc", "phase": "Running", "serviceId": "hwlab-box-simu", "containers": [ @@ -487,13 +517,13 @@ "name": "hwlab-box-simu", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:2a12c1c462b2c01a9fa15efa75a6e1f17ecc8432dfa88bca53f0d8d4b6355daf" + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" } ] }, { - "name": "hwlab-box-simu-5d574d4675-qwp84", + "name": "hwlab-box-simu-55688cbcc7-sq5fx", "phase": "Running", "serviceId": "hwlab-box-simu", "containers": [ @@ -501,13 +531,13 @@ "name": "hwlab-box-simu", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:2a12c1c462b2c01a9fa15efa75a6e1f17ecc8432dfa88bca53f0d8d4b6355daf" + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" } ] }, { - "name": "hwlab-cloud-api-769848bfd8-k2hdb", + "name": "hwlab-cloud-api-f48dcdfb-9n6zh", "phase": "Running", "serviceId": "hwlab-cloud-api", "containers": [ @@ -515,13 +545,13 @@ "name": "hwlab-cloud-api", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-api@sha256:561e50494bf13bf61f83d28b371bc39b860b21333ef9991711853a095bd535bf" + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-api@sha256:3946efdd7360409cea6a897cb03a6f3172f14819d3132df3aab821621909513f" } ] }, { - "name": "hwlab-cloud-web-bc56bc464-l7mdl", + "name": "hwlab-cloud-web-cd9655b98-gxtmc", "phase": "Running", "serviceId": "hwlab-cloud-web", "containers": [ @@ -529,13 +559,13 @@ "name": "hwlab-cloud-web", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-web@sha256:c7939658af9f1109e5384619163dd0d11e1a1de14e7d15d4cbde7c4400b495c2" + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-web@sha256:868e2a11c56cc1a27c77de3fa71a7b092f21ae98a4052104e9071bd93960abfa" } ] }, { - "name": "hwlab-edge-proxy-544886546-656nq", + "name": "hwlab-edge-proxy-567f978cdd-8fwc8", "phase": "Running", "serviceId": "hwlab-edge-proxy", "containers": [ @@ -543,13 +573,13 @@ "name": "hwlab-edge-proxy", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-edge-proxy@sha256:480dea831051d45b6d63a489db9c0fa314f88bae35dbf72be4186bf1de356f38" + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-edge-proxy@sha256:384832fc579063b9b0a940973690734bb402607720fd36f4662e4ee6fa5a57d2" } ] }, { - "name": "hwlab-gateway-simu-744ffffbbd-66q2c", + "name": "hwlab-gateway-simu-699fd486b-j9pk7", "phase": "Running", "serviceId": "hwlab-gateway-simu", "containers": [ @@ -557,13 +587,13 @@ "name": "hwlab-gateway-simu", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:f5ccff29da331e424a482f92394d071e1be90c0244e7137e07ac33d9a7cc4082" + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" } ] }, { - "name": "hwlab-gateway-simu-744ffffbbd-rmlzq", + "name": "hwlab-gateway-simu-699fd486b-phcgp", "phase": "Running", "serviceId": "hwlab-gateway-simu", "containers": [ @@ -571,13 +601,13 @@ "name": "hwlab-gateway-simu", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:f5ccff29da331e424a482f92394d071e1be90c0244e7137e07ac33d9a7cc4082" + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" } ] }, { - "name": "hwlab-patch-panel-7d9487bb6c-82qqk", + "name": "hwlab-patch-panel-757f9f44d5-7np8r", "phase": "Running", "serviceId": "hwlab-patch-panel", "containers": [ @@ -585,13 +615,13 @@ "name": "hwlab-patch-panel", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-patch-panel@sha256:5bc9a1c7ce65026cd7bf6d20a311226b35c796a4d52a914266244bf3c1a17286" + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-patch-panel@sha256:5b3aa6505492c2ed398d21cbd35a8754672114acd0fa97faa3351d4b6a6dfd5a" } ] }, { - "name": "hwlab-router-77465567b6-9qs6t", + "name": "hwlab-router-5d9cbdf89b-p7r94", "phase": "Running", "serviceId": "hwlab-router", "containers": [ @@ -599,13 +629,13 @@ "name": "hwlab-router", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-router:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-router@sha256:356f37a7d78cc2f86230c2037f43215ad66979fbe4a949cb9241b5957692d9d8" + "image": "127.0.0.1:5000/hwlab/hwlab-router:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-router@sha256:217b1fdb0f704c4183e9d8503644177f0c8bae674da0433985c19ef909f0e342" } ] }, { - "name": "hwlab-tunnel-client-6449fcb5fb-27sk2", + "name": "hwlab-tunnel-client-65c7858c8-wjqsh", "phase": "Running", "serviceId": "hwlab-tunnel-client", "containers": [ @@ -613,12 +643,23 @@ "name": "hwlab-tunnel-client", "ready": true, "restartCount": 0, - "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:73b379f", - "imageID": "127.0.0.1:5000/hwlab/hwlab-tunnel-client@sha256:8ca553408a633d7f287ca9440f822fb1458dd9a028eb4db178b466b0b5505c49" + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-tunnel-client@sha256:9bcdd8a58132d7bc4dbfc71297f802fc0ab407a1b5c1bb1ea58e0791147a243b" } ] } ], + "dbSecret": { + "observable": true, + "secretName": "hwlab-cloud-api-dev-db", + "secretKey": "database-url", + "secretPresent": true, + "secretKeyPresent": true, + "secretValueRead": false, + "redacted": true, + "command": "kubectl describe secret (key-presence-only)", + "error": null + }, "notes": [] }, "clusterDns": [ @@ -626,33 +667,90 @@ "serviceId": "hwlab-cloud-api", "host": "hwlab-cloud-api.hwlab-dev.svc.cluster.local", "port": 6667, - "resolved": false, - "output": "" + "resolved": true, + "output": "10.43.88.144 hwlab-cloud-api.hwlab-dev.svc.cluster.local" }, { "serviceId": "hwlab-router", "host": "hwlab-router.hwlab-dev.svc.cluster.local", "port": 7401, - "resolved": false, - "output": "" + "resolved": true, + "output": "10.43.6.240 hwlab-router.hwlab-dev.svc.cluster.local" }, { "serviceId": "hwlab-tunnel-client", "host": "hwlab-tunnel-client.hwlab-dev.svc.cluster.local", "port": 7402, - "resolved": false, - "output": "" + "resolved": true, + "output": "10.43.129.225 hwlab-tunnel-client.hwlab-dev.svc.cluster.local" }, { "serviceId": "hwlab-edge-proxy", "host": "hwlab-edge-proxy.hwlab-dev.svc.cluster.local", "port": 6667, - "resolved": false, - "output": "" + "resolved": true, + "output": "10.43.104.107 hwlab-edge-proxy.hwlab-dev.svc.cluster.local" } ], - "status": "pass", - "classification": "none", - "blocker": null + "runtimeDbReadiness": { + "status": "blocked", + "configReady": true, + "envInjected": true, + "secret": { + "observable": true, + "secretName": "hwlab-cloud-api-dev-db", + "secretKey": "database-url", + "secretPresent": true, + "secretKeyPresent": true, + "secretValueRead": false, + "redacted": true, + "command": "kubectl describe secret (key-presence-only)", + "error": null, + "secretRefPresent": true, + "envInjected": true + }, + "connectionAttempted": false, + "connectionResult": "not_attempted", + "liveConnected": false, + "valuesRedacted": true, + "secretMaterialRead": false, + "liveDbEvidence": false, + "blocker": "cloud-api DB env is injected, but runtime health has not attempted a live DB connection", + "evidence": [ + { + "configReady": true, + "missingEnv": [], + "connectionAttempted": false, + "connectionResult": "not_attempted", + "liveConnected": false, + "endpointRedacted": true, + "valueRedacted": true + } + ] + }, + "status": "blocker", + "classification": "app_health_blocker", + "blocker": "cloud-api DB env is injected, but runtime health has not attempted a live DB connection", + "diagnosis": { + "likelyLayer": "cloud-api-db", + "confidence": "high", + "likelyCause": "cloud-api DB env is injected, but runtime health has not attempted a live DB connection", + "evidence": [ + { + "configReady": true, + "missingEnv": [], + "connectionAttempted": false, + "connectionResult": "not_attempted", + "liveConnected": false, + "endpointRedacted": true, + "valueRedacted": true + } + ], + "notProven": [ + "live DB endpoint reachability", + "authenticated SQL query readiness" + ], + "nextTask": "Deploy cloud-api DB runtime readiness probe and/or repair DEV DB connectivity, then rerun the read-only health smoke without reading or printing the DB secret value." + } } } diff --git a/reports/dev-gate/dev-gate-report.example.json b/reports/dev-gate/dev-gate-report.example.json index 20bf9274..993c8a02 100644 --- a/reports/dev-gate/dev-gate-report.example.json +++ b/reports/dev-gate/dev-gate-report.example.json @@ -43,7 +43,7 @@ "devPreconditions": { "status": "blocked", "requirements": [ - "DEV route remains frozen at http://74.48.78.17:6667", + "DEV route remains frozen at http://74.48.78.17:16667", "Runtime evidence names frozen HWLAB service IDs", "No PROD path, secret read, or real deployment is attempted" ], diff --git a/reports/dev-gate/dev-m3-hardware-loop.json b/reports/dev-gate/dev-m3-hardware-loop.json index c8f7e640..69364ada 100644 --- a/reports/dev-gate/dev-m3-hardware-loop.json +++ b/reports/dev-gate/dev-m3-hardware-loop.json @@ -26,7 +26,7 @@ "node scripts/validate-dev-gate-report.mjs" ], "runtimeTarget": { - "endpoint": "http://74.48.78.17:6667", + "endpoint": "http://74.48.78.17:16667", "namespace": "hwlab-dev", "environment": "dev", "requiredBoxSimulators": 2, @@ -57,21 +57,21 @@ { "id": "endpoint-freeze", "status": "pass", - "summary": "DEV endpoint is frozen at http://74.48.78.17:6667.", + "summary": "DEV endpoint is frozen at http://74.48.78.17:16667.", "evidence": [ - "http://74.48.78.17:6667" + "http://74.48.78.17:16667" ] }, { "id": "dev-ingress-health", "status": "blocked", "blockerClass": "network_blocker", - "summary": "DEV ingress at http://74.48.78.17:6667 did not return reachable HWLAB DEV health.", + "summary": "DEV ingress at http://74.48.78.17:16667 did not return reachable HWLAB DEV health.", "evidence": [ - "{\"url\":\"http://74.48.78.17:6667/health/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T17:29:33.794Z\",\"observedAt\":\"2026-05-21T17:29:33.964Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}", - "{\"url\":\"http://74.48.78.17:6667/health\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T17:29:33.964Z\",\"observedAt\":\"2026-05-21T17:29:34.132Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}", - "{\"url\":\"http://74.48.78.17:6667/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T17:29:34.132Z\",\"observedAt\":\"2026-05-21T17:29:34.313Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}", - "{\"url\":\"http://74.48.78.17:6667/v1\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T17:29:34.314Z\",\"observedAt\":\"2026-05-21T17:29:34.479Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:6667\",\"code\":\"ECONNREFUSED\"}}" + "{\"url\":\"http://74.48.78.17:16667/health/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T17:29:33.794Z\",\"observedAt\":\"2026-05-21T17:29:33.964Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:16667\",\"code\":\"ECONNREFUSED\"}}", + "{\"url\":\"http://74.48.78.17:16667/health\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T17:29:33.964Z\",\"observedAt\":\"2026-05-21T17:29:34.132Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:16667\",\"code\":\"ECONNREFUSED\"}}", + "{\"url\":\"http://74.48.78.17:16667/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T17:29:34.132Z\",\"observedAt\":\"2026-05-21T17:29:34.313Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:16667\",\"code\":\"ECONNREFUSED\"}}", + "{\"url\":\"http://74.48.78.17:16667/v1\",\"method\":\"GET\",\"startedAt\":\"2026-05-21T17:29:34.314Z\",\"observedAt\":\"2026-05-21T17:29:34.479Z\",\"reached\":false,\"ok\":false,\"error\":{\"name\":\"Error\",\"message\":\"connect ECONNREFUSED 74.48.78.17:16667\",\"code\":\"ECONNREFUSED\"}}" ] }, { diff --git a/reports/dev-gate/dev-m4-agent-loop.json b/reports/dev-gate/dev-m4-agent-loop.json index a1228cd7..c59346f5 100644 --- a/reports/dev-gate/dev-m4-agent-loop.json +++ b/reports/dev-gate/dev-m4-agent-loop.json @@ -47,18 +47,18 @@ "devPreconditions": { "status": "blocked", "requirements": [ - "DEV route remains frozen at http://74.48.78.17:6667", + "DEV route remains frozen at http://74.48.78.17:16667", "No secret reads, real deployment, or long-running agent task is attempted", "Agent manager, worker, and skills are reachable only through the cloud API boundary" ], - "summary": "Blocked before agent scheduling because http://74.48.78.17:6667 is unreachable from this runner." + "summary": "Blocked before agent scheduling because http://74.48.78.17:16667 is unreachable from this runner." }, "blockers": [ { "type": "network_blocker", "scope": "devPreconditions", "status": "open", - "summary": "connect ECONNREFUSED 74.48.78.17:6667" + "summary": "connect ECONNREFUSED 74.48.78.17:16667" } ], "livePreflight": { @@ -69,7 +69,7 @@ "evidence": [ "No live DEV observation was recorded." ], - "summary": "Blocked before agent scheduling because http://74.48.78.17:6667 is unreachable from this runner." + "summary": "Blocked before agent scheduling because http://74.48.78.17:16667 is unreachable from this runner." }, "notes": "DEV M4 agent loop live preflight report. No secret material is read." } diff --git a/reports/dev-gate/dev-m5-gate-aggregator-v2.json b/reports/dev-gate/dev-m5-gate-aggregator-v2.json index 035f3f0d..c9066757 100644 --- a/reports/dev-gate/dev-m5-gate-aggregator-v2.json +++ b/reports/dev-gate/dev-m5-gate-aggregator-v2.json @@ -20,7 +20,7 @@ "generatedAt": "2026-05-21T18:27:07.237Z", "generatedFromCommit": "1e9591a6611e", "environment": "dev", - "endpoint": "http://74.48.78.17:6667", + "endpoint": "http://74.48.78.17:16667", "devOnly": true, "prodDisabled": true, "safety": { @@ -128,7 +128,7 @@ "id": "dev-edge-frp-6667", "status": "blocked", "evidenceLevel": "BLOCKED", - "summary": "No committed report proves live HTTP 200/JSON on http://74.48.78.17:6667." + "summary": "No committed report proves live HTTP 200/JSON on http://74.48.78.17:16667." }, { "id": "cloud-api-db-ready", @@ -459,7 +459,7 @@ "evidence": [ "No live DEV observation was recorded." ], - "summary": "Blocked before agent scheduling because http://74.48.78.17:6667 is unreachable from this runner." + "summary": "Blocked before agent scheduling because http://74.48.78.17:16667 is unreachable from this runner." }, { "milestone": "M5", @@ -476,7 +476,7 @@ "node scripts/m5-mvp-e2e-dry-run.mjs" ], "evidence": [ - "hwlab-cli health -> status: ok, environment: dev, endpoint: http://74.48.78.17:6667, projects: 2", + "hwlab-cli health -> status: ok, environment: dev, endpoint: http://74.48.78.17:16667, projects: 2", "hwlab-cli dry-run -> 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls", "M5 MVP E2E dry-run passed: 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls" ], @@ -492,8 +492,8 @@ "status": "blocked", "category": "mvp-e2e-live", "commands": [ - "curl -fsS --max-time 5 http://74.48.78.17:6667/health", - "curl -fsS --max-time 5 http://74.48.78.17:6667/live" + "curl -fsS --max-time 5 http://74.48.78.17:16667/health", + "curl -fsS --max-time 5 http://74.48.78.17:16667/live" ], "evidence": [ "curl exit 7 for /health: could not connect to 74.48.78.17 port 6667", @@ -631,7 +631,7 @@ "status": "blocked", "category": "agent-loop-live-preflight", "reportPath": "reports/dev-gate/dev-m4-agent-loop.json", - "summary": "Blocked before agent scheduling because http://74.48.78.17:6667 is unreachable from this runner." + "summary": "Blocked before agent scheduling because http://74.48.78.17:16667 is unreachable from this runner." }, { "milestone": "M5", @@ -779,7 +779,7 @@ "scope": "dev-edge", "sourceIssue": "pikasTech/HWLAB#34", "source": "reports/dev-gate/dev-preflight-report.json", - "summary": "http://74.48.78.17:6667/health/live is not reachable from this runner.", + "summary": "http://74.48.78.17:16667/health/live is not reachable from this runner.", "nextTask": "Bring up or repair the D601-to-master frp route and hwlab-edge-proxy, then rerun the health probe." }, { @@ -808,7 +808,7 @@ "scope": "devPreconditions", "sourceIssue": "pikasTech/HWLAB#37", "source": "reports/dev-gate/dev-m4-agent-loop.json", - "summary": "connect ECONNREFUSED 74.48.78.17:6667" + "summary": "connect ECONNREFUSED 74.48.78.17:16667" }, { "priority": "P2", @@ -817,7 +817,7 @@ "scope": "devPreconditions", "sourceIssue": "pikasTech/HWLAB#39", "source": "reports/dev-gate/dev-mvp-gate-report.json", - "summary": "http://74.48.78.17:6667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing." + "summary": "http://74.48.78.17:16667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing." } ] }, @@ -1068,7 +1068,7 @@ "status": "open", "source": "reports/dev-gate/dev-preflight-report.json", "sourceIssue": "pikasTech/HWLAB#34", - "summary": "http://74.48.78.17:6667/health/live is not reachable from this runner.", + "summary": "http://74.48.78.17:16667/health/live is not reachable from this runner.", "nextTask": "Bring up or repair the D601-to-master frp route and hwlab-edge-proxy, then rerun the health probe.", "unblockOrder": 6, "unblocks": [ @@ -1124,7 +1124,7 @@ "status": "open", "source": "reports/dev-gate/dev-m4-agent-loop.json", "sourceIssue": "pikasTech/HWLAB#37", - "summary": "connect ECONNREFUSED 74.48.78.17:6667", + "summary": "connect ECONNREFUSED 74.48.78.17:16667", "unblockOrder": 6, "unblocks": [ "pikasTech/HWLAB#36", @@ -1142,7 +1142,7 @@ "status": "open", "source": "reports/dev-gate/dev-mvp-gate-report.json", "sourceIssue": "pikasTech/HWLAB#39", - "summary": "http://74.48.78.17:6667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing.", + "summary": "http://74.48.78.17:16667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing.", "unblockOrder": 6, "unblocks": [ "pikasTech/HWLAB#36", diff --git a/reports/dev-gate/dev-m5-gate-aggregator-v2.md b/reports/dev-gate/dev-m5-gate-aggregator-v2.md index b070d005..e06651af 100644 --- a/reports/dev-gate/dev-m5-gate-aggregator-v2.md +++ b/reports/dev-gate/dev-m5-gate-aggregator-v2.md @@ -27,7 +27,7 @@ SOURCE, LOCAL, and DRY-RUN evidence exists, but DEV-LIVE acceptance is blocked b | m1-local-smoke | pass | LOCAL | M1 local smoke is not a live DEV substitute. | | artifact-publish-digests | blocked | BLOCKED | artifactState=contract-skeleton, ciPublished=false, registryVerified=false, sha256=0, not_published=13 | | d601-k3s-observability | blocked | BLOCKED | D601 runner lacks kubectl/k3s/kubeconfig observability for hwlab-dev. | -| dev-edge-frp-6667 | blocked | BLOCKED | No committed report proves live HTTP 200/JSON on http://74.48.78.17:6667. | +| dev-edge-frp-6667 | blocked | BLOCKED | No committed report proves live HTTP 200/JSON on http://74.48.78.17:16667. | | cloud-api-db-ready | blocked | BLOCKED | Manifest-level DB env exists, but live DB health readiness is still blocked/missing. | | m5-mvp-dev-live | blocked | BLOCKED | M5 dry-run passed; bounded DEV-LIVE MVP e2e has not passed. | @@ -48,11 +48,11 @@ SOURCE, LOCAL, and DRY-RUN evidence exists, but DEV-LIVE acceptance is blocked b | P1 | 4 | environment_blocker | d601-k3s | D601 runner lacks kubectl and no default kubeconfig was used by this preflight. | | P1 | 5 | runtime_blocker | cloud-api-db | cloud-api health reports DB config blocked; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE | | P1 | 5 | runtime_blocker | cloud-api-db-health-gate | cloud-api DB runtime env is not ready; missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE. | -| P2 | 6 | network_blocker | dev-edge | http://74.48.78.17:6667/health/live is not reachable from this runner. | +| P2 | 6 | network_blocker | dev-edge | http://74.48.78.17:16667/health/live is not reachable from this runner. | | P2 | 6 | network_blocker | dev-edge-health | live network probes require --live | | P2 | 6 | network_blocker | dev-ingress-health | Blocked at the #33 DEV runtime readiness condition: public DEV ingress does not accept HWLAB health requests, so #36/M3 hardware-loop checks were not reached. | -| P2 | 6 | network_blocker | devPreconditions | connect ECONNREFUSED 74.48.78.17:6667 | -| P2 | 6 | network_blocker | devPreconditions | http://74.48.78.17:6667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing. | +| P2 | 6 | network_blocker | devPreconditions | connect ECONNREFUSED 74.48.78.17:16667 | +| P2 | 6 | network_blocker | devPreconditions | http://74.48.78.17:16667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing. | ## Next Unblock Order diff --git a/reports/dev-gate/dev-mvp-gate-report.json b/reports/dev-gate/dev-mvp-gate-report.json index cdd1a7c3..53c80885 100644 --- a/reports/dev-gate/dev-mvp-gate-report.json +++ b/reports/dev-gate/dev-mvp-gate-report.json @@ -58,7 +58,7 @@ "validated 2 evidence chain fixtures and 2 evidence records", "[m1-smoke] passed", "[m2-smoke] validated 11 service contracts", - "[m2-smoke] mode=dry-run endpoint=http://74.48.78.17:6667", + "[m2-smoke] mode=dry-run endpoint=http://74.48.78.17:16667", "[m2-smoke] no real DEV/PROD request was made", "M3 hardware loop smoke passed", "[m4-smoke] ok m4-agent-automation-loop-local-contract-smoke" @@ -73,7 +73,7 @@ "node scripts/m5-mvp-e2e-dry-run.mjs" ], "evidence": [ - "hwlab-cli health -> status: ok, environment: dev, endpoint: http://74.48.78.17:6667, projects: 2", + "hwlab-cli health -> status: ok, environment: dev, endpoint: http://74.48.78.17:16667, projects: 2", "hwlab-cli dry-run -> 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls", "M5 MVP E2E dry-run passed: 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls" ], @@ -82,13 +82,13 @@ "devPreconditions": { "status": "blocked", "requirements": [ - "Obtain a live HTTP 200/JSON health observation from http://74.48.78.17:6667/health and /live", + "Obtain a live HTTP 200/JSON health observation from http://74.48.78.17:16667/health and /live", "Record live route observations for hwlab-edge-proxy, hwlab-tunnel-client, hwlab-router, hwlab-cloud-api/web, hwlab-gateway, hwlab-gateway-simu, hwlab-box-simu, hwlab-patch-panel, hwlab-agent-mgr, hwlab-agent-worker, and hwlab-agent-skills", "Keep the run DEV-only and avoid PROD, heavy e2e, secrets, restarts, or UniDesk substitution" ], "commands": [ - "curl -fsS --max-time 5 http://74.48.78.17:6667/health", - "curl -fsS --max-time 5 http://74.48.78.17:6667/live" + "curl -fsS --max-time 5 http://74.48.78.17:16667/health", + "curl -fsS --max-time 5 http://74.48.78.17:16667/live" ], "evidence": [ "curl exit 7 for /health: could not connect to 74.48.78.17 port 6667", @@ -101,7 +101,7 @@ "type": "network_blocker", "scope": "devPreconditions", "status": "open", - "summary": "http://74.48.78.17:6667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing." + "summary": "http://74.48.78.17:16667/health and /live both fail with curl exit 7; live DEV ingress evidence is missing." } ], "milestones": [ @@ -148,7 +148,7 @@ ], "evidence": [ "[m2-smoke] validated 11 service contracts", - "[m2-smoke] mode=dry-run endpoint=http://74.48.78.17:6667", + "[m2-smoke] mode=dry-run endpoint=http://74.48.78.17:16667", "[m2-smoke] no real DEV/PROD request was made" ], "summary": "The deploy smoke remains fixture-only and validates the frozen route phases and artifact metadata." @@ -186,7 +186,7 @@ "node scripts/m5-mvp-e2e-dry-run.mjs" ], "evidence": [ - "hwlab-cli health -> status: ok, environment: dev, endpoint: http://74.48.78.17:6667, projects: 2", + "hwlab-cli health -> status: ok, environment: dev, endpoint: http://74.48.78.17:16667, projects: 2", "hwlab-cli dry-run -> 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls", "M5 MVP E2E dry-run passed: 9 steps, 13 artifacts, 14 health contracts, 2 hardware operations, 2 evidence records, 0 network calls" ], diff --git a/reports/dev-gate/dev-mvp-gate-report.md b/reports/dev-gate/dev-mvp-gate-report.md index 771641dd..255f8336 100644 --- a/reports/dev-gate/dev-mvp-gate-report.md +++ b/reports/dev-gate/dev-mvp-gate-report.md @@ -7,7 +7,7 @@ Scope: DEV only ## Summary -M0-M5 local contract, smoke, and dry-run checks pass on the frozen `origin/main` baseline. The live DEV gate is still blocked because `http://74.48.78.17:6667` is not reachable from this runner. +M0-M5 local contract, smoke, and dry-run checks pass on the frozen `origin/main` baseline. The live DEV gate is still blocked because `http://74.48.78.17:16667` is not reachable from this runner. ## Evidence @@ -24,8 +24,8 @@ M0-M5 local contract, smoke, and dry-run checks pass on the frozen `origin/main` - `node tools/hwlab-cli/bin/hwlab-cli.mjs health` - `node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run` - `node scripts/m5-mvp-e2e-dry-run.mjs` -- `curl -fsS --max-time 5 http://74.48.78.17:6667/health` -- `curl -fsS --max-time 5 http://74.48.78.17:6667/live` +- `curl -fsS --max-time 5 http://74.48.78.17:16667/health` +- `curl -fsS --max-time 5 http://74.48.78.17:16667/live` ## Result @@ -39,7 +39,7 @@ M0-M5 local contract, smoke, and dry-run checks pass on the frozen `origin/main` ## Minimal Next Step -Obtain live `200`/JSON health observations from `http://74.48.78.17:6667/health` and `/live`, then capture the live route observations for the DEV ingress chain without switching to PROD or heavy e2e. +Obtain live `200`/JSON health observations from `http://74.48.78.17:16667/health` and `/live`, then capture the live route observations for the DEV ingress chain without switching to PROD or heavy e2e. ## Blocker diff --git a/reports/dev-gate/dev-preflight-report.json b/reports/dev-gate/dev-preflight-report.json index 957f43db..bb5dc83c 100644 --- a/reports/dev-gate/dev-preflight-report.json +++ b/reports/dev-gate/dev-preflight-report.json @@ -613,7 +613,7 @@ "evidence": [ { "ok": false, - "url": "http://74.48.78.17:6667/health/live", + "url": "http://74.48.78.17:16667/health/live", "method": "GET", "error": "fetch failed" } @@ -796,7 +796,7 @@ "status": "open", "type": "network_blocker", "scope": "dev-edge", - "summary": "http://74.48.78.17:6667/health/live is not reachable from this runner.", + "summary": "http://74.48.78.17:16667/health/live is not reachable from this runner.", "nextTask": "Bring up or repair the D601-to-master frp route and hwlab-edge-proxy, then rerun the health probe." }, { diff --git a/scripts/cloud-api-runtime-smoke.mjs b/scripts/cloud-api-runtime-smoke.mjs index 28ce1137..c9d47ed2 100644 --- a/scripts/cloud-api-runtime-smoke.mjs +++ b/scripts/cloud-api-runtime-smoke.mjs @@ -86,6 +86,9 @@ async function run() { assert.equal(health.body.status, "degraded"); assert.equal(health.body.db.status, "blocked"); assert.equal(health.body.db.connected, false); + assert.equal(health.body.db.liveConnected, false); + assert.equal(health.body.db.connectionAttempted, false); + assert.equal(health.body.db.connectionResult, "not_attempted_missing_env"); assert.equal(health.body.db.ready, false); assert.deepEqual(health.body.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]); assert.equal(health.body.runtime.durable, false); diff --git a/scripts/m1-contract-smoke.mjs b/scripts/m1-contract-smoke.mjs index e344a574..f8f5a65e 100644 --- a/scripts/m1-contract-smoke.mjs +++ b/scripts/m1-contract-smoke.mjs @@ -153,6 +153,9 @@ async function smokeCloudApi() { assert.equal(health.body.environment, ENVIRONMENT_DEV); assert.equal(health.body.status, "degraded"); assert.equal(health.body.db.connected, false); + assert.equal(health.body.db.liveConnected, false); + assert.equal(health.body.db.connectionAttempted, false); + assert.equal(health.body.db.connectionResult, "not_attempted_missing_env"); assert.equal(health.body.db.status, "blocked"); assert.equal(health.body.runtime.durable, false); assert.deepEqual(health.body.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]); diff --git a/scripts/src/dev-edge-health-smoke-lib.mjs b/scripts/src/dev-edge-health-smoke-lib.mjs index 6f2ee984..b0c862da 100644 --- a/scripts/src/dev-edge-health-smoke-lib.mjs +++ b/scripts/src/dev-edge-health-smoke-lib.mjs @@ -5,7 +5,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { summarizeDbContract } from "../../internal/cloud/db-contract.mjs"; +import { DEV_DB_ENV_CONTRACT, summarizeDbContract } from "../../internal/cloud/db-contract.mjs"; import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); @@ -33,7 +33,8 @@ export async function runDevEdgeHealthSmoke(argv) { status: "not_run", classification: "not_run", blocker: "live network probes require --live", - contracts: await inspectContracts() + contracts: await inspectContracts(), + runtimeDbReadiness: notRunDbReadiness("live network probes require --live") }; const report = await createDevGateReport(edgeHealth); await maybeWriteReport(report, args); @@ -65,6 +66,7 @@ export async function runDevEdgeHealthSmoke(argv) { kubernetes, clusterDns }; + edgeHealth.runtimeDbReadiness = inspectRuntimeDbReadiness(edgeHealth); Object.assign(edgeHealth, classify(edgeHealth)); const report = await createDevGateReport(edgeHealth); await maybeWriteReport(report, args); @@ -250,22 +252,39 @@ async function inspectContracts() { function inspectCloudApiDbContract(deploy) { const env = deploy.services?.find((service) => service.serviceId === "hwlab-cloud-api")?.env ?? {}; const configReady = Boolean(env.HWLAB_CLOUD_DB_URL && env.HWLAB_CLOUD_DB_SSL_MODE); + const connection = { + attempted: false, + networkAttempted: false, + result: "manifest_only_not_attempted", + classification: "manifest_only", + probeType: "tcp-connect", + endpointRedacted: true, + valueRedacted: true, + timeoutMs: null, + durationMs: 0, + errorCode: null + }; const db = { status: configReady ? "degraded" : "blocked", ready: false, configReady, connected: false, + liveConnected: false, connectionChecked: false, + connectionAttempted: false, + connectionResult: connection.result, fields: [ { name: "HWLAB_CLOUD_DB_URL", present: Boolean(env.HWLAB_CLOUD_DB_URL), - redacted: true + redacted: true, + source: "k8s-secret-ref" }, { name: "HWLAB_CLOUD_DB_SSL_MODE", present: Boolean(env.HWLAB_CLOUD_DB_SSL_MODE), - redacted: false + redacted: false, + source: "runtime-env" } ], missingEnv: ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"].filter((name) => !env[name]), @@ -275,18 +294,143 @@ function inspectCloudApiDbContract(deploy) { secretName: "hwlab-cloud-api-dev-db", secretKey: "database-url", present: env.HWLAB_CLOUD_DB_URL === "secretRef:hwlab-cloud-api-dev-db/database-url", + envInjected: false, + secretPresent: "not_observed_by_manifest", + secretKeyPresent: "declared", redacted: true } ], + connection, safety: { + secretsRead: false, + valuesRedacted: true, liveDbEvidence: false - } + }, + blocker: configReady + ? "Manifest declares DB env/Secret references but does not prove runtime env injection or live DB connection" + : "Manifest is missing DB env/Secret references" }; return { ...summarizeDbContract(db), + manifestConfigReady: configReady, + envInjected: false, secretMaterialRead: false, valuesRedacted: true, - note: "Manifest-only DB env placeholder; this is not live DB evidence." + note: "Manifest-only DB env placeholder; this is not runtime env injection or live DB evidence." + }; +} + +function notRunDbReadiness(blockerMessage) { + return { + status: "not_run", + configReady: false, + envInjected: false, + secret: unknownDbSecretPresence("not_observed"), + connectionAttempted: false, + connectionResult: "not_run", + liveConnected: false, + valuesRedacted: true, + secretMaterialRead: false, + liveDbEvidence: false, + blocker: blockerMessage, + evidence: [] + }; +} + +function inspectRuntimeDbReadiness(edgeHealth) { + const db = selectRuntimeDb(edgeHealth.publicHttp); + return summarizeRuntimeDbReadiness(db, edgeHealth.kubernetes?.dbSecret ?? unknownDbSecretPresence("not_observed")); +} + +function selectRuntimeDb(publicHttp) { + const live = publicHttp.find((probe) => probe.url.endsWith("/health/live") && probe.ok && probe.json?.db); + const health = publicHttp.find((probe) => probe.url.endsWith("/health") && probe.ok && probe.json?.db); + return live?.json?.db ?? health?.json?.db ?? null; +} + +function summarizeRuntimeDbReadiness(db, secretPresence = unknownDbSecretPresence("not_observed")) { + if (!db) { + return { + status: "blocked", + configReady: false, + envInjected: false, + secret: secretPresence, + connectionAttempted: false, + connectionResult: "health_db_missing", + liveConnected: false, + valuesRedacted: true, + secretMaterialRead: false, + liveDbEvidence: false, + blocker: "cloud-api health is reachable but does not expose redacted DB readiness fields", + evidence: [] + }; + } + + const requiredEnv = Array.isArray(db.fields) ? db.fields : []; + const envInjected = DEV_DB_ENV_CONTRACT.requiredEnv.every((name) => + requiredEnv.some((field) => field.name === name && field.present === true) + ); + const connectionAttempted = Boolean(db.connectionAttempted ?? db.connectionChecked); + const connectionResult = db.connectionResult ?? db.connection?.result ?? (connectionAttempted ? "unknown" : "not_attempted"); + const liveConnected = Boolean(db.liveConnected ?? db.connected ?? db.ready); + const blockerMessage = liveConnected ? null : db.blocker ?? blockerForRuntimeDb(db, connectionAttempted, connectionResult); + const status = liveConnected ? "pass" : "blocked"; + const secretRef = Array.isArray(db.secretRefs) ? db.secretRefs[0] : null; + + return { + status, + configReady: Boolean(db.configReady), + envInjected, + secret: { + ...secretPresence, + secretName: secretRef?.secretName ?? DEV_DB_ENV_CONTRACT.secretRefs[0].secretName, + secretKey: secretRef?.secretKey ?? DEV_DB_ENV_CONTRACT.secretRefs[0].secretKey, + secretRefPresent: Boolean(secretRef?.present), + envInjected: Boolean(secretRef?.envInjected ?? secretRef?.present), + redacted: true + }, + connectionAttempted, + connectionResult, + liveConnected, + valuesRedacted: db.safety?.valuesRedacted === true, + secretMaterialRead: false, + liveDbEvidence: Boolean(db.safety?.liveDbEvidence), + blocker: blockerMessage, + evidence: [ + { + configReady: Boolean(db.configReady), + missingEnv: Array.isArray(db.missingEnv) ? db.missingEnv : [], + connectionAttempted, + connectionResult, + liveConnected, + endpointRedacted: db.connection?.endpointRedacted !== false, + valueRedacted: true + } + ] + }; +} + +function blockerForRuntimeDb(db, connectionAttempted, connectionResult) { + if (db.configReady !== true) { + const missing = Array.isArray(db.missingEnv) && db.missingEnv.length > 0 ? db.missingEnv.join(", ") : "required DB env"; + return `cloud-api DB runtime env is not ready; missing ${missing}`; + } + if (!connectionAttempted) { + return "cloud-api DB env is injected, but runtime health has not attempted a live DB connection"; + } + return `cloud-api DB live connection is blocked; connectionResult=${connectionResult}`; +} + +function unknownDbSecretPresence(state) { + const ref = DEV_DB_ENV_CONTRACT.secretRefs[0]; + return { + observable: false, + secretName: ref.secretName, + secretKey: ref.secretKey, + secretPresent: state, + secretKeyPresent: state, + secretValueRead: false, + redacted: true }; } @@ -299,6 +443,7 @@ async function inspectKubernetes() { services: [], endpoints: [], pods: [], + dbSecret: unknownDbSecretPresence("not_observed"), notes: [] }; if (!hasKubectl) { @@ -316,6 +461,7 @@ async function inspectKubernetes() { await collectKubectlJson(result, "pods", [ "-n", namespace, "get", "pods", "-l", "hwlab.pikastech.local/profile=dev", "-o", "json" ]); + await collectDbSecretPresence(result); return result; } @@ -363,6 +509,46 @@ async function collectKubectlJson(result, kind, args) { } } +async function collectDbSecretPresence(result) { + const ref = DEV_DB_ENV_CONTRACT.secretRefs[0]; + const base = { + observable: true, + secretName: ref.secretName, + secretKey: ref.secretKey, + secretPresent: false, + secretKeyPresent: false, + secretValueRead: false, + redacted: true + }; + const exists = await runCommand("kubectl", ["-n", namespace, "get", "secret", ref.secretName, "-o", "name"], { + timeoutMs: 5000 + }); + if (exists.exitCode !== 0) { + result.dbSecret = { + ...base, + command: "kubectl get secret -o name", + error: exists.stderr.trim() || "secret not observed" + }; + result.notes.push(`kubectl DB Secret presence probe failed: ${exists.stderr.trim()}`); + return; + } + + const keyPresence = await runCommand("kubectl", ["-n", namespace, "describe", "secret", ref.secretName], { + timeoutMs: 5000 + }); + const keyLinePattern = new RegExp(`^\\s*${escapeRegExp(ref.secretKey)}:\\s+\\d+\\s+bytes\\s*$`, "mu"); + result.dbSecret = { + ...base, + secretPresent: true, + secretKeyPresent: keyPresence.exitCode === 0 && keyLinePattern.test(keyPresence.stdout), + command: "kubectl describe secret (key-presence-only)", + error: keyPresence.exitCode === 0 ? null : keyPresence.stderr.trim() + }; + if (keyPresence.exitCode !== 0) { + result.notes.push(`kubectl DB Secret key presence probe failed: ${keyPresence.stderr.trim()}`); + } +} + async function inspectClusterDns() { const results = []; for (const service of runtimeServices) { @@ -386,7 +572,7 @@ function classify(report) { const publicHealth = report.publicHttp.find((probe) => probe.url.endsWith("/health")); if (publicHealth?.ok && publicHealth.json?.serviceId && publicHealth.json?.environment === ENVIRONMENT_DEV) { - return classifyReachableHealth(publicHealth.json); + return classifyReachableHealth(publicHealth.json, selectRuntimeDb(report.publicHttp)); } if (publicEdgePort?.status === "error" && publicEdgePort.code === "ECONNREFUSED") { return classifyRefusedPort(publicEdgePort, frpsControl, tunnelHealth); @@ -403,7 +589,7 @@ function classify(report) { return blocker("dns_port_firewall_blocker", publicHealth?.error?.message ?? "public DEV health did not return a usable response"); } -function classifyReachableHealth(json) { +function classifyReachableHealth(json, runtimeDb = null) { const missingEvidence = []; if (!json.commit) missingEvidence.push("commit"); if (!json.image) missingEvidence.push("image"); @@ -411,6 +597,19 @@ function classifyReachableHealth(json) { if (missingEvidence.length > 0) { return blocker("app_health_blocker", `public /health is reachable but missing ${missingEvidence.join(", ")} evidence`); } + const dbReadiness = summarizeRuntimeDbReadiness(runtimeDb ?? json.db); + if (dbReadiness.status !== "pass") { + return blocker("app_health_blocker", dbReadiness.blocker, { + likelyLayer: "cloud-api-db", + confidence: dbReadiness.configReady ? "high" : "medium", + likelyCause: dbReadiness.blocker, + evidence: dbReadiness.evidence, + notProven: dbReadiness.connectionAttempted + ? ["authenticated SQL query readiness"] + : ["live DB endpoint reachability", "authenticated SQL query readiness"], + nextTask: "Deploy cloud-api DB runtime readiness probe and/or repair DEV DB connectivity, then rerun the read-only health smoke without reading or printing the DB secret value." + }); + } return { status: "pass", classification: "none", blocker: null }; } @@ -491,6 +690,10 @@ function summarizeTcpProbe(probe) { }; } +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); +} + async function commandExists(command) { const result = await runCommand("command", ["-v", command], { timeoutMs: 3000 }); return result.exitCode === 0; diff --git a/scripts/validate-dev-gate-report.mjs b/scripts/validate-dev-gate-report.mjs index 4de283d9..11d25057 100644 --- a/scripts/validate-dev-gate-report.mjs +++ b/scripts/validate-dev-gate-report.mjs @@ -242,6 +242,40 @@ function assertTimestamp(value, label) { assert.ok(!Number.isNaN(Date.parse(value)), `${label} must be an RFC 3339 timestamp`); } +function assertDbLayerBooleans(value, label) { + assertObject(value, label); + for (const field of ["configReady", "liveConnected", "connectionAttempted"]) { + assertBoolean(value[field], `${label}.${field}`); + } + assertString(value.connectionResult, `${label}.connectionResult`); + assertNoDbSecretMaterial(value, label); +} + +function assertDbRuntimeReadiness(value, label) { + assertDbLayerBooleans(value, label); + for (const field of ["envInjected", "valuesRedacted", "secretMaterialRead", "liveDbEvidence"]) { + assertBoolean(value[field], `${label}.${field}`); + } + assert.equal(value.valuesRedacted, true, `${label}.valuesRedacted`); + assert.equal(value.secretMaterialRead, false, `${label}.secretMaterialRead`); + assertObject(value.secret, `${label}.secret`); + assertString(value.secret.secretName, `${label}.secret.secretName`); + assertString(value.secret.secretKey, `${label}.secret.secretKey`); + assert.equal(value.secret.redacted, true, `${label}.secret.redacted`); + assert.equal(value.secret.secretValueRead, false, `${label}.secret.secretValueRead`); + assertArray(value.evidence, `${label}.evidence`); + if (value.status === "blocked") { + assertString(value.blocker, `${label}.blocker`); + } +} + +function assertNoDbSecretMaterial(value, label) { + const serialized = JSON.stringify(value); + for (const forbidden of ["postgres://", "postgresql://", "password=", "://user:"]) { + assert.equal(serialized.includes(forbidden), false, `${label} must not include ${forbidden}`); + } +} + function assertRepoRelativePath(value, label) { assertString(value, label); assert.ok(!path.isAbsolute(value), `${label} must be repo-relative`); @@ -274,6 +308,9 @@ async function readJsonFile(relativePath) { } async function collectReportFiles() { + if (process.argv.length > 2) { + return process.argv.slice(2).map((entry) => path.relative(repoRoot, path.resolve(process.cwd(), entry))); + } const entries = await readdir(reportsDir, { withFileTypes: true }); return entries .filter((entry) => entry.isFile() && entry.name.endsWith(".json")) @@ -824,6 +861,14 @@ async function validateDevEdgeReport(report, label) { ); assertString(report.edgeHealth.classification, `${label}.edgeHealth.classification`); assertObject(report.edgeHealth.contracts, `${label}.edgeHealth.contracts`); + assertDbLayerBooleans( + report.edgeHealth.contracts?.deploy?.cloudApiDb, + `${label}.edgeHealth.contracts.deploy.cloudApiDb` + ); + assertDbRuntimeReadiness( + report.edgeHealth.runtimeDbReadiness, + `${label}.edgeHealth.runtimeDbReadiness` + ); assertArray(report.edgeHealth.publicTcp ?? [], `${label}.edgeHealth.publicTcp`); assertArray(report.edgeHealth.publicHttp ?? [], `${label}.edgeHealth.publicHttp`); assertObject(report.edgeHealth.kubernetes ?? {}, `${label}.edgeHealth.kubernetes`);