fix: guard code agent provider env readiness

This commit is contained in:
Code Queue Review
2026-05-23 00:50:10 +00:00
parent 89d64092a0
commit eb4686f452
8 changed files with 638 additions and 10 deletions
+8 -2
View File
@@ -31,7 +31,10 @@ for future compatibility, but PROD deployment is not part of MVP acceptance.
`hwlab-code-agent-provider/openai-api-key` and
`HWLAB_CODE_AGENT_OPENAI_BASE_URL` through the DEV egress/proxy path. Source
checks verify the Secret ref and base-url contract only; they do not prove
provider connectivity and must not print API key material.
provider connectivity and must not print API key material. Controlled apply
reports also check that the live `hwlab-cloud-api` Deployment preserves the
same env names and `secretKeyRef` name/key metadata after apply without
reading Kubernetes Secret data.
- `hwlab-cloud-api` also declares `HWLAB_CLOUD_RUNTIME_ADAPTER=postgres` and
`HWLAB_CLOUD_RUNTIME_DURABLE=true` so DEV can use the Postgres-backed runtime
adapter once the schema is applied. `/health/live` must still remain degraded
@@ -66,7 +69,10 @@ The desired-state plan reads only `deploy/deploy.json`,
`deploy/artifact-catalog.dev.json`, `deploy/k8s/base/workloads.yaml`, and the
optional artifact report snapshot. It checks that commit IDs, service image
tags, workload images, and present env mirrors such as `HWLAB_COMMIT_ID`,
`HWLAB_IMAGE`, `HWLAB_IMAGE_TAG`, and `HWLAB_SKILLS_COMMIT_ID` converge. With
`HWLAB_IMAGE`, `HWLAB_IMAGE_TAG`, and `HWLAB_SKILLS_COMMIT_ID` converge. It
also blocks if `hwlab-cloud-api` loses the Code Agent provider Secret ref
`hwlab-code-agent-provider/openai-api-key`, the `OPENAI_API_KEY` env name, or
the DEV egress proxy base-url env. With
`--target-ref` or `--target-tag`, a uniform older desired-state is reported as a
read-only promotion plan. Use `--promotion-commit <sha>` when reviewing or
automating a concrete promotion; then `--check` also fails if any authoritative
+11 -1
View File
@@ -15,6 +15,10 @@ not be used for PROD.
any apply review.
- Validate artifact identity, image commit tags, Kubernetes workload/service
shape, DEV health, and cloud-api DB connectivity.
- Validate that `hwlab-cloud-api` desired state declares the Code Agent
provider env contract: `OPENAI_API_KEY` from
`hwlab-code-agent-provider/openai-api-key` and
`HWLAB_CODE_AGENT_OPENAI_BASE_URL` through the DEV egress/proxy path.
- Emit an operator-readable plan in `devDeployApply`: plan/apply boundary,
expected artifact commit, target namespace, workloads, services, remaining
blockers, minimal manual commands, and rollback hints.
@@ -22,7 +26,8 @@ not be used for PROD.
template Jobs whose image changed but whose Kubernetes `spec.template` is
immutable.
- Stop before mutation when artifact publish evidence, registry digests,
`kubectl`, DEV health, or cloud-api DB connectivity are missing.
`kubectl`, DEV health, cloud-api DB connectivity, or Code Agent provider
env preservation are missing.
## Commands
@@ -115,6 +120,11 @@ operator fields:
reference, digest, live image, and Kubernetes rollout revision when read-only
Deployment access is available. If access is missing, this field records the
read/rollout commands and the blocker instead of claiming a live rollout.
- `devDeployApply.codeAgentProvider`: no-secret provider validation gate. It
records desired-state env/ref readiness plus live `hwlab-cloud-api`
Deployment env preservation before apply and, in apply mode, after apply.
This gate reads Deployment env metadata only; it must not read Kubernetes
Secret data or attempt a provider call.
- `devDeployApply.manualCommands`: dry-run/report commands before human
approval; the apply command is present only when the plan is ready.
- `devDeployApply.rollbackHint`: pre-apply capture commands, deployment
+8 -1
View File
@@ -20,12 +20,19 @@ Secret 或 token。
当前 DEV 部署合同中 `hwlab-cloud-api` 的 Code Agent provider 使用 OpenAI
provider,运行时通过 `OPENAI_API_KEY` 读取 provider 凭证。该环境变量应由授权路径把
`hwlab-code-agent-provider/openai-api-key` 注入到 DEV runtime。
`hwlab-code-agent-provider/openai-api-key` 注入到 DEV runtime。DEV Pod 的 OpenAI
Responses 请求必须使用 `HWLAB_CODE_AGENT_OPENAI_BASE_URL` 指向受控 DEV egress/proxy
路径 `http://172.26.26.227:17680/v1/responses`,不能直接指向 public
`api.openai.com`
Runner 不得尝试修补、读取、回显或替换该 Secret。若 DEV runtime 缺少该授权凭证注入,
`provider_unavailable``error.missingEnv` 包含 `OPENAI_API_KEY` 必须判为
`BLOCKED/credential`
部署前和部署后的自动化只允许证明 env 名称、`secretKeyRef` 的 Secret 名和 key 名、以及
DEV egress/base-url 合同是否声明和保留;不得读取 Secret data,也不得把 Secret 值写入
report、issue、PR 或截图。
## 判定标准
| 观测结果 | readiness |
+200
View File
@@ -37,6 +37,172 @@ export function codeAgentSecretRefPlaceholder() {
return `secretRef:${ref.secretName}/${ref.secretKey}`;
}
export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadEnv = {} } = {}) {
const contract = DEV_CODE_AGENT_PROVIDER_CONTRACT;
const secretRef = contract.secretRefs[0];
const expectedSecretRef = codeAgentSecretRefPlaceholder();
const requiredRuntimeEnv = codeAgentRequiredRuntimeEnv();
const expectedValues = {
HWLAB_CODE_AGENT_PROVIDER: contract.provider,
HWLAB_CODE_AGENT_MODEL: contract.model,
[contract.egress.env]: contract.egress.defaultBaseUrl,
[secretRef.env]: expectedSecretRef
};
const workloadInspection = inspectCodeAgentProviderWorkloadEnv(workloadEnv);
const missingDeployEnv = requiredRuntimeEnv.filter((name) => !Object.hasOwn(deployEnv ?? {}, name));
const deployMismatches = requiredRuntimeEnv
.filter((name) => Object.hasOwn(deployEnv ?? {}, name) && !Object.is(deployEnv[name], expectedValues[name]))
.map((name) => ({
name,
expected: name === secretRef.env ? expectedSecretRef : expectedValues[name],
redacted: name === secretRef.env || name === contract.egress.env
}));
const missingSecretRefs = [];
if (deployEnv?.[secretRef.env] !== expectedSecretRef || workloadInspection.secretRef.present !== true) {
missingSecretRefs.push(`${secretRef.secretName}/${secretRef.secretKey}`);
}
const missingEgressContract = [];
if (deployEnv?.[contract.egress.env] !== contract.egress.defaultBaseUrl || workloadInspection.egress.matchesDevProxy !== true) {
missingEgressContract.push(`${contract.egress.env} must use ${contract.egress.target}`);
}
if (
isForbiddenDirectOpenAiBaseUrl(deployEnv?.[contract.egress.env]) ||
workloadInspection.egress.directPublicOpenAi === true
) {
missingEgressContract.push(`${contract.egress.env} must not point directly at public api.openai.com`);
}
const ready =
missingDeployEnv.length === 0 &&
deployMismatches.length === 0 &&
workloadInspection.ready === true &&
missingSecretRefs.length === 0 &&
missingEgressContract.length === 0;
return {
contractVersion: contract.contractVersion,
environment: contract.environment,
provider: contract.runtimeProvider,
model: contract.model,
status: ready ? "pass" : "blocked",
ready,
requiredEnv: requiredRuntimeEnv.map((name) => ({
name,
deployPresent: Object.hasOwn(deployEnv ?? {}, name),
workloadPresent: workloadInspection.requiredEnv.find((entry) => entry.name === name)?.present === true,
redacted: name === secretRef.env || name === contract.egress.env,
source: name === secretRef.env ? "k8s-secret-ref" : "runtime-env"
})),
secretRef: {
env: secretRef.env,
secretName: secretRef.secretName,
secretKey: secretRef.secretKey,
deployPlaceholderPresent: deployEnv?.[secretRef.env] === expectedSecretRef,
workloadSecretRefPresent: workloadInspection.secretRef.present === true,
present: deployEnv?.[secretRef.env] === expectedSecretRef && workloadInspection.secretRef.present === true,
redacted: true
},
egress: {
env: contract.egress.env,
target: contract.egress.target,
deployPresent: Boolean(deployEnv?.[contract.egress.env]),
workloadPresent: workloadInspection.egress.present,
deployMatchesDevProxy: deployEnv?.[contract.egress.env] === contract.egress.defaultBaseUrl,
workloadMatchesDevProxy: workloadInspection.egress.matchesDevProxy,
directPublicOpenAi:
isForbiddenDirectOpenAiBaseUrl(deployEnv?.[contract.egress.env]) ||
workloadInspection.egress.directPublicOpenAi === true,
valueRedacted: true
},
missingDeployEnv,
missingWorkloadEnv: workloadInspection.missingEnv,
deployMismatches,
workloadMismatches: workloadInspection.mismatchedEnv,
missingSecretRefs,
missingEgressContract,
secretMaterialRead: false,
valuesRedacted: true,
providerConnected: false,
fixtureEvidence: false
};
}
export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
const contract = DEV_CODE_AGENT_PROVIDER_CONTRACT;
const secretRef = contract.secretRefs[0];
const requiredRuntimeEnv = codeAgentRequiredRuntimeEnv();
const expectedValues = {
HWLAB_CODE_AGENT_PROVIDER: contract.provider,
HWLAB_CODE_AGENT_MODEL: contract.model,
[contract.egress.env]: contract.egress.defaultBaseUrl
};
const requiredEnv = requiredRuntimeEnv.map((name) => {
const entry = getEnvEntry(workloadEnv, name);
return {
name,
present: Boolean(entry),
redacted: name === secretRef.env || name === contract.egress.env,
source: name === secretRef.env ? "k8s-secret-ref" : "runtime-env"
};
});
const missingEnv = requiredEnv.filter((entry) => !entry.present).map((entry) => entry.name);
const mismatchedEnv = [];
for (const [name, expected] of Object.entries(expectedValues)) {
const entry = getEnvEntry(workloadEnv, name);
if (entry && !Object.is(envEntryValue(entry), expected)) {
mismatchedEnv.push({
name,
expected,
redacted: name === contract.egress.env
});
}
}
const secretKeyRef = envEntrySecretKeyRef(getEnvEntry(workloadEnv, secretRef.env));
const secretPresent =
secretKeyRef?.name === secretRef.secretName &&
secretKeyRef?.key === secretRef.secretKey;
const baseUrl = envEntryValue(getEnvEntry(workloadEnv, contract.egress.env));
const directPublicOpenAi = isForbiddenDirectOpenAiBaseUrl(baseUrl);
const matchesDevProxy = baseUrl === contract.egress.defaultBaseUrl;
const ready =
missingEnv.length === 0 &&
mismatchedEnv.length === 0 &&
secretPresent &&
matchesDevProxy &&
!directPublicOpenAi;
return {
contractVersion: contract.contractVersion,
environment: contract.environment,
provider: contract.runtimeProvider,
model: contract.model,
status: ready ? "pass" : "blocked",
ready,
requiredEnv,
missingEnv,
mismatchedEnv,
secretRef: {
env: secretRef.env,
secretName: secretRef.secretName,
secretKey: secretRef.secretKey,
present: secretPresent,
redacted: true
},
egress: {
env: contract.egress.env,
target: contract.egress.target,
present: Boolean(baseUrl),
matchesDevProxy,
directPublicOpenAi,
valueRedacted: true
},
secretMaterialRead: false,
valuesRedacted: true,
providerConnected: false
};
}
export function buildCodeAgentProviderManifestPlaceholder() {
const contract = DEV_CODE_AGENT_PROVIDER_CONTRACT;
return {
@@ -135,3 +301,37 @@ function firstNonEmpty(...values) {
}
return "";
}
function codeAgentRequiredRuntimeEnv() {
return [
"HWLAB_CODE_AGENT_PROVIDER",
"HWLAB_CODE_AGENT_MODEL",
...DEV_CODE_AGENT_PROVIDER_CONTRACT.requiredEnv
];
}
function getEnvEntry(env, name) {
if (!env) return undefined;
if (env instanceof Map) return env.get(name);
if (Array.isArray(env)) return env.find((entry) => entry?.name === name);
return env[name];
}
function envEntryValue(entry) {
if (typeof entry === "string") return entry;
if (entry && typeof entry === "object" && Object.hasOwn(entry, "value")) return entry.value;
return undefined;
}
function envEntrySecretKeyRef(entry) {
if (!entry || typeof entry !== "object") return null;
return entry.valueFrom?.secretKeyRef ?? null;
}
function isForbiddenDirectOpenAiBaseUrl(value) {
const baseUrl = firstNonEmpty(value);
return (
baseUrl === DEV_CODE_AGENT_PROVIDER_CONTRACT.egress.forbiddenDirectBaseUrl ||
/^https:\/\/api\.openai\.com\/v1\/responses\/?$/u.test(baseUrl)
);
}
+68 -4
View File
@@ -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" });
+67
View File
@@ -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
+170 -1
View File
@@ -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),
+106 -1
View File
@@ -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");
});