From 85da2dcb1917e8d8e96682111ea606a18d571ba7 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Fri, 22 May 2026 20:44:30 +0800 Subject: [PATCH] fix: separate runner kubeconfig gap from D601 status (#144) Commander review: direction is correct. This PR separates runner kubeconfig/read-only observability gaps from D601 public endpoint/k3s availability and keeps M3 blocked until real operation/trace/audit/evidence are observed. It does not touch PROD or restart services. --- reports/d601-k3s-readonly-observability.json | 92 +++++++- reports/dev-gate/dev-m3-hardware-loop.json | 78 ++++--- .../dev-gate/dev-m5-gate-aggregator-v2.json | 187 ++++++++++++----- reports/dev-gate/dev-m5-gate-aggregator-v2.md | 18 +- scripts/dev-m3-hardware-loop-smoke.mjs | 84 +++++++- .../src/d601-k3s-readonly-observability.mjs | 196 +++++++++++++++++- .../src/dev-evidence-blocker-aggregator.mjs | 107 +++++++--- scripts/validate-dev-gate-report.mjs | 48 +++++ 8 files changed, 669 insertions(+), 141 deletions(-) diff --git a/reports/d601-k3s-readonly-observability.json b/reports/d601-k3s-readonly-observability.json index 9756c263..3b3acd8e 100644 --- a/reports/d601-k3s-readonly-observability.json +++ b/reports/d601-k3s-readonly-observability.json @@ -12,7 +12,7 @@ "pikasTech/HWLAB#39", "pikasTech/HWLAB#42" ], - "generatedAt": "2026-05-22T07:21:35.915Z", + "generatedAt": "2026-05-22T12:34:14.403Z", "mode": "read-only", "devOnly": true, "prodDisabled": true, @@ -48,7 +48,24 @@ "node --check scripts/src/d601-k3s-readonly-observability.mjs", "node scripts/d601-k3s-readonly-observability.mjs" ], - "conclusion": "ready", + "conclusion": "blocked", + "runnerKubeconfigReadable": false, + "runnerKubeconfigProbeOk": true, + "runnerKubeconfigProbeExitCode": 0, + "runnerKubeconfigProbeStderr": "empty", + "d601PublicEndpointsReachable": true, + "d601K3sUnavailable": false, + "statusInference": { + "runnerKubeconfigReadable": false, + "runnerKubeconfigProbeOk": true, + "runnerKubeconfigProbeExitCode": 0, + "runnerKubeconfigProbeStderr": "empty", + "d601PublicEndpointsReachable": true, + "d601K3sUnavailable": false, + "conclusion": "Runner kubeconfig readability is an observability signal only; it is not used to infer D601 k3s or public DEV endpoint availability.", + "blockerClassification": "runner_permission_mount_gap", + "sourceIssue": "pikasTech/HWLAB#46" + }, "environment": { "binaries": { "kubectl": { @@ -118,6 +135,22 @@ "error": "ENOENT" } ], + "runnerKubeconfig": { + "path": "/etc/rancher/k3s/k3s.yaml", + "mode": "read-only", + "resource": "pods", + "command": "KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n hwlab-dev get pods -o name", + "runnerKubeconfigReadable": false, + "runnerKubeconfigProbeOk": true, + "runnerKubeconfigProbeExitCode": 0, + "runnerKubeconfigProbeStderr": "empty", + "stdoutSummary": "13 pod name(s) observed", + "classification": "runner_permission_mount_gap", + "sourceIssue": "pikasTech/HWLAB#46", + "secretValuesRead": false, + "secretResourcesRead": false, + "note": "This probe forces KUBECONFIG=/etc/rancher/k3s/k3s.yaml for a read-only kubectl get pods command and records only exit code plus redacted stderr/stdout summary. A successful kubectl exit code does not make the kubeconfig file readable; kubectl may use an alternate in-cluster fallback." + }, "sshBridge": { "sshBinaryAvailable": true, "env": [ @@ -172,6 +205,41 @@ "note": "SSH bridge discovery checks only binary, environment variable presence, and file metadata; it does not read SSH config or private keys." } }, + "publicEndpoints": { + "activeFrontendEndpoint": "http://74.48.78.17:16666", + "activeApiEndpoint": "http://74.48.78.17:16667", + "d601PublicEndpointsReachable": true, + "probes": [ + { + "id": "frontend-16666", + "url": "http://74.48.78.17:16666/", + "reached": true, + "ok": true, + "status": 200, + "bodySummary": " " + }, + { + "id": "api-health-16667", + "url": "http://74.48.78.17:16667/health", + "reached": true, + "ok": true, + "status": 200, + "serviceId": "hwlab-edge-proxy", + "environment": "dev", + "bodySummary": "json serviceId=hwlab-edge-proxy status=ok" + }, + { + "id": "api-live-16667", + "url": "http://74.48.78.17:16667/health/live", + "reached": true, + "ok": true, + "status": 200, + "serviceId": "hwlab-cloud-api", + "environment": "dev", + "bodySummary": "json serviceId=hwlab-cloud-api status=degraded" + } + ] + }, "cluster": { "namespace": "hwlab-dev", "attemptedExecutors": [ @@ -285,7 +353,7 @@ "nodeName": "d601" }, { - "name": "hwlab-cloud-web-cd9655b98-gxtmc", + "name": "hwlab-cloud-web-5b7487b5c4-fzvgr", "phase": "Running", "readyContainers": 1, "totalContainers": 1, @@ -623,10 +691,20 @@ } ] }, - "blockers": [], + "blockers": [ + { + "status": "open", + "type": "observability_blocker", + "scope": "runner-kubeconfig-readonly-gap", + "classification": "runner_permission_mount_gap", + "sourceIssue": "pikasTech/HWLAB#46", + "summary": "The runner cannot read /etc/rancher/k3s/k3s.yaml; classify this as a runner permission/mount gap, not a D601 global outage. Alternate read-only cluster probes and public DEV endpoint probes are reported separately.", + "nextTask": "Provide the approved read-only runner kubeconfig mount or document the intended alternate KUBECONFIG path, then rerun the read-only report." + } + ], "runnerImageFollowUp": { - "needed": false, - "suggestedUniDeskIssue": null + "needed": true, + "suggestedUniDeskIssue": "Code Queue runner image should include kubectl and a documented read-only hwlab-dev kubeconfig mount/permission path; keep Secret resources and token material inaccessible to reports." }, - "notes": "This report uses command availability checks, file metadata checks, kubectl/k3s kubectl read-only get probes, and auth can-i checks only. It does not print kubeconfig contents, token material, Secret resources, ConfigMap values, SSH config contents, or private keys." + "notes": "This report uses command availability checks, file metadata checks, public DEV endpoint GET probes, kubectl/k3s kubectl read-only get probes, and auth can-i checks only. It does not print kubeconfig contents, token material, Secret resources, ConfigMap values, SSH config contents, or private keys. A failed runner /etc/rancher/k3s/k3s.yaml probe is classified as a runner permission/mount or read-only observability gap and is not evidence that D601 k3s is globally unavailable." } diff --git a/reports/dev-gate/dev-m3-hardware-loop.json b/reports/dev-gate/dev-m3-hardware-loop.json index 27ec8f93..3372552c 100644 --- a/reports/dev-gate/dev-m3-hardware-loop.json +++ b/reports/dev-gate/dev-m3-hardware-loop.json @@ -4,10 +4,18 @@ "reportVersion": "v1", "issue": "pikasTech/HWLAB#38", "taskId": "dev-m3-hardware-loop", - "commitId": "86d769525caf", + "commitId": "bf47a95a97a9", "acceptanceLevel": "dev_m3_hardware_loop", "devOnly": true, "prodDisabled": true, + "reportLifecycle": { + "version": "v1", + "state": "active", + "activeEndpoint": "http://74.48.78.17:16667", + "activeBrowserEndpoint": "http://74.48.78.17:16666", + "deprecatedEndpoint": null, + "summary": "Current M3 DEV hardware trusted-loop report; DEV-LIVE requires res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 with operation, audit, and evidence ids." + }, "sourceContract": { "status": "pass", "documents": [ @@ -27,6 +35,7 @@ ], "runtimeTarget": { "endpoint": "http://74.48.78.17:16667", + "frontendEndpoint": "http://74.48.78.17:16666", "namespace": "hwlab-dev", "environment": "dev", "requiredBoxSimulators": 2, @@ -67,16 +76,16 @@ "status": "pass", "summary": "DEV ingress returned HWLAB identity at /health/live.", "evidence": [ - "{\"url\":\"http://74.48.78.17:16667/health/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-22T04:30:50.824Z\",\"observedAt\":\"2026-05-22T04:30:51.649Z\",\"reached\":true,\"ok\":true,\"status\":200,\"statusText\":\"OK\",\"json\":{\"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:30:51.463Z\",\"db\":{\"contractVersion\":\"v1\",\"environment\":\"dev\",\"connected\":false,\"connectionChecked\":false,\"configReady\":true,\"ready\":false,\"status\":\"degraded\",\"mode\":\"configured_without_live_connection\",\"fields\":[{\"name\":\"HWLAB_CLOUD_DB_URL\",\"present\":true,\"redacted\":true,\"source\":\"k8s-secret-ref\",\"required\":true},{\"name\":\"HWLAB_CLOUD_DB_SSL_MODE\",\"present\":true,\"redacted\":false,\"source\":\"runtime-env\",\"required\":true}],\"missingEnv\":[],\"secretRefs\":[{\"env\":\"HWLAB_CLOUD_DB_URL\",\"secretName\":\"hwlab-cloud-api-dev-db\",\"secretKey\":\"database-url\",\"present\":true,\"redacted\":true}],\"safety\":{\"devOnly\":true,\"prodAllowed\":false,\"secretsRead\":false,\"valuesRedacted\":true,\"liveDbEvidence\":false},\"evidence\":\"env_presence_only_no_live_db\"},\"runtime\":{\"adapter\":\"memory\",\"durable\":false,\"status\":\"degraded\",\"reason\":\"live DB persistence is not connected; L1 runtime writes are process-local only\",\"counts\":{\"gatewaySessions\":0,\"boxResources\":0,\"boxCapabilities\":0,\"hardwareOperations\":0,\"auditEvents\":0,\"evidenceRecords\":0}}}}", - "{\"url\":\"http://74.48.78.17:16667/health\",\"method\":\"GET\",\"startedAt\":\"2026-05-22T04:30:51.650Z\",\"observedAt\":\"2026-05-22T04:30:52.025Z\",\"reached\":true,\"ok\":true,\"status\":200,\"statusText\":\"OK\",\"json\":{\"serviceId\":\"hwlab-edge-proxy\",\"environment\":\"dev\",\"status\":\"ok\",\"service\":{\"id\":\"hwlab-edge-proxy\",\"role\":\"public-dev-ingress\",\"healthPath\":\"/health\",\"livePath\":\"/health/live\"},\"commit\":{\"id\":\"cb35ada6860653b27269b0a57991184118cbf4b1\",\"source\":\"runtime-env\"},\"image\":{\"reference\":\"ghcr.io/pikastech/hwlab-edge-proxy:cb35ada\",\"tag\":\"cb35ada\",\"digest\":\"unknown\"},\"endpoint\":\"http://74.48.78.17:16667\",\"observedAt\":\"2026-05-22T04:30:51.840Z\",\"details\":{\"upstream\":\"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667\",\"mode\":\"dev-edge-proxy\"}}}", - "{\"url\":\"http://74.48.78.17:16667/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-22T04:30:52.025Z\",\"observedAt\":\"2026-05-22T04:30:52.401Z\",\"reached\":true,\"ok\":true,\"status\":200,\"statusText\":\"OK\",\"json\":{\"serviceId\":\"hwlab-cloud-api\",\"status\":\"live\"}}", - "{\"url\":\"http://74.48.78.17:16667/v1\",\"method\":\"GET\",\"startedAt\":\"2026-05-22T04:30:52.401Z\",\"observedAt\":\"2026-05-22T04:30:53.001Z\",\"reached\":true,\"ok\":true,\"status\":200,\"statusText\":\"OK\",\"json\":{\"serviceId\":\"hwlab-cloud-api\",\"adapter\":\"rest\",\"status\":\"degraded\",\"rpcBridge\":\"POST /v1/rpc/{method}\",\"methods\":[\"system.health\",\"cloud.adapter.describe\",\"gateway.session.register\",\"box.resource.register\",\"box.capability.report\",\"hardware.operation.request\",\"hardware.invoke.shell\",\"audit.event.write\",\"audit.event.query\",\"evidence.record.write\",\"evidence.record.query\"],\"auditFields\":[\"requestId\",\"actor\",\"source\",\"operation\",\"target\",\"result\",\"timestamp\"],\"db\":{\"contractVersion\":\"v1\",\"environment\":\"dev\",\"connected\":false,\"connectionChecked\":false,\"configReady\":true,\"ready\":false,\"status\":\"degraded\",\"mode\":\"configured_without_live_connection\",\"fields\":[{\"name\":\"HWLAB_CLOUD_DB_URL\",\"present\":true,\"redacted\":true,\"source\":\"k8s-secret-ref\",\"required\":true},{\"name\":\"HWLAB_CLOUD_DB_SSL_MODE\",\"present\":true,\"redacted\":false,\"source\":\"runtime-env\",\"required\":true}],\"missingEnv\":[],\"secretRefs\":[{\"env\":\"HWLAB_CLOUD_DB_URL\",\"secretName\":\"hwlab-cloud-api-dev-db\",\"secretKey\":\"database-url\",\"present\":true,\"redacted\":true}],\"safety\":{\"devOnly\":true,\"prodAllowed\":false,\"secretsRead\":false,\"valuesRedacted\":true,\"liveDbEvidence\":false},\"evidence\":\"env_presence_only_no_live_db\"},\"runtime\":{\"adapter\":\"memory\",\"durable\":false,\"status\":\"degraded\",\"reason\":\"live DB persistence is not connected; L1 runtime writes are process-local only\",\"counts\":{\"gatewaySessions\":0,\"boxResources\":0,\"boxCapabilities\":0,\"hardwareOperations\":0,\"auditEvents\":0,\"evidenceRecords\":0}}}}" + "{\"url\":\"http://74.48.78.17:16667/health/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-22T12:36:42.977Z\",\"observedAt\":\"2026-05-22T12:36:43.545Z\",\"reached\":true,\"ok\":true,\"status\":200,\"statusText\":\"OK\",\"json\":{\"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-22T12:36:43.343Z\",\"db\":{\"contractVersion\":\"v1\",\"environment\":\"dev\",\"connected\":false,\"connectionChecked\":false,\"configReady\":true,\"ready\":false,\"status\":\"degraded\",\"mode\":\"configured_without_live_connection\",\"fields\":[{\"name\":\"HWLAB_CLOUD_DB_URL\",\"present\":true,\"redacted\":true,\"source\":\"k8s-secret-ref\",\"required\":true},{\"name\":\"HWLAB_CLOUD_DB_SSL_MODE\",\"present\":true,\"redacted\":false,\"source\":\"runtime-env\",\"required\":true}],\"missingEnv\":[],\"secretRefs\":[{\"env\":\"HWLAB_CLOUD_DB_URL\",\"secretName\":\"hwlab-cloud-api-dev-db\",\"secretKey\":\"database-url\",\"present\":true,\"redacted\":true}],\"safety\":{\"devOnly\":true,\"prodAllowed\":false,\"secretsRead\":false,\"valuesRedacted\":true,\"liveDbEvidence\":false},\"evidence\":\"env_presence_only_no_live_db\"},\"runtime\":{\"adapter\":\"memory\",\"durable\":false,\"status\":\"degraded\",\"reason\":\"live DB persistence is not connected; L1 runtime writes are process-local only\",\"counts\":{\"gatewaySessions\":0,\"boxResources\":0,\"boxCapabilities\":0,\"hardwareOperations\":0,\"auditEvents\":0,\"evidenceRecords\":0}}}}", + "{\"url\":\"http://74.48.78.17:16667/health\",\"method\":\"GET\",\"startedAt\":\"2026-05-22T12:36:43.545Z\",\"observedAt\":\"2026-05-22T12:36:44.191Z\",\"reached\":true,\"ok\":true,\"status\":200,\"statusText\":\"OK\",\"json\":{\"serviceId\":\"hwlab-edge-proxy\",\"environment\":\"dev\",\"status\":\"ok\",\"service\":{\"id\":\"hwlab-edge-proxy\",\"role\":\"public-dev-ingress\",\"healthPath\":\"/health\",\"livePath\":\"/health/live\"},\"commit\":{\"id\":\"cb35ada6860653b27269b0a57991184118cbf4b1\",\"source\":\"runtime-env\"},\"image\":{\"reference\":\"ghcr.io/pikastech/hwlab-edge-proxy:cb35ada\",\"tag\":\"cb35ada\",\"digest\":\"unknown\"},\"endpoint\":\"http://74.48.78.17:16667\",\"observedAt\":\"2026-05-22T12:36:43.989Z\",\"details\":{\"upstream\":\"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667\",\"mode\":\"dev-edge-proxy\"}}}", + "{\"url\":\"http://74.48.78.17:16667/live\",\"method\":\"GET\",\"startedAt\":\"2026-05-22T12:36:44.192Z\",\"observedAt\":\"2026-05-22T12:36:44.570Z\",\"reached\":true,\"ok\":true,\"status\":200,\"statusText\":\"OK\",\"json\":{\"serviceId\":\"hwlab-cloud-api\",\"status\":\"live\"}}", + "{\"url\":\"http://74.48.78.17:16667/v1\",\"method\":\"GET\",\"startedAt\":\"2026-05-22T12:36:44.570Z\",\"observedAt\":\"2026-05-22T12:36:45.204Z\",\"reached\":true,\"ok\":true,\"status\":200,\"statusText\":\"OK\",\"json\":{\"serviceId\":\"hwlab-cloud-api\",\"adapter\":\"rest\",\"status\":\"degraded\",\"rpcBridge\":\"POST /v1/rpc/{method}\",\"methods\":[\"system.health\",\"cloud.adapter.describe\",\"gateway.session.register\",\"box.resource.register\",\"box.capability.report\",\"hardware.operation.request\",\"hardware.invoke.shell\",\"audit.event.write\",\"audit.event.query\",\"evidence.record.write\",\"evidence.record.query\"],\"auditFields\":[\"requestId\",\"actor\",\"source\",\"operation\",\"target\",\"result\",\"timestamp\"],\"db\":{\"contractVersion\":\"v1\",\"environment\":\"dev\",\"connected\":false,\"connectionChecked\":false,\"configReady\":true,\"ready\":false,\"status\":\"degraded\",\"mode\":\"configured_without_live_connection\",\"fields\":[{\"name\":\"HWLAB_CLOUD_DB_URL\",\"present\":true,\"redacted\":true,\"source\":\"k8s-secret-ref\",\"required\":true},{\"name\":\"HWLAB_CLOUD_DB_SSL_MODE\",\"present\":true,\"redacted\":false,\"source\":\"runtime-env\",\"required\":true}],\"missingEnv\":[],\"secretRefs\":[{\"env\":\"HWLAB_CLOUD_DB_URL\",\"secretName\":\"hwlab-cloud-api-dev-db\",\"secretKey\":\"database-url\",\"present\":true,\"redacted\":true}],\"safety\":{\"devOnly\":true,\"prodAllowed\":false,\"secretsRead\":false,\"valuesRedacted\":true,\"liveDbEvidence\":false},\"evidence\":\"env_presence_only_no_live_db\"},\"runtime\":{\"adapter\":\"memory\",\"durable\":false,\"status\":\"degraded\",\"reason\":\"live DB persistence is not connected; L1 runtime writes are process-local only\",\"counts\":{\"gatewaySessions\":0,\"boxResources\":0,\"boxCapabilities\":0,\"hardwareOperations\":0,\"auditEvents\":0,\"evidenceRecords\":0}}}}" ] }, { "id": "two-box-simu-online", "status": "not_run", - "summary": "A live DEV M3 runtime contract check failed before all checks passed.", + "summary": "Stopped before M3 direct checks because service target URLs were not provided.", "evidence": [ "No live DEV evidence collected for this check." ] @@ -84,7 +93,7 @@ { "id": "two-gateway-simu-online", "status": "not_run", - "summary": "A live DEV M3 runtime contract check failed before all checks passed.", + "summary": "Stopped before M3 direct checks because service target URLs were not provided.", "evidence": [ "No live DEV evidence collected for this check." ] @@ -92,7 +101,7 @@ { "id": "patch-panel-healthy", "status": "not_run", - "summary": "A live DEV M3 runtime contract check failed before all checks passed.", + "summary": "Stopped before M3 direct checks because service target URLs were not provided.", "evidence": [ "No live DEV evidence collected for this check." ] @@ -100,7 +109,7 @@ { "id": "wiring-do1-di1-applied", "status": "not_run", - "summary": "A live DEV M3 runtime contract check failed before all checks passed.", + "summary": "Stopped before M3 direct checks because service target URLs were not provided.", "evidence": [ "No live DEV evidence collected for this check." ] @@ -108,7 +117,7 @@ { "id": "direct-call-do-write-di-read", "status": "not_run", - "summary": "A live DEV M3 runtime contract check failed before all checks passed.", + "summary": "Stopped before M3 direct checks because service target URLs were not provided.", "evidence": [ "No live DEV evidence collected for this check." ] @@ -116,7 +125,7 @@ { "id": "audit-evidence-traceable", "status": "not_run", - "summary": "A live DEV M3 runtime contract check failed before all checks passed.", + "summary": "Stopped before M3 direct checks because service target URLs were not provided.", "evidence": [ "No live DEV evidence collected for this check." ] @@ -170,9 +179,31 @@ "id": "kubectl-hwlab-dev", "status": "observed", "command": "kubectl get deploy,po,svc -n hwlab-dev -o wide", - "summary": "NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR deployment.apps/hwlab-agent-mgr 1/1 1 1 4h38m hwlab-agent-mgr 127.0.0.1:5000/hwlab/hwlab-agent-mgr:cb35ada app.kubernetes.io/name=hwlab-agent-mgr,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-agent-skills 1/1 1 1 66m hwlab-agent-skills 127.0.0.1:5000/hwlab/hwlab-agent-skills:cb35ada app.kubernetes.io/name=hwlab-agent-skills,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-box-simu 2/2 2 2 4h38m hwlab-box-simu 127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada app.kubernetes.io/name=hwlab-box-simu,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-cloud-api 1/1 1 1 4h38m hwlab-cloud-api 127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada app.kubernetes.io/name=hwlab-cloud-api,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-cloud-web 1/1 1 1 4h38m hwlab-cloud-web 127.0.0.1:5000/hwlab/hwlab-cloud-web:cb35ada app.kubernetes.io/name=hwlab-cloud-web,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-edge-proxy 1/1 1 1 4h38m hwlab-edge-proxy 127.0.0.1:5000/hwlab/hwlab-edge-proxy:cb35ada app.kubernetes.io/name=hwlab-edge-proxy,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-frpc 1/1 1 1 4h33m frpc 127.0.0.1:5000/hwlab/frpc:v0.68.1 app.kubernetes.io/name=hwlab-frpc deployment.apps/hwlab-gateway 0/0 0 0 4h38m hwlab-gateway 127.0.0.1:5000/hwlab/hwlab-gateway:cb35ada app.kubernetes.io/name=hwlab-gateway,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-gateway-simu 2/2 2 2 4h38m hwlab-gateway-simu 127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada app.kubernetes.io/name=hwlab-gateway-simu,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-patch-panel 1/1 1 1 4h38m hwlab-patch-panel 127.0.0.1:5000/hwlab/hwlab-patch-panel:cb35ada app.kubernetes.io/name=hwlab-patch-panel,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/prof" + "summary": "NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR deployment.apps/hwlab-agent-mgr 1/1 1 1 12h hwlab-agent-mgr 127.0.0.1:5000/hwlab/hwlab-agent-mgr:cb35ada app.kubernetes.io/name=hwlab-agent-mgr,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-agent-skills 1/1 1 1 9h hwlab-agent-skills 127.0.0.1:5000/hwlab/hwlab-agent-skills:cb35ada app.kubernetes.io/name=hwlab-agent-skills,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-box-simu 2/2 2 2 12h hwlab-box-simu 127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada app.kubernetes.io/name=hwlab-box-simu,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-cloud-api 1/1 1 1 12h hwlab-cloud-api 127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada app.kubernetes.io/name=hwlab-cloud-api,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-cloud-web 1/1 1 1 12h hwlab-cloud-web 127.0.0.1:5000/hwlab/hwlab-cloud-web:bf47a95 app.kubernetes.io/name=hwlab-cloud-web,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-edge-proxy 1/1 1 1 12h hwlab-edge-proxy 127.0.0.1:5000/hwlab/hwlab-edge-proxy:cb35ada app.kubernetes.io/name=hwlab-edge-proxy,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-frpc 1/1 1 1 12h frpc 127.0.0.1:5000/hwlab/frpc:v0.68.1 app.kubernetes.io/name=hwlab-frpc deployment.apps/hwlab-gateway 0/0 0 0 12h hwlab-gateway 127.0.0.1:5000/hwlab/hwlab-gateway:cb35ada app.kubernetes.io/name=hwlab-gateway,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-gateway-simu 2/2 2 2 12h hwlab-gateway-simu 127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada app.kubernetes.io/name=hwlab-gateway-simu,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.apps/hwlab-patch-panel 1/1 1 1 12h hwlab-patch-panel 127.0.0.1:5000/hwlab/hwlab-patch-panel:cb35ada app.kubernetes.io/name=hwlab-patch-panel,app.kubernetes.io/part-of=hwlab,hwlab.pikastech.local/environment=dev,hwlab.pikastech.local/profile=dev deployment.app" + }, + { + "id": "runner-k3s-kubeconfig-readonly", + "status": "observed", + "command": "KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n hwlab-dev get pods -o name", + "summary": "Runner /etc/rancher/k3s/k3s.yaml is not treated as readable from this container; a kubectl success may come from alternate in-cluster config, so file readability and command exit code stay separate. runnerKubeconfigReadable=false; exitCode=0; stderr=empty." } ], + "d601Observability": { + "runnerKubeconfigReadable": false, + "runnerKubeconfigProbeOk": true, + "runnerKubeconfigProbeExitCode": 0, + "runnerKubeconfigProbeStderr": "empty", + "d601PublicEndpointsReachable": true, + "d601K3sUnavailable": false, + "classification": "runner_permission_mount_gap", + "sourceIssue": "pikasTech/HWLAB#46", + "command": "KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n hwlab-dev get pods -o name", + "stdoutSummary": "13 pod name(s) observed", + "summary": "Runner /etc/rancher/k3s/k3s.yaml is not treated as readable from this container; a kubectl success may come from alternate in-cluster config, so file readability and command exit code stay separate.", + "inferenceRule": "runnerKubeconfigReadable=false must not imply d601K3sUnavailable=true; public 16666/16667 endpoint reachability is tracked separately.", + "secretValuesRead": false, + "secretResourcesRead": false + }, "liveOperation": { "status": "not_run", "operationId": "not_observed", @@ -183,25 +214,18 @@ }, "blockers": [ { - "type": "runtime_blocker", - "scope": "m3-hardware-loop-runtime", + "type": "observability_blocker", + "scope": "m3-service-discovery", "status": "open", - "classification": "hardware loop", - "summary": "patch-panel active DO1 -> DI1 connection missing" + "classification": "runner permission/mount gap", + "sourceIssue": "pikasTech/HWLAB#46", + "summary": "DEV ingress is reachable on frozen :16667, but this runner cannot use /etc/rancher/k3s/k3s.yaml as readable kubeconfig to discover direct M3 service URLs (HWLAB_DEV_BOX_SIMU_1_URL, HWLAB_DEV_BOX_SIMU_2_URL, HWLAB_DEV_GATEWAY_SIMU_1_URL, HWLAB_DEV_GATEWAY_SIMU_2_URL, HWLAB_DEV_PATCH_PANEL_URL); this is a #46 runner permission/mount or read-only observability gap, not D601 global offline." } ], "summary": { "status": "blocked", - "classification": "hardware loop", - "observedAt": "2026-05-22T04:30:50.820Z", - "result": "Live DEV M3 runtime was reachable but did not satisfy the M3 hardware-loop contract." - }, - "reportLifecycle": { - "version": "v1", - "state": "active", - "activeEndpoint": "http://74.48.78.17:16667", - "activeBrowserEndpoint": "http://74.48.78.17:16666", - "deprecatedEndpoint": null, - "summary": "Current M3 DEV hardware trusted-loop report; DEV-LIVE requires res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 with operation, audit, and evidence ids." + "classification": "runner permission/mount gap", + "observedAt": "2026-05-22T12:36:42.972Z", + "result": "DEV ingress was reachable, but live M3 simulator and patch-panel targets were not discoverable." } } diff --git a/reports/dev-gate/dev-m5-gate-aggregator-v2.json b/reports/dev-gate/dev-m5-gate-aggregator-v2.json index dda7c15d..477a504f 100644 --- a/reports/dev-gate/dev-m5-gate-aggregator-v2.json +++ b/reports/dev-gate/dev-m5-gate-aggregator-v2.json @@ -20,8 +20,8 @@ "pikasTech/HWLAB#46", "pikasTech/HWLAB#64" ], - "generatedAt": "2026-05-22T11:50:42.733Z", - "generatedFromCommit": "f933a81c83a5", + "generatedAt": "2026-05-22T12:36:45.366Z", + "generatedFromCommit": "bf47a95a97a9", "environment": "dev", "endpoint": "http://74.48.78.17:16667", "frontendEndpoint": "http://74.48.78.17:16666", @@ -108,7 +108,7 @@ "taskId": "dev-m3-hardware-loop", "lifecycleState": "active", "status": "blocked", - "commitId": "86d769525caf" + "commitId": "bf47a95a97a9" }, "devM4Agent": { "path": "reports/dev-gate/dev-m4-agent-loop.json", @@ -131,7 +131,7 @@ "issue": "pikasTech/HWLAB#46", "taskId": "d601-k3s-readonly-observability", "lifecycleState": "active", - "status": "ready", + "status": "blocked", "commitId": "unknown" } }, @@ -192,9 +192,9 @@ }, { "id": "d601-k3s-observability", - "status": "pass", + "status": "blocked", "evidenceLevel": "DEV-LIVE", - "summary": "D601 hwlab-dev cluster was readable." + "summary": "D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline. Alternate read-only cluster probes are readable. runnerKubeconfigReadable=false, runnerKubeconfigProbeExitCode=0, runnerKubeconfigProbeStderr=empty, d601PublicEndpointsReachable=true, d601K3sUnavailable=false." }, { "id": "dev-edge-frp-16667", @@ -265,11 +265,25 @@ ], "nextRequired": "Provide live DB connection evidence through redacted health output; route reachability alone is insufficient." }, + "d601RunnerObservability": { + "label": "D601 runner observability", + "status": "blocked", + "evidenceLevel": "DEV-LIVE", + "summary": "D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline. Alternate read-only cluster probes are readable. runnerKubeconfigReadable=false, runnerKubeconfigProbeExitCode=0, runnerKubeconfigProbeStderr=empty, d601PublicEndpointsReachable=true, d601K3sUnavailable=false.", + "evidence": [ + "runnerKubeconfigReadable=false", + "runnerKubeconfigProbeExitCode=0", + "runnerKubeconfigProbeStderr=empty", + "d601PublicEndpointsReachable=true", + "d601K3sUnavailable=false" + ], + "nextRequired": "Treat #46 runner kubeconfig/readonly gaps separately from D601 service health; rerun read-only observability after the mount or permission path is repaired." + }, "m3HardwareTrustedLoop": { "label": "M3 hardware trusted loop", "status": "blocked", "evidenceLevel": "BLOCKED", - "summary": "No live DEV operation has run yet. Blocker: patch-panel active DO1 -> DI1 connection missing.", + "summary": "No live DEV operation has run yet. Blocker: DEV ingress is reachable on frozen :16667, but this runner cannot use /etc/rancher/k3s/k3s.yaml as readable kubeconfig to discover direct M3 service URLs (HWLAB_DEV_BOX_SIMU_1_URL, HWLAB_DEV_BOX_SIMU_2_URL, HWLAB_DEV_GATEWAY_SIMU_1_URL, HWLAB_DEV_GATEWAY_SIMU_2_URL, HWLAB_DEV_PATCH_PANEL_URL); this is a #46 runner permission/mount or read-only observability gap, not D601 global offline..", "evidence": [ "operationId=not_observed", "traceId=not_observed", @@ -357,15 +371,18 @@ "milestone": "M3", "status": "blocked", "currentLevel": "BLOCKED", - "blockerClass": "hardware-loop-runtime", - "dependency": "Real DEV trusted loop through res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1.", + "blockerClass": "runner-readonly-observability-gap", + "dependency": "Runner read-only service discovery must be repaired before the real DEV trusted loop can be observed; this does not mean D601/k3s is globally unavailable.", "evidence": [ "operationId=not_observed", "traceId=not_observed", "auditId=not_observed", - "evidenceId=not_observed" + "evidenceId=not_observed", + "runnerKubeconfigReadable=false", + "d601PublicEndpointsReachable=true", + "d601K3sUnavailable=false" ], - "nextRequired": "Run the bounded DEV M3 live smoke only after patch-panel topology can prove operation, trace, audit, and evidence IDs.", + "nextRequired": "Repair the #46 runner readonly kubeconfig/service-discovery gap, then run the bounded DEV M3 live smoke only when direct simulator and patch-panel targets are discoverable.", "nonPromotionReason": "Public frontend, route, and artifact evidence do not prove the required hardware loop." }, { @@ -424,7 +441,7 @@ "highestVisibleLevel": "DEV-LIVE", "liveEvidence": "pass", "evidenceCount": 7, - "blockerCount": 5, + "blockerCount": 7, "summary": "deploy/runtime readiness is blocked before live DEV; highest visible level is DEV-LIVE; status is blocked." }, { @@ -433,7 +450,7 @@ "highestVisibleLevel": "LOCAL", "liveEvidence": "missing_or_blocked", "evidenceCount": 3, - "blockerCount": 2, + "blockerCount": 3, "summary": "hardware loop has source/local shape but no live operation; highest visible level is LOCAL; status is blocked." }, { @@ -451,7 +468,7 @@ "highestVisibleLevel": "DRY-RUN", "liveEvidence": "missing_or_blocked", "evidenceCount": 2, - "blockerCount": 9, + "blockerCount": 8, "summary": "dry-run is green but live MVP gate is blocked; highest visible level is DRY-RUN; status is blocked." } ], @@ -651,8 +668,8 @@ "reportPath": "reports/d601-k3s-readonly-observability.json", "commitId": "unknown", "lifecycleState": "active", - "level": "BLOCKED", - "status": "ready", + "level": "DEV-LIVE", + "status": "blocked", "category": "d601-observability", "commands": [ "node --check scripts/d601-k3s-readonly-observability.mjs", @@ -660,18 +677,20 @@ "node scripts/d601-k3s-readonly-observability.mjs" ], "evidence": [ - "kubectl=available", - "k3s=available", - "clusterReadable=yes" + "runnerKubeconfigReadable=false", + "runnerKubeconfigProbeExitCode=0", + "runnerKubeconfigProbeStderr=empty", + "d601PublicEndpointsReachable=true", + "d601K3sUnavailable=false" ], - "summary": "D601 k3s observability is blocked; cluster state has not been read." + "summary": "D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline. Alternate read-only cluster probes are readable. runnerKubeconfigReadable=false, runnerKubeconfigProbeExitCode=0, runnerKubeconfigProbeStderr=empty, d601PublicEndpointsReachable=true, d601K3sUnavailable=false." }, { "milestone": "M3", "issue": "pikasTech/HWLAB#38", "taskId": "dev-m3-hardware-loop", "reportPath": "reports/dev-gate/dev-m3-hardware-loop.json", - "commitId": "86d769525caf", + "commitId": "bf47a95a97a9", "lifecycleState": "active", "level": "SOURCE", "status": "manifest-ready", @@ -710,7 +729,7 @@ "issue": "pikasTech/HWLAB#38", "taskId": "dev-m3-hardware-loop", "reportPath": "reports/dev-gate/dev-m3-hardware-loop.json", - "commitId": "86d769525caf", + "commitId": "bf47a95a97a9", "lifecycleState": "active", "level": "BLOCKED", "status": "not_run", @@ -724,7 +743,7 @@ "auditId=not_observed", "evidenceId=not_observed" ], - "summary": "No live DEV operation has run yet. Blocker: patch-panel active DO1 -> DI1 connection missing." + "summary": "No live DEV operation has run yet. Blocker: DEV ingress is reachable on frozen :16667, but this runner cannot use /etc/rancher/k3s/k3s.yaml as readable kubeconfig to discover direct M3 service URLs (HWLAB_DEV_BOX_SIMU_1_URL, HWLAB_DEV_BOX_SIMU_2_URL, HWLAB_DEV_GATEWAY_SIMU_1_URL, HWLAB_DEV_GATEWAY_SIMU_2_URL, HWLAB_DEV_PATCH_PANEL_URL); this is a #46 runner permission/mount or read-only observability gap, not D601 global offline.." }, { "milestone": "M4", @@ -970,19 +989,19 @@ "category": "edge-frp-contract", "reportPath": "reports/dev-gate/dev-edge-health.json", "summary": "Committed edge report proves read-only public HTTP on :16667 /health and /health/live; remaining edge report blocker is DB readiness, not route/frp reachability." - } - ], - "BLOCKED": [ + }, { "milestone": "M2", "issue": "pikasTech/HWLAB#46", "taskId": "d601-k3s-readonly-observability", "lifecycleState": "active", - "status": "ready", + "status": "blocked", "category": "d601-observability", "reportPath": "reports/d601-k3s-readonly-observability.json", - "summary": "D601 k3s observability is blocked; cluster state has not been read." - }, + "summary": "D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline. Alternate read-only cluster probes are readable. runnerKubeconfigReadable=false, runnerKubeconfigProbeExitCode=0, runnerKubeconfigProbeStderr=empty, d601PublicEndpointsReachable=true, d601K3sUnavailable=false." + } + ], + "BLOCKED": [ { "milestone": "M3", "issue": "pikasTech/HWLAB#38", @@ -991,7 +1010,7 @@ "status": "not_run", "category": "hardware-loop-live", "reportPath": "reports/dev-gate/dev-m3-hardware-loop.json", - "summary": "No live DEV operation has run yet. Blocker: patch-panel active DO1 -> DI1 connection missing." + "summary": "No live DEV operation has run yet. Blocker: DEV ingress is reachable on frozen :16667, but this runner cannot use /etc/rancher/k3s/k3s.yaml as readable kubeconfig to discover direct M3 service URLs (HWLAB_DEV_BOX_SIMU_1_URL, HWLAB_DEV_BOX_SIMU_2_URL, HWLAB_DEV_GATEWAY_SIMU_1_URL, HWLAB_DEV_GATEWAY_SIMU_2_URL, HWLAB_DEV_PATCH_PANEL_URL); this is a #46 runner permission/mount or read-only observability gap, not D601 global offline.." }, { "milestone": "M4", @@ -1033,6 +1052,25 @@ "summary": "reports/dev-gate/dev-artifacts.json does not prove all required HWLAB service artifacts for origin/main 8e89409; current status is published with 13/13 required services published and source states resolved: 13 source-present, 0 intentionally-disabled.", "nextTask": "Complete DEV artifact publishing for every enabled required HWLAB service at the artifact source commit, or prove the target commit has no artifact build input changes; keep disabled services marked not_published with reasons." }, + { + "priority": "P1", + "order": 4, + "type": "observability_blocker", + "scope": "runner-kubeconfig-readonly-gap", + "sourceIssue": "pikasTech/HWLAB#46", + "source": "reports/d601-k3s-readonly-observability.json", + "summary": "The runner cannot read /etc/rancher/k3s/k3s.yaml; classify this as a runner permission/mount gap, not a D601 global outage. Alternate read-only cluster probes and public DEV endpoint probes are reported separately.", + "nextTask": "Provide the approved read-only runner kubeconfig mount or document the intended alternate KUBECONFIG path, then rerun the read-only report." + }, + { + "priority": "P1", + "order": 4, + "type": "observability_blocker", + "scope": "m3-service-discovery", + "sourceIssue": "pikasTech/HWLAB#38", + "source": "reports/dev-gate/dev-m3-hardware-loop.json", + "summary": "DEV ingress is reachable on frozen :16667, but this runner cannot use /etc/rancher/k3s/k3s.yaml as readable kubeconfig to discover direct M3 service URLs (HWLAB_DEV_BOX_SIMU_1_URL, HWLAB_DEV_BOX_SIMU_2_URL, HWLAB_DEV_GATEWAY_SIMU_1_URL, HWLAB_DEV_GATEWAY_SIMU_2_URL, HWLAB_DEV_PATCH_PANEL_URL); this is a #46 runner permission/mount or read-only observability gap, not D601 global offline." + }, { "priority": "P1", "order": 5, @@ -1080,15 +1118,6 @@ "source": "reports/dev-gate/dev-mvp-gate-report.json", "summary": "cloud-api /health/live reports DB degraded; db.ready=false, db.connected=false, and live DB evidence is not established." }, - { - "priority": "P0", - "order": 6, - "type": "runtime_blocker", - "scope": "m3-hardware-loop-runtime", - "sourceIssue": "pikasTech/HWLAB#38", - "source": "reports/dev-gate/dev-m3-hardware-loop.json", - "summary": "patch-panel active DO1 -> DI1 connection missing" - }, { "priority": "P0", "order": 6, @@ -1137,6 +1166,47 @@ ], "rationale": "The gate cannot promote deploy or runtime observations without immutable image provenance and digests." }, + { + "id": "d601-k3s-readonly-observability:runner-kubeconfig-readonly-gap", + "priority": "P1", + "type": "observability_blocker", + "scope": "runner-kubeconfig-readonly-gap", + "status": "open", + "source": "reports/d601-k3s-readonly-observability.json", + "sourceIssue": "pikasTech/HWLAB#46", + "summary": "The runner cannot read /etc/rancher/k3s/k3s.yaml; classify this as a runner permission/mount gap, not a D601 global outage. Alternate read-only cluster probes and public DEV endpoint probes are reported separately.", + "nextTask": "Provide the approved read-only runner kubeconfig mount or document the intended alternate KUBECONFIG path, then rerun the read-only report.", + "unblockOrder": 4, + "unblocks": [ + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#46", + "pikasTech/HWLAB#64" + ], + "rationale": "Runner read-only observability must be repaired without treating the runner gap as proof that D601 k3s or public DEV endpoints are unavailable." + }, + { + "id": "dev-m3-hardware-loop:m3-service-discovery", + "priority": "P1", + "type": "observability_blocker", + "scope": "m3-service-discovery", + "status": "open", + "source": "reports/dev-gate/dev-m3-hardware-loop.json", + "sourceIssue": "pikasTech/HWLAB#38", + "summary": "DEV ingress is reachable on frozen :16667, but this runner cannot use /etc/rancher/k3s/k3s.yaml as readable kubeconfig to discover direct M3 service URLs (HWLAB_DEV_BOX_SIMU_1_URL, HWLAB_DEV_BOX_SIMU_2_URL, HWLAB_DEV_GATEWAY_SIMU_1_URL, HWLAB_DEV_GATEWAY_SIMU_2_URL, HWLAB_DEV_PATCH_PANEL_URL); this is a #46 runner permission/mount or read-only observability gap, not D601 global offline.", + "unblockOrder": 4, + "unblocks": [ + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#46", + "pikasTech/HWLAB#64" + ], + "rationale": "Runner read-only observability must be repaired without treating the runner gap as proof that D601 k3s or public DEV endpoints are unavailable." + }, { "id": "dev-edge-health:cloud-api-db", "priority": "P1", @@ -1222,23 +1292,6 @@ ], "rationale": "M4 and M5 cannot claim live agent or MVP evidence until cloud-api /health/live proves DB readiness with redacted live evidence." }, - { - "id": "dev-m3-hardware-loop:m3-hardware-loop-runtime", - "priority": "P0", - "type": "runtime_blocker", - "scope": "m3-hardware-loop-runtime", - "status": "open", - "source": "reports/dev-gate/dev-m3-hardware-loop.json", - "sourceIssue": "pikasTech/HWLAB#38", - "summary": "patch-panel active DO1 -> DI1 connection missing", - "unblockOrder": 6, - "unblocks": [ - "pikasTech/HWLAB#38", - "pikasTech/HWLAB#39", - "pikasTech/HWLAB#64" - ], - "rationale": "M3 remains blocked until the real DEV hardware trusted loop proves DO1 -> patch-panel -> DI1 with operation, trace, audit, and evidence identifiers." - }, { "id": "dev-mvp-gate-report:m3-hardware-loop-runtime", "priority": "P0", @@ -1278,6 +1331,26 @@ }, { "order": 2, + "blockerOrder": 4, + "priority": "P1", + "scopes": [ + "runner-kubeconfig-readonly-gap", + "m3-service-discovery" + ], + "sourceIssues": [ + "pikasTech/HWLAB#33", + "pikasTech/HWLAB#34", + "pikasTech/HWLAB#36", + "pikasTech/HWLAB#38", + "pikasTech/HWLAB#46", + "pikasTech/HWLAB#64" + ], + "rationale": "Runner read-only observability must be repaired without treating the runner gap as proof that D601 k3s or public DEV endpoints are unavailable.", + "action": "Provide the approved read-only runner kubeconfig mount or document the intended alternate KUBECONFIG path, then rerun the read-only report.", + "evidenceRequired": "Read-only report with runnerKubeconfigReadable, runnerKubeconfigProbeExitCode/stderr, d601PublicEndpointsReachable, and d601K3sUnavailable recorded separately, plus direct M3 service target discovery before any DO write." + }, + { + "order": 3, "blockerOrder": 5, "priority": "P1", "scopes": [ @@ -1297,7 +1370,7 @@ "evidenceRequired": "Cloud API health/live output showing DB env ready and redacted secret references, without secret material." }, { - "order": 3, + "order": 4, "blockerOrder": 6, "priority": "P0", "scopes": [ diff --git a/reports/dev-gate/dev-m5-gate-aggregator-v2.md b/reports/dev-gate/dev-m5-gate-aggregator-v2.md index b24f26f1..bcebd7c3 100644 --- a/reports/dev-gate/dev-m5-gate-aggregator-v2.md +++ b/reports/dev-gate/dev-m5-gate-aggregator-v2.md @@ -1,7 +1,7 @@ # HWLAB M5 DEV Gate Aggregator v2 Status: blocked -Generated from: `f933a81c83a5` +Generated from: `bf47a95a97a9` Scope: DEV only, report-only Active frontend: `http://74.48.78.17:16666/` Active API/live: `http://74.48.78.17:16667/health/live` @@ -22,7 +22,8 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be | Frontend DEV revision | pass | DEV-LIVE | http://74.48.78.17:16666/ serves the accepted Cloud Workbench frontend revision 1e8805664970839b72be40c34636b08f6d18b131; this is browser/frontend evidence only. | Keep frontend revision proof separate from DB live readiness, M3 hardware-loop evidence, M4 agent-loop evidence, and M5 acceptance. | | EDGE/ROUTE live | pass | DEV-LIVE | http://74.48.78.17:16666/, http://74.48.78.17:16667/health, and http://74.48.78.17:16667/health/live returned accepted HWLAB DEV responses in the active M2 read-only smoke. | Keep this separated from DB readiness, M3/M4 loop evidence, and M5 acceptance. | | DB live/degraded | blocked | BLOCKED | cloud-api DB status=degraded; configReady=true; ready=false; connected=false; liveDbEvidence=false. | Provide live DB connection evidence through redacted health output; route reachability alone is insufficient. | -| M3 hardware trusted loop | blocked | BLOCKED | No live DEV operation has run yet. Blocker: patch-panel active DO1 -> DI1 connection missing. | Only a real DEV res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 observation with operation/trace/audit/evidence can clear M3. | +| D601 runner observability | blocked | DEV-LIVE | D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline. Alternate read-only cluster probes are readable. runnerKubeconfigReadable=false, runnerKubeconfigProbeExitCode=0, runnerKubeconfigProbeStderr=empty, d601PublicEndpointsReachable=true, d601K3sUnavailable=false. | Treat #46 runner kubeconfig/readonly gaps separately from D601 service health; rerun read-only observability after the mount or permission path is repaired. | +| M3 hardware trusted loop | blocked | BLOCKED | No live DEV operation has run yet. Blocker: DEV ingress is reachable on frozen :16667, but this runner cannot use /etc/rancher/k3s/k3s.yaml as readable kubeconfig to discover direct M3 service URLs (HWLAB_DEV_BOX_SIMU_1_URL, HWLAB_DEV_BOX_SIMU_2_URL, HWLAB_DEV_GATEWAY_SIMU_1_URL, HWLAB_DEV_GATEWAY_SIMU_2_URL, HWLAB_DEV_PATCH_PANEL_URL); this is a #46 runner permission/mount or read-only observability gap, not D601 global offline.. | Only a real DEV res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 observation with operation/trace/audit/evidence can clear M3. | | M4 agent loop | blocked | BLOCKED | Blocked at DB live readiness before scheduling a DEV agent task. | Do not schedule or claim the agent loop as live until DB live and required runtime/evidence preconditions pass. | | artifact/desired-state source | blocked | BLOCKED | artifact targetCovered=false; artifactSource=7e29522b65c8; target=8e89409dda5d; desiredApplyMode=dry-run; mutationAttempted=false. | Refresh artifact/source coverage for current origin/main and keep desired-state apply separate from read-only route proof. | @@ -55,7 +56,7 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be | m0-source-contract | pass | SOURCE | M0 contract checks are source-level evidence only. | | m1-local-smoke | pass | LOCAL | M1 local smoke is not a live DEV substitute. | | artifact-publish-digests | blocked | BLOCKED | artifactState=published, ciPublished=true, registryVerified=true, sha256=13, not_published=0, targetCovered=false | -| d601-k3s-observability | pass | DEV-LIVE | D601 hwlab-dev cluster was readable. | +| d601-k3s-observability | blocked | DEV-LIVE | D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline. Alternate read-only cluster probes are readable. runnerKubeconfigReadable=false, runnerKubeconfigProbeExitCode=0, runnerKubeconfigProbeStderr=empty, d601PublicEndpointsReachable=true, d601K3sUnavailable=false. | | dev-edge-frp-16667 | pass | DEV-LIVE | Committed edge report proves read-only public HTTP on :16667 /health and /health/live; this is route evidence, not DB/M3/M4/M5 acceptance. | | cloud-api-db-ready | blocked | BLOCKED | cloud-api DB status=degraded; ready=false; connected=false; liveDbEvidence=false. | | m3-hardware-trusted-loop | blocked | BLOCKED | M3 trusted loop is blocked until res_boxsimu_1:DO1 -> patch-panel -> res_boxsimu_2:DI1 is proven with operation/trace/audit/evidence. | @@ -66,7 +67,7 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be | Milestone | Status | Current level | Blocker class | Dependency | Required next proof | | --- | --- | --- | --- | --- | --- | -| M3 | blocked | BLOCKED | hardware-loop-runtime | Real DEV trusted loop through res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1. | Run the bounded DEV M3 live smoke only after patch-panel topology can prove operation, trace, audit, and evidence IDs. | +| M3 | blocked | BLOCKED | runner-readonly-observability-gap | Runner read-only service discovery must be repaired before the real DEV trusted loop can be observed; this does not mean D601/k3s is globally unavailable. | Repair the #46 runner readonly kubeconfig/service-discovery gap, then run the bounded DEV M3 live smoke only when direct simulator and patch-panel targets are discoverable. | | M4 | blocked | BLOCKED | db-live-readiness | Cloud API /health/live must report DB ready=true, connected=true, and liveDbEvidence=true before live agent scheduling/evidence closure. | Repair DB live readiness and rerun the M4 live preflight without scheduling a DEV agent task before preconditions pass. | | M5 | blocked | BLOCKED | composite-db-m3-m4-live | M5 needs DB live readiness, M3 trusted-loop DEV evidence, M4 live preflight/evidence closure, and current source/artifact coverage. | After DB/M3/M4 blockers are cleared, run only the bounded DEV MVP live gate command with explicit DEV/non-PROD confirmations. | @@ -76,21 +77,24 @@ The latest accepted #99/#108 frontend DEV fact is revision `1e8805664970839b72be | --- | ---: | --- | --- | --- | | P1 | 3 | contract_blocker | artifact-source-commit | source commit origin/main 8e89409 is not covered by artifact source 7e29522; target changes since the artifact source touch artifact build inputs. | | P1 | 3 | runtime_blocker | dev-artifact-publish | reports/dev-gate/dev-artifacts.json does not prove all required HWLAB service artifacts for origin/main 8e89409; current status is published with 13/13 required services published and source states resolved: 13 source-present, 0 intentionally-disabled. | +| P1 | 4 | observability_blocker | runner-kubeconfig-readonly-gap | The runner cannot read /etc/rancher/k3s/k3s.yaml; classify this as a runner permission/mount gap, not a D601 global outage. Alternate read-only cluster probes and public DEV endpoint probes are reported separately. | +| P1 | 4 | observability_blocker | m3-service-discovery | DEV ingress is reachable on frozen :16667, but this runner cannot use /etc/rancher/k3s/k3s.yaml as readable kubeconfig to discover direct M3 service URLs (HWLAB_DEV_BOX_SIMU_1_URL, HWLAB_DEV_BOX_SIMU_2_URL, HWLAB_DEV_GATEWAY_SIMU_1_URL, HWLAB_DEV_GATEWAY_SIMU_2_URL, HWLAB_DEV_PATCH_PANEL_URL); this is a #46 runner permission/mount or read-only observability gap, not D601 global offline. | | P1 | 5 | runtime_blocker | cloud-api-db | cloud-api DB env is injected, but runtime health has not attempted a live DB connection | | P1 | 5 | network_blocker | cloud-api-db | cloud-api DB env is injected, but runtime health has not attempted a live DB connection | | 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. | | P1 | 5 | runtime_blocker | db-live | cloud-api /health/live reports DB degraded; connected=false; ready=false. | | P1 | 5 | runtime_blocker | db-live | cloud-api /health/live reports DB degraded; db.ready=false, db.connected=false, and live DB evidence is not established. | -| P0 | 6 | runtime_blocker | m3-hardware-loop-runtime | patch-panel active DO1 -> DI1 connection missing | | P0 | 6 | runtime_blocker | m3-hardware-loop-runtime | Live M3 smoke reached DEV simulators, but patch-panel active DO1 -> DI1 connection is missing. | ## Next Unblock Order 1. Refresh without fake digests using `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked`, or after a successful publish run `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json`. Evidence required: Artifact publish report with ciPublished=true, registryVerified=true, and sha256 digest for each frozen service ID. -2. 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. +2. Provide the approved read-only runner kubeconfig mount or document the intended alternate KUBECONFIG path, then rerun the read-only report. + Evidence required: Read-only report with runnerKubeconfigReadable, runnerKubeconfigProbeExitCode/stderr, d601PublicEndpointsReachable, and d601K3sUnavailable recorded separately, plus direct M3 service target discovery before any DO write. +3. 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. Evidence required: Cloud API health/live output showing DB env ready and redacted secret references, without secret material. -3. Prove the real DEV M3 trusted loop res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 with operation, trace, audit, and evidence identifiers. +4. Prove the real DEV M3 trusted loop res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 with operation, trace, audit, and evidence identifiers. Evidence required: Operation, trace, audit, and evidence IDs from the real DEV DO1 -> patch-panel -> DI1 trusted loop. ## Validation diff --git a/scripts/dev-m3-hardware-loop-smoke.mjs b/scripts/dev-m3-hardware-loop-smoke.mjs index 71ede2e3..cb38f7dc 100644 --- a/scripts/dev-m3-hardware-loop-smoke.mjs +++ b/scripts/dev-m3-hardware-loop-smoke.mjs @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { randomUUID } from "node:crypto"; +import { accessSync, constants as fsConstants } from "node:fs"; import { access, mkdir, readFile, writeFile } from "node:fs/promises"; import { request as httpRequest } from "node:http"; import { request as httpsRequest } from "node:https"; @@ -9,7 +10,7 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { activeReportLifecycle } from "../internal/dev-report-lifecycle.mjs"; -import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs"; +import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const defaultReportPath = "reports/dev-gate/dev-m3-hardware-loop.json"; @@ -19,6 +20,7 @@ const deployWorkloadsPath = "deploy/k8s/base/workloads.yaml"; const requiredManifestCardinalityCommand = "node scripts/validate-dev-m3-cardinality.mjs"; const requiredM3BoxIds = Object.freeze(["boxsimu_1", "boxsimu_2"]); const requiredM3BoxResources = Object.freeze(["res_boxsimu_1", "res_boxsimu_2"]); +const runnerK3sKubeconfigPath = "/etc/rancher/k3s/k3s.yaml"; const serviceTargets = Object.freeze([ { @@ -156,12 +158,57 @@ function commandResult(command, args, { timeoutMs = 5000, maxChars = 3000 } = {} } } +function runnerKubeconfigCommandText(args) { + return [`KUBECONFIG=${runnerK3sKubeconfigPath}`, "kubectl", ...args].join(" "); +} + function commandExists(command) { const result = commandResult("bash", ["-lc", `command -v ${command}`], { timeoutMs: 2000 }); return result.exitCode === 0 && result.stdout.trim().length > 0; } -function collectLocalRuntimeObservations() { +function fileReadable(filePath) { + try { + accessSync(filePath, fsConstants.R_OK); + return true; + } catch { + return false; + } +} + +function collectD601RunnerObservability(ingress) { + const args = ["-n", namespace, "get", "pods", "-o", "name"]; + const probe = commandResult("env", [`KUBECONFIG=${runnerK3sKubeconfigPath}`, "kubectl", ...args], { + timeoutMs: 5000, + maxChars: 2000 + }); + const stderr = oneLine(probe.stderr || "") || "empty"; + const stdoutLines = String(probe.stdout ?? "") + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + const d601PublicEndpointsReachable = ingress.probes.some((item) => item.ok && isHwlabDevIdentity(item)); + const runnerKubeconfigReadable = fileReadable(runnerK3sKubeconfigPath); + + return { + runnerKubeconfigReadable, + runnerKubeconfigProbeOk: probe.exitCode === 0, + runnerKubeconfigProbeExitCode: probe.exitCode, + runnerKubeconfigProbeStderr: stderr, + d601PublicEndpointsReachable, + d601K3sUnavailable: false, + classification: "runner_permission_mount_gap", + sourceIssue: "pikasTech/HWLAB#46", + command: runnerKubeconfigCommandText(args), + stdoutSummary: probe.exitCode === 0 ? `${stdoutLines.length} pod name(s) observed` : undefined, + summary: "Runner /etc/rancher/k3s/k3s.yaml is not treated as readable from this container; a kubectl success may come from alternate in-cluster config, so file readability and command exit code stay separate.", + inferenceRule: "runnerKubeconfigReadable=false must not imply d601K3sUnavailable=true; public 16666/16667 endpoint reachability is tracked separately.", + secretValuesRead: false, + secretResourcesRead: false + }; +} + +function collectLocalRuntimeObservations(d601Observability) { const observations = []; if (commandExists("docker")) { @@ -206,6 +253,13 @@ function collectLocalRuntimeObservations() { }); } + observations.push({ + id: "runner-k3s-kubeconfig-readonly", + status: d601Observability.runnerKubeconfigProbeOk ? "observed" : "blocked", + command: d601Observability.command, + summary: `${d601Observability.summary} runnerKubeconfigReadable=${d601Observability.runnerKubeconfigReadable}; exitCode=${d601Observability.runnerKubeconfigProbeExitCode}; stderr=${d601Observability.runnerKubeconfigProbeStderr}.` + }); + return observations; } @@ -587,6 +641,7 @@ function baseReport({ commitId, observedAt }) { ], runtimeTarget: { endpoint: DEV_ENDPOINT, + frontendEndpoint: DEV_FRONTEND_ENDPOINT, namespace, environment: ENVIRONMENT_DEV, requiredBoxSimulators: 2, @@ -623,6 +678,17 @@ function baseReport({ commitId, observedAt }) { ], readOnlySupplementalEvidence: [], localRuntimeObservations: [], + d601Observability: { + runnerKubeconfigReadable: false, + runnerKubeconfigProbeOk: false, + runnerKubeconfigProbeExitCode: null, + runnerKubeconfigProbeStderr: "not_observed", + d601PublicEndpointsReachable: false, + d601K3sUnavailable: false, + classification: "not_observed", + sourceIssue: "pikasTech/HWLAB#46", + inferenceRule: "runnerKubeconfigReadable=false must not imply d601K3sUnavailable=true; public 16666/16667 endpoint reachability is tracked separately." + }, liveOperation: { status: "not_run", operationId: "not_observed", @@ -683,7 +749,8 @@ async function main() { report.readOnlySupplementalEvidence = await collectReadOnlySupplementalEvidence(); const ingress = await observeDevIngress(); - report.localRuntimeObservations = collectLocalRuntimeObservations(); + report.d601Observability = collectD601RunnerObservability(ingress); + report.localRuntimeObservations = collectLocalRuntimeObservations(report.d601Observability); report.liveChecks.push({ id: "dev-ingress-health", status: ingress.accepted ? "pass" : "blocked", @@ -727,21 +794,22 @@ async function main() { if (missing.length > 0) { addNotRunM3Checks(report, "Stopped before M3 direct checks because service target URLs were not provided."); report.blockers.push({ - type: "runtime_blocker", + type: "observability_blocker", scope: "m3-service-discovery", status: "open", - classification: "hardware loop", - summary: `DEV ingress is reachable, but M3 smoke lacks direct DEV service URLs: ${missing.join(", ")}.` + classification: "runner permission/mount gap", + sourceIssue: "pikasTech/HWLAB#46", + summary: `DEV ingress is reachable on frozen :16667, but this runner cannot use ${runnerK3sKubeconfigPath} as readable kubeconfig to discover direct M3 service URLs (${missing.join(", ")}); this is a #46 runner permission/mount or read-only observability gap, not D601 global offline.` }); report.summary = { status: "blocked", - classification: "hardware loop", + classification: "runner permission/mount gap", observedAt, result: "DEV ingress was reachable, but live M3 simulator and patch-panel targets were not discoverable." }; await writeReport(report, reportPath); console.log(`[dev-m3-smoke] status=blocked report=${relativePath(reportPath)}`); - console.log("[dev-m3-smoke] blocker=runtime_blocker scope=m3-service-discovery"); + console.log("[dev-m3-smoke] blocker=observability_blocker scope=m3-service-discovery"); return; } diff --git a/scripts/src/d601-k3s-readonly-observability.mjs b/scripts/src/d601-k3s-readonly-observability.mjs index 13a2af35..02c0b3d8 100644 --- a/scripts/src/d601-k3s-readonly-observability.mjs +++ b/scripts/src/d601-k3s-readonly-observability.mjs @@ -7,6 +7,8 @@ import path from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; +import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT } from "../../internal/protocol/index.mjs"; + const execFileAsync = promisify(execFile); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const namespace = "hwlab-dev"; @@ -26,6 +28,12 @@ const kubeconfigCandidatePaths = [ "/etc/rancher/k3s/k3s.yaml", "/var/lib/rancher/k3s/server/cred/admin.kubeconfig" ]; +const runnerK3sKubeconfigPath = "/etc/rancher/k3s/k3s.yaml"; +const publicEndpointProbeTargets = [ + { id: "frontend-16666", url: `${DEV_FRONTEND_ENDPOINT}/` }, + { id: "api-health-16667", url: `${DEV_ENDPOINT}/health` }, + { id: "api-live-16667", url: `${DEV_ENDPOINT}/health/live` } +]; const sshEnvVars = [ "HWLAB_D601_SSH_HOST", "HWLAB_D601_SSH_TARGET", @@ -106,6 +114,10 @@ function commandText(command, args) { return [command, ...args].join(" "); } +function runnerKubeconfigCommandText(args) { + return [`KUBECONFIG=${runnerK3sKubeconfigPath}`, "kubectl", ...args].join(" "); +} + async function run(command, args = [], options = {}) { try { const result = await execFileAsync(command, args, { @@ -176,6 +188,89 @@ async function fileProbe(filePath) { } } +async function collectRunnerKubeconfigProbe(timeoutMs) { + const metadata = await fileProbe(runnerK3sKubeconfigPath); + const args = ["-n", namespace, "get", "pods", "-o", "name"]; + const probe = await run("env", [`KUBECONFIG=${runnerK3sKubeconfigPath}`, "kubectl", ...args], { + timeoutMs + }); + const stdoutLines = probe.stdout + ? probe.stdout.split("\n").map((line) => line.trim()).filter(Boolean) + : []; + + return { + path: runnerK3sKubeconfigPath, + mode: "read-only", + resource: "pods", + command: runnerKubeconfigCommandText(args), + runnerKubeconfigReadable: metadata.readable === true, + runnerKubeconfigProbeOk: probe.ok, + runnerKubeconfigProbeExitCode: probe.exitCode, + runnerKubeconfigProbeStderr: oneLine(probe.stderr || probe.error || "") || "empty", + stdoutSummary: probe.ok ? `${stdoutLines.length} pod name(s) observed` : undefined, + classification: metadata.readable + ? probe.ok + ? "readable" + : "readonly_observability_gap" + : "runner_permission_mount_gap", + sourceIssue: issue, + secretValuesRead: false, + secretResourcesRead: false, + note: "This probe forces KUBECONFIG=/etc/rancher/k3s/k3s.yaml for a read-only kubectl get pods command and records only exit code plus redacted stderr/stdout summary. A successful kubectl exit code does not make the kubeconfig file readable; kubectl may use an alternate in-cluster fallback." + }; +} + +async function probePublicEndpoint(target, timeoutMs) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(target.url, { + method: "GET", + signal: controller.signal, + redirect: "manual" + }); + const body = await response.text(); + let json = null; + try { + json = body ? JSON.parse(body) : null; + } catch {} + return { + id: target.id, + url: target.url, + reached: true, + ok: response.status >= 200 && response.status < 400, + status: response.status, + serviceId: json?.serviceId, + environment: json?.environment, + bodySummary: json + ? `json serviceId=${json.serviceId ?? "unknown"} status=${json.status ?? "unknown"}` + : oneLine(body.slice(0, 160)) + }; + } catch (error) { + return { + id: target.id, + url: target.url, + reached: false, + ok: false, + error: error instanceof Error ? error.message : String(error) + }; + } finally { + clearTimeout(timeout); + } +} + +async function collectPublicEndpoints(timeoutMs) { + const probes = await Promise.all( + publicEndpointProbeTargets.map((target) => probePublicEndpoint(target, timeoutMs)) + ); + return { + activeFrontendEndpoint: DEV_FRONTEND_ENDPOINT, + activeApiEndpoint: DEV_ENDPOINT, + d601PublicEndpointsReachable: probes.every((probe) => probe.ok === true), + probes + }; +} + async function collectBinary(name, versionArgs) { const executablePath = await findExecutable(name); const result = { @@ -391,6 +486,51 @@ async function collectClusterObservability(binaries, timeoutMs) { }; } +function probeLooksLikeClusterServerUnavailable(probe) { + const text = oneLine(`${probe?.stderr ?? ""} ${probe?.error ?? ""}`).toLowerCase(); + return [ + "connection refused", + "no route to host", + "i/o timeout", + "context deadline exceeded", + "unable to connect to the server", + "the server is currently unable to handle the request" + ].some((needle) => text.includes(needle)); +} + +function inferD601K3sUnavailable({ cluster, publicEndpoints }) { + if (cluster.readable === true || publicEndpoints.d601PublicEndpointsReachable === true) { + return false; + } + + return (cluster.executors ?? []).some((executor) => + Object.values(executor.probes ?? {}).some((probe) => { + if (probe && typeof probe === "object" && !Array.isArray(probe) && Object.hasOwn(probe, "ok")) { + return probeLooksLikeClusterServerUnavailable(probe); + } + return Object.values(probe ?? {}).some((nested) => probeLooksLikeClusterServerUnavailable(nested)); + }) + ); +} + +function d601StatusInference({ runnerKubeconfig, publicEndpoints, d601K3sUnavailable }) { + return { + runnerKubeconfigReadable: runnerKubeconfig.runnerKubeconfigReadable, + runnerKubeconfigProbeOk: runnerKubeconfig.runnerKubeconfigProbeOk, + runnerKubeconfigProbeExitCode: runnerKubeconfig.runnerKubeconfigProbeExitCode, + runnerKubeconfigProbeStderr: runnerKubeconfig.runnerKubeconfigProbeStderr, + d601PublicEndpointsReachable: publicEndpoints.d601PublicEndpointsReachable, + d601K3sUnavailable, + conclusion: d601K3sUnavailable + ? "Direct read-only probes indicate D601 k3s may be unavailable." + : "Runner kubeconfig readability is an observability signal only; it is not used to infer D601 k3s or public DEV endpoint availability.", + blockerClassification: runnerKubeconfig.runnerKubeconfigReadable + ? "none" + : "runner_permission_mount_gap", + sourceIssue: issue + }; +} + function addBlocker(blockers, blocker) { assert.ok(blockerTypes.has(blocker.type), `unknown blocker type ${blocker.type}`); const key = `${blocker.type}:${blocker.scope}`; @@ -399,7 +539,7 @@ function addBlocker(blockers, blocker) { } } -function buildBlockers({ binaries, kubeconfig, sshBridge, cluster }) { +function buildBlockers({ binaries, kubeconfig, runnerKubeconfig, sshBridge, cluster }) { const blockers = []; const hasDirectClient = binaries.kubectl.available || binaries.k3s.available; if (!hasDirectClient) { @@ -412,12 +552,24 @@ function buildBlockers({ binaries, kubeconfig, sshBridge, cluster }) { } if (!kubeconfig.some((item) => item.readable)) { addBlocker(blockers, { - type: "environment_blocker", - scope: "d601-kubeconfig-path", - summary: "No readable KUBECONFIG/default k3s kubeconfig path was found by metadata checks.", + type: "observability_blocker", + scope: "runner-kubeconfig-readonly-gap", + classification: "runner_permission_mount_gap", + sourceIssue: issue, + summary: "No readable KUBECONFIG/default k3s kubeconfig path was found by metadata checks; this is a runner permission/mount gap and does not prove D601 k3s is unavailable.", nextTask: "Mount a read-only kubeconfig for hwlab-dev, or document the approved k3s local kubeconfig path without exposing token material." }); } + if (!runnerKubeconfig.runnerKubeconfigReadable) { + addBlocker(blockers, { + type: "observability_blocker", + scope: "runner-kubeconfig-readonly-gap", + classification: "runner_permission_mount_gap", + sourceIssue: issue, + summary: "The runner cannot read /etc/rancher/k3s/k3s.yaml; classify this as a runner permission/mount gap, not a D601 global outage. Alternate read-only cluster probes and public DEV endpoint probes are reported separately.", + nextTask: "Provide the approved read-only runner kubeconfig mount or document the intended alternate KUBECONFIG path, then rerun the read-only report." + }); + } if (!sshBridge.configured && !cluster.readable) { addBlocker(blockers, { type: "environment_blocker", @@ -431,7 +583,9 @@ function buildBlockers({ binaries, kubeconfig, sshBridge, cluster }) { addBlocker(blockers, { type: "observability_blocker", scope: "hwlab-dev-readonly-rbac", - summary: `Read-only cluster probes did not return pods, services, and ConfigMaps for ${namespace}; attempted executors: ${attempted}.`, + classification: "readonly_observability_gap", + sourceIssue: issue, + summary: `Read-only runner probes did not return pods, services, and ConfigMaps for ${namespace}; attempted executors: ${attempted}. This is an observability gap unless independent evidence proves D601 k3s unavailable.`, nextTask: "Grant or mount read-only access that can get namespace, pods, services, and ConfigMaps in hwlab-dev, then rerun this preflight." }); } @@ -440,7 +594,7 @@ function buildBlockers({ binaries, kubeconfig, sshBridge, cluster }) { function runnerImageFollowUp(blockers) { const scopes = new Set(blockers.map((blocker) => blocker.scope)); - const needed = scopes.has("d601-k3s-client-binary") || scopes.has("d601-kubeconfig-path"); + const needed = scopes.has("d601-k3s-client-binary") || scopes.has("runner-kubeconfig-readonly-gap"); return { needed, suggestedUniDeskIssue: needed @@ -457,13 +611,21 @@ async function writeReport(report, reportPath) { async function buildReport(args) { const binaries = await collectBinaries(); - const [kubeconfig, sshBridge, cluster] = await Promise.all([ + const [kubeconfig, runnerKubeconfig, sshBridge, cluster, publicEndpoints] = await Promise.all([ collectKubeconfigPaths(), + collectRunnerKubeconfigProbe(args.timeoutMs), collectSshBridge(binaries), - collectClusterObservability(binaries, args.timeoutMs) + collectClusterObservability(binaries, args.timeoutMs), + collectPublicEndpoints(args.timeoutMs) ]); - const blockers = buildBlockers({ binaries, kubeconfig, sshBridge, cluster }); + const d601K3sUnavailable = inferD601K3sUnavailable({ cluster, publicEndpoints }); + const blockers = buildBlockers({ binaries, kubeconfig, runnerKubeconfig, sshBridge, cluster }); const conclusion = cluster.readable && blockers.length === 0 ? "ready" : "blocked"; + const statusInference = d601StatusInference({ + runnerKubeconfig, + publicEndpoints, + d601K3sUnavailable + }); return { $schema: "https://hwlab.pikastech.local/schemas/d601-k3s-readonly-observability-report.schema.json", $id: "https://hwlab.pikastech.local/reports/d601-k3s-readonly-observability.json", @@ -497,15 +659,24 @@ async function buildReport(args) { "node scripts/d601-k3s-readonly-observability.mjs" ], conclusion, + runnerKubeconfigReadable: statusInference.runnerKubeconfigReadable, + runnerKubeconfigProbeOk: statusInference.runnerKubeconfigProbeOk, + runnerKubeconfigProbeExitCode: statusInference.runnerKubeconfigProbeExitCode, + runnerKubeconfigProbeStderr: statusInference.runnerKubeconfigProbeStderr, + d601PublicEndpointsReachable: statusInference.d601PublicEndpointsReachable, + d601K3sUnavailable: statusInference.d601K3sUnavailable, + statusInference, environment: { binaries, kubeconfig, + runnerKubeconfig, sshBridge }, + publicEndpoints, cluster, blockers, runnerImageFollowUp: runnerImageFollowUp(blockers), - notes: "This report uses command availability checks, file metadata checks, kubectl/k3s kubectl read-only get probes, and auth can-i checks only. It does not print kubeconfig contents, token material, Secret resources, ConfigMap values, SSH config contents, or private keys." + notes: "This report uses command availability checks, file metadata checks, public DEV endpoint GET probes, kubectl/k3s kubectl read-only get probes, and auth can-i checks only. It does not print kubeconfig contents, token material, Secret resources, ConfigMap values, SSH config contents, or private keys. A failed runner /etc/rancher/k3s/k3s.yaml probe is classified as a runner permission/mount or read-only observability gap and is not evidence that D601 k3s is globally unavailable." }; } @@ -517,6 +688,11 @@ function printSummary(args, report) { namespace: report.namespace, report: args.writeReport ? args.reportPath : null, readable: report.cluster.readable, + runnerKubeconfigReadable: report.runnerKubeconfigReadable, + runnerKubeconfigProbeOk: report.runnerKubeconfigProbeOk, + runnerKubeconfigProbeExitCode: report.runnerKubeconfigProbeExitCode, + d601PublicEndpointsReachable: report.d601PublicEndpointsReachable, + d601K3sUnavailable: report.d601K3sUnavailable, attemptedExecutors: report.cluster.attemptedExecutors, blockers: report.blockers.map((blocker) => ({ type: blocker.type, diff --git a/scripts/src/dev-evidence-blocker-aggregator.mjs b/scripts/src/dev-evidence-blocker-aggregator.mjs index a7bf116b..9c4e8c78 100644 --- a/scripts/src/dev-evidence-blocker-aggregator.mjs +++ b/scripts/src/dev-evidence-blocker-aggregator.mjs @@ -263,13 +263,19 @@ function applyPriority(blocker) { }; } - if (blocker.scope === "kubectl" || blocker.scope === "d601-k3s" || blocker.scope.startsWith("d601-")) { + if ( + blocker.scope === "kubectl" || + blocker.scope === "d601-k3s" || + blocker.scope === "runner-kubeconfig-readonly-gap" || + blocker.scope === "m3-service-discovery" || + blocker.scope.startsWith("d601-") + ) { return { ...blocker, priority: "P1", unblockOrder: 4, - unblocks: [issue(34), issue(33), issue(36), issue(38)], - rationale: "D601 hwlab-dev cluster state must be observable read-only before any live apply or M3/M4/M5 acceptance." + unblocks: [issue(34), issue(33), issue(36), issue(38), issue(46), issue(64)], + rationale: "Runner read-only observability must be repaired without treating the runner gap as proof that D601 k3s or public DEV endpoints are unavailable." }; } @@ -279,7 +285,7 @@ function applyPriority(blocker) { priority: "P1", unblockOrder: 4, unblocks: [issue(34), issue(33), issue(36), issue(38), issue(39)], - rationale: "D601 has a client path, but read-only hwlab-dev pods/services/configmaps must be observable before live acceptance evidence can be trusted." + rationale: "D601 has a client path, but read-only hwlab-dev pods/services/configmaps must be observable before live acceptance evidence can be trusted; this is not evidence that D601 is globally offline." }; } @@ -462,17 +468,13 @@ function collectM2Evidence(reports) { }), reportEvidence({ milestone: "M2", - level: "BLOCKED", + level: d601.cluster?.readable === true || d601.d601PublicEndpointsReachable === true ? "DEV-LIVE" : "BLOCKED", report: d601, status: d601.conclusion, category: "d601-observability", - evidence: [ - `kubectl=${d601.environment?.binaries?.kubectl?.available === true ? "available" : "missing"}`, - `k3s=${d601.environment?.binaries?.k3s?.available === true ? "available" : "missing"}`, - `clusterReadable=${d601.cluster?.readable === true ? "yes" : "no"}` - ], + evidence: d601ObservationSummary(d601), commands: d601.validationCommands, - summary: "D601 k3s observability is blocked; cluster state has not been read." + summary: d601ObservabilitySummary(d601) }) ]; } @@ -522,7 +524,10 @@ function collectM3Evidence(reports) { function m3TrustedLoopSummary(m3Report) { const operationSummary = m3Report.liveOperation?.summary ?? "No live M3 hardware operation was observed."; - const blockerSummary = m3Report.blockers?.find((blocker) => blocker.scope === "m3-hardware-loop-runtime")?.summary; + const blockerSummary = m3Report.blockers?.find((blocker) => + blocker.scope === "m3-hardware-loop-runtime" || + blocker.scope === "m3-service-discovery" + )?.summary; if (statusIsPass(m3Report.liveOperation?.status) || !blockerSummary) { return operationSummary; } @@ -658,6 +663,32 @@ function publicEndpointEvidence(m2Report) { }); } +function d601ObservationSummary(d601) { + return [ + `runnerKubeconfigReadable=${d601.runnerKubeconfigReadable === true}`, + `runnerKubeconfigProbeExitCode=${d601.runnerKubeconfigProbeExitCode ?? "unknown"}`, + `runnerKubeconfigProbeStderr=${d601.runnerKubeconfigProbeStderr || "empty"}`, + `d601PublicEndpointsReachable=${d601.d601PublicEndpointsReachable === true}`, + `d601K3sUnavailable=${d601.d601K3sUnavailable === true}` + ]; +} + +function d601ObservabilitySummary(d601) { + if (d601.d601PublicEndpointsReachable === true && d601.runnerKubeconfigReadable === false) { + const clusterNote = d601.cluster?.readable === true + ? " Alternate read-only cluster probes are readable." + : ""; + return `D601 public DEV endpoints are reachable, but the runner cannot read /etc/rancher/k3s/k3s.yaml; classify as #46 runner permission/mount or read-only observability gap, not D601 global offline.${clusterNote} ${d601ObservationSummary(d601).join(", ")}.`; + } + if (d601.cluster?.readable === true) { + return `D601 hwlab-dev cluster is readable from this runner; ${d601ObservationSummary(d601).join(", ")}.`; + } + if (d601.d601K3sUnavailable === true) { + return `Direct read-only probes indicate D601 k3s may be unavailable; ${d601ObservationSummary(d601).join(", ")}.`; + } + return `D601 read-only observability is blocked without proving D601 unavailable; ${d601ObservationSummary(d601).join(", ")}.`; +} + function isStaleLegacyIngressBlocker(blocker, sourceReport, reports) { const text = `${blocker.scope ?? ""} ${blocker.summary ?? ""}`; const staleLegacyPort = deprecatedLegacyPublicEndpoints.some((endpoint) => text.includes(endpoint)); @@ -792,8 +823,6 @@ function buildDoD(reports, milestones, blockers) { const preflight = reports.devPreflight; const artifactIdentity = preflight.artifactIdentity; const d601 = reports.d601Observability; - const d601HasClient = d601.environment?.binaries?.kubectl?.available === true || - d601.environment?.binaries?.k3s?.available === true; const edgeLive = hasCurrentLiveEdgeEvidence(reports.devEdgeHealth); const artifactCurrent = artifactIdentity.publishVerified === true && artifactIdentity.targetCoverage?.covered !== false && @@ -827,13 +856,9 @@ function buildDoD(reports, milestones, blockers) { }, { id: "d601-k3s-observability", - status: d601.cluster?.readable ? "pass" : "blocked", - evidenceLevel: d601.cluster?.readable ? "DEV-LIVE" : "BLOCKED", - summary: d601.cluster?.readable - ? "D601 hwlab-dev cluster was readable." - : d601HasClient - ? "D601 kubectl/k3s clients are present, but read-only hwlab-dev pods/services/configmaps probes are still blocked." - : "D601 runner lacks kubectl/k3s/kubeconfig observability for hwlab-dev." + status: d601.runnerKubeconfigReadable === false ? "blocked" : d601.cluster?.readable ? "pass" : "blocked", + evidenceLevel: d601.cluster?.readable || d601.d601PublicEndpointsReachable ? "DEV-LIVE" : "BLOCKED", + summary: d601ObservabilitySummary(d601) }, { id: "dev-edge-frp-16667", @@ -928,6 +953,21 @@ function buildCurrentDevLayering(reports, blockers) { evidence: reports.devM5Gate.devPreconditions?.evidence?.filter((line) => line.includes("/health/live") || line.includes("DB")) ?? [], nextRequired: "Provide live DB connection evidence through redacted health output; route reachability alone is insufficient." }, + d601RunnerObservability: { + label: "D601 runner observability", + status: reports.d601Observability.runnerKubeconfigReadable === false + ? "blocked" + : reports.d601Observability.cluster?.readable === true + ? "pass" + : "blocked", + evidenceLevel: reports.d601Observability.cluster?.readable === true || + reports.d601Observability.d601PublicEndpointsReachable === true + ? "DEV-LIVE" + : "BLOCKED", + summary: d601ObservabilitySummary(reports.d601Observability), + evidence: d601ObservationSummary(reports.d601Observability), + nextRequired: "Treat #46 runner kubeconfig/readonly gaps separately from D601 service health; rerun read-only observability after the mount or permission path is repaired." + }, m3HardwareTrustedLoop: { label: "M3 hardware trusted loop", status: m3Live ? "pass" : "blocked", @@ -968,21 +1008,36 @@ function buildMilestoneBlockerClassification(reports) { const cloudDb = cloudApiDbStatus(reports); const m3Live = statusIsPass(reports.devM3Hardware.liveOperation?.status); const m4Live = statusIsPass(reports.devM4Agent.livePreflight?.status); + const m3ServiceDiscoveryBlocked = reports.devM3Hardware.blockers?.some((blocker) => + blocker.scope === "m3-service-discovery" + ) === true; + const m3BlockerClass = m3Live + ? "cleared" + : m3ServiceDiscoveryBlocked + ? "runner-readonly-observability-gap" + : "hardware-loop-runtime"; return [ { milestone: "M3", status: m3Live ? "pass" : "blocked", currentLevel: m3Live ? "DEV-LIVE" : "BLOCKED", - blockerClass: m3Live ? "cleared" : "hardware-loop-runtime", - dependency: "Real DEV trusted loop through res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1.", + blockerClass: m3BlockerClass, + dependency: m3ServiceDiscoveryBlocked + ? "Runner read-only service discovery must be repaired before the real DEV trusted loop can be observed; this does not mean D601/k3s is globally unavailable." + : "Real DEV trusted loop through res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1.", evidence: [ `operationId=${reports.devM3Hardware.liveOperation?.operationId ?? "not_observed"}`, `traceId=${reports.devM3Hardware.liveOperation?.traceId ?? "not_observed"}`, `auditId=${reports.devM3Hardware.liveOperation?.auditId ?? "not_observed"}`, - `evidenceId=${reports.devM3Hardware.liveOperation?.evidenceId ?? "not_observed"}` + `evidenceId=${reports.devM3Hardware.liveOperation?.evidenceId ?? "not_observed"}`, + `runnerKubeconfigReadable=${reports.devM3Hardware.d601Observability?.runnerKubeconfigReadable === true}`, + `d601PublicEndpointsReachable=${reports.devM3Hardware.d601Observability?.d601PublicEndpointsReachable === true}`, + `d601K3sUnavailable=${reports.devM3Hardware.d601Observability?.d601K3sUnavailable === true}` ], - nextRequired: "Run the bounded DEV M3 live smoke only after patch-panel topology can prove operation, trace, audit, and evidence IDs.", + nextRequired: m3ServiceDiscoveryBlocked + ? "Repair the #46 runner readonly kubeconfig/service-discovery gap, then run the bounded DEV M3 live smoke only when direct simulator and patch-panel targets are discoverable." + : "Run the bounded DEV M3 live smoke only after patch-panel topology can prove operation, trace, audit, and evidence IDs.", nonPromotionReason: "Public frontend, route, and artifact evidence do not prove the required hardware loop." }, { @@ -1071,6 +1126,7 @@ function fallbackAction(scope) { if (scope.includes("cloud-api-db")) return "Configure DEV cloud-api DB env readiness and rerun health/preflight without exposing secrets."; if (scope === "db-live") return "Repair DEV cloud-api DB live readiness, then rerun the read-only health and M4 preflight reports without exposing secret values."; if (scope === "m3-hardware-loop-runtime") return "Prove the real DEV M3 trusted loop res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 with operation, trace, audit, and evidence identifiers."; + if (scope === "runner-kubeconfig-readonly-gap" || scope === "m3-service-discovery") return "Repair the #46 runner kubeconfig mount/permission or document the approved alternate read-only KUBECONFIG/service-discovery path; do not classify this as D601 global offline."; if (scope.includes("kubectl") || scope.includes("k3s")) return "Provide read-only kubectl/kubeconfig observability for hwlab-dev."; return "Resolve the blocker and attach source/local/dry-run/DEV-live evidence at the correct level."; } @@ -1083,6 +1139,7 @@ function evidenceRequiredFor(scope) { if (scope.includes("cloud-api-db")) return "Cloud API health/live output showing DB env ready and redacted secret references, without secret material."; if (scope === "db-live") return "Cloud API /health/live output with ready=true, connected=true, liveDbEvidence=true, and redacted secret references."; if (scope === "m3-hardware-loop-runtime") return "Operation, trace, audit, and evidence IDs from the real DEV DO1 -> patch-panel -> DI1 trusted loop."; + if (scope === "runner-kubeconfig-readonly-gap" || scope === "m3-service-discovery") return "Read-only report with runnerKubeconfigReadable, runnerKubeconfigProbeExitCode/stderr, d601PublicEndpointsReachable, and d601K3sUnavailable recorded separately, plus direct M3 service target discovery before any DO write."; if (scope.includes("edge") || scope.includes("ingress") || scope.includes("frp")) return "Read-only DEV route observation for :16667/frp/edge/router with HWLAB service identity and artifact identity."; return "A committed report with the exact evidence level and command used."; } diff --git a/scripts/validate-dev-gate-report.mjs b/scripts/validate-dev-gate-report.mjs index ef2d0772..de61ac04 100644 --- a/scripts/validate-dev-gate-report.mjs +++ b/scripts/validate-dev-gate-report.mjs @@ -1271,6 +1271,13 @@ async function validateDevM3Report(report, label) { assertObject(report.runtimeTarget, `${label}.runtimeTarget`); assert.equal(report.runtimeTarget.endpoint, "http://74.48.78.17:16667", `${label}.runtimeTarget.endpoint`); + if (Object.hasOwn(report.runtimeTarget, "frontendEndpoint")) { + assert.equal( + report.runtimeTarget.frontendEndpoint, + "http://74.48.78.17:16666", + `${label}.runtimeTarget.frontendEndpoint` + ); + } assert.equal(report.runtimeTarget.namespace, "hwlab-dev", `${label}.runtimeTarget.namespace`); assert.equal(report.runtimeTarget.environment, "dev", `${label}.runtimeTarget.environment`); assert.equal(report.runtimeTarget.requiredBoxSimulators, 2, `${label}.runtimeTarget.requiredBoxSimulators`); @@ -1322,6 +1329,47 @@ async function validateDevM3Report(report, label) { } } + if (Object.hasOwn(report, "d601Observability")) { + assertObject(report.d601Observability, `${label}.d601Observability`); + for (const field of [ + "runnerKubeconfigReadable", + "runnerKubeconfigProbeExitCode", + "runnerKubeconfigProbeStderr", + "d601PublicEndpointsReachable", + "d601K3sUnavailable", + "classification", + "sourceIssue", + "inferenceRule" + ]) { + assert.ok(Object.hasOwn(report.d601Observability, field), `${label}.d601Observability missing ${field}`); + } + assertBoolean(report.d601Observability.runnerKubeconfigReadable, `${label}.d601Observability.runnerKubeconfigReadable`); + if (Object.hasOwn(report.d601Observability, "runnerKubeconfigProbeOk")) { + assertBoolean(report.d601Observability.runnerKubeconfigProbeOk, `${label}.d601Observability.runnerKubeconfigProbeOk`); + } + assert.ok( + Number.isInteger(report.d601Observability.runnerKubeconfigProbeExitCode) || + report.d601Observability.runnerKubeconfigProbeExitCode === null, + `${label}.d601Observability.runnerKubeconfigProbeExitCode` + ); + assertString(report.d601Observability.runnerKubeconfigProbeStderr, `${label}.d601Observability.runnerKubeconfigProbeStderr`); + assertBoolean(report.d601Observability.d601PublicEndpointsReachable, `${label}.d601Observability.d601PublicEndpointsReachable`); + assertBoolean(report.d601Observability.d601K3sUnavailable, `${label}.d601Observability.d601K3sUnavailable`); + if ( + report.d601Observability.runnerKubeconfigReadable === false && + report.d601Observability.d601PublicEndpointsReachable === true + ) { + assert.equal( + report.d601Observability.d601K3sUnavailable, + false, + `${label}.d601Observability runner gap must not imply D601 k3s unavailable` + ); + } + assertString(report.d601Observability.classification, `${label}.d601Observability.classification`); + assertString(report.d601Observability.sourceIssue, `${label}.d601Observability.sourceIssue`); + assertString(report.d601Observability.inferenceRule, `${label}.d601Observability.inferenceRule`); + } + if (Object.hasOwn(report, "readOnlySupplementalEvidence")) { assertArray(report.readOnlySupplementalEvidence, `${label}.readOnlySupplementalEvidence`); for (const [index, evidence] of report.readOnlySupplementalEvidence.entries()) {