Files
pikasTech-HWLAB/scripts/src/dev-gate-preflight.mjs
T
2026-05-22 23:23:13 +00:00

1363 lines
55 KiB
JavaScript

import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import {
DEV_DB_ENV_CONTRACT,
buildDbHealthContract,
summarizeDbContract
} from "../../internal/cloud/db-contract.mjs";
import {
DEV_CODE_AGENT_PROVIDER_CONTRACT,
codeAgentSecretRefPlaceholder
} from "../../internal/cloud/code-agent-contract.mjs";
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.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", "#35", "#39", "#43", "#48", "#49", "#57", "#66", "#143", "#164"].map(
(id) => `pikasTech/HWLAB${id}`
);
const forbiddenActions = [
"prod-deploy",
"secret-material-read",
"unidesk-runtime-substitute",
"heavy-e2e",
"browser-e2e",
"force-push",
"runtime-restart"
];
const blockerTypes = new Set([
"contract_blocker",
"environment_blocker",
"network_blocker",
"runtime_blocker",
"agent_blocker",
"observability_blocker",
"safety_blocker"
]);
const digestPattern = /^sha256:[a-f0-9]{64}$/;
const artifactBuildInputPrefixes = [
"cmd/",
"internal/",
"skills/",
"tools/",
"web/"
];
const artifactBuildInputFiles = new Set([
"package.json",
"package-lock.json",
"npm-shrinkwrap.json",
"scripts/dev-artifact-publish.mjs",
"scripts/src/dev-artifact-services.mjs"
]);
function parseArgs(argv) {
const args = {
targetRef: "origin/main",
reportPath: defaultReportPath,
timeoutMs: 5000,
writeReport: true,
failOnBlocked: false
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--target-ref") {
args.targetRef = argv[++index];
} else if (arg === "--report") {
args.reportPath = argv[++index];
} else if (arg === "--timeout-ms") {
args.timeoutMs = Number.parseInt(argv[++index], 10);
} else if (arg === "--no-write") {
args.writeReport = false;
} else if (arg === "--fail-on-blocked") {
args.failOnBlocked = true;
} else if (arg === "--help") {
args.help = true;
} else {
throw new Error(`unknown argument ${arg}`);
}
}
assert.ok(Number.isInteger(args.timeoutMs) && args.timeoutMs > 0, "--timeout-ms must be positive");
return args;
}
function usage() {
return "Usage: node scripts/dev-gate-preflight.mjs [--target-ref origin/main] [--report reports/dev-gate/dev-preflight-report.json] [--timeout-ms 5000] [--no-write] [--fail-on-blocked]";
}
function commandLine(command, args) {
return [command, ...args].join(" ");
}
async function run(command, args = [], options = {}) {
const commandText = commandLine(command, args);
try {
const result = await execFileAsync(command, args, {
cwd: repoRoot,
timeout: options.timeoutMs ?? 5000,
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) {
return (await run("which", [command])).ok;
}
async function readJson(relativePath) {
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
return JSON.parse(raw);
}
async function readOptionalJson(relativePath) {
try {
return await readJson(relativePath);
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
return null;
}
throw error;
}
}
async function parseToml(relativePath) {
const absolutePath = path.join(repoRoot, relativePath);
const code = [
"import json,sys,tomllib",
"with open(sys.argv[1], 'rb') as handle:",
" print(json.dumps(tomllib.load(handle), sort_keys=True))"
].join("\n");
const result = await run("python3", ["-c", code, absolutePath]);
if (!result.ok) {
throw new Error(`could not parse ${relativePath}: ${result.stderr || result.error}`);
}
return JSON.parse(result.stdout);
}
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, { method = "GET", timeoutMs = 5000, headers = {} } = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method,
headers,
signal: controller.signal
});
const body = method === "HEAD" ? "" : await response.text();
return {
ok: response.ok,
url,
method,
status: response.status,
statusText: response.statusText,
body: body.slice(0, 500)
};
} catch (error) {
return {
ok: false,
url,
method,
error: fetchErrorMessage(error)
};
} finally {
clearTimeout(timer);
}
}
function unique(values) {
return new Set(values).size === values.length;
}
function addBlocker(blockers, blocker) {
assert.ok(blockerTypes.has(blocker.type), `unknown blocker type ${blocker.type}`);
const key = `${blocker.type}:${blocker.scope}`;
if (!blockers.some((item) => `${item.type}:${item.scope}` === key)) {
blockers.push({ status: "open", ...blocker });
}
}
function imageToManifestUrl(image) {
const match = image.match(/^ghcr\.io\/pikastech\/([^:@]+):([^:@]+)$/);
if (match) return `https://ghcr.io/v2/pikastech/${match[1]}/manifests/${match[2]}`;
const localMatch = image.match(/^(127\.0\.0\.1:5000|localhost:5000)\/hwlab\/([^:@]+):([^:@]+)$/);
if (localMatch) return `http://${localMatch[1]}/v2/hwlab/${localMatch[2]}/manifests/${localMatch[3]}`;
return null;
}
function matchesTargetCommit(value, targetCommit, targetShortCommit) {
return value === targetCommit || value === targetShortCommit;
}
function isArtifactBuildInputPath(relativePath) {
return artifactBuildInputFiles.has(relativePath) ||
artifactBuildInputPrefixes.some((prefix) => relativePath.startsWith(prefix));
}
async function resolveCommit(ref) {
if (!ref) return null;
const result = await run("git", ["rev-parse", `${ref}^{commit}`]);
if (!result.ok || !/^[a-f0-9]{40}$/u.test(result.stdout)) return null;
return result.stdout;
}
async function sourceCoverageFor({ catalog, artifactReport, targetCommit, targetShortCommit, targetRef }) {
const reportSource = artifactReport?.artifactPublish?.sourceCommitId ?? artifactReport?.commitId ?? null;
const sourceRef = reportSource ?? catalog.commitId;
const sourceKind = reportSource ? "artifact-publish-report" : "artifact-catalog";
const sourceCommit = await resolveCommit(sourceRef);
if (!sourceCommit) {
return {
sourceKind,
sourceRef,
sourceCommitId: null,
sourceShortCommitId: null,
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "unresolved-source",
covered: false,
artifactBuildInputChanged: true,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: `artifact source ${sourceRef ?? "unknown"} does not resolve to a local git commit`
};
}
if (matchesTargetCommit(sourceCommit, targetCommit, targetShortCommit)) {
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "exact-target",
covered: true,
artifactBuildInputChanged: false,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: "artifact source commit exactly matches the target ref"
};
}
const ancestor = await run("git", ["merge-base", "--is-ancestor", sourceCommit, targetCommit]);
if (!ancestor.ok) {
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "non-ancestor-source",
covered: false,
artifactBuildInputChanged: true,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: `artifact source ${sourceCommit.slice(0, 12)} is not an ancestor of ${targetRef} ${targetShortCommit}`
};
}
const diff = await run("git", ["diff", "--name-only", `${sourceCommit}..${targetCommit}`]);
if (!diff.ok) {
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: "diff-unavailable",
covered: false,
artifactBuildInputChanged: true,
changedPathCount: 0,
changedArtifactInputPaths: [],
changedNonArtifactPathCount: 0,
reason: diff.stderr || diff.error || "git diff failed"
};
}
const changedPaths = diff.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
const changedArtifactInputPaths = changedPaths.filter(isArtifactBuildInputPath);
const covered = changedArtifactInputPaths.length === 0;
return {
sourceKind,
sourceRef,
sourceCommitId: sourceCommit,
sourceShortCommitId: sourceCommit.slice(0, 7),
targetRef,
targetCommitId: targetCommit,
targetShortCommitId: targetShortCommit,
mode: covered ? "report-only-target" : "artifact-input-drift",
covered,
artifactBuildInputChanged: changedArtifactInputPaths.length > 0,
changedPathCount: changedPaths.length,
changedArtifactInputPaths,
changedNonArtifactPathCount: changedPaths.length - changedArtifactInputPaths.length,
reason: covered
? "target changes since the artifact source do not touch artifact build inputs"
: "target changes since the artifact source touch artifact build inputs"
};
}
function uniqueSorted(values) {
return [...new Set(values)].sort();
}
function catalogDigestCounts(catalog) {
const counts = {
sha256: 0,
notPublished: 0,
invalid: 0
};
for (const service of catalog.services ?? []) {
if (digestPattern.test(service.digest)) counts.sha256 += 1;
else if (service.digest === "not_published") counts.notPublished += 1;
else counts.invalid += 1;
}
return counts;
}
function requiredCatalogServices(catalog) {
return (catalog.services ?? []).filter((service) => service.artifactRequired !== false);
}
function disabledCatalogServices(catalog) {
return (catalog.services ?? []).filter((service) => service.artifactRequired === false);
}
async function artifactIdentityFor({ deploy, catalog, artifactReport, targetCommit, targetShortCommit, targetRef }) {
const targetCoverage = await sourceCoverageFor({
catalog,
artifactReport,
targetCommit,
targetShortCommit,
targetRef
});
const artifactSourceCommit = targetCoverage.sourceCommitId ?? targetCommit;
const artifactSourceShortCommit = targetCoverage.sourceShortCommitId ?? targetShortCommit;
const serviceCommitIds = uniqueSorted((catalog.services ?? []).map((service) => service.commitId));
const digestCounts = catalogDigestCounts(catalog);
const requiredServices = requiredCatalogServices(catalog);
const disabledServices = disabledCatalogServices(catalog);
const catalogMatchesSource = matchesTargetCommit(catalog.commitId, artifactSourceCommit, artifactSourceShortCommit);
const deployMatchesSource = matchesTargetCommit(deploy.commitId, artifactSourceCommit, artifactSourceShortCommit);
const servicesMatchSource = serviceCommitIds.length === 1 &&
matchesTargetCommit(serviceCommitIds[0], artifactSourceCommit, artifactSourceShortCommit);
const allRequiredDigestsPublished = requiredServices.length > 0 &&
requiredServices.every((service) => digestPattern.test(service.digest) && service.publishState === "published") &&
disabledServices.every((service) => service.digest === "not_published" && service.publishState === "skeleton-only") &&
digestCounts.invalid === 0;
const publishVerified = catalog.publish?.ciPublished === true &&
catalog.publish?.registryVerified === true &&
catalog.artifactState === "published" &&
allRequiredDigestsPublished;
return {
source: {
ref: targetRef,
commitId: targetCommit,
shortCommitId: targetShortCommit
},
artifactSource: {
source: targetCoverage.sourceKind,
ref: targetCoverage.sourceRef,
commitId: targetCoverage.sourceCommitId,
shortCommitId: targetCoverage.sourceShortCommitId
},
targetCoverage,
deployManifest: {
path: "deploy/deploy.json",
commitId: deploy.commitId,
matchesSource: deployMatchesSource,
matchesTarget: matchesTargetCommit(deploy.commitId, targetCommit, targetShortCommit)
},
artifactCatalog: {
path: "deploy/artifact-catalog.dev.json",
commitId: catalog.commitId,
artifactState: catalog.artifactState,
ciPublished: catalog.publish?.ciPublished === true,
registryVerified: catalog.publish?.registryVerified === true,
provenance: catalog.publish?.provenance ?? "unknown",
digestCounts,
requiredServiceCount: requiredServices.length,
disabledServiceCount: disabledServices.length,
matchesSource: catalogMatchesSource,
matchesTarget: matchesTargetCommit(catalog.commitId, targetCommit, targetShortCommit)
},
services: (catalog.services ?? []).map((service) => ({
serviceId: service.serviceId,
commitId: service.commitId,
matchesSource: matchesTargetCommit(service.commitId, artifactSourceCommit, artifactSourceShortCommit),
matchesTarget: matchesTargetCommit(service.commitId, targetCommit, targetShortCommit),
image: service.image,
imageTag: service.imageTag,
digest: service.digest,
publishState: service.publishState,
artifactRequired: service.artifactRequired !== false,
notPublishedReason: service.notPublishedReason ?? null
})),
serviceCommitIds,
matchesSource: targetCoverage.covered && deployMatchesSource && catalogMatchesSource && servicesMatchSource,
publishVerified,
refreshCommands: {
blocked: `node scripts/refresh-artifact-catalog.mjs --target-ref ${targetRef} --blocked`,
published: `node scripts/refresh-artifact-catalog.mjs --target-ref ${targetRef} --publish-report reports/dev-gate/dev-artifacts.json`
}
};
}
function assertStaticContract(deploy, catalog) {
assert.equal(deploy.environment, ENVIRONMENT_DEV, "deploy environment must be dev");
assert.equal(deploy.namespace, "hwlab-dev", "deploy namespace must be hwlab-dev");
assert.equal(deploy.endpoint, DEV_ENDPOINT, "deploy endpoint must stay frozen");
assert.equal(deploy.profiles.dev.enabled, true, "dev profile must be enabled");
assert.equal(deploy.profiles.prod.enabled, false, "prod profile must stay disabled");
assert.deepEqual(deploy.services.map((service) => service.serviceId), SERVICE_IDS);
assert.equal(catalog.environment, ENVIRONMENT_DEV, "catalog environment must be dev");
assert.equal(catalog.namespace, "hwlab-dev", "catalog namespace must be hwlab-dev");
assert.equal(catalog.endpoint, DEV_ENDPOINT, "catalog endpoint must stay frozen");
assert.deepEqual(catalog.allowedProfiles, [ENVIRONMENT_DEV]);
assert.ok(catalog.forbiddenProfiles.includes("prod"), "catalog must forbid prod");
assert.deepEqual(catalog.services.map((service) => service.serviceId), SERVICE_IDS);
assert.ok(unique(catalog.services.map((service) => service.serviceId)), "catalog service IDs must be unique");
assert.equal(catalog.commitId, deploy.commitId, "catalog commitId must match deploy commitId");
const deployByService = new Map(deploy.services.map((service) => [service.serviceId, service]));
for (const service of catalog.services) {
const deployService = deployByService.get(service.serviceId);
assert.ok(deployService, `${service.serviceId} missing from deploy manifest`);
assert.equal(service.image, deployService.image, `${service.serviceId} image mismatch`);
assert.equal(service.namespace, "hwlab-dev", `${service.serviceId} namespace mismatch`);
assert.equal(service.profile, ENVIRONMENT_DEV, `${service.serviceId} profile mismatch`);
assert.equal(service.healthPath, "/health/live", `${service.serviceId} health path mismatch`);
}
}
function assertK8sStatic(namespace, workloads, services, devKustomization, healthContract) {
assert.equal(namespace.kind, "Namespace");
assert.equal(namespace.metadata.name, "hwlab-dev");
assert.deepEqual(devKustomization.resources, ["../base", "health-contract.yaml"]);
assert.equal(devKustomization.namespace, "hwlab-dev");
assert.equal(healthContract.kind, "ConfigMap");
assert.equal(healthContract.metadata.namespace, "hwlab-dev");
assert.equal(healthContract.data.endpoint, DEV_ENDPOINT);
assert.ok(healthContract.data["runtime-substitute-policy"].includes("Do not replace HWLAB runtime"));
const resources = [...workloads.items, ...services.items, healthContract];
assert.ok(resources.length > SERVICE_IDS.length, "k8s resources must cover DEV services");
for (const resource of resources) {
assert.notEqual(resource.kind, "Secret", "preflight contract must not require Secret resources");
if (resource.metadata?.namespace) {
assert.equal(resource.metadata.namespace, "hwlab-dev", `${resource.kind}/${resource.metadata.name} namespace`);
}
}
}
function assertFrpStatic(frpc, frps, masterEdge) {
assert.equal(frpc.serverAddr, "74.48.78.17");
assert.equal(frpc.serverPort, 7000);
assert.equal(frps.bindPort, 7000);
assert.equal(Object.hasOwn(frps, "vhostHTTPPort"), false, "frps must not bind vhostHTTPPort because 16667 is reserved for the TCP proxy");
assert.ok(frpc.proxies.some((proxy) => proxy.name === "hwlab-dev-edge-proxy" && proxy.remotePort === 16667));
assert.ok(frpc.proxies.some((proxy) => proxy.name === "hwlab-dev-cloud-web" && proxy.remotePort === 16666));
assert.ok(frps.allowPorts.some((port) => port.start === 16667 && port.end === 16667));
assert.ok(frps.allowPorts.some((port) => port.start === 16666 && port.end === 16666));
assert.equal(masterEdge.environment, ENVIRONMENT_DEV);
assert.equal(masterEdge.endpoint, DEV_ENDPOINT);
assert.equal(masterEdge.reverseLink.mode, "frp");
assert.equal(masterEdge.reverseLink.direction, "d601-to-master");
assert.equal(masterEdge.prodAcceptance, false);
}
async function loadContracts() {
return Promise.all([
readJson("deploy/deploy.json"),
readJson("deploy/artifact-catalog.dev.json"),
readJson("deploy/k8s/base/namespace.yaml"),
readJson("deploy/k8s/base/workloads.yaml"),
readJson("deploy/k8s/base/services.yaml"),
readJson("deploy/k8s/dev/kustomization.yaml"),
readJson("deploy/k8s/dev/health-contract.yaml"),
readJson("deploy/master-edge/health-contract.json")
]);
}
async function loadOptionalReports() {
return {
artifactPublish: await readOptionalJson("reports/dev-gate/dev-artifacts.json"),
edgeHealth: await readOptionalJson("reports/dev-gate/dev-edge-health.json")
};
}
function makeReporter() {
const checks = [];
const blockers = [];
return {
checks,
blockers,
check(id, category, status, summary, evidence = []) {
checks.push({ id, category, status, summary, evidence });
},
block(blocker) {
addBlocker(blockers, blocker);
}
};
}
async function validateLocalContracts(reporter, contracts, optionalReports, targetShortCommit, targetCommit, targetRef) {
const [deploy, catalog, namespace, workloads, services, devKustomization, healthContract, masterEdge] = contracts;
const artifactIdentity = await artifactIdentityFor({
deploy,
catalog,
artifactReport: optionalReports.artifactPublish,
targetCommit,
targetShortCommit,
targetRef
});
try {
assertStaticContract(deploy, catalog);
reporter.check("source-contract-static", "contract", "pass", "Local deploy manifest and artifact catalog are internally consistent.");
} catch (error) {
reporter.check("source-contract-static", "contract", "blocked", error.message);
reporter.block({
type: "contract_blocker",
scope: "source-contract",
summary: error.message,
nextTask: "Repair the local DEV deploy manifest and artifact catalog contract before any live deploy."
});
}
if (artifactIdentity.matchesSource) {
const coverage = artifactIdentity.targetCoverage;
const coverageSuffix = coverage.mode === "exact-target"
? "exact target commit"
: `${coverage.mode}; ${coverage.changedPathCount} changed path(s), ${coverage.changedArtifactInputPaths.length} artifact input change(s)`;
reporter.check(
"target-commit-pinning",
"contract",
"pass",
`Source ${targetRef} ${targetShortCommit} is covered by artifact source ${artifactIdentity.artifactSource.shortCommitId}; ${coverageSuffix}.`,
[artifactIdentity]
);
} else {
const coverage = artifactIdentity.targetCoverage;
reporter.check(
"target-commit-pinning",
"contract",
"blocked",
`Source ${targetRef} ${targetShortCommit} is not covered by artifact source ${artifactIdentity.artifactSource.shortCommitId ?? "unknown"}: ${coverage.reason}.`,
[artifactIdentity]
);
reporter.block({
type: "contract_blocker",
scope: "artifact-source-commit",
summary: `source commit ${targetRef} ${targetShortCommit} is not covered by artifact source ${artifactIdentity.artifactSource.shortCommitId ?? "unknown"}; ${coverage.reason}.`,
nextTask: `Refresh without fake digests using \`${artifactIdentity.refreshCommands.blocked}\`, or after a successful publish run \`${artifactIdentity.refreshCommands.published}\`.`
});
}
if (artifactIdentity.publishVerified) {
reporter.check(
"artifact-catalog-publish-state",
"registry",
"pass",
"Artifact catalog records CI publish, registry verification, and immutable service digests.",
[artifactIdentity.artifactCatalog]
);
} else {
reporter.check(
"artifact-catalog-publish-state",
"registry",
"blocked",
`Artifact catalog publish state is ${catalog.artifactState}; ciPublished=${artifactIdentity.artifactCatalog.ciPublished}, registryVerified=${artifactIdentity.artifactCatalog.registryVerified}, digests sha256=${artifactIdentity.artifactCatalog.digestCounts.sha256}, not_published=${artifactIdentity.artifactCatalog.digestCounts.notPublished}, invalid=${artifactIdentity.artifactCatalog.digestCounts.invalid}.`,
[artifactIdentity.artifactCatalog]
);
reporter.block({
type: "runtime_blocker",
scope: "artifact-catalog",
summary: `deploy/artifact-catalog.dev.json does not prove published artifacts for ${targetRef} ${targetShortCommit}; ciPublished=${artifactIdentity.artifactCatalog.ciPublished}, registryVerified=${artifactIdentity.artifactCatalog.registryVerified}, not_published=${artifactIdentity.artifactCatalog.digestCounts.notPublished}.`,
nextTask: `Run the DEV artifact publish workflow, then record only real sha256 digests with \`${artifactIdentity.refreshCommands.published}\`; if publish is still blocked, keep not_published via \`${artifactIdentity.refreshCommands.blocked}\`.`
});
}
try {
assertK8sStatic(namespace, workloads, services, devKustomization, healthContract);
reporter.check("k8s-manifest-static", "k3s", "pass", "DEV k3s manifest files parse and stay scoped to hwlab-dev.");
} catch (error) {
reporter.check("k8s-manifest-static", "k3s", "blocked", error.message);
reporter.block({
type: "contract_blocker",
scope: "deploy/k8s/dev",
summary: error.message,
nextTask: "Repair the DEV k3s manifests so they parse and target only hwlab-dev."
});
}
return { deploy, catalog, masterEdge, artifactIdentity };
}
function validateArtifactPublishReport(reporter, artifactReport, artifactIdentity, targetShortCommit, targetCommit, targetRef) {
if (!artifactReport) {
reporter.check("dev-artifact-publish-report", "registry", "blocked", "No DEV artifact publish report is present.");
reporter.block({
type: "runtime_blocker",
scope: "dev-artifact-publish",
summary: "reports/dev-gate/dev-artifacts.json is missing, so the preflight has no publish evidence for HWLAB runtime artifacts.",
nextTask: "Run the DEV artifact publish workflow and record reports/dev-gate/dev-artifacts.json before real deployment."
});
return;
}
const artifactPublish = artifactReport.artifactPublish;
const services = artifactPublish?.services ?? [];
const sourcePresentCount = services.filter((service) => service.sourceState === "source-present").length;
const intentionallyDisabledCount = services.filter((service) => service.sourceState === "intentionally-disabled").length;
const sourcePresenceResolved =
services.length === SERVICE_IDS.length &&
services.every((service) => service.sourceState === "source-present" || service.sourceState === "intentionally-disabled");
const requiredServices = services.filter((service) => service.artifactRequired !== false);
const disabledServices = services.filter((service) => service.artifactRequired === false);
const allServicesAccountedFor = services.length === SERVICE_IDS.length &&
[...services].map((service) => service.serviceId).sort().join("\n") === [...SERVICE_IDS].sort().join("\n");
const allRequiredServicesPublished = requiredServices.length > 0 &&
requiredServices.every((service) => service.status === "published" && /^sha256:[a-f0-9]{64}$/.test(service.digest));
const disabledServicesNotPublished = disabledServices.every(
(service) => service.digest === "not_published" && service.notPublishedReason?.startsWith("disabled_")
);
const sourceMatchesTarget = [targetCommit, targetShortCommit].includes(artifactPublish?.sourceCommitId) ||
[targetCommit, targetShortCommit].includes(artifactReport.commitId);
const sourceCoversTarget = artifactIdentity?.targetCoverage?.covered === true &&
artifactIdentity?.artifactSource?.commitId &&
(
artifactPublish?.sourceCommitId === artifactIdentity.artifactSource.commitId ||
artifactPublish?.sourceCommitId === artifactIdentity.artifactSource.shortCommitId ||
artifactReport.commitId === artifactIdentity.artifactSource.commitId ||
artifactReport.commitId === artifactIdentity.artifactSource.shortCommitId
);
const publishReady = artifactPublish?.status === "published" &&
artifactPublish.publishedCount === requiredServices.length &&
allServicesAccountedFor &&
allRequiredServicesPublished &&
disabledServicesNotPublished &&
(sourceMatchesTarget || sourceCoversTarget);
if (publishReady) {
const summary = sourceMatchesTarget
? "DEV artifact publish report covers all required enabled service IDs with immutable digests for the target commit, and records disabled service reasons."
: `DEV artifact publish report covers artifact source ${artifactIdentity.artifactSource.shortCommitId}; target ${targetRef} ${targetShortCommit} has no artifact build input changes since that source.`;
reporter.check("dev-artifact-publish-report", "registry", "pass", summary, [artifactIdentity?.targetCoverage].filter(Boolean));
return;
}
const publishedCount = artifactPublish?.publishedCount ?? 0;
const serviceCount = artifactPublish?.serviceCount ?? SERVICE_IDS.length;
const sourceSummary = sourcePresenceResolved
? `source states resolved: ${sourcePresentCount} source-present, ${intentionallyDisabledCount} intentionally-disabled`
: "source states are not fully resolved";
reporter.check(
"dev-artifact-publish-report",
"registry",
"blocked",
`DEV artifact publish report status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${requiredServices.length || serviceCount} required services published for source ${artifactPublish?.sourceCommitId ?? artifactReport.commitId ?? "unknown"}; ${sourceSummary}.`
);
reporter.block({
type: "runtime_blocker",
scope: "dev-artifact-publish",
summary: `reports/dev-gate/dev-artifacts.json does not prove all required HWLAB service artifacts for ${targetRef} ${targetShortCommit}; current status is ${artifactPublish?.status ?? "missing"} with ${publishedCount}/${requiredServices.length || serviceCount} required services published and ${sourceSummary}.`,
nextTask: "Complete DEV artifact publishing for every enabled required HWLAB service at the artifact source commit, or prove the target commit has no artifact build input changes; keep disabled services marked not_published with reasons."
});
}
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.");
reporter.block({
type: "network_blocker",
scope: "dev-edge-health-report",
summary: "reports/dev-gate/dev-edge-health.json is missing, so the preflight has no committed edge/frp health evidence.",
nextTask: "Run the read-only DEV edge health smoke and commit reports/dev-gate/dev-edge-health.json before real deployment."
});
return;
}
const edgeHealth = edgeReport.edgeHealth;
const pass = edgeReport.status === "pass" && edgeHealth?.status === "pass";
const evidence = [
{
report: "reports/dev-gate/dev-edge-health.json",
status: edgeReport.status,
classification: edgeHealth?.classification ?? "unknown",
publicTcp: edgeHealth?.publicTcp ?? [],
kubernetesObservable: edgeHealth?.kubernetes?.observable ?? false
}
];
if (pass) {
reporter.check("dev-edge-health-report", "edge", "pass", "Committed DEV edge health report shows public edge health passing.", evidence);
return;
}
const classification = edgeHealth?.classification ?? "unknown";
const summary = edgeHealth?.blocker ?? edgeReport.devPreconditions?.summary ?? "DEV edge health report is not passing.";
reporter.check("dev-edge-health-report", "edge", "blocked", `Committed DEV edge health report is ${edgeReport.status} with classification ${classification}.`, evidence);
reporter.block({
type: "network_blocker",
scope: edgeHealth?.diagnosis?.likelyLayer ?? "dev-edge-health",
summary,
nextTask: edgeHealth?.diagnosis?.nextTask ?? "Repair the master frps/public DEV edge route and rerun the read-only DEV edge health smoke before real deployment."
});
}
function validateCloudApiDbContract(reporter, deploy, workloads, services) {
const staticContract = inspectCloudApiDbStaticContract(deploy, workloads, services);
const healthContract = summarizeDbContract(buildDbHealthContract());
const evidence = [
{
staticContract,
healthContract,
fixtureEvidence: false,
liveDbEvidence: false
}
];
if (staticContract.ready) {
reporter.check(
"cloud-api-db-env-contract",
"db",
"pass",
"cloud-api DEV DB manifest declares the required env names and redacted Secret reference; optional cloud-api-db alias is not a readiness gate.",
evidence
);
} else {
const missing = [
...staticContract.missingDeployEnv,
...staticContract.missingK8sEnv,
...staticContract.missingSecretRefs,
...staticContract.missingDnsContract
];
reporter.check(
"cloud-api-db-env-contract",
"db",
"blocked",
`cloud-api DEV DB manifest is missing ${missing.join(", ")}.`,
evidence
);
reporter.block({
type: "contract_blocker",
scope: "cloud-api-db-env-contract",
summary: `cloud-api DEV DB contract is incomplete; missing ${missing.join(", ")}.`,
nextTask: "Repair deploy/deploy.json and deploy/k8s/base/workloads.yaml so they expose the required DB env names and redacted Secret reference. Do not make cloud-api-db a hard readiness gate unless this repo also owns its Service/Endpoint rollout contract."
});
}
if (healthContract.ready) {
reporter.check(
"cloud-api-db-health-gate",
"db",
"pass",
"cloud-api DB health gate reports required env presence with redacted values.",
evidence
);
return;
}
reporter.check(
"cloud-api-db-health-gate",
"db",
"blocked",
`cloud-api DB health gate is missing ${healthContract.missingEnv.join(", ")}.`,
evidence
);
reporter.block({
type: "runtime_blocker",
scope: "cloud-api-db-health-gate",
summary: `cloud-api DB runtime env is not ready; missing ${healthContract.missingEnv.join(", ")}.`,
nextTask: "Configure DEV hwlab-cloud-api with Secret hwlab-cloud-api-dev-db/database-url and HWLAB_CLOUD_DB_SSL_MODE=require, then rerun health/preflight without printing the secret value."
});
}
function inspectCloudApiDbStaticContract(deploy, workloads, services) {
const cloudApi = deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api") ?? {};
const deployEnv = cloudApi.env ?? {};
const workloadEnv = getWorkloadEnvByServiceId(workloads, "hwlab-cloud-api");
const secretEnvNames = new Set(DEV_DB_ENV_CONTRACT.secretRefs.map((ref) => ref.env));
const fields = DEV_DB_ENV_CONTRACT.requiredEnv.map((name) => {
const secretRef = DEV_DB_ENV_CONTRACT.secretRefs.find((ref) => ref.env === name) ?? null;
const workloadEntry = workloadEnv.get(name) ?? null;
const manifestPresent = Object.hasOwn(deployEnv, name);
const k8sPresent = workloadEnv.has(name);
const secretRefMatches = secretRef
? deployEnv[name] === `secretRef:${secretRef.secretName}/${secretRef.secretKey}` &&
workloadEntry?.valueFrom?.secretKeyRef?.name === secretRef.secretName &&
workloadEntry?.valueFrom?.secretKeyRef?.key === secretRef.secretKey
: true;
return {
name,
manifestPresent,
k8sPresent,
redacted: secretEnvNames.has(name),
source: secretEnvNames.has(name) ? "k8s-secret-ref" : "runtime-env",
secretRef: secretRef
? {
secretName: secretRef.secretName,
secretKey: secretRef.secretKey,
present: secretRefMatches,
redacted: true
}
: null
};
});
const missingDeployEnv = fields.filter((field) => !field.manifestPresent).map((field) => field.name);
const missingK8sEnv = fields.filter((field) => !field.k8sPresent).map((field) => field.name);
const missingSecretRefs = fields
.filter((field) => field.secretRef && !field.secretRef.present)
.map((field) => `${field.secretRef.secretName}/${field.secretRef.secretKey}`);
const dnsContract = inspectCloudApiDbOptionalAliasContract(deployEnv, workloadEnv, services);
return {
contractVersion: DEV_DB_ENV_CONTRACT.contractVersion,
environment: DEV_DB_ENV_CONTRACT.environment,
ready:
missingDeployEnv.length === 0 &&
missingK8sEnv.length === 0 &&
missingSecretRefs.length === 0 &&
dnsContract.ready,
requiredEnv: fields,
dns: dnsContract,
missingDeployEnv,
missingK8sEnv,
missingSecretRefs,
missingDnsContract: dnsContract.missing,
secretMaterialRead: false,
valuesRedacted: true,
liveDbEvidence: false,
fixtureEvidence: false
};
}
function inspectCloudApiDbOptionalAliasContract(deployEnv, workloadEnv, services) {
const alias = DEV_DB_ENV_CONTRACT.dns;
const service = listManifestItems(services).find((item) => item?.metadata?.name === alias.serviceName) ?? null;
const actual = {
deployServiceName: deployEnv.HWLAB_CLOUD_DB_SERVICE_NAME,
deployServiceNamespace: deployEnv.HWLAB_CLOUD_DB_SERVICE_NAMESPACE,
deployHost: deployEnv.HWLAB_CLOUD_DB_HOST,
deployPort: deployEnv.HWLAB_CLOUD_DB_PORT,
workloadServiceName: workloadEnv.get("HWLAB_CLOUD_DB_SERVICE_NAME")?.value,
workloadServiceNamespace: workloadEnv.get("HWLAB_CLOUD_DB_SERVICE_NAMESPACE")?.value,
workloadHost: workloadEnv.get("HWLAB_CLOUD_DB_HOST")?.value,
workloadPort: workloadEnv.get("HWLAB_CLOUD_DB_PORT")?.value,
serviceName: service?.metadata?.name
};
const missing = [];
for (const [name, value] of Object.entries(actual)) {
if (value !== undefined) {
missing.push(`${name} must be absent because ${alias.serviceName} is not the readiness authority`);
}
}
return {
ready: missing.length === 0,
source: alias.source,
serviceName: alias.serviceName,
namespace: alias.namespace,
port: alias.port,
portName: alias.portName,
requiredForReadiness: alias.requiredForReadiness,
usedForProbe: alias.usedForProbe,
servicePresent: Boolean(service),
secretMaterialRead: false,
liveDbEvidence: false,
missing,
actual
};
}
function getWorkloadEnvByServiceId(workloads, serviceId) {
const envByName = new Map();
for (const item of listManifestItems(workloads)) {
for (const container of item?.spec?.template?.spec?.containers ?? []) {
const itemServiceId =
item?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
item?.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
container?.name;
if (itemServiceId !== serviceId) {
continue;
}
for (const env of container.env ?? []) {
envByName.set(env.name, env);
}
}
}
return envByName;
}
function listManifestItems(document) {
if (!document) {
return [];
}
return document.kind === "List" ? document.items ?? [] : [document];
}
async function validateEdgeContracts(reporter, masterEdge) {
try {
const [frpc, frps] = await Promise.all([
parseToml("deploy/frp/frpc.dev.toml"),
parseToml("deploy/frp/frps.dev.toml")
]);
assertFrpStatic(frpc, frps, masterEdge);
reporter.check("frp-master-edge-static", "edge", "pass", "FRP and master-edge contracts describe D601-to-master DEV on public ports 16666/16667.");
} catch (error) {
reporter.check("frp-master-edge-static", "edge", "blocked", error.message);
reporter.block({
type: "contract_blocker",
scope: "deploy/frp",
summary: error.message,
nextTask: "Repair FRP and master-edge DEV contracts before live reachability checks."
});
}
}
async function validateLiveProbes(reporter, catalog, timeoutMs) {
const kubectlExists = await commandExists("kubectl");
if (!kubectlExists) {
reporter.check("d601-k3s-read-access", "k3s", "blocked", "kubectl is not installed in this runner, so hwlab-dev live cluster evidence cannot be collected.");
reporter.block({
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."
});
} else {
const k3sEvidence = [];
for (const probe of [
["kubectl", ["version", "--client=true"]],
["kubectl", ["config", "current-context"]],
["kubectl", ["auth", "can-i", "get", "pods", "-n", "hwlab-dev"]],
["kubectl", ["get", "namespace", "hwlab-dev", "-o", "json"]],
["kubectl", ["-n", "hwlab-dev", "get", "deploy,svc,job,cm", "-o", "name"]]
]) {
k3sEvidence.push(await run(probe[0], probe[1], { timeoutMs }));
}
const ok = k3sEvidence.every((probe) => probe.ok);
reporter.check("d601-k3s-read-access", "k3s", ok ? "pass" : "blocked", ok ? "Read-only kubectl probes reached hwlab-dev." : "At least one read-only kubectl probe failed.", k3sEvidence);
if (!ok) {
reporter.block({
type: "environment_blocker",
scope: "d601-k3s",
summary: "Read-only kubectl probes could not prove hwlab-dev namespace access.",
nextTask: "Fix read-only D601 k3s credentials and confirm kubectl can get namespace, pods, services, jobs, and ConfigMaps in hwlab-dev."
});
}
}
const edgeProbe = await httpProbe(`${DEV_ENDPOINT}/health/live`, { timeoutMs });
if (edgeProbe.ok) {
reporter.check("public-dev-edge-health", "edge", "pass", "Public DEV health endpoint responded successfully.", [edgeProbe]);
} else {
reporter.check("public-dev-edge-health", "edge", "blocked", "Public DEV health endpoint did not respond successfully.", [edgeProbe]);
reporter.block({
type: "network_blocker",
scope: "dev-edge",
summary: `${DEV_ENDPOINT}/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."
});
}
const registryServices = requiredCatalogServices(catalog);
const registryEvidence = await Promise.all(registryServices.map(async (service) => {
const url = imageToManifestUrl(service.image);
if (!url) {
return { serviceId: service.serviceId, ok: false, image: service.image, error: "unsupported image format" };
}
const probe = await httpProbe(url, {
method: "HEAD",
timeoutMs,
headers: {
Accept: "application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json"
}
});
return { serviceId: service.serviceId, image: service.image, ...probe };
}));
const registryStatus = manifestReadStatus(registryEvidence);
reporter.check(
"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 (registryStatus === "blocked") {
reporter.block({
type: "runtime_blocker",
scope: "registry-manifest-read",
summary: "This preflight could not verify catalog image manifests through the runner process path; this is a #66 registry reachability dimension, not evidence that publish digests are missing.",
nextTask: "Resolve the #66 registry reachability path or provide a non-secret registry reachability artifact; do not rerun heavy publish solely for this manifest-read probe."
});
}
}
function validateRuntimeBoundary(reporter, deploy) {
const substituteImages = deploy.services
.map((service) => service.image)
.filter((image) => /unidesk|provider-gateway|microservice-proxy/.test(image));
if (substituteImages.length === 0) {
reporter.check("runtime-substitution-boundary", "safety", "pass", "Deploy images do not point at UniDesk/provider-gateway/microservice-proxy substitutes.");
return;
}
reporter.check("runtime-substitution-boundary", "safety", "blocked", `Forbidden substitute images: ${substituteImages.join(", ")}`);
reporter.block({
type: "safety_blocker",
scope: "runtime-boundary",
summary: "DEV deploy manifest references a forbidden non-HWLAB runtime substitute.",
nextTask: "Replace substitute runtime references with HWLAB-owned service images."
});
}
async function writeReport(report, reportPath) {
const absoluteReportPath = path.join(repoRoot, reportPath);
await mkdir(path.dirname(absoluteReportPath), { recursive: true });
await writeFile(absoluteReportPath, `${JSON.stringify(report, null, 2)}\n`);
}
function makeReport(args, targetCommit, targetShortCommit, reporter, artifactIdentity, registryCapabilities) {
const blockingCheckStatuses = new Set(["blocked", "failed"]);
const conclusion = reporter.blockers.length === 0 &&
reporter.checks.every((item) => !blockingCheckStatuses.has(item.status)) ? "ready" : "blocked";
return {
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-preflight-report.schema.json",
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-preflight-report.json",
reportVersion: "v1",
reportKind: "dev-gate-preflight",
issue,
supports,
target: {
ref: args.targetRef,
commitId: targetCommit,
shortCommitId: targetShortCommit
},
generatedAt: new Date().toISOString(),
mode: "read-only",
devOnly: true,
prodDisabled: true,
reportLifecycle: activeReportLifecycle("Current read-only DEV preflight report; source, local, and dry-run checks are not DEV-LIVE acceptance."),
forbiddenActions,
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,
notes: "No PROD action, secret read, UniDesk runtime substitution, heavy e2e, browser e2e, runtime restart, or force push was performed."
};
}
function printSummary(args, report) {
console.log(JSON.stringify({
issue,
targetRef: args.targetRef,
targetCommit: report.target.commitId,
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) => {
counts[item.status] = (counts[item.status] ?? 0) + 1;
return counts;
}, {}),
blockers: report.blockers.map((blocker) => ({
type: blocker.type,
scope: blocker.scope,
nextTask: blocker.nextTask
}))
}, null, 2));
}
export async function runPreflight(argv) {
const args = parseArgs(argv);
if (args.help) {
console.log(usage());
return;
}
const targetCommit = (await run("git", ["rev-parse", args.targetRef])).stdout;
const targetShortCommit = (await run("git", ["rev-parse", "--short=7", args.targetRef])).stdout;
assert.match(targetCommit, /^[a-f0-9]{40}$/, `target ref ${args.targetRef} must resolve to a full SHA`);
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 } = await validateLocalContracts(
reporter,
contracts,
optionalReports,
targetShortCommit,
targetCommit,
args.targetRef
);
validateRegistryCapabilities(reporter, registryCapabilities);
validateCloudApiDbContract(reporter, deploy, contracts[3], contracts[4]);
validateCodeAgentProviderContract(reporter, deploy, contracts[3]);
validateArtifactPublishReport(reporter, optionalReports.artifactPublish, artifactIdentity, targetShortCommit, targetCommit, args.targetRef);
validateEdgeHealthReport(reporter, optionalReports.edgeHealth);
await validateEdgeContracts(reporter, masterEdge);
validateRuntimeBoundary(reporter, deploy);
await validateLiveProbes(reporter, catalog, args.timeoutMs);
const report = makeReport(args, targetCommit, targetShortCommit, reporter, artifactIdentity, registryCapabilities);
if (args.writeReport) {
await writeReport(report, args.reportPath);
}
printSummary(args, report);
if (report.conclusion === "blocked" && args.failOnBlocked) {
process.exitCode = 2;
}
}
function validateCodeAgentProviderContract(reporter, deploy, workloads) {
const staticContract = inspectCodeAgentProviderStaticContract(deploy, workloads);
const evidence = [
{
staticContract,
fixtureEvidence: false,
providerConnected: false
}
];
if (staticContract.ready) {
reporter.check(
"code-agent-provider-env-contract",
"agent",
"pass",
"cloud-api DEV Code Agent manifest declares the OpenAI provider Secret reference and DEV egress/proxy base URL without secret values.",
evidence
);
return;
}
const missing = [
...staticContract.missingDeployEnv,
...staticContract.missingK8sEnv,
...staticContract.missingSecretRefs,
...staticContract.missingEgressContract
];
reporter.check(
"code-agent-provider-env-contract",
"agent",
"blocked",
`cloud-api DEV Code Agent provider contract is missing ${missing.join(", ")}.`,
evidence
);
reporter.block({
type: "agent_blocker",
scope: "code-agent-provider-env-contract",
summary: `cloud-api DEV Code Agent provider contract is incomplete; missing ${missing.join(", ")}.`,
nextTask: "Repair deploy/deploy.json and deploy/k8s/base/workloads.yaml so hwlab-cloud-api has OPENAI_API_KEY from hwlab-code-agent-provider/openai-api-key and HWLAB_CODE_AGENT_OPENAI_BASE_URL through DEV egress/proxy; do not print the Secret value."
});
}
function inspectCodeAgentProviderStaticContract(deploy, workloads) {
const contract = DEV_CODE_AGENT_PROVIDER_CONTRACT;
const cloudApi = deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api") ?? {};
const deployEnv = cloudApi.env ?? {};
const workloadEnv = getWorkloadEnvByServiceId(workloads, "hwlab-cloud-api");
const secretRef = contract.secretRefs[0];
const expectedSecretRef = codeAgentSecretRefPlaceholder();
const workloadSecretRef = workloadEnv.get(secretRef.env)?.valueFrom?.secretKeyRef;
const fields = contract.requiredEnv.map((name) => ({
name,
manifestPresent: Object.hasOwn(deployEnv, name),
k8sPresent: workloadEnv.has(name),
redacted: name === secretRef.env,
source: name === secretRef.env ? "k8s-secret-ref" : "runtime-env"
}));
const secretRefMatches =
deployEnv[secretRef.env] === expectedSecretRef &&
workloadSecretRef?.name === secretRef.secretName &&
workloadSecretRef?.key === secretRef.secretKey;
const deployBaseUrl = deployEnv[contract.egress.env];
const workloadBaseUrl = workloadEnv.get(contract.egress.env)?.value;
const egressChecks = [
["deployEnv.HWLAB_CODE_AGENT_PROVIDER", deployEnv.HWLAB_CODE_AGENT_PROVIDER, contract.provider],
["deployEnv.HWLAB_CODE_AGENT_MODEL", deployEnv.HWLAB_CODE_AGENT_MODEL, contract.model],
["deployEnv.HWLAB_CODE_AGENT_OPENAI_BASE_URL", deployBaseUrl, contract.egress.defaultBaseUrl],
["workloadEnv.HWLAB_CODE_AGENT_PROVIDER", workloadEnv.get("HWLAB_CODE_AGENT_PROVIDER")?.value, contract.provider],
["workloadEnv.HWLAB_CODE_AGENT_MODEL", workloadEnv.get("HWLAB_CODE_AGENT_MODEL")?.value, contract.model],
["workloadEnv.HWLAB_CODE_AGENT_OPENAI_BASE_URL", workloadBaseUrl, contract.egress.defaultBaseUrl]
];
const missingEgressContract = egressChecks
.filter(([, actualValue, expectedValue]) => !Object.is(actualValue, expectedValue))
.map(([name, actualValue, expectedValue]) => `${name} expected ${expectedValue} got ${actualValue ?? "missing"}`);
if (deployBaseUrl === contract.egress.forbiddenDirectBaseUrl || workloadBaseUrl === contract.egress.forbiddenDirectBaseUrl) {
missingEgressContract.push("HWLAB_CODE_AGENT_OPENAI_BASE_URL must not point directly at public api.openai.com");
}
const missingDeployEnv = fields.filter((field) => !field.manifestPresent).map((field) => field.name);
const missingK8sEnv = fields.filter((field) => !field.k8sPresent).map((field) => field.name);
const missingSecretRefs = secretRefMatches ? [] : [`${secretRef.secretName}/${secretRef.secretKey}`];
return {
contractVersion: contract.contractVersion,
environment: contract.environment,
provider: contract.runtimeProvider,
model: contract.model,
ready:
missingDeployEnv.length === 0 &&
missingK8sEnv.length === 0 &&
missingSecretRefs.length === 0 &&
missingEgressContract.length === 0,
requiredEnv: fields,
secretRef: {
env: secretRef.env,
secretName: secretRef.secretName,
secretKey: secretRef.secretKey,
present: secretRefMatches,
redacted: true
},
egress: {
env: contract.egress.env,
target: contract.egress.target,
deployPresent: Boolean(deployBaseUrl),
workloadPresent: Boolean(workloadBaseUrl),
directPublicOpenAi: deployBaseUrl === contract.egress.forbiddenDirectBaseUrl || workloadBaseUrl === contract.egress.forbiddenDirectBaseUrl,
valueRedacted: true
},
missingDeployEnv,
missingK8sEnv,
missingSecretRefs,
missingEgressContract,
secretMaterialRead: false,
valuesRedacted: true,
providerConnected: false,
fixtureEvidence: false
};
}