fix: guard code agent provider env readiness
This commit is contained in:
+8
-2
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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