fix: persist code agent workspace

This commit is contained in:
Codex
2026-05-27 13:25:52 +08:00
parent 3c7f13191b
commit d02b7cd8a6
8 changed files with 421 additions and 16 deletions
+115 -1
View File
@@ -10,6 +10,8 @@ export const DEV_CODE_AGENT_PROVIDER_CONTRACT = Object.freeze({
model: "gpt-5.5",
requiredEnv: Object.freeze([
"OPENAI_API_KEY",
"HWLAB_CODE_AGENT_WORKSPACE",
"HWLAB_CODE_AGENT_CODEX_WORKSPACE",
"HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE",
"HWLAB_CODE_AGENT_OPENAI_BASE_URL",
"HWLAB_CODE_AGENT_DEEPSEEK_MODEL",
@@ -63,6 +65,19 @@ export const DEV_CODE_AGENT_PROVIDER_CONTRACT = Object.freeze({
authKey: "auth.json",
mountContract: "writable emptyDir CODEX_HOME with read-only config.toml/auth.json file mounts"
}),
workspace: Object.freeze({
path: "/workspace/hwlab",
mountPath: "/workspace",
sourcePath: "/app",
volumeName: "hwlab-code-agent-workspace",
claimName: "hwlab-code-agent-workspace",
accessMode: "ReadWriteOnce",
storage: "8Gi",
initContainerName: "hwlab-code-agent-workspace-init",
initMarker: ".hwlab-workspace-initialized",
rolloutStrategy: "Recreate",
mountContract: "persistent RWO PVC mounted at /workspace with copy-once /app bootstrap into /workspace/hwlab"
}),
noProxyRequired: Object.freeze([
"hyueapi.com",
".hyueapi.com",
@@ -102,6 +117,8 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
[contract.forwarder.upstreamEnv]: contract.forwarder.upstreamBaseUrl,
[contract.forwarder.portEnv]: String(contract.forwarder.port),
[contract.egress.env]: contract.egress.defaultBaseUrl,
HWLAB_CODE_AGENT_WORKSPACE: contract.workspace.path,
HWLAB_CODE_AGENT_CODEX_WORKSPACE: contract.workspace.path,
CODEX_HOME: contract.codexHome.path,
NO_PROXY: contract.noProxyRequired.join(","),
no_proxy: contract.noProxyRequired.join(","),
@@ -146,6 +163,26 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
if (workloadInspection.codexHome.authSecretPresent !== true) {
missingCodexHomeContract.push(`${contract.codexHome.secretName}/${contract.codexHome.authKey}`);
}
const missingWorkspaceContract = [];
if (
deployEnv?.HWLAB_CODE_AGENT_WORKSPACE !== contract.workspace.path ||
deployEnv?.HWLAB_CODE_AGENT_CODEX_WORKSPACE !== contract.workspace.path ||
workloadInspection.workspace.present !== true
) {
missingWorkspaceContract.push(`Code Agent workspace must be ${contract.workspace.path}`);
}
if (workloadInspection.workspace.writableMountPresent !== true) {
missingWorkspaceContract.push(`${contract.workspace.mountPath} must be a writable volumeMount`);
}
if (workloadInspection.workspace.persistentVolumeClaimPresent !== true) {
missingWorkspaceContract.push(`${contract.workspace.volumeName} must use PVC ${contract.workspace.claimName}`);
}
if (workloadInspection.workspace.initContainerPresent !== true) {
missingWorkspaceContract.push(`${contract.workspace.initContainerName} must bootstrap ${contract.workspace.path} from ${contract.workspace.sourcePath}`);
}
if (workloadInspection.workspace.recreateStrategy !== true) {
missingWorkspaceContract.push(`hwlab-cloud-api rollout strategy must be ${contract.workspace.rolloutStrategy}`);
}
const missingNoProxyContract = [...new Set([
...noProxyMissing(deployEnv?.NO_PROXY, contract.noProxyRequired),
...noProxyMissing(deployEnv?.no_proxy, contract.noProxyRequired),
@@ -160,6 +197,7 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
missingSecretRefs.length === 0 &&
missingEgressContract.length === 0 &&
missingCodexHomeContract.length === 0 &&
missingWorkspaceContract.length === 0 &&
missingNoProxyContract.length === 0 &&
missingForwarderContract.length === 0;
@@ -213,9 +251,11 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
missingSecretRefs,
missingEgressContract,
missingCodexHomeContract,
missingWorkspaceContract,
missingNoProxyContract,
missingForwarderContract,
codexHome: workloadInspection.codexHome,
workspace: workloadInspection.workspace,
forwarder: workloadInspection.forwarder,
noProxy: workloadInspection.noProxy,
secretMaterialRead: false,
@@ -240,6 +280,8 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
[contract.forwarder.upstreamEnv]: contract.forwarder.upstreamBaseUrl,
[contract.forwarder.portEnv]: String(contract.forwarder.port),
[contract.egress.env]: contract.egress.defaultBaseUrl,
HWLAB_CODE_AGENT_WORKSPACE: contract.workspace.path,
HWLAB_CODE_AGENT_CODEX_WORKSPACE: contract.workspace.path,
CODEX_HOME: contract.codexHome.path,
NO_PROXY: contract.noProxyRequired.join(","),
no_proxy: contract.noProxyRequired.join(",")
@@ -276,11 +318,15 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
const matchesDevProxy = baseUrl === contract.egress.defaultBaseUrl;
const upstreamMatchesDirectHyueapi = upstreamBaseUrl === contract.egress.upstreamBaseUrl;
const codexHomePresent = envEntryValue(getEnvEntry(workloadEnv, "CODEX_HOME")) === contract.codexHome.path;
const workspacePathPresent =
envEntryValue(getEnvEntry(workloadEnv, "HWLAB_CODE_AGENT_WORKSPACE")) === contract.workspace.path &&
envEntryValue(getEnvEntry(workloadEnv, "HWLAB_CODE_AGENT_CODEX_WORKSPACE")) === contract.workspace.path;
const noProxyMissingEntries = [...new Set([
...noProxyMissing(envEntryValue(getEnvEntry(workloadEnv, "NO_PROXY")), contract.noProxyRequired),
...noProxyMissing(envEntryValue(getEnvEntry(workloadEnv, "no_proxy")), contract.noProxyRequired)
])];
const codexHome = inspectCodexHomeWorkloadMounts(workloadEnv);
const workspace = inspectCodeAgentWorkspaceWorkloadMounts(workloadEnv);
const forwarder = inspectCodexApiForwarderWorkload(workloadEnv);
const ready =
missingEnv.length === 0 &&
@@ -292,6 +338,8 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
codexHome.present === true &&
codexHome.configMapPresent === true &&
codexHome.authSecretPresent === true &&
workspacePathPresent &&
workspace.present === true &&
noProxyMissingEntries.length === 0 &&
forwarder.present === true;
@@ -327,6 +375,10 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
...codexHome,
present: codexHomePresent && codexHome.present === true
},
workspace: {
...workspace,
present: workspacePathPresent && workspace.present === true
},
forwarder,
noProxy: {
required: [...contract.noProxyRequired],
@@ -357,6 +409,7 @@ export function buildCodeAgentProviderManifestPlaceholder() {
command: [...contract.forwarder.command]
},
codexHome: { ...contract.codexHome },
workspace: { ...contract.workspace },
noProxyRequired: [...contract.noProxyRequired],
fixtureEvidence: {
providerConnected: false,
@@ -384,7 +437,9 @@ export function inspectCodeAgentProviderEnv(env = {}) {
const noProxyReady = noProxyMissing(env.NO_PROXY, contract.noProxyRequired).length === 0 &&
noProxyMissing(env.no_proxy, contract.noProxyRequired).length === 0;
const codexHomeReady = firstNonEmpty(env.CODEX_HOME) === contract.codexHome.path;
const ready = missingEnv.length === 0 && egressReady && codexHomeReady && noProxyReady;
const workspaceReady = firstNonEmpty(env.HWLAB_CODE_AGENT_WORKSPACE) === contract.workspace.path &&
firstNonEmpty(env.HWLAB_CODE_AGENT_CODEX_WORKSPACE) === contract.workspace.path;
const ready = missingEnv.length === 0 && egressReady && codexHomeReady && workspaceReady && noProxyReady;
return {
contractVersion: contract.contractVersion,
@@ -443,6 +498,15 @@ export function inspectCodeAgentProviderEnv(env = {}) {
secretMaterialRead: false,
valuesRedacted: true
},
workspace: {
path: contract.workspace.path,
mountPath: contract.workspace.mountPath,
claimName: contract.workspace.claimName,
initContainerName: contract.workspace.initContainerName,
rolloutStrategy: contract.workspace.rolloutStrategy,
present: workspaceReady,
persistentVolumeClaimPresent: "not_observed_by_runtime"
},
noProxy: {
required: [...contract.noProxyRequired],
missing: [...new Set([
@@ -562,6 +626,56 @@ function inspectCodexHomeWorkloadMounts(workloadEnv = {}) {
};
}
function inspectCodeAgentWorkspaceWorkloadMounts(workloadEnv = {}) {
const contract = DEV_CODE_AGENT_PROVIDER_CONTRACT;
const volumeMounts = Array.isArray(workloadEnv.__volumeMounts) ? workloadEnv.__volumeMounts : [];
const volumes = Array.isArray(workloadEnv.__volumes) ? workloadEnv.__volumes : [];
const initContainers = Array.isArray(workloadEnv.__initContainers) ? workloadEnv.__initContainers : [];
const deploymentStrategy = workloadEnv.__deploymentStrategy;
const workspaceMount = volumeMounts.find((entry) =>
entry?.name === contract.workspace.volumeName &&
entry?.mountPath === contract.workspace.mountPath
);
const writableMountPresent = Boolean(workspaceMount && workspaceMount.readOnly !== true);
const workspaceVolume = volumes.find((entry) =>
entry?.name === contract.workspace.volumeName &&
entry?.persistentVolumeClaim?.claimName === contract.workspace.claimName
);
const initContainer = initContainers.find((entry) => entry?.name === contract.workspace.initContainerName);
const initContainerMountPresent = (initContainer?.volumeMounts ?? []).some((entry) =>
entry?.name === contract.workspace.volumeName &&
entry?.mountPath === contract.workspace.mountPath
);
const initCommand = [...(initContainer?.command ?? []), ...(initContainer?.args ?? [])].join("\n");
const copyOnceCommandPresent = initCommand.includes(contract.workspace.sourcePath) &&
initCommand.includes(contract.workspace.path) &&
initCommand.includes(contract.workspace.initMarker);
const recreateStrategy = deploymentStrategy?.type === contract.workspace.rolloutStrategy;
const persistentVolumeClaimPresent = Boolean(workspaceVolume);
const initContainerPresent = Boolean(initContainer && initContainerMountPresent && copyOnceCommandPresent);
return {
path: contract.workspace.path,
mountPath: contract.workspace.mountPath,
volumeName: contract.workspace.volumeName,
claimName: contract.workspace.claimName,
accessMode: contract.workspace.accessMode,
storage: contract.workspace.storage,
initContainerName: contract.workspace.initContainerName,
initMarker: contract.workspace.initMarker,
rolloutStrategy: contract.workspace.rolloutStrategy,
present: Boolean(writableMountPresent && persistentVolumeClaimPresent && initContainerPresent && recreateStrategy),
writableMountPresent,
persistentVolumeClaimPresent,
initContainerPresent,
initContainerMountPresent,
copyOnceCommandPresent,
recreateStrategy,
mountContract: contract.workspace.mountContract,
secretMaterialRead: false,
valuesRedacted: true
};
}
function noProxyMissing(value, required) {
const entries = new Set(String(value ?? "")
.split(/[,;\s]+/u)