From 7e29522b65c8a316b7c3fde12bebcb5f6baf8c33 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Fri, 22 May 2026 12:46:59 +0800 Subject: [PATCH] fix: split registry capability probes Refs #66. Merged by commander after reviewing Code Queue task codex_1779422778882_1. Separates process HTTP, Docker daemon push, and k3s pull registry capabilities so runner loopback HTTP degradation does not block artifact publish when Docker/k3s paths are proven. --- docs/dev-artifact-publish.md | 24 +- docs/dev-gate-preflight.md | 30 +- package.json | 2 +- reports/dev-gate/dev-artifacts.json | 361 +++++- reports/dev-gate/dev-preflight-report.json | 1308 +++++++++++++++----- scripts/dev-artifact-publish.mjs | 78 +- scripts/src/dev-gate-preflight.mjs | 89 +- scripts/src/registry-capabilities.mjs | 389 ++++++ scripts/validate-dev-gate-report.mjs | 81 +- 9 files changed, 1977 insertions(+), 385 deletions(-) create mode 100644 scripts/src/registry-capabilities.mjs diff --git a/docs/dev-artifact-publish.md b/docs/dev-artifact-publish.md index a057678c..546417f9 100644 --- a/docs/dev-artifact-publish.md +++ b/docs/dev-artifact-publish.md @@ -30,6 +30,7 @@ Static check: node --check scripts/preflight-dev-base-image.mjs node --check scripts/src/dev-base-image-preflight.mjs node --check scripts/src/dev-artifact-services.mjs +node --check scripts/src/registry-capabilities.mjs node --check scripts/dev-artifact-publish.mjs node --check scripts/refresh-artifact-catalog.mjs ``` @@ -89,7 +90,22 @@ The report also carries `artifactPublish.serviceInventory` and `artifactPublish.publishPlan`. `serviceInventory` is the resolved frozen service list with enabled/disabled state. `publishPlan` is the machine-readable v2 plan: source commit, image tag, per-service registry target, image reference, digest -placeholder or real digest, and the reason a service was not published. +placeholder or real digest, registry capability evidence, and the reason a +service was not published. + +The report also includes `artifactPublish.registryCapabilities`, split into +three dimensions: + +- `process-http-access`: runner-process HTTP access to the registry `/v2/` + endpoint. This is diagnostic only. A failed + `http://127.0.0.1:5000/v2/` fetch from the runner process is `degraded`, not + proof that Docker publish failed. +- `docker-daemon-push-access`: Docker daemon view of the local/internal + registry target. This is the publish-path capability and is the only registry + capability that blocks `--publish` before a push attempt. +- `k3s-pull-access`: read-only k3s view of `hwlab-dev` image pull state. This + is the deploy-path capability and does not prove or disprove Docker daemon + push access. `digest` is only set to a registry digest after `docker push` succeeds and the push output contains a `sha256:<64 hex>` digest. If the push succeeds but no @@ -120,8 +136,10 @@ preflight is blocked, artifact publish is blocked and no build or push is attempted. If the D601 builder has no approved Node 20 base image, or the selected DEV -registry is not reachable, the report must remain `status: "blocked"`. Do not -turn a base-image or registry blocker into a fake digest or published state. +registry is not reachable from the Docker daemon publish path, the report must +remain `status: "blocked"`. Do not turn a base-image or registry blocker into a +fake digest or published state. Do not treat runner-process loopback HTTP +failure as a publish failure by itself. ## Known Implementation States diff --git a/docs/dev-gate-preflight.md b/docs/dev-gate-preflight.md index 64b65ad5..5018c2ac 100644 --- a/docs/dev-gate-preflight.md +++ b/docs/dev-gate-preflight.md @@ -15,6 +15,7 @@ Run from the repository root: ```sh node --check scripts/dev-gate-preflight.mjs node --check scripts/src/dev-gate-preflight.mjs +node --check scripts/src/registry-capabilities.mjs node --check scripts/refresh-artifact-catalog.mjs node scripts/dev-gate-preflight.mjs ``` @@ -48,6 +49,9 @@ The preflight checks: blocked with `not_published` digests. - `reports/dev-gate/dev-artifacts.json`, when present, proves all frozen DEV service artifacts were published for the selected `origin/main` commit. +- Registry capability evidence is split into `process-http-access`, + `docker-daemon-push-access`, and `k3s-pull-access` instead of one ambiguous + "registry reachable" result. - `deploy/k8s/base` and `deploy/k8s/dev` parse and remain scoped to `hwlab-dev`. - `hwlab-cloud-api` declares the DEV DB env contract: @@ -62,17 +66,35 @@ The preflight checks: - `reports/dev-gate/dev-edge-health.json`, when present, records read-only public edge, frps, tunnel-health, and k3s observability evidence. - `http://74.48.78.17:16667/health/live` responds. -- GHCR manifests are visible without reading credentials. +- Catalog image manifests are visible without reading credentials. This is + artifact catalog evidence and is separate from Docker daemon push access. - Deploy images do not point at UniDesk, provider-gateway, or microservice-proxy substitutes. +## Registry Capability Dimensions + +`registryCapabilities` appears in both the preflight report and the artifact +publish report. The dimensions are: + +- `process-http-access`: runner-process HTTP access to `/v2/`. A failed + `127.0.0.1` request can happen while Docker daemon push access still works, + so this dimension is diagnostic and may be `degraded` without blocking + publish. +- `docker-daemon-push-access`: read-only Docker daemon evidence for the + local/internal registry target. This is the publish-path capability. +- `k3s-pull-access`: read-only `kubectl` evidence for whether the DEV cluster + can inspect image pull state. This is the deploy-path capability. + ## Verdict Rules `ready` requires all checks to pass and no open blocker. -`blocked` is expected until the real DEV runtime path can be proven. Each -blocker includes a `type`, `scope`, `summary`, and `nextTask` so the next task is -the smallest repair needed before rerunning the preflight. +`degraded` means a non-publish-path capability is observable but incomplete, +such as runner-process loopback HTTP refusing `127.0.0.1:5000/v2/` while Docker +daemon registry visibility is still present. `blocked` is expected until the +real DEV runtime path can be proven. Each blocker includes a `type`, `scope`, +`summary`, and `nextTask` so the next task is the smallest repair needed before +rerunning the preflight. The DB gate has two common blocked scopes: diff --git a/package.json b/package.json index 254267f9..7d122882 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "validate": "node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check", - "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node --test internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", + "check": "node --check internal/protocol/index.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node --test internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/server.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", "dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs", "cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs", "m1:smoke": "node scripts/m1-contract-smoke.mjs", diff --git a/reports/dev-gate/dev-artifacts.json b/reports/dev-gate/dev-artifacts.json index 019a2c3b..17e841da 100644 --- a/reports/dev-gate/dev-artifacts.json +++ b/reports/dev-gate/dev-artifacts.json @@ -27,7 +27,8 @@ "node scripts/preflight-dev-base-image.mjs", "node scripts/dev-artifact-publish.mjs --preflight --no-report", "node --check scripts/validate-dev-gate-report.mjs", - "node scripts/validate-dev-gate-report.mjs" + "node scripts/validate-dev-gate-report.mjs", + "node --check scripts/src/registry-capabilities.mjs" ], "localSmoke": { "status": "not_run", @@ -526,7 +527,184 @@ "status": "published", "notPublishedReason": null } - ] + ], + "registryCapabilities": { + "version": "v1", + "registryPrefix": "127.0.0.1:5000/hwlab", + "generatedAt": "2026-05-22T04:26:32.593Z", + "classification": "degraded", + "dimensions": { + "processHttpAccess": { + "id": "process-http-access", + "status": "degraded", + "requiredForPublish": false, + "requiredForDeploy": false, + "endpoint": "http://127.0.0.1:5000/v2/", + "probeKind": "runner-process-http-v2", + "summary": "Runner process cannot reach http://127.0.0.1:5000/v2/; this is process HTTP access only and does not prove Docker daemon push failure.", + "evidence": { + "probe": { + "ok": false, + "url": "http://127.0.0.1:5000/v2/", + "method": "GET", + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" + }, + "publishFailure": false + } + }, + "dockerDaemonPushAccess": { + "id": "docker-daemon-push-access", + "status": "pass", + "requiredForPublish": true, + "requiredForDeploy": false, + "endpoint": "127.0.0.1:5000/hwlab", + "probeKind": "docker-ps-read-only", + "summary": "Docker daemon can see a local/internal registry target for publish preflight purposes; no push was attempted.", + "evidence": { + "dockerVersion": { + "command": "docker --version", + "ok": true, + "exitCode": 0, + "stdout": "Docker version 26.1.5+dfsg1, build a72d7cd", + "stderr": "", + "error": null + }, + "registryPrefix": "127.0.0.1:5000/hwlab", + "loopbackPort5000": true, + "registryContainerVisible": true, + "matchingContainers": [ + { + "id": "00091efce380", + "image": "registry:2.8.3", + "names": "unidesk-artifact-registry", + "ports": "127.0.0.1:5000->5000/tcp", + "state": "running", + "status": "Up 27 hours" + } + ], + "pushAttempted": false, + "mutationAttempted": false + } + }, + "k3sPullAccess": { + "id": "k3s-pull-access", + "status": "pass", + "requiredForPublish": false, + "requiredForDeploy": true, + "endpoint": "127.0.0.1:5000/hwlab", + "probeKind": "kubectl-read-only", + "summary": "Read-only kubectl observed hwlab-dev pods with pulled registry images.", + "evidence": { + "kubectlAvailable": true, + "context": { + "command": "kubectl config current-context", + "ok": true, + "exitCode": 0, + "stdout": "in-cluster", + "stderr": "", + "error": null + }, + "canGetPods": { + "command": "kubectl auth can-i get pods -n hwlab-dev", + "ok": true, + "exitCode": 0, + "stdout": "yes", + "stderr": "", + "error": null + }, + "podCount": 13, + "pulledImages": [ + { + "pod": "hwlab-agent-mgr-75544c7747-5dzt9", + "container": "hwlab-agent-mgr", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-mgr@sha256:c16c733306a28ae9385087653b79f5bb2f28724d917a89bb296646f6a02c5318" + }, + { + "pod": "hwlab-agent-skills-849d9976f4-2nsmw", + "container": "hwlab-agent-skills", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-skills@sha256:246fb7bb48597a7179aab62cb091e8213e47b71b6871ba94e6028032992e916c" + }, + { + "pod": "hwlab-box-simu-55688cbcc7-pftxc", + "container": "hwlab-box-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" + }, + { + "pod": "hwlab-box-simu-55688cbcc7-sq5fx", + "container": "hwlab-box-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" + }, + { + "pod": "hwlab-cloud-api-f48dcdfb-9n6zh", + "container": "hwlab-cloud-api", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-api@sha256:3946efdd7360409cea6a897cb03a6f3172f14819d3132df3aab821621909513f" + }, + { + "pod": "hwlab-cloud-web-cd9655b98-gxtmc", + "container": "hwlab-cloud-web", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-web@sha256:868e2a11c56cc1a27c77de3fa71a7b092f21ae98a4052104e9071bd93960abfa" + }, + { + "pod": "hwlab-edge-proxy-567f978cdd-8fwc8", + "container": "hwlab-edge-proxy", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-edge-proxy@sha256:384832fc579063b9b0a940973690734bb402607720fd36f4662e4ee6fa5a57d2" + }, + { + "pod": "hwlab-frpc-59f57d479c-wm5l2", + "container": "frpc", + "image": "127.0.0.1:5000/hwlab/frpc:v0.68.1", + "imageID": "127.0.0.1:5000/hwlab/frpc@sha256:ce400cda42a260964e40ab1ca68b868f72c569cdce4b243aef36ce40194d287b" + }, + { + "pod": "hwlab-gateway-simu-699fd486b-j9pk7", + "container": "hwlab-gateway-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" + }, + { + "pod": "hwlab-gateway-simu-699fd486b-phcgp", + "container": "hwlab-gateway-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" + }, + { + "pod": "hwlab-patch-panel-757f9f44d5-7np8r", + "container": "hwlab-patch-panel", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-patch-panel@sha256:5b3aa6505492c2ed398d21cbd35a8754672114acd0fa97faa3351d4b6a6dfd5a" + }, + { + "pod": "hwlab-router-5d9cbdf89b-p7r94", + "container": "hwlab-router", + "image": "127.0.0.1:5000/hwlab/hwlab-router:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-router@sha256:217b1fdb0f704c4183e9d8503644177f0c8bae674da0433985c19ef909f0e342" + }, + { + "pod": "hwlab-tunnel-client-65c7858c8-wjqsh", + "container": "hwlab-tunnel-client", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-tunnel-client@sha256:9bcdd8a58132d7bc4dbfc71297f802fc0ab407a1b5c1bb1ea58e0791147a243b" + } + ], + "pullFailures": [], + "pullAttempted": false, + "mutationAttempted": false + } + } + }, + "interpretation": { + "processHttpAccess": "Runner process HTTP access to /v2/ is diagnostic only and must not be treated as Docker daemon push access.", + "dockerDaemonPushAccess": "Docker daemon push access is the publish-path capability. This probe is read-only and does not push.", + "k3sPullAccess": "k3s pull access is the deploy-path capability. This probe is read-only and does not create pods or read secrets." + } + } }, "serviceCount": 13, "requiredServiceCount": 13, @@ -757,7 +935,184 @@ "localImageId": "sha256:060596f374ddf14c756fffa3565f65a2178a29e805441fa8c00f92c1868e6be3" } ], - "blockers": [] + "blockers": [], + "registryCapabilities": { + "version": "v1", + "registryPrefix": "127.0.0.1:5000/hwlab", + "generatedAt": "2026-05-22T04:26:32.593Z", + "classification": "degraded", + "dimensions": { + "processHttpAccess": { + "id": "process-http-access", + "status": "degraded", + "requiredForPublish": false, + "requiredForDeploy": false, + "endpoint": "http://127.0.0.1:5000/v2/", + "probeKind": "runner-process-http-v2", + "summary": "Runner process cannot reach http://127.0.0.1:5000/v2/; this is process HTTP access only and does not prove Docker daemon push failure.", + "evidence": { + "probe": { + "ok": false, + "url": "http://127.0.0.1:5000/v2/", + "method": "GET", + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" + }, + "publishFailure": false + } + }, + "dockerDaemonPushAccess": { + "id": "docker-daemon-push-access", + "status": "pass", + "requiredForPublish": true, + "requiredForDeploy": false, + "endpoint": "127.0.0.1:5000/hwlab", + "probeKind": "docker-ps-read-only", + "summary": "Docker daemon can see a local/internal registry target for publish preflight purposes; no push was attempted.", + "evidence": { + "dockerVersion": { + "command": "docker --version", + "ok": true, + "exitCode": 0, + "stdout": "Docker version 26.1.5+dfsg1, build a72d7cd", + "stderr": "", + "error": null + }, + "registryPrefix": "127.0.0.1:5000/hwlab", + "loopbackPort5000": true, + "registryContainerVisible": true, + "matchingContainers": [ + { + "id": "00091efce380", + "image": "registry:2.8.3", + "names": "unidesk-artifact-registry", + "ports": "127.0.0.1:5000->5000/tcp", + "state": "running", + "status": "Up 27 hours" + } + ], + "pushAttempted": false, + "mutationAttempted": false + } + }, + "k3sPullAccess": { + "id": "k3s-pull-access", + "status": "pass", + "requiredForPublish": false, + "requiredForDeploy": true, + "endpoint": "127.0.0.1:5000/hwlab", + "probeKind": "kubectl-read-only", + "summary": "Read-only kubectl observed hwlab-dev pods with pulled registry images.", + "evidence": { + "kubectlAvailable": true, + "context": { + "command": "kubectl config current-context", + "ok": true, + "exitCode": 0, + "stdout": "in-cluster", + "stderr": "", + "error": null + }, + "canGetPods": { + "command": "kubectl auth can-i get pods -n hwlab-dev", + "ok": true, + "exitCode": 0, + "stdout": "yes", + "stderr": "", + "error": null + }, + "podCount": 13, + "pulledImages": [ + { + "pod": "hwlab-agent-mgr-75544c7747-5dzt9", + "container": "hwlab-agent-mgr", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-mgr@sha256:c16c733306a28ae9385087653b79f5bb2f28724d917a89bb296646f6a02c5318" + }, + { + "pod": "hwlab-agent-skills-849d9976f4-2nsmw", + "container": "hwlab-agent-skills", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-skills@sha256:246fb7bb48597a7179aab62cb091e8213e47b71b6871ba94e6028032992e916c" + }, + { + "pod": "hwlab-box-simu-55688cbcc7-pftxc", + "container": "hwlab-box-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" + }, + { + "pod": "hwlab-box-simu-55688cbcc7-sq5fx", + "container": "hwlab-box-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" + }, + { + "pod": "hwlab-cloud-api-f48dcdfb-9n6zh", + "container": "hwlab-cloud-api", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-api@sha256:3946efdd7360409cea6a897cb03a6f3172f14819d3132df3aab821621909513f" + }, + { + "pod": "hwlab-cloud-web-cd9655b98-gxtmc", + "container": "hwlab-cloud-web", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-web@sha256:868e2a11c56cc1a27c77de3fa71a7b092f21ae98a4052104e9071bd93960abfa" + }, + { + "pod": "hwlab-edge-proxy-567f978cdd-8fwc8", + "container": "hwlab-edge-proxy", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-edge-proxy@sha256:384832fc579063b9b0a940973690734bb402607720fd36f4662e4ee6fa5a57d2" + }, + { + "pod": "hwlab-frpc-59f57d479c-wm5l2", + "container": "frpc", + "image": "127.0.0.1:5000/hwlab/frpc:v0.68.1", + "imageID": "127.0.0.1:5000/hwlab/frpc@sha256:ce400cda42a260964e40ab1ca68b868f72c569cdce4b243aef36ce40194d287b" + }, + { + "pod": "hwlab-gateway-simu-699fd486b-j9pk7", + "container": "hwlab-gateway-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" + }, + { + "pod": "hwlab-gateway-simu-699fd486b-phcgp", + "container": "hwlab-gateway-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" + }, + { + "pod": "hwlab-patch-panel-757f9f44d5-7np8r", + "container": "hwlab-patch-panel", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-patch-panel@sha256:5b3aa6505492c2ed398d21cbd35a8754672114acd0fa97faa3351d4b6a6dfd5a" + }, + { + "pod": "hwlab-router-5d9cbdf89b-p7r94", + "container": "hwlab-router", + "image": "127.0.0.1:5000/hwlab/hwlab-router:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-router@sha256:217b1fdb0f704c4183e9d8503644177f0c8bae674da0433985c19ef909f0e342" + }, + { + "pod": "hwlab-tunnel-client-65c7858c8-wjqsh", + "container": "hwlab-tunnel-client", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-tunnel-client@sha256:9bcdd8a58132d7bc4dbfc71297f802fc0ab407a1b5c1bb1ea58e0791147a243b" + } + ], + "pullFailures": [], + "pullAttempted": false, + "mutationAttempted": false + } + } + }, + "interpretation": { + "processHttpAccess": "Runner process HTTP access to /v2/ is diagnostic only and must not be treated as Docker daemon push access.", + "dockerDaemonPushAccess": "Docker daemon push access is the publish-path capability. This probe is read-only and does not push.", + "k3sPullAccess": "k3s pull access is the deploy-path capability. This probe is read-only and does not create pods or read secrets." + } + } }, "notes": "DEV-only artifact report. Do not treat runtime placeholder images as real service implementations." } diff --git a/reports/dev-gate/dev-preflight-report.json b/reports/dev-gate/dev-preflight-report.json index bb5dc83c..9bd3e46a 100644 --- a/reports/dev-gate/dev-preflight-report.json +++ b/reports/dev-gate/dev-preflight-report.json @@ -16,16 +16,17 @@ "pikasTech/HWLAB#33", "pikasTech/HWLAB#39", "pikasTech/HWLAB#49", + "pikasTech/HWLAB#66", "pikasTech/HWLAB#33", "pikasTech/HWLAB#35", "pikasTech/HWLAB#48" ], "target": { "ref": "origin/main", - "commitId": "1a2efd4915b2d7a6f640a22cc22087e7cae4c654", - "shortCommitId": "1a2efd4" + "commitId": "a1c3cb58da0067a12424277d47d9d50d0cbf7a9d", + "shortCommitId": "a1c3cb5" }, - "generatedAt": "2026-05-21T18:00:08.924Z", + "generatedAt": "2026-05-22T04:31:08.449Z", "mode": "read-only", "devOnly": true, "prodDisabled": true, @@ -41,6 +42,7 @@ "validationCommands": [ "node --check scripts/dev-gate-preflight.mjs", "node --check scripts/src/dev-gate-preflight.mjs", + "node --check scripts/src/registry-capabilities.mjs", "node --check scripts/refresh-artifact-catalog.mjs", "node scripts/dev-gate-preflight.mjs", "node --check scripts/validate-dev-gate-report.mjs", @@ -49,157 +51,362 @@ "artifactIdentity": { "source": { "ref": "origin/main", - "commitId": "1a2efd4915b2d7a6f640a22cc22087e7cae4c654", - "shortCommitId": "1a2efd4" + "commitId": "a1c3cb58da0067a12424277d47d9d50d0cbf7a9d", + "shortCommitId": "a1c3cb5" }, "deployManifest": { "path": "deploy/deploy.json", - "commitId": "1a2efd4", - "matchesSource": true + "commitId": "73b379f", + "matchesSource": false }, "artifactCatalog": { "path": "deploy/artifact-catalog.dev.json", - "commitId": "1a2efd4", - "artifactState": "contract-skeleton", - "ciPublished": false, - "registryVerified": false, - "provenance": "not_available_until_publish", + "commitId": "73b379f", + "artifactState": "published", + "ciPublished": true, + "registryVerified": true, + "provenance": "reports/dev-gate/dev-artifacts.json", "digestCounts": { - "sha256": 0, - "notPublished": 13, + "sha256": 13, + "notPublished": 0, "invalid": 0 }, - "matchesSource": true + "requiredServiceCount": 13, + "disabledServiceCount": 0, + "matchesSource": false }, "services": [ { "serviceId": "hwlab-cloud-api", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:73b379f", + "imageTag": "73b379f", + "digest": "sha256:561e50494bf13bf61f83d28b371bc39b860b21333ef9991711853a095bd535bf", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-cloud-web", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:73b379f", + "imageTag": "73b379f", + "digest": "sha256:c7939658af9f1109e5384619163dd0d11e1a1de14e7d15d4cbde7c4400b495c2", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-agent-mgr", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:73b379f", + "imageTag": "73b379f", + "digest": "sha256:ae6ca76cd09a7d21e46e0076e12dea1843417a0d59d57b3115915c831e88ebef", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-agent-worker", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:73b379f", + "imageTag": "73b379f", + "digest": "sha256:4d5f8d8d404fc2826256fb690fdea5570285a34d1bdffbaf1e0c7ca39603ffd8", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-gateway", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:73b379f", + "imageTag": "73b379f", + "digest": "sha256:ff9f3f3dc6332c3d8e4e9825657c296c9664d84a8d40c3ffeab3c1e91a484008", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-gateway-simu", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:73b379f", + "imageTag": "73b379f", + "digest": "sha256:f5ccff29da331e424a482f92394d071e1be90c0244e7137e07ac33d9a7cc4082", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-box-simu", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:73b379f", + "imageTag": "73b379f", + "digest": "sha256:2a12c1c462b2c01a9fa15efa75a6e1f17ecc8432dfa88bca53f0d8d4b6355daf", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-patch-panel", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:73b379f", + "imageTag": "73b379f", + "digest": "sha256:5bc9a1c7ce65026cd7bf6d20a311226b35c796a4d52a914266244bf3c1a17286", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-router", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-router:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-router:73b379f", + "imageTag": "73b379f", + "digest": "sha256:356f37a7d78cc2f86230c2037f43215ad66979fbe4a949cb9241b5957692d9d8", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-tunnel-client", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:73b379f", + "imageTag": "73b379f", + "digest": "sha256:8ca553408a633d7f287ca9440f822fb1458dd9a028eb4db178b466b0b5505c49", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-edge-proxy", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:73b379f", + "imageTag": "73b379f", + "digest": "sha256:480dea831051d45b6d63a489db9c0fa314f88bae35dbf72be4186bf1de356f38", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-cli", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-cli:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-cli:73b379f", + "imageTag": "73b379f", + "digest": "sha256:125154cf59727ec230618c65b07a6235c18c1411f9ad51525ae411040169c9b5", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-agent-skills", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:73b379f", + "imageTag": "73b379f", + "digest": "sha256:5bdf9a4d17f8f4c7f3ecead99dcbd4de0ac346a2c28fef020b8853cf494df573", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null } ], "serviceCommitIds": [ - "1a2efd4" + "73b379f" ], - "matchesSource": true, - "publishVerified": false, + "matchesSource": false, + "publishVerified": true, "refreshCommands": { "blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked", "published": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json" } }, + "registryCapabilities": { + "version": "v1", + "registryPrefix": "127.0.0.1:5000/hwlab", + "generatedAt": "2026-05-22T04:31:07.663Z", + "classification": "degraded", + "dimensions": { + "processHttpAccess": { + "id": "process-http-access", + "status": "degraded", + "requiredForPublish": false, + "requiredForDeploy": false, + "endpoint": "http://127.0.0.1:5000/v2/", + "probeKind": "runner-process-http-v2", + "summary": "Runner process cannot reach http://127.0.0.1:5000/v2/; this is process HTTP access only and does not prove Docker daemon push failure.", + "evidence": { + "probe": { + "ok": false, + "url": "http://127.0.0.1:5000/v2/", + "method": "GET", + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" + }, + "publishFailure": false + } + }, + "dockerDaemonPushAccess": { + "id": "docker-daemon-push-access", + "status": "pass", + "requiredForPublish": true, + "requiredForDeploy": false, + "endpoint": "127.0.0.1:5000/hwlab", + "probeKind": "docker-ps-read-only", + "summary": "Docker daemon can see a local/internal registry target for publish preflight purposes; no push was attempted.", + "evidence": { + "dockerVersion": { + "command": "docker --version", + "ok": true, + "exitCode": 0, + "stdout": "Docker version 26.1.5+dfsg1, build a72d7cd", + "stderr": "", + "error": null + }, + "registryPrefix": "127.0.0.1:5000/hwlab", + "loopbackPort5000": true, + "registryContainerVisible": true, + "matchingContainers": [ + { + "id": "00091efce380", + "image": "registry:2.8.3", + "names": "unidesk-artifact-registry", + "ports": "127.0.0.1:5000->5000/tcp", + "state": "running", + "status": "Up 27 hours" + } + ], + "pushAttempted": false, + "mutationAttempted": false + } + }, + "k3sPullAccess": { + "id": "k3s-pull-access", + "status": "pass", + "requiredForPublish": false, + "requiredForDeploy": true, + "endpoint": "127.0.0.1:5000/hwlab", + "probeKind": "kubectl-read-only", + "summary": "Read-only kubectl observed hwlab-dev pods with pulled registry images.", + "evidence": { + "kubectlAvailable": true, + "context": { + "command": "kubectl config current-context", + "ok": true, + "exitCode": 0, + "stdout": "in-cluster", + "stderr": "", + "error": null + }, + "canGetPods": { + "command": "kubectl auth can-i get pods -n hwlab-dev", + "ok": true, + "exitCode": 0, + "stdout": "yes", + "stderr": "", + "error": null + }, + "podCount": 13, + "pulledImages": [ + { + "pod": "hwlab-agent-mgr-75544c7747-5dzt9", + "container": "hwlab-agent-mgr", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-mgr@sha256:c16c733306a28ae9385087653b79f5bb2f28724d917a89bb296646f6a02c5318" + }, + { + "pod": "hwlab-agent-skills-849d9976f4-2nsmw", + "container": "hwlab-agent-skills", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-skills@sha256:246fb7bb48597a7179aab62cb091e8213e47b71b6871ba94e6028032992e916c" + }, + { + "pod": "hwlab-box-simu-55688cbcc7-pftxc", + "container": "hwlab-box-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" + }, + { + "pod": "hwlab-box-simu-55688cbcc7-sq5fx", + "container": "hwlab-box-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" + }, + { + "pod": "hwlab-cloud-api-f48dcdfb-9n6zh", + "container": "hwlab-cloud-api", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-api@sha256:3946efdd7360409cea6a897cb03a6f3172f14819d3132df3aab821621909513f" + }, + { + "pod": "hwlab-cloud-web-cd9655b98-gxtmc", + "container": "hwlab-cloud-web", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-web@sha256:868e2a11c56cc1a27c77de3fa71a7b092f21ae98a4052104e9071bd93960abfa" + }, + { + "pod": "hwlab-edge-proxy-567f978cdd-8fwc8", + "container": "hwlab-edge-proxy", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-edge-proxy@sha256:384832fc579063b9b0a940973690734bb402607720fd36f4662e4ee6fa5a57d2" + }, + { + "pod": "hwlab-frpc-59f57d479c-wm5l2", + "container": "frpc", + "image": "127.0.0.1:5000/hwlab/frpc:v0.68.1", + "imageID": "127.0.0.1:5000/hwlab/frpc@sha256:ce400cda42a260964e40ab1ca68b868f72c569cdce4b243aef36ce40194d287b" + }, + { + "pod": "hwlab-gateway-simu-699fd486b-j9pk7", + "container": "hwlab-gateway-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" + }, + { + "pod": "hwlab-gateway-simu-699fd486b-phcgp", + "container": "hwlab-gateway-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" + }, + { + "pod": "hwlab-patch-panel-757f9f44d5-7np8r", + "container": "hwlab-patch-panel", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-patch-panel@sha256:5b3aa6505492c2ed398d21cbd35a8754672114acd0fa97faa3351d4b6a6dfd5a" + }, + { + "pod": "hwlab-router-5d9cbdf89b-p7r94", + "container": "hwlab-router", + "image": "127.0.0.1:5000/hwlab/hwlab-router:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-router@sha256:217b1fdb0f704c4183e9d8503644177f0c8bae674da0433985c19ef909f0e342" + }, + { + "pod": "hwlab-tunnel-client-65c7858c8-wjqsh", + "container": "hwlab-tunnel-client", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-tunnel-client@sha256:9bcdd8a58132d7bc4dbfc71297f802fc0ab407a1b5c1bb1ea58e0791147a243b" + } + ], + "pullFailures": [], + "pullAttempted": false, + "mutationAttempted": false + } + } + }, + "interpretation": { + "processHttpAccess": "Runner process HTTP access to /v2/ is diagnostic only and must not be treated as Docker daemon push access.", + "dockerDaemonPushAccess": "Docker daemon push access is the publish-path capability. This probe is read-only and does not push.", + "k3sPullAccess": "k3s pull access is the deploy-path capability. This probe is read-only and does not create pods or read secrets." + } + }, "conclusion": "blocked", "checks": [ { @@ -212,158 +419,186 @@ { "id": "target-commit-pinning", "category": "contract", - "status": "pass", - "summary": "Source origin/main 1a2efd4 matches deploy/catalog artifact commit 1a2efd4.", + "status": "blocked", + "summary": "Source origin/main a1c3cb5 is not covered by deploy commit 73b379f, catalog commit 73b379f, service commits 73b379f.", "evidence": [ { "source": { "ref": "origin/main", - "commitId": "1a2efd4915b2d7a6f640a22cc22087e7cae4c654", - "shortCommitId": "1a2efd4" + "commitId": "a1c3cb58da0067a12424277d47d9d50d0cbf7a9d", + "shortCommitId": "a1c3cb5" }, "deployManifest": { "path": "deploy/deploy.json", - "commitId": "1a2efd4", - "matchesSource": true + "commitId": "73b379f", + "matchesSource": false }, "artifactCatalog": { "path": "deploy/artifact-catalog.dev.json", - "commitId": "1a2efd4", - "artifactState": "contract-skeleton", - "ciPublished": false, - "registryVerified": false, - "provenance": "not_available_until_publish", + "commitId": "73b379f", + "artifactState": "published", + "ciPublished": true, + "registryVerified": true, + "provenance": "reports/dev-gate/dev-artifacts.json", "digestCounts": { - "sha256": 0, - "notPublished": 13, + "sha256": 13, + "notPublished": 0, "invalid": 0 }, - "matchesSource": true + "requiredServiceCount": 13, + "disabledServiceCount": 0, + "matchesSource": false }, "services": [ { "serviceId": "hwlab-cloud-api", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:73b379f", + "imageTag": "73b379f", + "digest": "sha256:561e50494bf13bf61f83d28b371bc39b860b21333ef9991711853a095bd535bf", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-cloud-web", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:73b379f", + "imageTag": "73b379f", + "digest": "sha256:c7939658af9f1109e5384619163dd0d11e1a1de14e7d15d4cbde7c4400b495c2", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-agent-mgr", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:73b379f", + "imageTag": "73b379f", + "digest": "sha256:ae6ca76cd09a7d21e46e0076e12dea1843417a0d59d57b3115915c831e88ebef", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-agent-worker", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:73b379f", + "imageTag": "73b379f", + "digest": "sha256:4d5f8d8d404fc2826256fb690fdea5570285a34d1bdffbaf1e0c7ca39603ffd8", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-gateway", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:73b379f", + "imageTag": "73b379f", + "digest": "sha256:ff9f3f3dc6332c3d8e4e9825657c296c9664d84a8d40c3ffeab3c1e91a484008", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-gateway-simu", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:73b379f", + "imageTag": "73b379f", + "digest": "sha256:f5ccff29da331e424a482f92394d071e1be90c0244e7137e07ac33d9a7cc4082", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-box-simu", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:73b379f", + "imageTag": "73b379f", + "digest": "sha256:2a12c1c462b2c01a9fa15efa75a6e1f17ecc8432dfa88bca53f0d8d4b6355daf", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-patch-panel", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:73b379f", + "imageTag": "73b379f", + "digest": "sha256:5bc9a1c7ce65026cd7bf6d20a311226b35c796a4d52a914266244bf3c1a17286", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-router", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-router:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-router:73b379f", + "imageTag": "73b379f", + "digest": "sha256:356f37a7d78cc2f86230c2037f43215ad66979fbe4a949cb9241b5957692d9d8", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-tunnel-client", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:73b379f", + "imageTag": "73b379f", + "digest": "sha256:8ca553408a633d7f287ca9440f822fb1458dd9a028eb4db178b466b0b5505c49", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-edge-proxy", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:73b379f", + "imageTag": "73b379f", + "digest": "sha256:480dea831051d45b6d63a489db9c0fa314f88bae35dbf72be4186bf1de356f38", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-cli", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-cli:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-cli:73b379f", + "imageTag": "73b379f", + "digest": "sha256:125154cf59727ec230618c65b07a6235c18c1411f9ad51525ae411040169c9b5", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null }, { "serviceId": "hwlab-agent-skills", - "commitId": "1a2efd4", - "matchesSource": true, - "image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4", - "imageTag": "1a2efd4", - "digest": "not_published", - "publishState": "skeleton-only" + "commitId": "73b379f", + "matchesSource": false, + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:73b379f", + "imageTag": "73b379f", + "digest": "sha256:5bdf9a4d17f8f4c7f3ecead99dcbd4de0ac346a2c28fef020b8853cf494df573", + "publishState": "published", + "artifactRequired": true, + "notPublishedReason": null } ], "serviceCommitIds": [ - "1a2efd4" + "73b379f" ], - "matchesSource": true, - "publishVerified": false, + "matchesSource": false, + "publishVerified": true, "refreshCommands": { "blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked", "published": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json" @@ -374,22 +609,24 @@ { "id": "artifact-catalog-publish-state", "category": "registry", - "status": "blocked", - "summary": "Artifact catalog publish state is contract-skeleton; ciPublished=false, registryVerified=false, digests sha256=0, not_published=13, invalid=0.", + "status": "pass", + "summary": "Artifact catalog records CI publish, registry verification, and immutable service digests.", "evidence": [ { "path": "deploy/artifact-catalog.dev.json", - "commitId": "1a2efd4", - "artifactState": "contract-skeleton", - "ciPublished": false, - "registryVerified": false, - "provenance": "not_available_until_publish", + "commitId": "73b379f", + "artifactState": "published", + "ciPublished": true, + "registryVerified": true, + "provenance": "reports/dev-gate/dev-artifacts.json", "digestCounts": { - "sha256": 0, - "notPublished": 13, + "sha256": 13, + "notPublished": 0, "invalid": 0 }, - "matchesSource": true + "requiredServiceCount": 13, + "disabledServiceCount": 0, + "matchesSource": false } ] }, @@ -400,6 +637,379 @@ "summary": "DEV k3s manifest files parse and stay scoped to hwlab-dev.", "evidence": [] }, + { + "id": "registry-capability-process-http-access", + "category": "registry", + "status": "degraded", + "summary": "Runner process cannot reach http://127.0.0.1:5000/v2/; this is process HTTP access only and does not prove Docker daemon push failure.", + "evidence": [ + { + "id": "process-http-access", + "status": "degraded", + "requiredForPublish": false, + "requiredForDeploy": false, + "endpoint": "http://127.0.0.1:5000/v2/", + "probeKind": "runner-process-http-v2", + "summary": "Runner process cannot reach http://127.0.0.1:5000/v2/; this is process HTTP access only and does not prove Docker daemon push failure.", + "evidence": { + "probe": { + "ok": false, + "url": "http://127.0.0.1:5000/v2/", + "method": "GET", + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" + }, + "publishFailure": false + } + } + ] + }, + { + "id": "registry-capability-docker-daemon-push-access", + "category": "registry", + "status": "pass", + "summary": "Docker daemon can see a local/internal registry target for publish preflight purposes; no push was attempted.", + "evidence": [ + { + "id": "docker-daemon-push-access", + "status": "pass", + "requiredForPublish": true, + "requiredForDeploy": false, + "endpoint": "127.0.0.1:5000/hwlab", + "probeKind": "docker-ps-read-only", + "summary": "Docker daemon can see a local/internal registry target for publish preflight purposes; no push was attempted.", + "evidence": { + "dockerVersion": { + "command": "docker --version", + "ok": true, + "exitCode": 0, + "stdout": "Docker version 26.1.5+dfsg1, build a72d7cd", + "stderr": "", + "error": null + }, + "registryPrefix": "127.0.0.1:5000/hwlab", + "loopbackPort5000": true, + "registryContainerVisible": true, + "matchingContainers": [ + { + "id": "00091efce380", + "image": "registry:2.8.3", + "names": "unidesk-artifact-registry", + "ports": "127.0.0.1:5000->5000/tcp", + "state": "running", + "status": "Up 27 hours" + } + ], + "pushAttempted": false, + "mutationAttempted": false + } + } + ] + }, + { + "id": "registry-capability-k3s-pull-access", + "category": "registry", + "status": "pass", + "summary": "Read-only kubectl observed hwlab-dev pods with pulled registry images.", + "evidence": [ + { + "id": "k3s-pull-access", + "status": "pass", + "requiredForPublish": false, + "requiredForDeploy": true, + "endpoint": "127.0.0.1:5000/hwlab", + "probeKind": "kubectl-read-only", + "summary": "Read-only kubectl observed hwlab-dev pods with pulled registry images.", + "evidence": { + "kubectlAvailable": true, + "context": { + "command": "kubectl config current-context", + "ok": true, + "exitCode": 0, + "stdout": "in-cluster", + "stderr": "", + "error": null + }, + "canGetPods": { + "command": "kubectl auth can-i get pods -n hwlab-dev", + "ok": true, + "exitCode": 0, + "stdout": "yes", + "stderr": "", + "error": null + }, + "podCount": 13, + "pulledImages": [ + { + "pod": "hwlab-agent-mgr-75544c7747-5dzt9", + "container": "hwlab-agent-mgr", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-mgr@sha256:c16c733306a28ae9385087653b79f5bb2f28724d917a89bb296646f6a02c5318" + }, + { + "pod": "hwlab-agent-skills-849d9976f4-2nsmw", + "container": "hwlab-agent-skills", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-skills@sha256:246fb7bb48597a7179aab62cb091e8213e47b71b6871ba94e6028032992e916c" + }, + { + "pod": "hwlab-box-simu-55688cbcc7-pftxc", + "container": "hwlab-box-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" + }, + { + "pod": "hwlab-box-simu-55688cbcc7-sq5fx", + "container": "hwlab-box-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" + }, + { + "pod": "hwlab-cloud-api-f48dcdfb-9n6zh", + "container": "hwlab-cloud-api", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-api@sha256:3946efdd7360409cea6a897cb03a6f3172f14819d3132df3aab821621909513f" + }, + { + "pod": "hwlab-cloud-web-cd9655b98-gxtmc", + "container": "hwlab-cloud-web", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-web@sha256:868e2a11c56cc1a27c77de3fa71a7b092f21ae98a4052104e9071bd93960abfa" + }, + { + "pod": "hwlab-edge-proxy-567f978cdd-8fwc8", + "container": "hwlab-edge-proxy", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-edge-proxy@sha256:384832fc579063b9b0a940973690734bb402607720fd36f4662e4ee6fa5a57d2" + }, + { + "pod": "hwlab-frpc-59f57d479c-wm5l2", + "container": "frpc", + "image": "127.0.0.1:5000/hwlab/frpc:v0.68.1", + "imageID": "127.0.0.1:5000/hwlab/frpc@sha256:ce400cda42a260964e40ab1ca68b868f72c569cdce4b243aef36ce40194d287b" + }, + { + "pod": "hwlab-gateway-simu-699fd486b-j9pk7", + "container": "hwlab-gateway-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" + }, + { + "pod": "hwlab-gateway-simu-699fd486b-phcgp", + "container": "hwlab-gateway-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" + }, + { + "pod": "hwlab-patch-panel-757f9f44d5-7np8r", + "container": "hwlab-patch-panel", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-patch-panel@sha256:5b3aa6505492c2ed398d21cbd35a8754672114acd0fa97faa3351d4b6a6dfd5a" + }, + { + "pod": "hwlab-router-5d9cbdf89b-p7r94", + "container": "hwlab-router", + "image": "127.0.0.1:5000/hwlab/hwlab-router:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-router@sha256:217b1fdb0f704c4183e9d8503644177f0c8bae674da0433985c19ef909f0e342" + }, + { + "pod": "hwlab-tunnel-client-65c7858c8-wjqsh", + "container": "hwlab-tunnel-client", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-tunnel-client@sha256:9bcdd8a58132d7bc4dbfc71297f802fc0ab407a1b5c1bb1ea58e0791147a243b" + } + ], + "pullFailures": [], + "pullAttempted": false, + "mutationAttempted": false + } + } + ] + }, + { + "id": "registry-capability-classification", + "category": "registry", + "status": "degraded", + "summary": "Registry capability classification is degraded: process HTTP, Docker daemon push path, and k3s pull path are reported separately.", + "evidence": [ + { + "version": "v1", + "registryPrefix": "127.0.0.1:5000/hwlab", + "generatedAt": "2026-05-22T04:31:07.663Z", + "classification": "degraded", + "dimensions": { + "processHttpAccess": { + "id": "process-http-access", + "status": "degraded", + "requiredForPublish": false, + "requiredForDeploy": false, + "endpoint": "http://127.0.0.1:5000/v2/", + "probeKind": "runner-process-http-v2", + "summary": "Runner process cannot reach http://127.0.0.1:5000/v2/; this is process HTTP access only and does not prove Docker daemon push failure.", + "evidence": { + "probe": { + "ok": false, + "url": "http://127.0.0.1:5000/v2/", + "method": "GET", + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" + }, + "publishFailure": false + } + }, + "dockerDaemonPushAccess": { + "id": "docker-daemon-push-access", + "status": "pass", + "requiredForPublish": true, + "requiredForDeploy": false, + "endpoint": "127.0.0.1:5000/hwlab", + "probeKind": "docker-ps-read-only", + "summary": "Docker daemon can see a local/internal registry target for publish preflight purposes; no push was attempted.", + "evidence": { + "dockerVersion": { + "command": "docker --version", + "ok": true, + "exitCode": 0, + "stdout": "Docker version 26.1.5+dfsg1, build a72d7cd", + "stderr": "", + "error": null + }, + "registryPrefix": "127.0.0.1:5000/hwlab", + "loopbackPort5000": true, + "registryContainerVisible": true, + "matchingContainers": [ + { + "id": "00091efce380", + "image": "registry:2.8.3", + "names": "unidesk-artifact-registry", + "ports": "127.0.0.1:5000->5000/tcp", + "state": "running", + "status": "Up 27 hours" + } + ], + "pushAttempted": false, + "mutationAttempted": false + } + }, + "k3sPullAccess": { + "id": "k3s-pull-access", + "status": "pass", + "requiredForPublish": false, + "requiredForDeploy": true, + "endpoint": "127.0.0.1:5000/hwlab", + "probeKind": "kubectl-read-only", + "summary": "Read-only kubectl observed hwlab-dev pods with pulled registry images.", + "evidence": { + "kubectlAvailable": true, + "context": { + "command": "kubectl config current-context", + "ok": true, + "exitCode": 0, + "stdout": "in-cluster", + "stderr": "", + "error": null + }, + "canGetPods": { + "command": "kubectl auth can-i get pods -n hwlab-dev", + "ok": true, + "exitCode": 0, + "stdout": "yes", + "stderr": "", + "error": null + }, + "podCount": 13, + "pulledImages": [ + { + "pod": "hwlab-agent-mgr-75544c7747-5dzt9", + "container": "hwlab-agent-mgr", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-mgr@sha256:c16c733306a28ae9385087653b79f5bb2f28724d917a89bb296646f6a02c5318" + }, + { + "pod": "hwlab-agent-skills-849d9976f4-2nsmw", + "container": "hwlab-agent-skills", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-agent-skills@sha256:246fb7bb48597a7179aab62cb091e8213e47b71b6871ba94e6028032992e916c" + }, + { + "pod": "hwlab-box-simu-55688cbcc7-pftxc", + "container": "hwlab-box-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" + }, + { + "pod": "hwlab-box-simu-55688cbcc7-sq5fx", + "container": "hwlab-box-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-box-simu@sha256:ae5fcc06a276918561c2d752f2221156bd2bd4db7fb93967ae76af97bfdc51bf" + }, + { + "pod": "hwlab-cloud-api-f48dcdfb-9n6zh", + "container": "hwlab-cloud-api", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-api@sha256:3946efdd7360409cea6a897cb03a6f3172f14819d3132df3aab821621909513f" + }, + { + "pod": "hwlab-cloud-web-cd9655b98-gxtmc", + "container": "hwlab-cloud-web", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-cloud-web@sha256:868e2a11c56cc1a27c77de3fa71a7b092f21ae98a4052104e9071bd93960abfa" + }, + { + "pod": "hwlab-edge-proxy-567f978cdd-8fwc8", + "container": "hwlab-edge-proxy", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-edge-proxy@sha256:384832fc579063b9b0a940973690734bb402607720fd36f4662e4ee6fa5a57d2" + }, + { + "pod": "hwlab-frpc-59f57d479c-wm5l2", + "container": "frpc", + "image": "127.0.0.1:5000/hwlab/frpc:v0.68.1", + "imageID": "127.0.0.1:5000/hwlab/frpc@sha256:ce400cda42a260964e40ab1ca68b868f72c569cdce4b243aef36ce40194d287b" + }, + { + "pod": "hwlab-gateway-simu-699fd486b-j9pk7", + "container": "hwlab-gateway-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" + }, + { + "pod": "hwlab-gateway-simu-699fd486b-phcgp", + "container": "hwlab-gateway-simu", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-gateway-simu@sha256:00599721b910b5667b6be43c416cab4c410c6cc29b7f56ecdfb6eff71123d01c" + }, + { + "pod": "hwlab-patch-panel-757f9f44d5-7np8r", + "container": "hwlab-patch-panel", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-patch-panel@sha256:5b3aa6505492c2ed398d21cbd35a8754672114acd0fa97faa3351d4b6a6dfd5a" + }, + { + "pod": "hwlab-router-5d9cbdf89b-p7r94", + "container": "hwlab-router", + "image": "127.0.0.1:5000/hwlab/hwlab-router:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-router@sha256:217b1fdb0f704c4183e9d8503644177f0c8bae674da0433985c19ef909f0e342" + }, + { + "pod": "hwlab-tunnel-client-65c7858c8-wjqsh", + "container": "hwlab-tunnel-client", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:cb35ada", + "imageID": "127.0.0.1:5000/hwlab/hwlab-tunnel-client@sha256:9bcdd8a58132d7bc4dbfc71297f802fc0ab407a1b5c1bb1ea58e0791147a243b" + } + ], + "pullFailures": [], + "pullAttempted": false, + "mutationAttempted": false + } + } + }, + "interpretation": { + "processHttpAccess": "Runner process HTTP access to /v2/ is diagnostic only and must not be treated as Docker daemon push access.", + "dockerDaemonPushAccess": "Docker daemon push access is the publish-path capability. This probe is read-only and does not push.", + "k3sPullAccess": "k3s pull access is the deploy-path capability. This probe is read-only and does not create pods or read secrets." + } + } + ] + }, { "id": "cloud-api-db-env-contract", "category": "db", @@ -447,17 +1057,22 @@ "ready": false, "configReady": false, "connected": false, + "liveConnected": false, "connectionChecked": false, + "connectionAttempted": false, + "connectionResult": "not_attempted_missing_env", "requiredEnv": [ { "name": "HWLAB_CLOUD_DB_URL", "present": false, - "redacted": true + "redacted": true, + "source": "k8s-secret-ref" }, { "name": "HWLAB_CLOUD_DB_SSL_MODE", "present": false, - "redacted": false + "redacted": false, + "source": "runtime-env" } ], "missingEnv": [ @@ -470,9 +1085,25 @@ "secretName": "hwlab-cloud-api-dev-db", "secretKey": "database-url", "present": false, + "envInjected": false, + "secretPresent": "not_observed_by_runtime", + "secretKeyPresent": "not_observed_by_runtime", "redacted": true } ], + "connection": { + "attempted": false, + "networkAttempted": false, + "result": "not_attempted_missing_env", + "classification": "missing_runtime_env", + "probeType": "tcp-connect", + "endpointRedacted": true, + "valueRedacted": true, + "timeoutMs": null, + "durationMs": 0, + "errorCode": null + }, + "blocker": "DB runtime config is missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE", "liveDbEvidence": false, "fixtureEvidence": false }, @@ -528,17 +1159,22 @@ "ready": false, "configReady": false, "connected": false, + "liveConnected": false, "connectionChecked": false, + "connectionAttempted": false, + "connectionResult": "not_attempted_missing_env", "requiredEnv": [ { "name": "HWLAB_CLOUD_DB_URL", "present": false, - "redacted": true + "redacted": true, + "source": "k8s-secret-ref" }, { "name": "HWLAB_CLOUD_DB_SSL_MODE", "present": false, - "redacted": false + "redacted": false, + "source": "runtime-env" } ], "missingEnv": [ @@ -551,9 +1187,25 @@ "secretName": "hwlab-cloud-api-dev-db", "secretKey": "database-url", "present": false, + "envInjected": false, + "secretPresent": "not_observed_by_runtime", + "secretKeyPresent": "not_observed_by_runtime", "redacted": true } ], + "connection": { + "attempted": false, + "networkAttempted": false, + "result": "not_attempted_missing_env", + "classification": "missing_runtime_env", + "probeType": "tcp-connect", + "endpointRedacted": true, + "valueRedacted": true, + "timeoutMs": null, + "durationMs": 0, + "errorCode": null + }, + "blocker": "DB runtime config is missing HWLAB_CLOUD_DB_URL, HWLAB_CLOUD_DB_SSL_MODE", "liveDbEvidence": false, "fixtureEvidence": false }, @@ -566,21 +1218,40 @@ "id": "dev-artifact-publish-report", "category": "registry", "status": "blocked", - "summary": "DEV artifact publish report status is blocked with 0/13 published for source 70bb9168ead272ec1f77b752df8e178e70858df4.", + "summary": "DEV artifact publish report status is published with 13/13 required services published for source 73b379f4441d47a99dfd978a80fd43727410af51; source states resolved: 13 source-present, 0 intentionally-disabled.", "evidence": [] }, { "id": "dev-edge-health-report", "category": "edge", "status": "blocked", - "summary": "Committed DEV edge health report is not_run with classification not_run.", + "summary": "Committed DEV edge health report is blocked with classification app_health_blocker.", "evidence": [ { "report": "reports/dev-gate/dev-edge-health.json", - "status": "not_run", - "classification": "not_run", - "publicTcp": [], - "kubernetesObservable": false + "status": "blocked", + "classification": "app_health_blocker", + "publicTcp": [ + { + "host": "74.48.78.17", + "port": 16667, + "status": "connected", + "durationMs": 212 + }, + { + "host": "74.48.78.17", + "port": 7000, + "status": "connected", + "durationMs": 1205 + }, + { + "host": "74.48.78.17", + "port": 7402, + "status": "connected", + "durationMs": 232 + } + ], + "kubernetesObservable": true } ] }, @@ -588,7 +1259,7 @@ "id": "frp-master-edge-static", "category": "edge", "status": "pass", - "summary": "FRP and master-edge contracts describe D601-to-master DEV on port 6667.", + "summary": "FRP and master-edge contracts describe D601-to-master DEV on public ports 16666/16667.", "evidence": [] }, { @@ -601,157 +1272,171 @@ { "id": "d601-k3s-read-access", "category": "k3s", - "status": "blocked", - "summary": "kubectl is not installed in this runner, so hwlab-dev live cluster evidence cannot be collected.", - "evidence": [] - }, - { - "id": "public-dev-edge-health", - "category": "edge", - "status": "blocked", - "summary": "Public DEV health endpoint did not respond successfully.", + "status": "pass", + "summary": "Read-only kubectl probes reached hwlab-dev.", "evidence": [ { - "ok": false, - "url": "http://74.48.78.17:16667/health/live", - "method": "GET", - "error": "fetch failed" + "ok": true, + "command": "kubectl version --client=true", + "exitCode": 0, + "stdout": "Client Version: v1.36.1\nKustomize Version: v5.8.1", + "stderr": "" + }, + { + "ok": true, + "command": "kubectl config current-context", + "exitCode": 0, + "stdout": "in-cluster", + "stderr": "" + }, + { + "ok": true, + "command": "kubectl auth can-i get pods -n hwlab-dev", + "exitCode": 0, + "stdout": "yes", + "stderr": "" + }, + { + "ok": true, + "command": "kubectl get namespace hwlab-dev -o json", + "exitCode": 0, + "stdout": "{\n \"apiVersion\": \"v1\",\n \"kind\": \"Namespace\",\n \"metadata\": {\n \"annotations\": {\n \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Namespace\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"app.kubernetes.io/part-of\\\":\\\"hwlab\\\",\\\"hwlab.pikastech.local/environment\\\":\\\"dev\\\",\\\"hwlab.pikastech.local/profile\\\":\\\"dev\\\"},\\\"name\\\":\\\"hwlab-dev\\\"}}\\n\"\n },\n \"creationTimestamp\": \"2026-05-21T23:52:11Z\",\n \"labels\": {\n \"app.kubernetes.io/part-of\": \"hwlab\",\n \"hwlab.pikastech.local/environment\": \"dev\",\n \"hwlab.pikastech.local/profile\": \"dev\",\n \"kubernetes.io/metadata.name\": \"hwlab-dev\"\n },\n \"name\": \"hwlab-dev\",\n \"resourceVersion\": \"1081061\",\n \"uid\": \"97fbde04-c052-46f3-91ee-b15616f7690b\"\n },\n \"spec\": {\n \"finalizers\": [\n \"kubernetes\"\n ]\n },\n \"status\": {\n \"phase\": \"Active\"\n }\n}", + "stderr": "" + }, + { + "ok": true, + "command": "kubectl -n hwlab-dev get deploy,svc,job,cm -o name", + "exitCode": 0, + "stdout": "deployment.apps/hwlab-agent-mgr\ndeployment.apps/hwlab-agent-skills\ndeployment.apps/hwlab-box-simu\ndeployment.apps/hwlab-cloud-api\ndeployment.apps/hwlab-cloud-web\ndeployment.apps/hwlab-edge-proxy\ndeployment.apps/hwlab-frpc\ndeployment.apps/hwlab-gateway\ndeployment.apps/hwlab-gateway-simu\ndeployment.apps/hwlab-patch-panel\ndeployment.apps/hwlab-router\ndeployment.apps/hwlab-tunnel-client\nservice/hwlab-agent-mgr\nservice/hwlab-agent-skills\nservice/hwlab-box-simu\nservice/hwlab-cli\nservice/hwlab-cloud-api\nservice/hwlab-cloud-web\nservice/hwlab-edge-proxy\nservice/hwlab-gateway\nservice/hwlab-gateway-simu\nservice/hwlab-patch-panel\nservice/hwlab-router\nservice/hwlab-tunnel-client\njob.batch/hwlab-agent-worker-template\njob.batch/hwlab-cli-template\nconfigmap/hwlab-dev-health-contract\nconfigmap/hwlab-frpc-config\nconfigmap/kube-root-ca.crt", + "stderr": "" } ] }, { - "id": "ghcr-anonymous-manifest-read", - "category": "registry", - "status": "blocked", - "summary": "One or more catalog images could not be verified through anonymous GHCR manifest HEAD probes.", + "id": "public-dev-edge-health", + "category": "edge", + "status": "pass", + "summary": "Public DEV health endpoint responded successfully.", + "evidence": [ + { + "ok": true, + "url": "http://74.48.78.17:16667/health/live", + "method": "GET", + "status": 200, + "statusText": "OK", + "body": "{\"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:31:08.245Z\",\"db\":{\"contractVersion\":\"v1\",\"environment\":\"dev\",\"connected\":false,\"connectionChecked\":false,\"c" + } + ] + }, + { + "id": "catalog-image-manifest-read", + "category": "artifact-catalog", + "status": "degraded", + "summary": "One or more required catalog images could not be verified through manifest HEAD probes; this is runner-process catalog evidence, not Docker daemon push access.", "evidence": [ { "serviceId": "hwlab-cloud-api", - "image": "ghcr.io/pikastech/hwlab-cloud-api:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-cloud-api/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-cloud-api/manifests/73b379f", "method": "HEAD", - "error": "This operation was aborted" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-cloud-web", - "image": "ghcr.io/pikastech/hwlab-cloud-web:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-cloud-web/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-cloud-web/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-agent-mgr", - "image": "ghcr.io/pikastech/hwlab-agent-mgr:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-agent-mgr/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-agent-mgr/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-agent-worker", - "image": "ghcr.io/pikastech/hwlab-agent-worker:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-agent-worker/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-agent-worker/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-gateway", - "image": "ghcr.io/pikastech/hwlab-gateway:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-gateway/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-gateway/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-gateway-simu", - "image": "ghcr.io/pikastech/hwlab-gateway-simu:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-gateway-simu/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-gateway-simu/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-box-simu", - "image": "ghcr.io/pikastech/hwlab-box-simu:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-box-simu:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-box-simu/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-box-simu/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-patch-panel", - "image": "ghcr.io/pikastech/hwlab-patch-panel:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-patch-panel/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-patch-panel/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-router", - "image": "ghcr.io/pikastech/hwlab-router:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-router:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-router/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-router/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-tunnel-client", - "image": "ghcr.io/pikastech/hwlab-tunnel-client:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-tunnel-client/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-tunnel-client/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-edge-proxy", - "image": "ghcr.io/pikastech/hwlab-edge-proxy:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-edge-proxy/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-edge-proxy/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-cli", - "image": "ghcr.io/pikastech/hwlab-cli:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-cli:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-cli/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-cli/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" }, { "serviceId": "hwlab-agent-skills", - "image": "ghcr.io/pikastech/hwlab-agent-skills:1a2efd4", + "image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:73b379f", "ok": false, - "url": "https://ghcr.io/v2/pikastech/hwlab-agent-skills/manifests/1a2efd4", + "url": "http://127.0.0.1:5000/v2/hwlab/hwlab-agent-skills/manifests/73b379f", "method": "HEAD", - "status": 401, - "statusText": "Unauthorized", - "body": "" + "error": "fetch failed ECONNREFUSED 127.0.0.1 5000" } ] } @@ -759,10 +1444,10 @@ "blockers": [ { "status": "open", - "type": "runtime_blocker", - "scope": "artifact-catalog", - "summary": "deploy/artifact-catalog.dev.json does not prove published artifacts for origin/main 1a2efd4; ciPublished=false, registryVerified=false, not_published=13.", - "nextTask": "Run the DEV artifact publish workflow, then record only real sha256 digests with `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --publish-report reports/dev-gate/dev-artifacts.json`; if publish is still blocked, keep not_published via `node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked`." + "type": "contract_blocker", + "scope": "artifact-source-commit", + "summary": "source commit origin/main a1c3cb5 does not match deploy/catalog artifact identity: deploy=73b379f, catalog=73b379f, services=73b379f.", + "nextTask": "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`." }, { "status": "open", @@ -775,36 +1460,15 @@ "status": "open", "type": "runtime_blocker", "scope": "dev-artifact-publish", - "summary": "reports/dev-gate/dev-artifacts.json does not prove all HWLAB service artifacts for origin/main 1a2efd4; current status is blocked with 0/13 published.", - "nextTask": "Complete DEV artifact publishing for every frozen HWLAB service at the current origin/main commit and record immutable registry digests." + "summary": "reports/dev-gate/dev-artifacts.json does not prove all required HWLAB service artifacts for origin/main a1c3cb5; 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 current origin/main commit and record immutable registry digests; keep disabled services marked not_published with reasons." }, { "status": "open", "type": "network_blocker", - "scope": "dev-edge-health", - "summary": "live network probes require --live", - "nextTask": "Repair the master frps/public DEV edge route and rerun the read-only DEV edge health smoke before real deployment." - }, - { - "status": "open", - "type": "environment_blocker", - "scope": "d601-k3s", - "summary": "D601 runner lacks kubectl and no default kubeconfig was used by this preflight.", - "nextTask": "Provide a read-only kubectl/kubeconfig path for the real D601 hwlab-dev k3s cluster, then rerun this preflight." - }, - { - "status": "open", - "type": "network_blocker", - "scope": "dev-edge", - "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." - }, - { - "status": "open", - "type": "runtime_blocker", - "scope": "ghcr", - "summary": "This preflight could not verify GHCR manifests for the DEV catalog images without reading credentials.", - "nextTask": "Publish public DEV images or provide a non-secret registry evidence artifact with immutable digests for each HWLAB service." + "scope": "cloud-api-db", + "summary": "cloud-api DB env is injected, but runtime health has not attempted a live DB connection", + "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." } ], "notes": "No PROD action, secret read, UniDesk runtime substitution, heavy e2e, browser e2e, runtime restart, or force push was performed." diff --git a/scripts/dev-artifact-publish.mjs b/scripts/dev-artifact-publish.mjs index ad6165f8..2b8077da 100644 --- a/scripts/dev-artifact-publish.mjs +++ b/scripts/dev-artifact-publish.mjs @@ -16,6 +16,7 @@ import { serviceInventoryFromServices } from "./src/dev-artifact-services.mjs"; import { runDevBaseImagePreflight } from "./src/dev-base-image-preflight.mjs"; +import { probeRegistryCapabilities } from "./src/registry-capabilities.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const defaultRegistryPrefix = @@ -152,6 +153,17 @@ function summarizeBaseImagePreflight(result) { }; } +function summarizeRegistryCapabilities(result) { + return { + version: result.version, + registryPrefix: result.registryPrefix, + generatedAt: result.generatedAt, + classification: result.classification, + dimensions: result.dimensions, + interpretation: result.interpretation + }; +} + function baseImagePreflightBlocker(result) { return blocker({ type: "environment_blocker", @@ -454,7 +466,7 @@ function sourceStateBlocker(service) { }); } -async function preflight({ args, services, catalog, deployManifest, baseImagePreflight }) { +async function preflight({ args, services, catalog, deployManifest, baseImagePreflight, registryCapabilities }) { const blockers = []; let registryPrefix = args.registryPrefix; @@ -504,20 +516,15 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre } } - if (baseImagePreflight.containerEngine?.available) { - if (registryPrefix.startsWith("127.0.0.1:5000/") || registryPrefix.startsWith("localhost:5000/")) { - const ps = await run("docker", ["ps", "--format", "{{json .}}"]); - if (ps.code !== 0 || !dockerPsShowsRegistry5000(ps.stdout)) { - blockers.push( - blocker({ - type: "network_blocker", - scope: "registry", - summary: "No Docker-visible registry container is publishing port 5000.", - next: "Start or expose the D601 local registry before running --publish." - }) - ); - } - } + if (registryCapabilities?.dimensions?.dockerDaemonPushAccess?.status === "blocked") { + blockers.push( + blocker({ + type: "network_blocker", + scope: "docker-daemon-push-access", + summary: registryCapabilities.dimensions.dockerDaemonPushAccess.summary, + next: "Restore Docker daemon access to the D601 local/internal registry before running --publish." + }) + ); } for (const service of services) { @@ -561,22 +568,6 @@ function hasFatalBlocker(blockers) { return blockers.some((item) => fatalBlockerTypes.has(item.type)); } -function dockerPsShowsRegistry5000(stdout) { - return stdout - .split("\n") - .filter(Boolean) - .some((line) => { - try { - const row = JSON.parse(line); - const image = String(row.Image ?? ""); - const ports = String(row.Ports ?? ""); - return image.startsWith("registry:") && ports.includes("5000->5000/tcp"); - } catch { - return line.includes("registry:") && /5000(?:->|\\u003e)5000\/tcp/u.test(line); - } - }); -} - async function buildService({ args, repo, commitId, shortCommit, service }) { const tag = shortCommit; const ref = imageRef(args.registryPrefix, service.serviceId, tag); @@ -783,13 +774,14 @@ function artifactRecord({ args, service, shortCommit, status, digest = "not_publ }; } -function publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlocked }) { +function publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlocked, registryCapabilities }) { return { version: "v2", mode, sourceCommitId: commitId, imageTag: shortCommit, registryTarget: args.registryPrefix, + registryCapabilities: summarizeRegistryCapabilities(registryCapabilities), digestPlaceholder: "not_published", services: artifacts.map((artifact) => { const image = artifact.image ?? imageRef(args.registryPrefix, artifact.serviceId, shortCommit); @@ -813,12 +805,12 @@ function publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlo }; } -function createReport({ args, repo, commitId, shortCommit, mode, services, artifacts, blockers, baseImagePreflight }) { +function createReport({ args, repo, commitId, shortCommit, mode, services, artifacts, blockers, baseImagePreflight, registryCapabilities }) { const status = reportStatus(mode, artifacts, blockers); const fatalBlocked = hasFatalBlocker(blockers); const requiredArtifacts = artifacts.filter((artifact) => artifact.artifactRequired); const serviceInventory = serviceInventoryFromServices(services); - const publishPlan = publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlocked }); + const publishPlan = publishPlanFor({ args, commitId, shortCommit, mode, artifacts, fatalBlocked, registryCapabilities }); const publishedCount = requiredArtifacts.filter((artifact) => artifact.status === "published").length; const builtCount = requiredArtifacts.filter((artifact) => ["built", "published", "published_unverified_digest"].includes(artifact.status)).length; @@ -846,6 +838,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif validationCommands: [ "node --check scripts/dev-artifact-publish.mjs", "node --check scripts/src/dev-artifact-services.mjs", + "node --check scripts/src/registry-capabilities.mjs", "node --check scripts/preflight-dev-base-image.mjs", "node --check scripts/src/dev-base-image-preflight.mjs", "node scripts/preflight-dev-base-image.mjs", @@ -894,6 +887,7 @@ function createReport({ args, repo, commitId, shortCommit, mode, services, artif repo, sourceCommitId: commitId, registryPrefix: args.registryPrefix, + registryCapabilities: summarizeRegistryCapabilities(registryCapabilities), baseImage: args.baseImage ?? null, baseImagePreflight: summarizeBaseImagePreflight(baseImagePreflight), environment: ENVIRONMENT_DEV, @@ -942,9 +936,14 @@ async function main() { } args.registryPrefix = validateRegistryPrefix(args.registryPrefix); - const baseImagePreflight = await runDevBaseImagePreflight({ - env: preflightEnvForBaseImage(args.baseImage) - }); + const [baseImagePreflight, registryCapabilities] = await Promise.all([ + runDevBaseImagePreflight({ + env: preflightEnvForBaseImage(args.baseImage) + }), + probeRegistryCapabilities({ + registryPrefix: args.registryPrefix + }) + ]); if (baseImagePreflight.publishUsable) { args.baseImage = baseImagePreflight.localTag; } @@ -958,7 +957,7 @@ async function main() { const repo = repoLabelFromRemote(remoteUrl); const services = await resolveServices(args.services, catalog); - let blockers = await preflight({ args, services, catalog, deployManifest, baseImagePreflight }); + let blockers = await preflight({ args, services, catalog, deployManifest, baseImagePreflight, registryCapabilities }); let artifacts = services.map((service) => artifactRecord({ args, service, @@ -995,7 +994,7 @@ async function main() { artifacts = services.map((service) => artifactByServiceId.get(service.serviceId)); } - const report = createReport({ args, repo, commitId, shortCommit, mode: args.mode, services, artifacts, blockers, baseImagePreflight }); + const report = createReport({ args, repo, commitId, shortCommit, mode: args.mode, services, artifacts, blockers, baseImagePreflight, registryCapabilities }); if (args.emitReport) { await writeReport(args.reportPath, report); } @@ -1006,6 +1005,7 @@ async function main() { reportPath: args.emitReport ? args.reportPath : null, sourceCommitId: commitId, registryPrefix: args.registryPrefix, + registryCapabilities: summarizeRegistryCapabilities(registryCapabilities), baseImagePreflight: summarizeBaseImagePreflight(baseImagePreflight), publishPlan: report.artifactPublish.publishPlan, services: report.artifactPublish.services.map((service) => ({ diff --git a/scripts/src/dev-gate-preflight.mjs b/scripts/src/dev-gate-preflight.mjs index 15e15067..900101aa 100644 --- a/scripts/src/dev-gate-preflight.mjs +++ b/scripts/src/dev-gate-preflight.mjs @@ -11,12 +11,13 @@ import { summarizeDbContract } from "../../internal/cloud/db-contract.mjs"; import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs"; +import { probeRegistryCapabilities } from "./registry-capabilities.mjs"; const execFileAsync = promisify(execFile); const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const defaultReportPath = "reports/dev-gate/dev-preflight-report.json"; const issue = "pikasTech/HWLAB#34"; -const supports = ["#7", "#12", "#14", "#22", "#23", "#29", "#30", "#31", "#33", "#39", "#49"].map( +const supports = ["#7", "#12", "#14", "#22", "#23", "#29", "#30", "#31", "#33", "#39", "#49", "#66"].map( (id) => `pikasTech/HWLAB${id}` ).concat(["pikasTech/HWLAB#33", "pikasTech/HWLAB#35", "pikasTech/HWLAB#48"]); const forbiddenActions = [ @@ -540,6 +541,54 @@ function validateArtifactPublishReport(reporter, artifactReport, targetShortComm }); } +function validateRegistryCapabilities(reporter, registryCapabilities) { + for (const dimension of Object.values(registryCapabilities.dimensions)) { + reporter.check( + `registry-capability-${dimension.id}`, + "registry", + dimension.status, + dimension.summary, + [dimension] + ); + } + + reporter.check( + "registry-capability-classification", + "registry", + registryCapabilities.classification, + `Registry capability classification is ${registryCapabilities.classification}: process HTTP, Docker daemon push path, and k3s pull path are reported separately.`, + [registryCapabilities] + ); + + const dockerDaemonPushAccess = registryCapabilities.dimensions.dockerDaemonPushAccess; + if (dockerDaemonPushAccess.status === "blocked") { + reporter.block({ + type: "network_blocker", + scope: "docker-daemon-push-access", + summary: dockerDaemonPushAccess.summary, + nextTask: "Restore Docker daemon access to the D601 local/internal registry before claiming DEV artifact publish readiness." + }); + } + + const k3sPullAccess = registryCapabilities.dimensions.k3sPullAccess; + if (k3sPullAccess.status === "blocked") { + reporter.block({ + type: "network_blocker", + scope: "k3s-pull-access", + summary: k3sPullAccess.summary, + nextTask: "Repair read-only hwlab-dev k3s access or image pull configuration before claiming deploy-path registry readiness." + }); + } +} + +function manifestReadStatus(registryEvidence) { + if (registryEvidence.every((probe) => probe.ok)) return "pass"; + if (registryEvidence.some((probe) => probe.error || probe.status === 401 || probe.status === 403 || probe.status === 404)) { + return "degraded"; + } + return "blocked"; +} + function validateEdgeHealthReport(reporter, edgeReport) { if (!edgeReport) { reporter.check("dev-edge-health-report", "edge", "blocked", "No DEV edge health report is present."); @@ -806,18 +855,20 @@ async function validateLiveProbes(reporter, catalog, timeoutMs) { }); return { serviceId: service.serviceId, image: service.image, ...probe }; })); - const registryOk = registryEvidence.every((probe) => probe.ok); + const registryStatus = manifestReadStatus(registryEvidence); reporter.check( - "registry-manifest-read", - "registry", - registryOk ? "pass" : "blocked", - registryOk ? "All required catalog images were visible through registry manifest HEAD probes." : "One or more required catalog images could not be verified through registry manifest HEAD probes.", + "catalog-image-manifest-read", + "artifact-catalog", + registryStatus, + registryStatus === "pass" + ? "All required catalog images were visible through manifest HEAD probes." + : "One or more required catalog images could not be verified through manifest HEAD probes; this is runner-process catalog evidence, not Docker daemon push access.", registryEvidence ); - if (!registryOk) { + if (registryStatus === "blocked") { reporter.block({ type: "runtime_blocker", - scope: "registry-manifest-read", + scope: "catalog-image-manifest-read", summary: "This preflight could not verify registry manifests for the required DEV catalog images without reading credentials.", nextTask: "Publish required DEV images or provide a non-secret registry evidence artifact with immutable digests for each required HWLAB service." }); @@ -848,9 +899,10 @@ async function writeReport(report, reportPath) { await writeFile(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`); } -function makeReport(args, targetCommit, targetShortCommit, reporter, artifactIdentity) { +function makeReport(args, targetCommit, targetShortCommit, reporter, artifactIdentity, registryCapabilities) { + const blockingCheckStatuses = new Set(["blocked", "failed"]); const conclusion = reporter.blockers.length === 0 && - reporter.checks.every((item) => item.status === "pass") ? "ready" : "blocked"; + reporter.checks.every((item) => !blockingCheckStatuses.has(item.status)) ? "ready" : "blocked"; return { $schema: "https://hwlab.pikastech.local/schemas/dev-gate-preflight-report.schema.json", @@ -872,12 +924,14 @@ function makeReport(args, targetCommit, targetShortCommit, reporter, artifactIde validationCommands: [ "node --check scripts/dev-gate-preflight.mjs", "node --check scripts/src/dev-gate-preflight.mjs", + "node --check scripts/src/registry-capabilities.mjs", "node --check scripts/refresh-artifact-catalog.mjs", "node scripts/dev-gate-preflight.mjs", "node --check scripts/validate-dev-gate-report.mjs", "node scripts/validate-dev-gate-report.mjs" ], artifactIdentity, + registryCapabilities, conclusion, checks: reporter.checks, blockers: reporter.blockers, @@ -893,6 +947,13 @@ function printSummary(args, report) { targetShortCommit: report.target.shortCommitId, artifactCatalogCommit: report.artifactIdentity?.artifactCatalog?.commitId, catalogDigests: report.artifactIdentity?.artifactCatalog?.digestCounts, + registryCapabilities: { + classification: report.registryCapabilities?.classification, + dimensions: Object.fromEntries(Object.entries(report.registryCapabilities?.dimensions ?? {}).map(([key, value]) => [ + key, + value.status + ])) + }, conclusion: report.conclusion, report: args.writeReport ? args.reportPath : null, checks: report.checks.reduce((counts, item) => { @@ -921,6 +982,11 @@ export async function runPreflight(argv) { const reporter = makeReporter(); const contracts = await loadContracts(); const optionalReports = await loadOptionalReports(); + const registryPrefix = optionalReports.artifactPublish?.artifactPublish?.registryPrefix ?? "127.0.0.1:5000/hwlab"; + const registryCapabilities = await probeRegistryCapabilities({ + registryPrefix, + timeoutMs: args.timeoutMs + }); const { deploy, catalog, masterEdge, artifactIdentity } = validateLocalContracts( reporter, contracts, @@ -929,6 +995,7 @@ export async function runPreflight(argv) { args.targetRef ); + validateRegistryCapabilities(reporter, registryCapabilities); validateCloudApiDbContract(reporter, deploy, contracts[3]); validateArtifactPublishReport(reporter, optionalReports.artifactPublish, targetShortCommit, targetCommit, args.targetRef); validateEdgeHealthReport(reporter, optionalReports.edgeHealth); @@ -936,7 +1003,7 @@ export async function runPreflight(argv) { validateRuntimeBoundary(reporter, deploy); await validateLiveProbes(reporter, catalog, args.timeoutMs); - const report = makeReport(args, targetCommit, targetShortCommit, reporter, artifactIdentity); + const report = makeReport(args, targetCommit, targetShortCommit, reporter, artifactIdentity, registryCapabilities); if (args.writeReport) { await writeReport(report, args.reportPath); } diff --git a/scripts/src/registry-capabilities.mjs b/scripts/src/registry-capabilities.mjs new file mode 100644 index 00000000..e15af505 --- /dev/null +++ b/scripts/src/registry-capabilities.mjs @@ -0,0 +1,389 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); +const defaultRegistryPrefix = "127.0.0.1:5000/hwlab"; +const imagePullBackoffReasons = new Set(["ErrImagePull", "ImagePullBackOff", "InvalidImageName"]); + +function commandLine(command, args) { + return [command, ...args].join(" "); +} + +async function run(command, args = [], { timeoutMs = 5000 } = {}) { + const commandText = commandLine(command, args); + try { + const result = await execFileAsync(command, args, { + timeout: timeoutMs, + maxBuffer: 1024 * 1024 + }); + return { + ok: true, + command: commandText, + exitCode: 0, + stdout: result.stdout.trim(), + stderr: result.stderr.trim() + }; + } catch (error) { + return { + ok: false, + command: commandText, + exitCode: typeof error.code === "number" ? error.code : 1, + stdout: String(error.stdout ?? "").trim(), + stderr: String(error.stderr ?? "").trim(), + error: error.message + }; + } +} + +async function commandExists(command, timeoutMs) { + return (await run("which", [command], { timeoutMs })).ok; +} + +function fetchErrorMessage(error) { + if (!(error instanceof Error)) { + return String(error); + } + const cause = error.cause; + if (cause && typeof cause === "object") { + const code = "code" in cause ? String(cause.code) : ""; + const address = "address" in cause ? String(cause.address) : ""; + const port = "port" in cause ? String(cause.port) : ""; + return [error.message, code, address, port].filter(Boolean).join(" "); + } + return error.message; +} + +async function httpProbe(url, { timeoutMs = 5000 } = {}) { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(url, { + method: "GET", + signal: controller.signal + }); + const body = await response.text(); + return { + ok: response.ok, + url, + method: "GET", + status: response.status, + statusText: response.statusText, + body: body.slice(0, 500) + }; + } catch (error) { + return { + ok: false, + url, + method: "GET", + error: fetchErrorMessage(error) + }; + } finally { + clearTimeout(timer); + } +} + +export function parseRegistryPrefix(prefix = defaultRegistryPrefix) { + const normalized = prefix.replace(/\/+$/u, ""); + const [hostPort, ...pathParts] = normalized.split("/"); + const host = hostPort.split(":")[0].toLowerCase(); + const port = hostPort.includes(":") ? hostPort.split(":").at(-1) : null; + return { + normalized, + hostPort, + host, + port, + namespace: pathParts.join("/") + }; +} + +export function registryApiEndpoint(prefix = defaultRegistryPrefix) { + const parsed = parseRegistryPrefix(prefix); + return `http://${parsed.hostPort}/v2/`; +} + +function isLoopbackRegistry5000(prefix) { + const parsed = parseRegistryPrefix(prefix); + return (parsed.host === "127.0.0.1" || parsed.host === "localhost") && parsed.port === "5000"; +} + +function summarizeCommand(result) { + return { + command: result.command, + ok: result.ok, + exitCode: result.exitCode, + stdout: result.stdout.slice(0, 1000), + stderr: result.stderr.slice(0, 1000), + error: result.error ?? null + }; +} + +function parseDockerPs(stdout) { + return stdout + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .map((line) => { + try { + return JSON.parse(line); + } catch { + return null; + } + }) + .filter(Boolean); +} + +function registryContainersFromDockerPs(stdout) { + return parseDockerPs(stdout) + .filter((row) => { + const image = String(row.Image ?? ""); + const ports = String(row.Ports ?? ""); + return image.startsWith("registry:") && ports.includes("5000->5000/tcp"); + }) + .map((row) => ({ + id: row.ID ?? null, + image: row.Image ?? null, + names: row.Names ?? null, + ports: row.Ports ?? null, + state: row.State ?? null, + status: row.Status ?? null + })); +} + +async function probeProcessHttpAccess(registryPrefix, timeoutMs) { + const endpoint = registryApiEndpoint(registryPrefix); + const probe = await httpProbe(endpoint, { timeoutMs }); + const passed = probe.ok || probe.status === 401; + return { + id: "process-http-access", + status: passed ? "pass" : "degraded", + requiredForPublish: false, + requiredForDeploy: false, + endpoint, + probeKind: "runner-process-http-v2", + summary: passed + ? `Runner process can reach ${endpoint}.` + : `Runner process cannot reach ${endpoint}; this is process HTTP access only and does not prove Docker daemon push failure.`, + evidence: { + probe, + publishFailure: false + } + }; +} + +async function probeDockerDaemonPushAccess(registryPrefix, timeoutMs) { + const dockerVersion = await run("docker", ["--version"], { timeoutMs }); + if (!dockerVersion.ok) { + return { + id: "docker-daemon-push-access", + status: "blocked", + requiredForPublish: true, + requiredForDeploy: false, + endpoint: registryPrefix, + probeKind: "docker-cli-read-only", + summary: "Docker CLI is not available, so Docker daemon push access cannot be assessed.", + evidence: { + dockerVersion: summarizeCommand(dockerVersion), + pushAttempted: false, + mutationAttempted: false + } + }; + } + + const dockerPs = await run("docker", ["ps", "--format", "{{json .}}"], { timeoutMs }); + if (!dockerPs.ok) { + return { + id: "docker-daemon-push-access", + status: "blocked", + requiredForPublish: true, + requiredForDeploy: false, + endpoint: registryPrefix, + probeKind: "docker-ps-read-only", + summary: "Docker daemon is not readable, so Docker daemon push access cannot be assessed.", + evidence: { + dockerVersion: summarizeCommand(dockerVersion), + dockerPs: summarizeCommand(dockerPs), + pushAttempted: false, + mutationAttempted: false + } + }; + } + + const matchingContainers = registryContainersFromDockerPs(dockerPs.stdout); + const registryVisible = !isLoopbackRegistry5000(registryPrefix) || matchingContainers.length > 0; + return { + id: "docker-daemon-push-access", + status: registryVisible ? "pass" : "blocked", + requiredForPublish: true, + requiredForDeploy: false, + endpoint: registryPrefix, + probeKind: "docker-ps-read-only", + summary: registryVisible + ? "Docker daemon can see a local/internal registry target for publish preflight purposes; no push was attempted." + : "Docker daemon did not show a registry container publishing port 5000.", + evidence: { + dockerVersion: summarizeCommand(dockerVersion), + registryPrefix, + loopbackPort5000: isLoopbackRegistry5000(registryPrefix), + registryContainerVisible: registryVisible, + matchingContainers, + pushAttempted: false, + mutationAttempted: false + } + }; +} + +function parsePodList(stdout) { + try { + const parsed = JSON.parse(stdout); + return Array.isArray(parsed.items) ? parsed.items : []; + } catch { + return []; + } +} + +function podPullEvidence(pods, registryPrefix) { + const parsed = parseRegistryPrefix(registryPrefix); + const hostNeedle = `${parsed.hostPort}/`; + const pulledImages = []; + const pullFailures = []; + + for (const pod of pods) { + const podName = pod?.metadata?.name ?? "unknown"; + for (const status of pod?.status?.containerStatuses ?? []) { + const image = String(status.image ?? ""); + const imageID = String(status.imageID ?? ""); + const waitingReason = status.state?.waiting?.reason; + if (waitingReason && imagePullBackoffReasons.has(waitingReason)) { + pullFailures.push({ + pod: podName, + container: status.name ?? null, + image, + reason: waitingReason + }); + } + if (image.includes(hostNeedle) && imageID.includes("sha256:")) { + pulledImages.push({ + pod: podName, + container: status.name ?? null, + image, + imageID + }); + } + } + } + + return { + pulledImages, + pullFailures + }; +} + +async function probeK3sPullAccess(registryPrefix, timeoutMs) { + const kubectlExists = await commandExists("kubectl", timeoutMs); + if (!kubectlExists) { + return { + id: "k3s-pull-access", + status: "blocked", + requiredForPublish: false, + requiredForDeploy: true, + endpoint: registryPrefix, + probeKind: "kubectl-read-only", + summary: "kubectl is not installed, so k3s pull access cannot be assessed from this runner.", + evidence: { + kubectlAvailable: false, + pullAttempted: false, + mutationAttempted: false + } + }; + } + + const [context, canGetPods, podsResult] = await Promise.all([ + run("kubectl", ["config", "current-context"], { timeoutMs }), + run("kubectl", ["auth", "can-i", "get", "pods", "-n", "hwlab-dev"], { timeoutMs }), + run("kubectl", ["-n", "hwlab-dev", "get", "pods", "-o", "json"], { timeoutMs }) + ]); + const canReadPods = canGetPods.ok && canGetPods.stdout === "yes" && podsResult.ok; + if (!canReadPods) { + return { + id: "k3s-pull-access", + status: "blocked", + requiredForPublish: false, + requiredForDeploy: true, + endpoint: registryPrefix, + probeKind: "kubectl-read-only", + summary: "Read-only kubectl probes could not inspect hwlab-dev pods, so k3s pull access is blocked.", + evidence: { + kubectlAvailable: true, + context: summarizeCommand(context), + canGetPods: summarizeCommand(canGetPods), + pods: summarizeCommand(podsResult), + pullAttempted: false, + mutationAttempted: false + } + }; + } + + const pods = parsePodList(podsResult.stdout); + const evidence = podPullEvidence(pods, registryPrefix); + const hasFailures = evidence.pullFailures.length > 0; + const hasPulledImages = evidence.pulledImages.length > 0; + return { + id: "k3s-pull-access", + status: hasFailures ? "blocked" : (hasPulledImages ? "pass" : "degraded"), + requiredForPublish: false, + requiredForDeploy: true, + endpoint: registryPrefix, + probeKind: "kubectl-read-only", + summary: hasFailures + ? "Read-only kubectl observed image pull failures in hwlab-dev." + : (hasPulledImages + ? "Read-only kubectl observed hwlab-dev pods with pulled registry images." + : "Read-only kubectl can inspect hwlab-dev, but no current pod proves k3s pulled this registry prefix."), + evidence: { + kubectlAvailable: true, + context: summarizeCommand(context), + canGetPods: summarizeCommand(canGetPods), + podCount: pods.length, + pulledImages: evidence.pulledImages, + pullFailures: evidence.pullFailures, + pullAttempted: false, + mutationAttempted: false + } + }; +} + +function capabilityClassification(dimensions) { + const values = Object.values(dimensions).map((dimension) => dimension.status); + if (values.includes("blocked")) return "blocked"; + if (values.includes("degraded")) return "degraded"; + return "pass"; +} + +export async function probeRegistryCapabilities({ + registryPrefix = defaultRegistryPrefix, + timeoutMs = 5000 +} = {}) { + const [processHttpAccess, dockerDaemonPushAccess, k3sPullAccess] = await Promise.all([ + probeProcessHttpAccess(registryPrefix, timeoutMs), + probeDockerDaemonPushAccess(registryPrefix, timeoutMs), + probeK3sPullAccess(registryPrefix, timeoutMs) + ]); + const dimensions = { + processHttpAccess, + dockerDaemonPushAccess, + k3sPullAccess + }; + + return { + version: "v1", + registryPrefix, + generatedAt: new Date().toISOString(), + classification: capabilityClassification(dimensions), + dimensions, + interpretation: { + processHttpAccess: "Runner process HTTP access to /v2/ is diagnostic only and must not be treated as Docker daemon push access.", + dockerDaemonPushAccess: "Docker daemon push access is the publish-path capability. This probe is read-only and does not push.", + k3sPullAccess: "k3s pull access is the deploy-path capability. This probe is read-only and does not create pods or read secrets." + } + }; +} diff --git a/scripts/validate-dev-gate-report.mjs b/scripts/validate-dev-gate-report.mjs index 11d25057..94da72f4 100644 --- a/scripts/validate-dev-gate-report.mjs +++ b/scripts/validate-dev-gate-report.mjs @@ -58,6 +58,7 @@ const requiredDevDeployApplyValidationCommands = [ const requiredPreflightValidationCommands = [ "node --check scripts/dev-gate-preflight.mjs", "node --check scripts/src/dev-gate-preflight.mjs", + "node --check scripts/src/registry-capabilities.mjs", "node --check scripts/refresh-artifact-catalog.mjs", "node scripts/dev-gate-preflight.mjs" ]; @@ -166,6 +167,8 @@ const reportFamilyTemplates = new Map([ ], requiredValidationCommands: [ "node --check scripts/dev-artifact-publish.mjs", + "node --check scripts/src/dev-artifact-services.mjs", + "node --check scripts/src/registry-capabilities.mjs", "node --check scripts/preflight-dev-base-image.mjs", "node --check scripts/src/dev-base-image-preflight.mjs", "node scripts/preflight-dev-base-image.mjs", @@ -195,7 +198,7 @@ const reportFamilyTemplates = new Map([ ] ]); -const statusValues = new Set(["pass", "blocked", "not_run", "not_applicable", "failed"]); +const statusValues = new Set(["pass", "blocked", "degraded", "not_run", "not_applicable", "failed"]); const blockerTypes = new Set([ "contract_blocker", "environment_blocker", @@ -208,6 +211,16 @@ const blockerTypes = new Set([ const blockerStates = new Set(["open", "acknowledged", "closed"]); const artifactSourceStates = new Set(["source-present", "intentionally-disabled"]); const preflightConclusions = new Set(["ready", "blocked"]); +const registryCapabilityDimensionIds = new Set([ + "process-http-access", + "docker-daemon-push-access", + "k3s-pull-access" +]); +const registryCapabilityDimensionKeys = new Set([ + "processHttpAccess", + "dockerDaemonPushAccess", + "k3sPullAccess" +]); const requiredPreflightSupports = [ "pikasTech/HWLAB#7", "pikasTech/HWLAB#12", @@ -215,7 +228,8 @@ const requiredPreflightSupports = [ "pikasTech/HWLAB#23", "pikasTech/HWLAB#29", "pikasTech/HWLAB#30", - "pikasTech/HWLAB#31" + "pikasTech/HWLAB#31", + "pikasTech/HWLAB#66" ]; function assertObject(value, label) { @@ -461,6 +475,8 @@ async function validateReport(relativePath) { if (Object.hasOwn(report, "artifactPublish")) { assertObject(report.artifactPublish, `${label}.artifactPublish`); + assertRegistryCapabilities(report.artifactPublish.registryCapabilities, `${label}.artifactPublish.registryCapabilities`); + assertRegistryCapabilities(report.artifactPublish.publishPlan?.registryCapabilities, `${label}.artifactPublish.publishPlan.registryCapabilities`); assertArray(report.artifactPublish.services ?? [], `${label}.artifactPublish.services`); for (const [index, service] of report.artifactPublish.services.entries()) { const serviceLabel = `${label}.artifactPublish.services[${index}]`; @@ -709,6 +725,65 @@ function assertShaOrNotPublished(value, label) { ); } +function assertRegistryCapabilityDimension(dimension, label, expectedId) { + assertObject(dimension, label); + for (const field of [ + "id", + "status", + "requiredForPublish", + "requiredForDeploy", + "endpoint", + "probeKind", + "summary", + "evidence" + ]) { + assert.ok(Object.hasOwn(dimension, field), `${label} missing ${field}`); + } + assert.equal(dimension.id, expectedId, `${label}.id`); + assert.ok(registryCapabilityDimensionIds.has(dimension.id), `${label}.id must be a known registry capability dimension`); + assertStatus(dimension.status, `${label}.status`); + assert.equal(typeof dimension.requiredForPublish, "boolean", `${label}.requiredForPublish`); + assert.equal(typeof dimension.requiredForDeploy, "boolean", `${label}.requiredForDeploy`); + assertString(dimension.endpoint, `${label}.endpoint`); + assertString(dimension.probeKind, `${label}.probeKind`); + assertString(dimension.summary, `${label}.summary`); + assertObject(dimension.evidence, `${label}.evidence`); +} + +function assertRegistryCapabilities(capabilities, label) { + assertObject(capabilities, label); + for (const field of [ + "version", + "registryPrefix", + "generatedAt", + "classification", + "dimensions", + "interpretation" + ]) { + assert.ok(Object.hasOwn(capabilities, field), `${label} missing ${field}`); + } + assert.equal(capabilities.version, "v1", `${label}.version`); + assertString(capabilities.registryPrefix, `${label}.registryPrefix`); + assertTimestamp(capabilities.generatedAt, `${label}.generatedAt`); + assertStatus(capabilities.classification, `${label}.classification`); + assertObject(capabilities.dimensions, `${label}.dimensions`); + assert.deepEqual( + new Set(Object.keys(capabilities.dimensions)), + registryCapabilityDimensionKeys, + `${label}.dimensions must contain processHttpAccess, dockerDaemonPushAccess, and k3sPullAccess` + ); + assertRegistryCapabilityDimension(capabilities.dimensions.processHttpAccess, `${label}.dimensions.processHttpAccess`, "process-http-access"); + assertRegistryCapabilityDimension(capabilities.dimensions.dockerDaemonPushAccess, `${label}.dimensions.dockerDaemonPushAccess`, "docker-daemon-push-access"); + assertRegistryCapabilityDimension(capabilities.dimensions.k3sPullAccess, `${label}.dimensions.k3sPullAccess`, "k3s-pull-access"); + assert.equal(capabilities.dimensions.processHttpAccess.requiredForPublish, false, `${label}.dimensions.processHttpAccess.requiredForPublish`); + assert.equal(capabilities.dimensions.dockerDaemonPushAccess.requiredForPublish, true, `${label}.dimensions.dockerDaemonPushAccess.requiredForPublish`); + assert.equal(capabilities.dimensions.k3sPullAccess.requiredForDeploy, true, `${label}.dimensions.k3sPullAccess.requiredForDeploy`); + assertObject(capabilities.interpretation, `${label}.interpretation`); + for (const key of registryCapabilityDimensionKeys) { + assertString(capabilities.interpretation[key], `${label}.interpretation.${key}`); + } +} + function assertArtifactIdentity(identity, label, target) { assertObject(identity, label); for (const field of [ @@ -1062,6 +1137,7 @@ async function validatePreflightReport(relativePath, report) { "forbiddenActions", "validationCommands", "artifactIdentity", + "registryCapabilities", "conclusion", "checks", "blockers" @@ -1104,6 +1180,7 @@ async function validatePreflightReport(relativePath, report) { } assertArtifactIdentity(report.artifactIdentity, `${label}.artifactIdentity`, report.target); + assertRegistryCapabilities(report.registryCapabilities, `${label}.registryCapabilities`); assertArray(report.checks, `${label}.checks`); assert.ok(report.checks.length >= 1, `${label}.checks must not be empty`);