fix: guard code agent provider env readiness
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, mkdir, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
@@ -17,12 +17,14 @@ async function makeFixture({
|
||||
deployEnvImage = null,
|
||||
deployEnvMirrors = true,
|
||||
deploySkillsCommitId = null,
|
||||
deployCodeAgentProviderEnv = true,
|
||||
workloadTag = commitId,
|
||||
workloadEnvCommitId = commitId,
|
||||
workloadEnvImageTag = commitId,
|
||||
workloadEnvImage = null,
|
||||
workloadEnvMirrors = true,
|
||||
workloadSkillsCommitId = null
|
||||
workloadSkillsCommitId = null,
|
||||
workloadCodeAgentProviderEnv = true
|
||||
} = {}) {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-desired-state-"));
|
||||
await mkdir(path.join(root, "deploy/k8s/base"), { recursive: true });
|
||||
@@ -30,6 +32,33 @@ async function makeFixture({
|
||||
const image = `127.0.0.1:5000/hwlab/${serviceId}:${commitId}`;
|
||||
const catalogImage = `127.0.0.1:5000/hwlab/${serviceId}:${catalogImageTag}`;
|
||||
const workloadImage = `127.0.0.1:5000/hwlab/${serviceId}:${workloadTag}`;
|
||||
const providerBaseUrl = "http://172.26.26.227:17680/v1/responses";
|
||||
const providerEnv =
|
||||
serviceId === "hwlab-cloud-api" && deployCodeAgentProviderEnv
|
||||
? {
|
||||
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-5.5",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: providerBaseUrl,
|
||||
OPENAI_API_KEY: "secretRef:hwlab-code-agent-provider/openai-api-key"
|
||||
}
|
||||
: {};
|
||||
const workloadProviderEnv =
|
||||
serviceId === "hwlab-cloud-api" && workloadCodeAgentProviderEnv
|
||||
? [
|
||||
{ name: "HWLAB_CODE_AGENT_PROVIDER", value: "openai" },
|
||||
{ name: "HWLAB_CODE_AGENT_MODEL", value: "gpt-5.5" },
|
||||
{ name: "HWLAB_CODE_AGENT_OPENAI_BASE_URL", value: providerBaseUrl },
|
||||
{
|
||||
name: "OPENAI_API_KEY",
|
||||
valueFrom: {
|
||||
secretKeyRef: {
|
||||
name: "hwlab-code-agent-provider",
|
||||
key: "openai-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
: [];
|
||||
const deploy = {
|
||||
manifestVersion: "v1",
|
||||
environment: "dev",
|
||||
@@ -47,7 +76,8 @@ async function makeFixture({
|
||||
HWLAB_COMMIT_ID: deployEnvCommitId,
|
||||
HWLAB_IMAGE: deployEnvImage ?? image,
|
||||
HWLAB_IMAGE_TAG: deployEnvImageTag,
|
||||
...(deploySkillsCommitId ? { HWLAB_SKILLS_COMMIT_ID: deploySkillsCommitId } : {})
|
||||
...(deploySkillsCommitId ? { HWLAB_SKILLS_COMMIT_ID: deploySkillsCommitId } : {}),
|
||||
...providerEnv
|
||||
}
|
||||
: {}
|
||||
}
|
||||
@@ -105,7 +135,8 @@ async function makeFixture({
|
||||
{ name: "HWLAB_COMMIT_ID", value: workloadEnvCommitId },
|
||||
{ name: "HWLAB_IMAGE", value: workloadEnvImage ?? image },
|
||||
{ name: "HWLAB_IMAGE_TAG", value: workloadEnvImageTag },
|
||||
...(workloadSkillsCommitId ? [{ name: "HWLAB_SKILLS_COMMIT_ID", value: workloadSkillsCommitId }] : [])
|
||||
...(workloadSkillsCommitId ? [{ name: "HWLAB_SKILLS_COMMIT_ID", value: workloadSkillsCommitId }] : []),
|
||||
...workloadProviderEnv
|
||||
]
|
||||
: []
|
||||
}
|
||||
@@ -139,6 +170,16 @@ test("passes when cloud-web owns runtime identity mirrors", async () => {
|
||||
assert.deepEqual(plan.diagnostics, []);
|
||||
});
|
||||
|
||||
test("passes when cloud-api preserves provider env and DEV egress contract", async () => {
|
||||
const repoRoot = await makeFixture();
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "pass");
|
||||
assert.equal(plan.codeAgentProvider.ready, true);
|
||||
assert.equal(plan.codeAgentProvider.secretRef.present, true);
|
||||
assert.equal(plan.codeAgentProvider.egress.deployMatchesDevProxy, true);
|
||||
assert.deepEqual(plan.diagnostics, []);
|
||||
});
|
||||
|
||||
test("blocks when cloud-web runtime identity mirrors are missing", async () => {
|
||||
const repoRoot = await makeFixture({
|
||||
serviceId: "hwlab-cloud-web",
|
||||
@@ -158,6 +199,29 @@ test("blocks when cloud-web runtime identity mirrors are missing", async () => {
|
||||
));
|
||||
});
|
||||
|
||||
test("blocks when cloud-api provider env is missing from desired state", async () => {
|
||||
const repoRoot = await makeFixture({ deployCodeAgentProviderEnv: false, workloadCodeAgentProviderEnv: false });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.equal(plan.codeAgentProvider.ready, false);
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.code === "code_agent_provider_contract_mismatch"));
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.path === "deploy.codeAgentProvider"));
|
||||
});
|
||||
|
||||
test("blocks when cloud-api workload points Code Agent directly at public OpenAI", async () => {
|
||||
const repoRoot = await makeFixture();
|
||||
const workloadsPath = path.join(repoRoot, "deploy/k8s/base/workloads.yaml");
|
||||
const workloads = JSON.parse(await readFile(workloadsPath, "utf8"));
|
||||
const env = workloads.items[0].spec.template.spec.containers[0].env;
|
||||
env.find((entry) => entry.name === "HWLAB_CODE_AGENT_OPENAI_BASE_URL").value = "https://api.openai.com/v1/responses";
|
||||
await writeFile(workloadsPath, `${JSON.stringify(workloads, null, 2)}\n`);
|
||||
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.equal(plan.codeAgentProvider.egress.directPublicOpenAi, true);
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.code === "code_agent_provider_contract_mismatch"));
|
||||
});
|
||||
|
||||
test("target tag review is read-only promotion_pending when current state is uniformly older", async () => {
|
||||
const repoRoot = await makeFixture();
|
||||
const plan = await buildDesiredStatePlan({ repoRoot, targetTag: "def5678" });
|
||||
|
||||
@@ -4,6 +4,11 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import {
|
||||
DEV_CODE_AGENT_PROVIDER_CONTRACT,
|
||||
inspectCodeAgentProviderManifestRefs
|
||||
} from "../../internal/cloud/code-agent-contract.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
|
||||
@@ -275,6 +280,55 @@ function inspectEnvMirrors(ctx, { source, basePath, serviceId, env, service, des
|
||||
return { present, missing, diagnostics };
|
||||
}
|
||||
|
||||
function inspectCodeAgentProviderDesiredState(ctx, deployService, workloadRecords) {
|
||||
const workloadRecord = workloadRecords.find((workload) => workload.containerName === "hwlab-cloud-api") ?? workloadRecords[0] ?? null;
|
||||
const inspection = inspectCodeAgentProviderManifestRefs({
|
||||
deployEnv: deployService?.env ?? {},
|
||||
workloadEnv: workloadRecord?.env ?? {}
|
||||
});
|
||||
if (!inspection.ready) {
|
||||
addDiagnostic(ctx, {
|
||||
code: "code_agent_provider_contract_mismatch",
|
||||
path: "deploy.codeAgentProvider",
|
||||
message: "hwlab-cloud-api desired state must preserve Code Agent provider Secret ref and DEV egress proxy env",
|
||||
expected: {
|
||||
provider: DEV_CODE_AGENT_PROVIDER_CONTRACT.provider,
|
||||
secretRef: `${DEV_CODE_AGENT_PROVIDER_CONTRACT.secretRefs[0].secretName}/${DEV_CODE_AGENT_PROVIDER_CONTRACT.secretRefs[0].secretKey}`,
|
||||
egressTarget: DEV_CODE_AGENT_PROVIDER_CONTRACT.egress.target
|
||||
},
|
||||
actual: {
|
||||
missingDeployEnv: inspection.missingDeployEnv,
|
||||
missingWorkloadEnv: inspection.missingWorkloadEnv,
|
||||
deployMismatches: inspection.deployMismatches.map((entry) => entry.name),
|
||||
workloadMismatches: inspection.workloadMismatches.map((entry) => entry.name),
|
||||
missingSecretRefs: inspection.missingSecretRefs,
|
||||
missingEgressContract: inspection.missingEgressContract
|
||||
},
|
||||
secretMaterialRead: false,
|
||||
valuesRedacted: true
|
||||
});
|
||||
}
|
||||
recordObservation(ctx, {
|
||||
source: "deploy",
|
||||
serviceId: "hwlab-cloud-api",
|
||||
path: `${deployPath}.services.hwlab-cloud-api.env.OPENAI_API_KEY`,
|
||||
field: "OPENAI_API_KEY",
|
||||
kind: "secret-ref",
|
||||
value: inspection.secretRef.present ? "secretRef:declared" : "missing_or_mismatched",
|
||||
expected: "secretRef:hwlab-code-agent-provider/openai-api-key"
|
||||
});
|
||||
recordObservation(ctx, {
|
||||
source: "workload",
|
||||
serviceId: "hwlab-cloud-api",
|
||||
path: `${workloadsPath}.hwlab-cloud-api.env.HWLAB_CODE_AGENT_OPENAI_BASE_URL`,
|
||||
field: "HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
||||
kind: "egress-proxy",
|
||||
value: inspection.egress.workloadMatchesDevProxy ? "dev-egress-proxy" : "missing_or_mismatched",
|
||||
expected: DEV_CODE_AGENT_PROVIDER_CONTRACT.egress.target
|
||||
});
|
||||
return inspection;
|
||||
}
|
||||
|
||||
function indexWorkloads(workloads) {
|
||||
const byService = new Map();
|
||||
for (const item of listItems(workloads)) {
|
||||
@@ -561,6 +615,7 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
);
|
||||
|
||||
const services = [];
|
||||
let codeAgentProvider = null;
|
||||
for (const serviceId of serviceIds) {
|
||||
const deployService = deployByService.get(serviceId);
|
||||
const catalogService = catalogByService.get(serviceId);
|
||||
@@ -717,6 +772,10 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
|
||||
service.promotion = servicePromotion(service, target);
|
||||
services.push(service);
|
||||
|
||||
if (serviceId === "hwlab-cloud-api") {
|
||||
codeAgentProvider = inspectCodeAgentProviderDesiredState(ctx, deployService, workloadsForService);
|
||||
}
|
||||
}
|
||||
|
||||
const targetConvergence = buildTargetConvergence(ctx, target);
|
||||
@@ -779,6 +838,14 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
nonAuthoritativeEvidence: [artifactReportPath],
|
||||
note: "This planner reviews source desired-state only. It does not prove image existence, registry reachability, a real DEV apply, or M3 DEV-LIVE hardware-loop evidence."
|
||||
},
|
||||
codeAgentProvider: codeAgentProvider ?? {
|
||||
status: "blocked",
|
||||
ready: false,
|
||||
missingDeployEnv: ["hwlab-cloud-api"],
|
||||
missingWorkloadEnv: ["hwlab-cloud-api"],
|
||||
secretMaterialRead: false,
|
||||
valuesRedacted: true
|
||||
},
|
||||
reportHints: reportHints(artifactReport, desiredCommitId),
|
||||
services,
|
||||
diagnostics: ctx.diagnostics
|
||||
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
DEV_DB_ENV_CONTRACT,
|
||||
summarizeDbContract
|
||||
} from "../../internal/cloud/db-contract.mjs";
|
||||
import {
|
||||
DEV_CODE_AGENT_PROVIDER_CONTRACT,
|
||||
inspectCodeAgentProviderManifestRefs,
|
||||
inspectCodeAgentProviderWorkloadEnv
|
||||
} 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";
|
||||
|
||||
@@ -582,6 +587,12 @@ function blockerHint(blocker) {
|
||||
if (blocker.scope.startsWith("rollout-read-")) {
|
||||
return "Restore read-only Deployment visibility in hwlab-dev so rollout revision, image tag, digest, and source commit can be reported after apply.";
|
||||
}
|
||||
if (blocker.scope.startsWith("code-agent-provider-env-read-")) {
|
||||
return "Restore read-only Deployment visibility for hwlab-cloud-api so provider env names and secretKeyRef metadata can be verified without reading Secret values.";
|
||||
}
|
||||
if (blocker.scope.startsWith("code-agent-provider-env-preservation-") || blocker.scope === "code-agent-provider-desired-state") {
|
||||
return "Repair deploy/deploy.json and deploy/k8s/base/workloads.yaml so hwlab-cloud-api preserves OPENAI_API_KEY from hwlab-code-agent-provider/openai-api-key and the DEV egress proxy base URL.";
|
||||
}
|
||||
if (blocker.scope.startsWith("deploy-runtime-env-") || blocker.scope.startsWith("k8s-runtime-env-")) {
|
||||
return "Update deploy/deploy.json and deploy/k8s/base/workloads.yaml so the service owns HWLAB_COMMIT_ID, HWLAB_IMAGE, and HWLAB_IMAGE_TAG.";
|
||||
}
|
||||
@@ -631,6 +642,132 @@ function buildRollbackHint(workloads) {
|
||||
};
|
||||
}
|
||||
|
||||
function workloadEnvByServiceId(workloads, serviceId) {
|
||||
for (const item of listItems(workloads)) {
|
||||
for (const container of containersFor(item)) {
|
||||
if (serviceIdFor(item, container) !== serviceId) continue;
|
||||
return new Map((container.env ?? []).filter((entry) => entry?.name).map((entry) => [entry.name, entry]));
|
||||
}
|
||||
}
|
||||
return new Map();
|
||||
}
|
||||
|
||||
export function inspectCodeAgentProviderDesiredState(deploy, workloads) {
|
||||
const deployEnv = deploy?.services?.find((service) => service.serviceId === "hwlab-cloud-api")?.env ?? {};
|
||||
return inspectCodeAgentProviderManifestRefs({
|
||||
deployEnv,
|
||||
workloadEnv: workloadEnvByServiceId(workloads, "hwlab-cloud-api")
|
||||
});
|
||||
}
|
||||
|
||||
export function inspectCodeAgentProviderLiveDeployment(deployment) {
|
||||
const container = containerForDeployment(deployment, "hwlab-cloud-api");
|
||||
const workloadEnv = new Map((container?.env ?? []).filter((entry) => entry?.name).map((entry) => [entry.name, entry]));
|
||||
const inspection = inspectCodeAgentProviderWorkloadEnv(workloadEnv);
|
||||
return {
|
||||
...inspection,
|
||||
namespace: deployment?.metadata?.namespace ?? namespace,
|
||||
deployment: deployment?.metadata?.name ?? "hwlab-cloud-api",
|
||||
container: container?.name ?? "hwlab-cloud-api",
|
||||
validationScope: "deployment-env-and-secretKeyRef-metadata-only",
|
||||
secretValuesRead: false,
|
||||
kubernetesSecretDataRead: false
|
||||
};
|
||||
}
|
||||
|
||||
function containerForDeployment(deployment, containerName) {
|
||||
return (deployment?.spec?.template?.spec?.containers ?? []).find((container) => container.name === containerName) ?? null;
|
||||
}
|
||||
|
||||
function codeAgentProviderBlockerSummary(inspection) {
|
||||
const missing = [
|
||||
...(inspection.missingDeployEnv ?? []),
|
||||
...(inspection.missingWorkloadEnv ?? inspection.missingEnv ?? []),
|
||||
...(inspection.deployMismatches ?? []).map((entry) => entry.name),
|
||||
...(inspection.workloadMismatches ?? inspection.mismatchedEnv ?? []).map((entry) => entry.name),
|
||||
...(inspection.missingSecretRefs ?? []),
|
||||
...(inspection.missingEgressContract ?? [])
|
||||
];
|
||||
const detail = uniqueStrings(missing).join(", ");
|
||||
return detail
|
||||
? `Code Agent provider env preservation is incomplete; missing or mismatched ${detail}`
|
||||
: "Code Agent provider env preservation is incomplete";
|
||||
}
|
||||
|
||||
function addCodeAgentProviderDesiredStateBlocker(blockers, desiredState) {
|
||||
if (desiredState.ready) return;
|
||||
addBlocker(
|
||||
blockers,
|
||||
"agent_blocker",
|
||||
"code-agent-provider-desired-state",
|
||||
codeAgentProviderBlockerSummary(desiredState)
|
||||
);
|
||||
}
|
||||
|
||||
async function observeCodeAgentProviderLiveDeployment(kubectl, blockers, { phase, blockOnMismatch }) {
|
||||
const commandArgs = ["-n", namespace, "get", "deployment", "hwlab-cloud-api", "-o", "json"];
|
||||
const base = {
|
||||
phase,
|
||||
namespace,
|
||||
serviceId: "hwlab-cloud-api",
|
||||
status: "not_evaluated",
|
||||
readCommand: kubectlCommand(kubectl, commandArgs),
|
||||
kubeconfig: kubectl.kubeconfig,
|
||||
expectedSecretRef: `${DEV_CODE_AGENT_PROVIDER_CONTRACT.secretRefs[0].secretName}/${DEV_CODE_AGENT_PROVIDER_CONTRACT.secretRefs[0].secretKey}`,
|
||||
expectedEgressTarget: DEV_CODE_AGENT_PROVIDER_CONTRACT.egress.target,
|
||||
secretValuesRead: false,
|
||||
kubernetesSecretDataRead: false,
|
||||
valuesRedacted: true
|
||||
};
|
||||
|
||||
if (kubectl.status !== "ready") {
|
||||
if (blockOnMismatch) {
|
||||
addBlocker(blockers, "environment_blocker", `code-agent-provider-env-read-${phase}`, kubectl.reason);
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
status: "blocked",
|
||||
blocker: kubectl.reason
|
||||
};
|
||||
}
|
||||
|
||||
const result = await kubectlResult(kubectl, commandArgs, 15000);
|
||||
if (!result.ok) {
|
||||
const summary = oneLine(result.stderr || result.stdout);
|
||||
if (blockOnMismatch) {
|
||||
addBlocker(blockers, "environment_blocker", `code-agent-provider-env-read-${phase}`, `Cannot read DEV Deployment hwlab-cloud-api provider env: ${summary}`);
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
status: "blocked",
|
||||
blocker: summary
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const inspection = inspectCodeAgentProviderLiveDeployment(JSON.parse(result.stdout));
|
||||
if (!inspection.ready && blockOnMismatch) {
|
||||
addBlocker(blockers, "runtime_blocker", `code-agent-provider-env-preservation-${phase}`, codeAgentProviderBlockerSummary(inspection));
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
...inspection,
|
||||
phase,
|
||||
status: inspection.ready ? "pass" : "blocked"
|
||||
};
|
||||
} catch (error) {
|
||||
const summary = oneLine(error.message);
|
||||
if (blockOnMismatch) {
|
||||
addBlocker(blockers, "contract_blocker", `code-agent-provider-env-read-${phase}`, `Cannot parse DEV Deployment hwlab-cloud-api provider env: ${summary}`);
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
status: "blocked",
|
||||
blocker: summary
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildManualCommands(status) {
|
||||
if (status === "blocked") {
|
||||
return {
|
||||
@@ -1297,6 +1434,8 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
const artifactEvidence = validateDeployAndCatalog(deploy, catalog, commitId, blockers);
|
||||
const k8sManifest = validateK8s(devKustomization, namespaceDoc, workloads, services, healthContract, deploy, catalog, blockers);
|
||||
const cloudApiDb = await checkCloudApiDb(deploy, workloads, services, blockers);
|
||||
const codeAgentProviderDesiredState = inspectCodeAgentProviderDesiredState(deploy, workloads);
|
||||
addCodeAgentProviderDesiredStateBlocker(blockers, codeAgentProviderDesiredState);
|
||||
if (!(await readText("internal/cloud/server.mjs")).includes('url.pathname === "/health/live"')) {
|
||||
addBlocker(blockers, "contract_blocker", "cloud-api-health-path", "cloud-api does not implement the k8s /health/live path");
|
||||
}
|
||||
@@ -1307,6 +1446,10 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
observationPhase: "before_apply",
|
||||
blockRuntimeEnvDrift: false
|
||||
});
|
||||
const codeAgentProviderLiveBeforeApply = await observeCodeAgentProviderLiveDeployment(kubectl, blockers, {
|
||||
phase: "before_apply",
|
||||
blockOnMismatch: !args.apply
|
||||
});
|
||||
const liveProbe = args.skipLiveProbe ? { status: "not_run", reason: "--skip-live-probe" } : await probeDevEndpoint(blockers);
|
||||
const templateJobReplacements = await buildDevTemplateJobReplacementPlan(kubectl, workloads, blockers);
|
||||
const applyStep = await runApplyStep(
|
||||
@@ -1329,6 +1472,19 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
args.apply ? "apply did not complete successfully; post-apply rollout env verification was not run" : "dry-run mode; no post-apply rollout env verification"
|
||||
);
|
||||
const cloudWebRollout = args.apply ? cloudWebRolloutAfterApply : cloudWebRolloutBeforeApply;
|
||||
const codeAgentProviderLiveAfterApply = args.apply && applyStep.status === "pass"
|
||||
? await observeCodeAgentProviderLiveDeployment(kubectl, blockers, {
|
||||
phase: "after_apply",
|
||||
blockOnMismatch: true
|
||||
})
|
||||
: {
|
||||
phase: "after_apply",
|
||||
status: "not_run",
|
||||
reason: args.apply ? "apply did not complete" : "dry-run mode",
|
||||
secretValuesRead: false,
|
||||
kubernetesSecretDataRead: false,
|
||||
valuesRedacted: true
|
||||
};
|
||||
const status = blockers.length > 0 ? "blocked" : "pass";
|
||||
const artifactPlan = buildArtifactPlan(deploy, catalog, commitId, artifactEvidence);
|
||||
const workloadPlan = buildWorkloadPlan(workloads);
|
||||
@@ -1342,7 +1498,7 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-deploy-report.json",
|
||||
reportVersion: "v1",
|
||||
issue: "pikasTech/HWLAB#33",
|
||||
supports: ["pikasTech/HWLAB#63", "pikasTech/HWLAB#50", "pikasTech/HWLAB#7", "pikasTech/HWLAB#33", "pikasTech/HWLAB#17", "pikasTech/HWLAB#32", "pikasTech/HWLAB#30"],
|
||||
supports: ["pikasTech/HWLAB#164", "pikasTech/HWLAB#143", "pikasTech/HWLAB#99", "pikasTech/HWLAB#63", "pikasTech/HWLAB#50", "pikasTech/HWLAB#7", "pikasTech/HWLAB#33", "pikasTech/HWLAB#17", "pikasTech/HWLAB#32", "pikasTech/HWLAB#30"],
|
||||
taskId: "dev-deploy-apply",
|
||||
commitId,
|
||||
acceptanceLevel: "dev_deploy_apply",
|
||||
@@ -1385,6 +1541,8 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
`kubectl executor: ${kubectl.executor ?? "missing"}`,
|
||||
`kubectl kubeconfig: ${kubectl.kubeconfig}`,
|
||||
`live health: ${liveProbe.status}`,
|
||||
`code agent provider desired-state: ${codeAgentProviderDesiredState.status}`,
|
||||
`code agent provider live env before apply: ${codeAgentProviderLiveBeforeApply.status}`,
|
||||
`template job replacements: ${templateJobReplacements.filter((replacement) => replacement.replace).length}`
|
||||
],
|
||||
summary: status === "blocked" ? "DEV apply is blocked before mutation." : "DEV dry-run preflight passed."
|
||||
@@ -1395,6 +1553,7 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
"DEV artifact catalog must contain CI publish evidence and registry digests",
|
||||
"kubectl must be available for D601 hwlab-dev and must not target PROD",
|
||||
"hwlab-cloud-api /health/live must report serviceId, dev environment, and DB env readiness without exposing secret values",
|
||||
"hwlab-cloud-api must declare and preserve OPENAI_API_KEY from hwlab-code-agent-provider/openai-api-key plus the DEV Code Agent egress proxy env without reading Secret values",
|
||||
"pods, services, configmaps, and image commit tags must be observable in hwlab-dev"
|
||||
],
|
||||
summary: status === "blocked" ? "One or more required DEV deploy preconditions are blocked." : "DEV deploy preconditions are satisfied."
|
||||
@@ -1418,6 +1577,16 @@ export async function runDevDeployApply(argv, io = {}) {
|
||||
cloudWebRollout,
|
||||
cloudWebRolloutBeforeApply,
|
||||
cloudWebRolloutAfterApply,
|
||||
codeAgentProvider: {
|
||||
desiredState: codeAgentProviderDesiredState,
|
||||
liveBeforeApply: codeAgentProviderLiveBeforeApply,
|
||||
liveAfterApply: codeAgentProviderLiveAfterApply,
|
||||
validationGate: "no-secret provider env/secretKeyRef preservation",
|
||||
providerCallAttempted: false,
|
||||
secretValuesRead: false,
|
||||
kubernetesSecretDataRead: false,
|
||||
valuesRedacted: true
|
||||
},
|
||||
applyStep,
|
||||
manualCommands,
|
||||
rollbackHint: buildRollbackHint(workloads),
|
||||
|
||||
@@ -1,7 +1,76 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { compareRuntimeIdentityEnv, decideDevTemplateJobReplacement, resolveApplySourceCommit } from "./dev-deploy-apply.mjs";
|
||||
import {
|
||||
compareRuntimeIdentityEnv,
|
||||
decideDevTemplateJobReplacement,
|
||||
inspectCodeAgentProviderDesiredState,
|
||||
inspectCodeAgentProviderLiveDeployment,
|
||||
resolveApplySourceCommit
|
||||
} from "./dev-deploy-apply.mjs";
|
||||
|
||||
const codeAgentBaseUrl = "http://172.26.26.227:17680/v1/responses";
|
||||
|
||||
function codeAgentDeploy(env = {}) {
|
||||
return {
|
||||
services: [
|
||||
{
|
||||
serviceId: "hwlab-cloud-api",
|
||||
env: {
|
||||
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-5.5",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: codeAgentBaseUrl,
|
||||
OPENAI_API_KEY: "secretRef:hwlab-code-agent-provider/openai-api-key",
|
||||
...env
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function codeAgentWorkloads(envOverrides = []) {
|
||||
return {
|
||||
kind: "List",
|
||||
items: [
|
||||
{
|
||||
kind: "Deployment",
|
||||
metadata: {
|
||||
name: "hwlab-cloud-api",
|
||||
namespace: "hwlab-dev",
|
||||
labels: {
|
||||
"hwlab.pikastech.local/service-id": "hwlab-cloud-api"
|
||||
}
|
||||
},
|
||||
spec: {
|
||||
template: {
|
||||
spec: {
|
||||
containers: [
|
||||
{
|
||||
name: "hwlab-cloud-api",
|
||||
env: [
|
||||
{ name: "HWLAB_CODE_AGENT_PROVIDER", value: "openai" },
|
||||
{ name: "HWLAB_CODE_AGENT_MODEL", value: "gpt-5.5" },
|
||||
{ name: "HWLAB_CODE_AGENT_OPENAI_BASE_URL", value: codeAgentBaseUrl },
|
||||
{
|
||||
name: "OPENAI_API_KEY",
|
||||
valueFrom: {
|
||||
secretKeyRef: {
|
||||
name: "hwlab-code-agent-provider",
|
||||
key: "openai-api-key"
|
||||
}
|
||||
}
|
||||
},
|
||||
...envOverrides
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
test("allowlisted suspended template Job image change plans replacement", () => {
|
||||
const decision = decideDevTemplateJobReplacement({
|
||||
@@ -127,3 +196,39 @@ test("runtime identity env comparator detects stale cloud-web env drift", () =>
|
||||
["HWLAB_COMMIT_ID", "HWLAB_IMAGE", "HWLAB_IMAGE_TAG"]
|
||||
);
|
||||
});
|
||||
|
||||
test("Code Agent provider desired state preserves Secret ref and egress base URL", () => {
|
||||
const inspection = inspectCodeAgentProviderDesiredState(codeAgentDeploy(), codeAgentWorkloads());
|
||||
|
||||
assert.equal(inspection.ready, true);
|
||||
assert.equal(inspection.status, "pass");
|
||||
assert.equal(inspection.secretRef.present, true);
|
||||
assert.equal(inspection.secretMaterialRead, false);
|
||||
assert.equal(inspection.egress.deployMatchesDevProxy, true);
|
||||
assert.equal(inspection.egress.workloadMatchesDevProxy, true);
|
||||
});
|
||||
|
||||
test("Code Agent provider desired state blocks direct public OpenAI base URL", () => {
|
||||
const inspection = inspectCodeAgentProviderDesiredState(
|
||||
codeAgentDeploy({ HWLAB_CODE_AGENT_OPENAI_BASE_URL: "https://api.openai.com/v1/responses" }),
|
||||
codeAgentWorkloads([
|
||||
{ name: "HWLAB_CODE_AGENT_OPENAI_BASE_URL", value: "https://api.openai.com/v1/responses" }
|
||||
])
|
||||
);
|
||||
|
||||
assert.equal(inspection.ready, false);
|
||||
assert.equal(inspection.status, "blocked");
|
||||
assert.equal(inspection.egress.directPublicOpenAi, true);
|
||||
assert.ok(inspection.missingEgressContract.some((entry) => entry.includes("api.openai.com")));
|
||||
});
|
||||
|
||||
test("Code Agent live Deployment inspection reads only env metadata", () => {
|
||||
const deployment = codeAgentWorkloads().items[0];
|
||||
const inspection = inspectCodeAgentProviderLiveDeployment(deployment);
|
||||
|
||||
assert.equal(inspection.ready, true);
|
||||
assert.equal(inspection.secretRef.present, true);
|
||||
assert.equal(inspection.secretValuesRead, false);
|
||||
assert.equal(inspection.kubernetesSecretDataRead, false);
|
||||
assert.equal(inspection.validationScope, "deployment-env-and-secretKeyRef-metadata-only");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user