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.
This commit is contained in:
Lyon
2026-05-22 12:27:01 +08:00
committed by GitHub
parent 4ebb1e4aa6
commit 357a68bb95
29 changed files with 877 additions and 196 deletions
+2 -1
View File
@@ -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.
+1 -1
View File
@@ -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",
+17 -5
View File
@@ -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`.
+14 -16
View File
@@ -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
+5 -2
View File
@@ -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
@@ -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,
+1 -1
View File
@@ -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
+6 -6
View File
@@ -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",
+1 -1
View File
@@ -48,7 +48,7 @@
"worker cleanup"
],
"endpoints": {
"dev": "http://74.48.78.17:6667"
"dev": "http://74.48.78.17:16667"
},
"loops": [
{
+309 -9
View File
@@ -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
};
}
+5 -5
View File
@@ -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()
};
}
+5 -5
View File
@@ -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;
+23 -8
View File
@@ -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()));
});
}
});
@@ -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."
}
},
@@ -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": {
+1 -1
View File
@@ -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
+177 -79
View File
@@ -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 <name> (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 <name> (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."
}
}
}
@@ -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"
],
+8 -8
View File
@@ -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\"}}"
]
},
{
+4 -4
View File
@@ -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."
}
+13 -13
View File
@@ -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",
@@ -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
+8 -8
View File
@@ -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"
],
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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."
},
{
+3
View File
@@ -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);
+3
View File
@@ -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"]);
+211 -8
View File
@@ -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 <name> (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;
+45
View File
@@ -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`);