From 48abac41c6bf3ace543b313afc41adfe3cb030a2 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 24 May 2026 07:52:14 +0000 Subject: [PATCH] fix: preflight codex stdio mount resources --- deploy/k8s/base/code-agent-codex-config.yaml | 15 +++ deploy/k8s/base/kustomization.yaml | 1 + docs/reference/deployment-publish.md | 6 + scripts/src/dev-cd-apply.mjs | 119 ++++++++++++++++++- scripts/src/dev-cd-apply.test.mjs | 65 +++++++++- 5 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 deploy/k8s/base/code-agent-codex-config.yaml diff --git a/deploy/k8s/base/code-agent-codex-config.yaml b/deploy/k8s/base/code-agent-codex-config.yaml new file mode 100644 index 00000000..5045d0c0 --- /dev/null +++ b/deploy/k8s/base/code-agent-codex-config.yaml @@ -0,0 +1,15 @@ +{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "hwlab-code-agent-codex-config", + "namespace": "hwlab-dev", + "labels": { + "app.kubernetes.io/name": "hwlab-code-agent-codex-config", + "hwlab.pikastech.local/service-id": "hwlab-cloud-api" + } + }, + "data": { + "config.toml": "model_provider = \"OpenAI\"\nmodel = \"gpt-5.4\"\nreview_model = \"gpt-5.4\"\nmodel_reasoning_effort = \"xhigh\"\ndisable_response_storage = true\nnetwork_access = \"enabled\"\nmodel_context_window = 1000000\nmodel_auto_compact_token_limit = 900000\n\n[model_providers.OpenAI]\nname = \"OpenAI\"\nbase_url = \"https://hyueapi.com\"\nwire_api = \"responses\"\nrequires_openai_auth = true\n\n[model_providers.crs]\nrequest_max_retries = 1000\nstream_max_retries = 1000\nname = \"crs\"\nbase_url = \"https://api.gptclubapi.xyz/openai\"\nwire_api = \"responses\"\nrequires_openai_auth = true\nenv_key = \"CRS_OAI_KEY\"\n" + } +} diff --git a/deploy/k8s/base/kustomization.yaml b/deploy/k8s/base/kustomization.yaml index 8b582a59..acdb424f 100644 --- a/deploy/k8s/base/kustomization.yaml +++ b/deploy/k8s/base/kustomization.yaml @@ -3,6 +3,7 @@ "kind": "Kustomization", "resources": [ "namespace.yaml", + "code-agent-codex-config.yaml", "services.yaml", "workloads.yaml" ], diff --git a/docs/reference/deployment-publish.md b/docs/reference/deployment-publish.md index b6e4f968..86049553 100644 --- a/docs/reference/deployment-publish.md +++ b/docs/reference/deployment-publish.md @@ -108,6 +108,12 @@ apply preflight 检查的 SecretRef 固定为: - `hwlab-cloud-api-dev-db/database-url` - `hwlab-cloud-api-dev-db-admin/admin-url` - `hwlab-code-agent-provider/openai-api-key` +- `hwlab-code-agent-codex-auth/auth.json` + +Code Agent Codex stdio 还必须在 preflight 阶段验证非 secret ConfigMap +`hwlab-code-agent-codex-config/config.toml` 的存在性和 key 名;该 ConfigMap 由 +`deploy/k8s/base/code-agent-codex-config.yaml` 纳入 DEV desired-state。Codex +auth 仍然只能作为外部 Secret 管理,禁止把 `auth.json` 内容写进仓库或报告。 preflight 和报告只能记录 Secret 名、key 名、存在性、key-name 观察结果和脱敏 命令摘要;不得读取或打印 Secret value。阻塞时输出结构化 blocker,包含 diff --git a/scripts/src/dev-cd-apply.mjs b/scripts/src/dev-cd-apply.mjs index c881a04c..6014ce59 100644 --- a/scripts/src/dev-cd-apply.mjs +++ b/scripts/src/dev-cd-apply.mjs @@ -39,7 +39,11 @@ const forbiddenControlPlanePattern = /docker-desktop|desktop-control-plane|127\. const requiredDevSecretRefs = Object.freeze([ Object.freeze({ secretName: "hwlab-cloud-api-dev-db", secretKey: "database-url" }), Object.freeze({ secretName: "hwlab-cloud-api-dev-db-admin", secretKey: "admin-url" }), - Object.freeze({ secretName: "hwlab-code-agent-provider", secretKey: "openai-api-key" }) + Object.freeze({ secretName: "hwlab-code-agent-provider", secretKey: "openai-api-key" }), + Object.freeze({ secretName: "hwlab-code-agent-codex-auth", secretKey: "auth.json" }) +]); +const requiredDevConfigMapRefs = Object.freeze([ + Object.freeze({ configMapName: "hwlab-code-agent-codex-config", configMapKey: "config.toml" }) ]); const lockAnnotationPrefix = "hwlab.pikastech.local"; const lockAnnotationFields = { @@ -1288,6 +1292,13 @@ function parseSecretDescribeKeys(stdout) { return [...keys].sort(); } +function parseKeyLines(stdout) { + return String(stdout ?? "") + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean); +} + function commandObservation(args, result) { return { command: shellCommand("kubectl", args), @@ -1483,6 +1494,97 @@ function secretRefBlocker(ref, observation) { }); } +async function observeRequiredConfigMapRef(ctx, kubectlContext, args, ref) { + const existsArgs = ["-n", args.targetNamespace, "get", "configmap", ref.configMapName, "-o", "name"]; + const keyArgs = [ + "-n", + args.targetNamespace, + "get", + "configmap", + ref.configMapName, + "-o", + "go-template={{range $k,$v := .data}}{{printf \"%s\\n\" $k}}{{end}}" + ]; + const exists = await preflightKubectl(ctx, kubectlContext, existsArgs, { timeoutMs: 10000 }); + if (exists.code !== 0) { + return { + configMapName: ref.configMapName, + configMapKey: ref.configMapKey, + status: "missing-configmap", + exists: false, + keyPresent: false, + keysObserved: [], + existenceCommand: shellCommand("kubectl", existsArgs), + keyCommand: null, + error: commandSummary(exists), + configMapValueRead: false, + configMapValuePrinted: false + }; + } + const keys = await preflightKubectl(ctx, kubectlContext, keyArgs, { timeoutMs: 10000 }); + if (keys.code !== 0) { + return { + configMapName: ref.configMapName, + configMapKey: ref.configMapKey, + status: "key-observation-blocked", + exists: true, + keyPresent: false, + keysObserved: [], + existenceCommand: shellCommand("kubectl", existsArgs), + keyCommand: shellCommand("kubectl", keyArgs), + error: commandSummary(keys), + configMapValueRead: false, + configMapValuePrinted: false + }; + } + const keysObserved = parseKeyLines(keys.stdout); + const keyPresent = keysObserved.includes(ref.configMapKey); + return { + configMapName: ref.configMapName, + configMapKey: ref.configMapKey, + status: keyPresent ? "present" : "missing-key", + exists: true, + keyPresent, + keysObserved, + existenceCommand: shellCommand("kubectl", existsArgs), + keyCommand: shellCommand("kubectl", keyArgs), + configMapValueRead: false, + configMapValuePrinted: false + }; +} + +function configMapRefBlocker(ref, observation) { + const scope = `configmap:${ref.configMapName}/${ref.configMapKey}`; + if (observation.status === "missing-configmap") { + return blocker({ + type: "runtime_blocker", + scope, + reason: `Required DEV ConfigMap ${ref.configMapName} was not observed in ${defaultNamespace}.`, + impact: "The Code Agent Codex stdio provider pod would fail to mount config.toml after mutation.", + safeNextAction: `Create or restore ConfigMap ${ref.configMapName}/${ref.configMapKey} in ${defaultNamespace}, then retry DEV CD.`, + retryable: true + }); + } + if (observation.status === "key-observation-blocked") { + return blocker({ + type: "runtime_blocker", + scope, + reason: `Required DEV ConfigMap ${ref.configMapName} exists but key names could not be verified.`, + impact: "DEV CD cannot prove the Code Agent Codex config mount before applying workloads.", + safeNextAction: `Restore key-name visibility for ${ref.configMapName}, then retry.`, + retryable: true + }); + } + return blocker({ + type: "runtime_blocker", + scope, + reason: `Required DEV ConfigMap key ${ref.configMapName}/${ref.configMapKey} was not observed.`, + impact: "The Code Agent Codex stdio provider pod would reference a missing config.toml key after mutation.", + safeNextAction: `Add key ${ref.configMapKey} to ConfigMap ${ref.configMapName}, then retry DEV CD.`, + retryable: true + }); +} + async function runDevCdPreflight({ ctx, args, kubectlContext }) { const blockers = []; const preflight = { @@ -1502,11 +1604,14 @@ async function runDevCdPreflight({ ctx, args, kubectlContext }) { }, controlPlane: null, secretRefs: [], + configMaps: [], safety: { writeSideEffectsAttempted: false, prodTouched: false, secretValuesRead: false, secretValuesPrinted: false, + configMapValuesRead: false, + configMapValuesPrinted: false, secretKeyNamesOnly: true }, blockers @@ -1560,14 +1665,20 @@ async function runDevCdPreflight({ ctx, args, kubectlContext }) { } if (blockers.length === 0) { - preflight.secretRefs = await Promise.all( - requiredDevSecretRefs.map((ref) => observeRequiredSecretRef(ctx, kubectlContext, args, ref)) - ); + [preflight.secretRefs, preflight.configMaps] = await Promise.all([ + Promise.all(requiredDevSecretRefs.map((ref) => observeRequiredSecretRef(ctx, kubectlContext, args, ref))), + Promise.all(requiredDevConfigMapRefs.map((ref) => observeRequiredConfigMapRef(ctx, kubectlContext, args, ref))) + ]); for (const [index, observation] of preflight.secretRefs.entries()) { if (observation.status !== "present") { blockers.push(secretRefBlocker(requiredDevSecretRefs[index], observation)); } } + for (const [index, observation] of preflight.configMaps.entries()) { + if (observation.status !== "present") { + blockers.push(configMapRefBlocker(requiredDevConfigMapRefs[index], observation)); + } + } } preflight.status = blockers.length === 0 ? "pass" : "blocked"; diff --git a/scripts/src/dev-cd-apply.test.mjs b/scripts/src/dev-cd-apply.test.mjs index 1f926f0a..6dd39e0e 100644 --- a/scripts/src/dev-cd-apply.test.mjs +++ b/scripts/src/dev-cd-apply.test.mjs @@ -236,7 +236,11 @@ function makeRunCommand({ secretRefs = { "hwlab-cloud-api-dev-db": ["database-url"], "hwlab-cloud-api-dev-db-admin": ["admin-url"], - "hwlab-code-agent-provider": ["openai-api-key"] + "hwlab-code-agent-provider": ["openai-api-key"], + "hwlab-code-agent-codex-auth": ["auth.json"] + }, + configMaps = { + "hwlab-code-agent-codex-config": ["config.toml"] } } = {}) { let lease = heldLock ? leaseFromLock(heldLock) : null; @@ -314,6 +318,17 @@ function makeRunCommand({ stderr: "" }; } + if (command.includes("kubectl") && args.includes("get") && args.includes("configmap")) { + const name = args[args.indexOf("configmap") + 1]; + const keys = configMaps[name]; + if (!keys) { + return { code: 1, stdout: "", stderr: `Error from server (NotFound): configmaps "${name}" not found` }; + } + if (args.some((arg) => String(arg).startsWith("go-template="))) { + return { code: 0, stdout: `${keys.join("\n")}\n`, stderr: "" }; + } + return { code: 0, stdout: `configmap/${name}\n`, stderr: "" }; + } if (command.includes("kubectl") && args.includes("get") && args.includes("lease")) { if (!lease) return { code: 1, stdout: "", stderr: "Error from server (NotFound): leases.coordination.k8s.io \"hwlab-dev-cd-lock\" not found" }; return { code: 0, stdout: `${JSON.stringify(lease)}\n`, stderr: "" }; @@ -1169,11 +1184,18 @@ test("apply preflight passes with D601 native k3s and required SecretRef keys be [ "hwlab-cloud-api-dev-db/database-url:present", "hwlab-cloud-api-dev-db-admin/admin-url:present", - "hwlab-code-agent-provider/openai-api-key:present" + "hwlab-code-agent-provider/openai-api-key:present", + "hwlab-code-agent-codex-auth/auth.json:present" ] ); + assert.deepEqual( + preflight.configMaps.map((ref) => `${ref.configMapName}/${ref.configMapKey}:${ref.status}`), + ["hwlab-code-agent-codex-config/config.toml:present"] + ); assert.equal(preflight.safety.secretValuesRead, false); assert.equal(preflight.safety.secretValuesPrinted, false); + assert.equal(preflight.safety.configMapValuesRead, false); + assert.equal(preflight.safety.configMapValuesPrinted, false); const firstWriteIndex = commandLog.findIndex((entry) => entry.command.includes("kubectl") && ["create", "replace", "apply", "patch", "delete", "rollout", "wait", "logs"].some((verb) => entry.args.includes(verb)) @@ -1206,7 +1228,8 @@ test("apply preflight blocks missing SecretRef before Lease or side-effect comma commandLog, secretRefs: { "hwlab-cloud-api-dev-db": ["database-url"], - "hwlab-cloud-api-dev-db-admin": ["admin-url"] + "hwlab-cloud-api-dev-db-admin": ["admin-url"], + "hwlab-code-agent-codex-auth": ["auth.json"] } }), httpGetJson: makeHttpGetJson(), @@ -1227,6 +1250,42 @@ test("apply preflight blocks missing SecretRef before Lease or side-effect comma assertNoWriteSideEffects(commandLog); }); +test("apply preflight blocks missing Codex ConfigMap before Lease or workload apply", async () => { + const repoRoot = await makeRepo(); + const commandLog = []; + let output = ""; + const code = await runDevCdApply([ + "--apply", + "--confirm-dev", + "--confirmed-non-production", + "--owner-task-id", + "task-missing-codex-config", + "--kubeconfig", + "/etc/rancher/k3s/k3s.yaml", + "--report", stateReport(repoRoot, "dev-cd-apply.json") + ], { + repoRoot, + env: {}, + runCommand: makeRunCommand({ + commandLog, + configMaps: {} + }), + httpGetJson: makeHttpGetJson(), + now: () => new Date(iso(100000)), + stdout: { write: (chunk) => { output += chunk; } } + }); + + const summary = JSON.parse(output); + const writtenReport = JSON.parse(await readFile(stateReport(repoRoot, "dev-cd-apply.json"), "utf8")); + assert.equal(code, 2); + assert.equal(summary.status, "blocked"); + assert.equal(summary.preflight.status, "blocked"); + assert.equal(summary.blockers.some((blocker) => blocker.scope === "configmap:hwlab-code-agent-codex-config/config.toml"), true); + assert.equal(writtenReport.devCdApply.preflight.safety.writeSideEffectsAttempted, false); + assert.equal(writtenReport.devCdApply.preflight.safety.configMapValuesPrinted, false); + assertNoWriteSideEffects(commandLog); +}); + test("apply preflight blocks non-D601 kubeconfig path before kubectl mutation", async () => { const repoRoot = await makeRepo(); const commandLog = [];