fix: persist code agent workspace
This commit is contained in:
@@ -2,6 +2,28 @@
|
|||||||
"apiVersion": "v1",
|
"apiVersion": "v1",
|
||||||
"kind": "List",
|
"kind": "List",
|
||||||
"items": [
|
"items": [
|
||||||
|
{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "PersistentVolumeClaim",
|
||||||
|
"metadata": {
|
||||||
|
"labels": {
|
||||||
|
"app.kubernetes.io/name": "hwlab-code-agent-workspace",
|
||||||
|
"hwlab.pikastech.local/service-id": "hwlab-code-agent-workspace"
|
||||||
|
},
|
||||||
|
"name": "hwlab-code-agent-workspace",
|
||||||
|
"namespace": "hwlab-dev"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"accessModes": [
|
||||||
|
"ReadWriteOnce"
|
||||||
|
],
|
||||||
|
"resources": {
|
||||||
|
"requests": {
|
||||||
|
"storage": "8Gi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"apiVersion": "apps/v1",
|
"apiVersion": "apps/v1",
|
||||||
"kind": "Deployment",
|
"kind": "Deployment",
|
||||||
@@ -15,6 +37,9 @@
|
|||||||
},
|
},
|
||||||
"spec": {
|
"spec": {
|
||||||
"replicas": 1,
|
"replicas": 1,
|
||||||
|
"strategy": {
|
||||||
|
"type": "Recreate"
|
||||||
|
},
|
||||||
"selector": {
|
"selector": {
|
||||||
"matchLabels": {
|
"matchLabels": {
|
||||||
"app.kubernetes.io/name": "hwlab-cloud-api"
|
"app.kubernetes.io/name": "hwlab-cloud-api"
|
||||||
@@ -28,6 +53,30 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"spec": {
|
"spec": {
|
||||||
|
"initContainers": [
|
||||||
|
{
|
||||||
|
"command": [
|
||||||
|
"sh",
|
||||||
|
"-ceu",
|
||||||
|
"target=/workspace/hwlab\nmarker=\"$target/.hwlab-workspace-initialized\"\nmkdir -p \"$target\"\nif [ ! -e \"$marker\" ]; then\n if [ -z \"$(find \"$target\" -mindepth 1 -maxdepth 1 ! -name lost+found -print -quit)\" ]; then\n cp -a /app/. \"$target\"/\n mode=copied-from-image\n else\n mode=adopted-existing-workspace\n fi\n printf '%s\\n' \"$mode\" > \"$marker\"\n printf '%s\\n' \"${HWLAB_WORKSPACE_SOURCE_COMMIT:-unknown}\" > \"$target/.hwlab-workspace-base-revision\"\nfi\nprintf '%s\\n' \"${HWLAB_WORKSPACE_SOURCE_COMMIT:-unknown}\" > \"$target/.hwlab-workspace-current-image-revision\"\n"
|
||||||
|
],
|
||||||
|
"env": [
|
||||||
|
{
|
||||||
|
"name": "HWLAB_WORKSPACE_SOURCE_COMMIT",
|
||||||
|
"value": "af46386"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:af46386",
|
||||||
|
"imagePullPolicy": "IfNotPresent",
|
||||||
|
"name": "hwlab-code-agent-workspace-init",
|
||||||
|
"volumeMounts": [
|
||||||
|
{
|
||||||
|
"mountPath": "/workspace",
|
||||||
|
"name": "hwlab-code-agent-workspace"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"containers": [
|
"containers": [
|
||||||
{
|
{
|
||||||
"name": "hwlab-cloud-api",
|
"name": "hwlab-cloud-api",
|
||||||
@@ -303,7 +352,9 @@
|
|||||||
"volumes": [
|
"volumes": [
|
||||||
{
|
{
|
||||||
"name": "hwlab-code-agent-workspace",
|
"name": "hwlab-code-agent-workspace",
|
||||||
"emptyDir": {}
|
"persistentVolumeClaim": {
|
||||||
|
"claimName": "hwlab-code-agent-workspace"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "hwlab-code-agent-codex-home",
|
"name": "hwlab-code-agent-codex-home",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const DEV_CODE_AGENT_PROVIDER_CONTRACT = Object.freeze({
|
|||||||
model: "gpt-5.5",
|
model: "gpt-5.5",
|
||||||
requiredEnv: Object.freeze([
|
requiredEnv: Object.freeze([
|
||||||
"OPENAI_API_KEY",
|
"OPENAI_API_KEY",
|
||||||
|
"HWLAB_CODE_AGENT_WORKSPACE",
|
||||||
|
"HWLAB_CODE_AGENT_CODEX_WORKSPACE",
|
||||||
"HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE",
|
"HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE",
|
||||||
"HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
"HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
||||||
"HWLAB_CODE_AGENT_DEEPSEEK_MODEL",
|
"HWLAB_CODE_AGENT_DEEPSEEK_MODEL",
|
||||||
@@ -63,6 +65,19 @@ export const DEV_CODE_AGENT_PROVIDER_CONTRACT = Object.freeze({
|
|||||||
authKey: "auth.json",
|
authKey: "auth.json",
|
||||||
mountContract: "writable emptyDir CODEX_HOME with read-only config.toml/auth.json file mounts"
|
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([
|
noProxyRequired: Object.freeze([
|
||||||
"hyueapi.com",
|
"hyueapi.com",
|
||||||
".hyueapi.com",
|
".hyueapi.com",
|
||||||
@@ -102,6 +117,8 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
|
|||||||
[contract.forwarder.upstreamEnv]: contract.forwarder.upstreamBaseUrl,
|
[contract.forwarder.upstreamEnv]: contract.forwarder.upstreamBaseUrl,
|
||||||
[contract.forwarder.portEnv]: String(contract.forwarder.port),
|
[contract.forwarder.portEnv]: String(contract.forwarder.port),
|
||||||
[contract.egress.env]: contract.egress.defaultBaseUrl,
|
[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,
|
CODEX_HOME: contract.codexHome.path,
|
||||||
NO_PROXY: contract.noProxyRequired.join(","),
|
NO_PROXY: contract.noProxyRequired.join(","),
|
||||||
no_proxy: contract.noProxyRequired.join(","),
|
no_proxy: contract.noProxyRequired.join(","),
|
||||||
@@ -146,6 +163,26 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
|
|||||||
if (workloadInspection.codexHome.authSecretPresent !== true) {
|
if (workloadInspection.codexHome.authSecretPresent !== true) {
|
||||||
missingCodexHomeContract.push(`${contract.codexHome.secretName}/${contract.codexHome.authKey}`);
|
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([
|
const missingNoProxyContract = [...new Set([
|
||||||
...noProxyMissing(deployEnv?.NO_PROXY, contract.noProxyRequired),
|
...noProxyMissing(deployEnv?.NO_PROXY, contract.noProxyRequired),
|
||||||
...noProxyMissing(deployEnv?.no_proxy, contract.noProxyRequired),
|
...noProxyMissing(deployEnv?.no_proxy, contract.noProxyRequired),
|
||||||
@@ -160,6 +197,7 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
|
|||||||
missingSecretRefs.length === 0 &&
|
missingSecretRefs.length === 0 &&
|
||||||
missingEgressContract.length === 0 &&
|
missingEgressContract.length === 0 &&
|
||||||
missingCodexHomeContract.length === 0 &&
|
missingCodexHomeContract.length === 0 &&
|
||||||
|
missingWorkspaceContract.length === 0 &&
|
||||||
missingNoProxyContract.length === 0 &&
|
missingNoProxyContract.length === 0 &&
|
||||||
missingForwarderContract.length === 0;
|
missingForwarderContract.length === 0;
|
||||||
|
|
||||||
@@ -213,9 +251,11 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
|
|||||||
missingSecretRefs,
|
missingSecretRefs,
|
||||||
missingEgressContract,
|
missingEgressContract,
|
||||||
missingCodexHomeContract,
|
missingCodexHomeContract,
|
||||||
|
missingWorkspaceContract,
|
||||||
missingNoProxyContract,
|
missingNoProxyContract,
|
||||||
missingForwarderContract,
|
missingForwarderContract,
|
||||||
codexHome: workloadInspection.codexHome,
|
codexHome: workloadInspection.codexHome,
|
||||||
|
workspace: workloadInspection.workspace,
|
||||||
forwarder: workloadInspection.forwarder,
|
forwarder: workloadInspection.forwarder,
|
||||||
noProxy: workloadInspection.noProxy,
|
noProxy: workloadInspection.noProxy,
|
||||||
secretMaterialRead: false,
|
secretMaterialRead: false,
|
||||||
@@ -240,6 +280,8 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
|
|||||||
[contract.forwarder.upstreamEnv]: contract.forwarder.upstreamBaseUrl,
|
[contract.forwarder.upstreamEnv]: contract.forwarder.upstreamBaseUrl,
|
||||||
[contract.forwarder.portEnv]: String(contract.forwarder.port),
|
[contract.forwarder.portEnv]: String(contract.forwarder.port),
|
||||||
[contract.egress.env]: contract.egress.defaultBaseUrl,
|
[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,
|
CODEX_HOME: contract.codexHome.path,
|
||||||
NO_PROXY: contract.noProxyRequired.join(","),
|
NO_PROXY: contract.noProxyRequired.join(","),
|
||||||
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 matchesDevProxy = baseUrl === contract.egress.defaultBaseUrl;
|
||||||
const upstreamMatchesDirectHyueapi = upstreamBaseUrl === contract.egress.upstreamBaseUrl;
|
const upstreamMatchesDirectHyueapi = upstreamBaseUrl === contract.egress.upstreamBaseUrl;
|
||||||
const codexHomePresent = envEntryValue(getEnvEntry(workloadEnv, "CODEX_HOME")) === contract.codexHome.path;
|
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([
|
const noProxyMissingEntries = [...new Set([
|
||||||
...noProxyMissing(envEntryValue(getEnvEntry(workloadEnv, "NO_PROXY")), contract.noProxyRequired),
|
...noProxyMissing(envEntryValue(getEnvEntry(workloadEnv, "NO_PROXY")), contract.noProxyRequired),
|
||||||
...noProxyMissing(envEntryValue(getEnvEntry(workloadEnv, "no_proxy")), contract.noProxyRequired)
|
...noProxyMissing(envEntryValue(getEnvEntry(workloadEnv, "no_proxy")), contract.noProxyRequired)
|
||||||
])];
|
])];
|
||||||
const codexHome = inspectCodexHomeWorkloadMounts(workloadEnv);
|
const codexHome = inspectCodexHomeWorkloadMounts(workloadEnv);
|
||||||
|
const workspace = inspectCodeAgentWorkspaceWorkloadMounts(workloadEnv);
|
||||||
const forwarder = inspectCodexApiForwarderWorkload(workloadEnv);
|
const forwarder = inspectCodexApiForwarderWorkload(workloadEnv);
|
||||||
const ready =
|
const ready =
|
||||||
missingEnv.length === 0 &&
|
missingEnv.length === 0 &&
|
||||||
@@ -292,6 +338,8 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
|
|||||||
codexHome.present === true &&
|
codexHome.present === true &&
|
||||||
codexHome.configMapPresent === true &&
|
codexHome.configMapPresent === true &&
|
||||||
codexHome.authSecretPresent === true &&
|
codexHome.authSecretPresent === true &&
|
||||||
|
workspacePathPresent &&
|
||||||
|
workspace.present === true &&
|
||||||
noProxyMissingEntries.length === 0 &&
|
noProxyMissingEntries.length === 0 &&
|
||||||
forwarder.present === true;
|
forwarder.present === true;
|
||||||
|
|
||||||
@@ -327,6 +375,10 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
|
|||||||
...codexHome,
|
...codexHome,
|
||||||
present: codexHomePresent && codexHome.present === true
|
present: codexHomePresent && codexHome.present === true
|
||||||
},
|
},
|
||||||
|
workspace: {
|
||||||
|
...workspace,
|
||||||
|
present: workspacePathPresent && workspace.present === true
|
||||||
|
},
|
||||||
forwarder,
|
forwarder,
|
||||||
noProxy: {
|
noProxy: {
|
||||||
required: [...contract.noProxyRequired],
|
required: [...contract.noProxyRequired],
|
||||||
@@ -357,6 +409,7 @@ export function buildCodeAgentProviderManifestPlaceholder() {
|
|||||||
command: [...contract.forwarder.command]
|
command: [...contract.forwarder.command]
|
||||||
},
|
},
|
||||||
codexHome: { ...contract.codexHome },
|
codexHome: { ...contract.codexHome },
|
||||||
|
workspace: { ...contract.workspace },
|
||||||
noProxyRequired: [...contract.noProxyRequired],
|
noProxyRequired: [...contract.noProxyRequired],
|
||||||
fixtureEvidence: {
|
fixtureEvidence: {
|
||||||
providerConnected: false,
|
providerConnected: false,
|
||||||
@@ -384,7 +437,9 @@ export function inspectCodeAgentProviderEnv(env = {}) {
|
|||||||
const noProxyReady = noProxyMissing(env.NO_PROXY, contract.noProxyRequired).length === 0 &&
|
const noProxyReady = noProxyMissing(env.NO_PROXY, contract.noProxyRequired).length === 0 &&
|
||||||
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 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 {
|
return {
|
||||||
contractVersion: contract.contractVersion,
|
contractVersion: contract.contractVersion,
|
||||||
@@ -443,6 +498,15 @@ export function inspectCodeAgentProviderEnv(env = {}) {
|
|||||||
secretMaterialRead: false,
|
secretMaterialRead: false,
|
||||||
valuesRedacted: true
|
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: {
|
noProxy: {
|
||||||
required: [...contract.noProxyRequired],
|
required: [...contract.noProxyRequired],
|
||||||
missing: [...new Set([
|
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) {
|
function noProxyMissing(value, required) {
|
||||||
const entries = new Set(String(value ?? "")
|
const entries = new Set(String(value ?? "")
|
||||||
.split(/[,;\s]+/u)
|
.split(/[,;\s]+/u)
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ async function makeFixture({
|
|||||||
workloadCodeAgentProviderEnv = true,
|
workloadCodeAgentProviderEnv = true,
|
||||||
workloadCodexHomeVolumeMounts = true,
|
workloadCodexHomeVolumeMounts = true,
|
||||||
workloadCodexHomeVolumes = true,
|
workloadCodexHomeVolumes = true,
|
||||||
|
workloadWorkspaceVolumeMount = true,
|
||||||
|
workloadWorkspaceVolume = true,
|
||||||
|
workloadWorkspaceInitContainer = true,
|
||||||
|
workloadWorkspaceRecreateStrategy = true,
|
||||||
|
workloadWorkspacePvc = true,
|
||||||
workloadDbEnv = true,
|
workloadDbEnv = true,
|
||||||
workloadDbSslMode = "disable"
|
workloadDbSslMode = "disable"
|
||||||
} = {}) {
|
} = {}) {
|
||||||
@@ -65,6 +70,8 @@ async function makeFixture({
|
|||||||
HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL: "https://hyueapi.com",
|
HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL: "https://hyueapi.com",
|
||||||
HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT: "49280",
|
HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT: "49280",
|
||||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: providerBaseUrl,
|
HWLAB_CODE_AGENT_OPENAI_BASE_URL: providerBaseUrl,
|
||||||
|
HWLAB_CODE_AGENT_WORKSPACE: "/workspace/hwlab",
|
||||||
|
HWLAB_CODE_AGENT_CODEX_WORKSPACE: "/workspace/hwlab",
|
||||||
CODEX_HOME: "/codex-home",
|
CODEX_HOME: "/codex-home",
|
||||||
NO_PROXY: providerNoProxy,
|
NO_PROXY: providerNoProxy,
|
||||||
no_proxy: providerNoProxy,
|
no_proxy: providerNoProxy,
|
||||||
@@ -84,6 +91,8 @@ async function makeFixture({
|
|||||||
{ name: "HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL", value: "https://hyueapi.com" },
|
{ name: "HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL", value: "https://hyueapi.com" },
|
||||||
{ name: "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT", value: "49280" },
|
{ name: "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT", value: "49280" },
|
||||||
{ name: "HWLAB_CODE_AGENT_OPENAI_BASE_URL", value: providerBaseUrl },
|
{ name: "HWLAB_CODE_AGENT_OPENAI_BASE_URL", value: providerBaseUrl },
|
||||||
|
{ name: "HWLAB_CODE_AGENT_WORKSPACE", value: "/workspace/hwlab" },
|
||||||
|
{ name: "HWLAB_CODE_AGENT_CODEX_WORKSPACE", value: "/workspace/hwlab" },
|
||||||
{ name: "CODEX_HOME", value: "/codex-home" },
|
{ name: "CODEX_HOME", value: "/codex-home" },
|
||||||
{ name: "NO_PROXY", value: providerNoProxy },
|
{ name: "NO_PROXY", value: providerNoProxy },
|
||||||
{ name: "no_proxy", value: providerNoProxy },
|
{ name: "no_proxy", value: providerNoProxy },
|
||||||
@@ -175,6 +184,29 @@ async function makeFixture({
|
|||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
kind: "List",
|
kind: "List",
|
||||||
items: [
|
items: [
|
||||||
|
...(serviceId === "hwlab-cloud-api" && workloadCodeAgentProviderEnv && workloadWorkspacePvc
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
apiVersion: "v1",
|
||||||
|
kind: "PersistentVolumeClaim",
|
||||||
|
metadata: {
|
||||||
|
name: "hwlab-code-agent-workspace",
|
||||||
|
namespace: "hwlab-dev",
|
||||||
|
labels: {
|
||||||
|
"hwlab.pikastech.local/service-id": "hwlab-code-agent-workspace"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
accessModes: ["ReadWriteOnce"],
|
||||||
|
resources: {
|
||||||
|
requests: {
|
||||||
|
storage: "8Gi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
apiVersion: "apps/v1",
|
apiVersion: "apps/v1",
|
||||||
kind: "Deployment",
|
kind: "Deployment",
|
||||||
@@ -186,8 +218,33 @@ async function makeFixture({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
spec: {
|
spec: {
|
||||||
|
...(serviceId === "hwlab-cloud-api" && workloadCodeAgentProviderEnv && workloadWorkspaceRecreateStrategy
|
||||||
|
? {
|
||||||
|
strategy: {
|
||||||
|
type: "Recreate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
template: {
|
template: {
|
||||||
spec: {
|
spec: {
|
||||||
|
...(serviceId === "hwlab-cloud-api" && workloadCodeAgentProviderEnv && workloadWorkspaceInitContainer
|
||||||
|
? {
|
||||||
|
initContainers: [
|
||||||
|
{
|
||||||
|
name: "hwlab-code-agent-workspace-init",
|
||||||
|
image: workloadImage,
|
||||||
|
command: [
|
||||||
|
"sh",
|
||||||
|
"-ceu",
|
||||||
|
"target=/workspace/hwlab\nmarker=\"$target/.hwlab-workspace-initialized\"\nmkdir -p \"$target\"\nif [ ! -e \"$marker\" ]; then\n cp -a /app/. \"$target\"/\n printf '%s\\n' copied-from-image > \"$marker\"\nfi\n"
|
||||||
|
],
|
||||||
|
volumeMounts: [
|
||||||
|
{ name: "hwlab-code-agent-workspace", mountPath: "/workspace" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
containers: [
|
containers: [
|
||||||
{
|
{
|
||||||
name: serviceId,
|
name: serviceId,
|
||||||
@@ -202,12 +259,19 @@ async function makeFixture({
|
|||||||
...workloadProviderEnv
|
...workloadProviderEnv
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
...(serviceId === "hwlab-cloud-api" && workloadCodeAgentProviderEnv && workloadCodexHomeVolumeMounts
|
...(serviceId === "hwlab-cloud-api" && workloadCodeAgentProviderEnv && (workloadWorkspaceVolumeMount || workloadCodexHomeVolumeMounts)
|
||||||
? {
|
? {
|
||||||
volumeMounts: [
|
volumeMounts: [
|
||||||
{ name: "hwlab-code-agent-codex-home", mountPath: "/codex-home" },
|
...(workloadWorkspaceVolumeMount
|
||||||
{ name: "hwlab-code-agent-codex-config", mountPath: "/codex-home/config.toml", subPath: "config.toml", readOnly: true },
|
? [{ name: "hwlab-code-agent-workspace", mountPath: "/workspace" }]
|
||||||
{ name: "hwlab-code-agent-codex-auth", mountPath: "/codex-home/auth.json", subPath: "auth.json", readOnly: true }
|
: []),
|
||||||
|
...(workloadCodexHomeVolumeMounts
|
||||||
|
? [
|
||||||
|
{ name: "hwlab-code-agent-codex-home", mountPath: "/codex-home" },
|
||||||
|
{ name: "hwlab-code-agent-codex-config", mountPath: "/codex-home/config.toml", subPath: "config.toml", readOnly: true },
|
||||||
|
{ name: "hwlab-code-agent-codex-auth", mountPath: "/codex-home/auth.json", subPath: "auth.json", readOnly: true }
|
||||||
|
]
|
||||||
|
: [])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
@@ -230,12 +294,19 @@ async function makeFixture({
|
|||||||
]
|
]
|
||||||
: [])
|
: [])
|
||||||
],
|
],
|
||||||
...(serviceId === "hwlab-cloud-api" && workloadCodeAgentProviderEnv && workloadCodexHomeVolumes
|
...(serviceId === "hwlab-cloud-api" && workloadCodeAgentProviderEnv && (workloadWorkspaceVolume || workloadCodexHomeVolumes)
|
||||||
? {
|
? {
|
||||||
volumes: [
|
volumes: [
|
||||||
{ name: "hwlab-code-agent-codex-home", emptyDir: {} },
|
...(workloadWorkspaceVolume
|
||||||
{ name: "hwlab-code-agent-codex-config", configMap: { name: "hwlab-code-agent-codex-config", items: [{ key: "config.toml", path: "config.toml" }] } },
|
? [{ name: "hwlab-code-agent-workspace", persistentVolumeClaim: { claimName: "hwlab-code-agent-workspace" } }]
|
||||||
{ name: "hwlab-code-agent-codex-auth", secret: { secretName: "hwlab-code-agent-codex-auth", items: [{ key: "auth.json", path: "auth.json" }] } }
|
: []),
|
||||||
|
...(workloadCodexHomeVolumes
|
||||||
|
? [
|
||||||
|
{ name: "hwlab-code-agent-codex-home", emptyDir: {} },
|
||||||
|
{ name: "hwlab-code-agent-codex-config", configMap: { name: "hwlab-code-agent-codex-config", items: [{ key: "config.toml", path: "config.toml" }] } },
|
||||||
|
{ name: "hwlab-code-agent-codex-auth", secret: { secretName: "hwlab-code-agent-codex-auth", items: [{ key: "auth.json", path: "auth.json" }] } }
|
||||||
|
]
|
||||||
|
: [])
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
@@ -307,6 +378,11 @@ test("passes when cloud-api preserves provider env and DEV egress contract", asy
|
|||||||
assert.equal(plan.codeAgentProvider.codexHome.emptyDirPresent, true);
|
assert.equal(plan.codeAgentProvider.codexHome.emptyDirPresent, true);
|
||||||
assert.equal(plan.codeAgentProvider.codexHome.configMapPresent, true);
|
assert.equal(plan.codeAgentProvider.codexHome.configMapPresent, true);
|
||||||
assert.equal(plan.codeAgentProvider.codexHome.authSecretPresent, true);
|
assert.equal(plan.codeAgentProvider.codexHome.authSecretPresent, true);
|
||||||
|
assert.equal(plan.codeAgentProvider.workspace.present, true);
|
||||||
|
assert.equal(plan.codeAgentProvider.workspace.writableMountPresent, true);
|
||||||
|
assert.equal(plan.codeAgentProvider.workspace.persistentVolumeClaimPresent, true);
|
||||||
|
assert.equal(plan.codeAgentProvider.workspace.initContainerPresent, true);
|
||||||
|
assert.equal(plan.codeAgentProvider.workspace.recreateStrategy, true);
|
||||||
assert.deepEqual(plan.diagnostics, []);
|
assert.deepEqual(plan.diagnostics, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -324,11 +400,39 @@ test("blocks when cloud-api CODEX_HOME mount is missing its writable emptyDir vo
|
|||||||
assert.ok(diagnostic.actual.missingCodexHomeContract.includes("/codex-home must be backed by emptyDir"));
|
assert.ok(diagnostic.actual.missingCodexHomeContract.includes("/codex-home must be backed by emptyDir"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("blocks when cloud-api Code Agent workspace is not backed by PVC", async () => {
|
||||||
|
const repoRoot = await makeFixture({ workloadWorkspaceVolume: false });
|
||||||
|
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||||
|
const diagnostic = plan.diagnostics.find((entry) => entry.code === "code_agent_provider_contract_mismatch");
|
||||||
|
|
||||||
|
assert.equal(plan.status, "blocked");
|
||||||
|
assert.equal(plan.codeAgentProvider.ready, false);
|
||||||
|
assert.equal(plan.codeAgentProvider.workspace.present, false);
|
||||||
|
assert.equal(plan.codeAgentProvider.workspace.writableMountPresent, true);
|
||||||
|
assert.equal(plan.codeAgentProvider.workspace.persistentVolumeClaimPresent, false);
|
||||||
|
assert.ok(diagnostic);
|
||||||
|
assert.ok(diagnostic.actual.missingWorkspaceContract.includes("hwlab-code-agent-workspace must use PVC hwlab-code-agent-workspace"));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("blocks when cloud-api Code Agent workspace rollout strategy is not Recreate", async () => {
|
||||||
|
const repoRoot = await makeFixture({ workloadWorkspaceRecreateStrategy: false });
|
||||||
|
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||||
|
const diagnostic = plan.diagnostics.find((entry) => entry.code === "code_agent_provider_contract_mismatch");
|
||||||
|
|
||||||
|
assert.equal(plan.status, "blocked");
|
||||||
|
assert.equal(plan.codeAgentProvider.ready, false);
|
||||||
|
assert.equal(plan.codeAgentProvider.workspace.present, false);
|
||||||
|
assert.equal(plan.codeAgentProvider.workspace.recreateStrategy, false);
|
||||||
|
assert.ok(diagnostic);
|
||||||
|
assert.ok(diagnostic.actual.missingWorkspaceContract.includes("hwlab-cloud-api rollout strategy must be Recreate"));
|
||||||
|
});
|
||||||
|
|
||||||
test("blocks when cloud-api CODEX_HOME mount is read-only", async () => {
|
test("blocks when cloud-api CODEX_HOME mount is read-only", async () => {
|
||||||
const repoRoot = await makeFixture();
|
const repoRoot = await makeFixture();
|
||||||
const workloadsPath = path.join(repoRoot, "deploy/k8s/base/workloads.yaml");
|
const workloadsPath = path.join(repoRoot, "deploy/k8s/base/workloads.yaml");
|
||||||
const workloads = JSON.parse(await readFile(workloadsPath, "utf8"));
|
const workloads = JSON.parse(await readFile(workloadsPath, "utf8"));
|
||||||
workloads.items[0].spec.template.spec.containers[0].volumeMounts.find((entry) =>
|
const deployment = workloads.items.find((entry) => entry.kind === "Deployment");
|
||||||
|
deployment.spec.template.spec.containers[0].volumeMounts.find((entry) =>
|
||||||
entry.name === "hwlab-code-agent-codex-home"
|
entry.name === "hwlab-code-agent-codex-home"
|
||||||
).readOnly = true;
|
).readOnly = true;
|
||||||
await writeFile(workloadsPath, `${JSON.stringify(workloads, null, 2)}\n`);
|
await writeFile(workloadsPath, `${JSON.stringify(workloads, null, 2)}\n`);
|
||||||
@@ -391,7 +495,8 @@ test("blocks when cloud-api workload points Code Agent directly at public OpenAI
|
|||||||
const repoRoot = await makeFixture();
|
const repoRoot = await makeFixture();
|
||||||
const workloadsPath = path.join(repoRoot, "deploy/k8s/base/workloads.yaml");
|
const workloadsPath = path.join(repoRoot, "deploy/k8s/base/workloads.yaml");
|
||||||
const workloads = JSON.parse(await readFile(workloadsPath, "utf8"));
|
const workloads = JSON.parse(await readFile(workloadsPath, "utf8"));
|
||||||
const env = workloads.items[0].spec.template.spec.containers[0].env;
|
const deployment = workloads.items.find((entry) => entry.kind === "Deployment");
|
||||||
|
const env = deployment.spec.template.spec.containers[0].env;
|
||||||
env.find((entry) => entry.name === "HWLAB_CODE_AGENT_OPENAI_BASE_URL").value = "https://api.openai.com/v1/responses";
|
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`);
|
await writeFile(workloadsPath, `${JSON.stringify(workloads, null, 2)}\n`);
|
||||||
|
|
||||||
|
|||||||
@@ -293,6 +293,19 @@ function upsertEnvEntry(envList, entry) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncCodeAgentWorkspaceInitContainer(initContainers, { image, runtimeCommit, runtimeImageTag }) {
|
||||||
|
const initContainer = Array.isArray(initContainers)
|
||||||
|
? initContainers.find((entry) => entry?.name === "hwlab-code-agent-workspace-init")
|
||||||
|
: null;
|
||||||
|
if (!initContainer) return;
|
||||||
|
initContainer.image = image;
|
||||||
|
initContainer.imagePullPolicy = "IfNotPresent";
|
||||||
|
initContainer.env ??= [];
|
||||||
|
upsertEnv(initContainer.env, "HWLAB_WORKSPACE_SOURCE_COMMIT", runtimeCommit);
|
||||||
|
upsertEnv(initContainer.env, "HWLAB_IMAGE", image);
|
||||||
|
upsertEnv(initContainer.env, "HWLAB_IMAGE_TAG", runtimeImageTag);
|
||||||
|
}
|
||||||
|
|
||||||
function annotate(metadata, annotations) {
|
function annotate(metadata, annotations) {
|
||||||
metadata.annotations = { ...(metadata.annotations ?? {}), ...annotations };
|
metadata.annotations = { ...(metadata.annotations ?? {}), ...annotations };
|
||||||
}
|
}
|
||||||
@@ -502,6 +515,7 @@ function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix
|
|||||||
upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", runtimeEndpoint);
|
upsertEnv(container.env, "HWLAB_PUBLIC_ENDPOINT", runtimeEndpoint);
|
||||||
}
|
}
|
||||||
if (serviceId === "hwlab-cloud-api") {
|
if (serviceId === "hwlab-cloud-api") {
|
||||||
|
syncCodeAgentWorkspaceInitContainer(podTemplate.spec.initContainers, { image, runtimeCommit, runtimeImageTag });
|
||||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", "deepseek");
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", "deepseek");
|
||||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_MODEL", deepSeekProfileModel);
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_MODEL", deepSeekProfileModel);
|
||||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL", deepSeekBaseUrl(namespace));
|
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL", deepSeekBaseUrl(namespace));
|
||||||
|
|||||||
@@ -577,10 +577,15 @@ function validateCloudApiDbOptionalAliasArtifacts(ctx, services, workloads) {
|
|||||||
function validateCloudApiCodeAgentArtifacts(ctx, workloads) {
|
function validateCloudApiCodeAgentArtifacts(ctx, workloads) {
|
||||||
const contract = DEV_CODE_AGENT_PROVIDER_CONTRACT;
|
const contract = DEV_CODE_AGENT_PROVIDER_CONTRACT;
|
||||||
const secretRef = contract.secretRefs[0];
|
const secretRef = contract.secretRefs[0];
|
||||||
const container = mapByServiceId(listItems(workloads)).get("hwlab-cloud-api")?.spec?.template?.spec?.containers?.[0];
|
const workloadItems = mapByServiceId(listItems(workloads));
|
||||||
|
const workload = workloadItems.get("hwlab-cloud-api");
|
||||||
|
const templateSpec = workload?.spec?.template?.spec ?? {};
|
||||||
|
const container = templateSpec.containers?.[0];
|
||||||
const env = new Map((container?.env ?? []).map((entry) => [entry.name, entry]));
|
const env = new Map((container?.env ?? []).map((entry) => [entry.name, entry]));
|
||||||
const podContainers = mapByServiceId(listItems(workloads)).get("hwlab-cloud-api")?.spec?.template?.spec?.containers ?? [];
|
const podContainers = templateSpec.containers ?? [];
|
||||||
const volumes = mapByServiceId(listItems(workloads)).get("hwlab-cloud-api")?.spec?.template?.spec?.volumes ?? [];
|
const initContainers = templateSpec.initContainers ?? [];
|
||||||
|
const volumes = templateSpec.volumes ?? [];
|
||||||
|
const workspacePvc = workloadItems.get(contract.workspace.claimName);
|
||||||
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_PROVIDER")?.value, contract.codeAgentProvider, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_PROVIDER", "cloud API workload Code Agent provider");
|
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_PROVIDER")?.value, contract.codeAgentProvider, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_PROVIDER", "cloud API workload Code Agent provider");
|
||||||
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_MODEL")?.value, contract.model, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_MODEL", "cloud API workload Code Agent model");
|
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_MODEL")?.value, contract.model, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_MODEL", "cloud API workload Code Agent model");
|
||||||
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE")?.value, contract.profiles.defaultProfile, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", "cloud API workload default provider profile");
|
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE")?.value, contract.profiles.defaultProfile, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", "cloud API workload default provider profile");
|
||||||
@@ -616,6 +621,7 @@ function validateCloudApiCodeAgentArtifacts(ctx, workloads) {
|
|||||||
expectNoProxyIncludes(ctx, env.get("no_proxy")?.value, contract.noProxyRequired, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.no_proxy");
|
expectNoProxyIncludes(ctx, env.get("no_proxy")?.value, contract.noProxyRequired, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.no_proxy");
|
||||||
validateCodexApiForwarderArtifacts(ctx, podContainers, contract);
|
validateCodexApiForwarderArtifacts(ctx, podContainers, contract);
|
||||||
validateCodexHomeMountArtifacts(ctx, container?.volumeMounts ?? [], volumes, contract);
|
validateCodexHomeMountArtifacts(ctx, container?.volumeMounts ?? [], volumes, contract);
|
||||||
|
validateCodeAgentWorkspaceArtifacts(ctx, container?.volumeMounts ?? [], volumes, initContainers, workload?.spec?.strategy ?? null, workspacePvc, contract);
|
||||||
expect(
|
expect(
|
||||||
ctx,
|
ctx,
|
||||||
env.get("HWLAB_CODE_AGENT_OPENAI_BASE_URL")?.value !== contract.egress.forbiddenDirectBaseUrl,
|
env.get("HWLAB_CODE_AGENT_OPENAI_BASE_URL")?.value !== contract.egress.forbiddenDirectBaseUrl,
|
||||||
@@ -714,6 +720,75 @@ function validateCodexHomeMountArtifacts(ctx, volumeMounts, volumes, contract) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateCodeAgentWorkspaceArtifacts(ctx, volumeMounts, volumes, initContainers, strategy, pvc, contract) {
|
||||||
|
const workspace = contract.workspace;
|
||||||
|
expect(
|
||||||
|
ctx,
|
||||||
|
volumeMounts.some((entry) => entry.name === workspace.volumeName && entry.mountPath === workspace.mountPath && entry.readOnly !== true),
|
||||||
|
"code_agent_workspace_mount_missing",
|
||||||
|
"deploy/k8s/base/workloads.yaml.hwlab-cloud-api.volumeMounts.hwlab-code-agent-workspace",
|
||||||
|
"cloud API workload Code Agent workspace writable mount"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
ctx,
|
||||||
|
volumes.some((entry) => entry.name === workspace.volumeName && entry.persistentVolumeClaim?.claimName === workspace.claimName),
|
||||||
|
"code_agent_workspace_pvc_volume_missing",
|
||||||
|
"deploy/k8s/base/workloads.yaml.hwlab-cloud-api.volumes.hwlab-code-agent-workspace",
|
||||||
|
"cloud API workload Code Agent workspace PVC volume"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
ctx,
|
||||||
|
pvc?.kind === "PersistentVolumeClaim" && pvc?.metadata?.name === workspace.claimName,
|
||||||
|
"code_agent_workspace_pvc_missing",
|
||||||
|
"deploy/k8s/base/workloads.yaml.PersistentVolumeClaim.hwlab-code-agent-workspace",
|
||||||
|
"Code Agent workspace PVC resource"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
ctx,
|
||||||
|
(pvc?.spec?.accessModes ?? []).includes(workspace.accessMode),
|
||||||
|
"code_agent_workspace_pvc_access_mode_mismatch",
|
||||||
|
"deploy/k8s/base/workloads.yaml.PersistentVolumeClaim.hwlab-code-agent-workspace.spec.accessModes",
|
||||||
|
"Code Agent workspace PVC uses ReadWriteOnce"
|
||||||
|
);
|
||||||
|
expectEqual(
|
||||||
|
ctx,
|
||||||
|
pvc?.spec?.resources?.requests?.storage,
|
||||||
|
workspace.storage,
|
||||||
|
"deploy/k8s/base/workloads.yaml.PersistentVolumeClaim.hwlab-code-agent-workspace.spec.resources.requests.storage",
|
||||||
|
"Code Agent workspace PVC requested storage"
|
||||||
|
);
|
||||||
|
const initContainer = initContainers.find((entry) => entry.name === workspace.initContainerName);
|
||||||
|
expect(
|
||||||
|
ctx,
|
||||||
|
Boolean(initContainer),
|
||||||
|
"code_agent_workspace_init_container_missing",
|
||||||
|
"deploy/k8s/base/workloads.yaml.hwlab-cloud-api.initContainers.hwlab-code-agent-workspace-init",
|
||||||
|
"cloud API workload initializes persistent Code Agent workspace"
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
ctx,
|
||||||
|
(initContainer?.volumeMounts ?? []).some((entry) => entry.name === workspace.volumeName && entry.mountPath === workspace.mountPath),
|
||||||
|
"code_agent_workspace_init_mount_missing",
|
||||||
|
"deploy/k8s/base/workloads.yaml.hwlab-cloud-api.initContainers.hwlab-code-agent-workspace-init.volumeMounts",
|
||||||
|
"Code Agent workspace init container mounts the persistent workspace"
|
||||||
|
);
|
||||||
|
const initCommand = [...(initContainer?.command ?? []), ...(initContainer?.args ?? [])].join("\n");
|
||||||
|
expect(
|
||||||
|
ctx,
|
||||||
|
initCommand.includes(workspace.sourcePath) && initCommand.includes(workspace.path) && initCommand.includes(workspace.initMarker),
|
||||||
|
"code_agent_workspace_init_command_mismatch",
|
||||||
|
"deploy/k8s/base/workloads.yaml.hwlab-cloud-api.initContainers.hwlab-code-agent-workspace-init.command",
|
||||||
|
"Code Agent workspace init container uses copy-once /app bootstrap"
|
||||||
|
);
|
||||||
|
expectEqual(
|
||||||
|
ctx,
|
||||||
|
strategy?.type,
|
||||||
|
workspace.rolloutStrategy,
|
||||||
|
"deploy/k8s/base/workloads.yaml.hwlab-cloud-api.strategy.type",
|
||||||
|
"cloud API uses Recreate rollout for single-writer workspace PVC"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function validateCodeAgentProxyTimeoutArtifacts(ctx, workloads) {
|
function validateCodeAgentProxyTimeoutArtifacts(ctx, workloads) {
|
||||||
const workloadItems = mapByServiceId(listItems(workloads));
|
const workloadItems = mapByServiceId(listItems(workloads));
|
||||||
const cloudWebEnv = new Map((workloadItems.get("hwlab-cloud-web")?.spec?.template?.spec?.containers?.[0]?.env ?? []).map((entry) => [entry.name, entry.value]));
|
const cloudWebEnv = new Map((workloadItems.get("hwlab-cloud-web")?.spec?.template?.spec?.containers?.[0]?.env ?? []).map((entry) => [entry.name, entry.value]));
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ function inspectCodeAgentProviderDesiredState(ctx, deployService, workloadRecord
|
|||||||
missingSecretRefs: inspection.missingSecretRefs,
|
missingSecretRefs: inspection.missingSecretRefs,
|
||||||
missingEgressContract: inspection.missingEgressContract,
|
missingEgressContract: inspection.missingEgressContract,
|
||||||
missingCodexHomeContract: inspection.missingCodexHomeContract,
|
missingCodexHomeContract: inspection.missingCodexHomeContract,
|
||||||
|
missingWorkspaceContract: inspection.missingWorkspaceContract,
|
||||||
missingNoProxyContract: inspection.missingNoProxyContract,
|
missingNoProxyContract: inspection.missingNoProxyContract,
|
||||||
missingForwarderContract: inspection.missingForwarderContract
|
missingForwarderContract: inspection.missingForwarderContract
|
||||||
},
|
},
|
||||||
@@ -444,7 +445,9 @@ function indexWorkloads(workloads) {
|
|||||||
...envListToObject(container?.env),
|
...envListToObject(container?.env),
|
||||||
__volumeMounts: Array.isArray(container?.volumeMounts) ? container.volumeMounts : [],
|
__volumeMounts: Array.isArray(container?.volumeMounts) ? container.volumeMounts : [],
|
||||||
__volumes: Array.isArray(item?.spec?.template?.spec?.volumes) ? item.spec.template.spec.volumes : [],
|
__volumes: Array.isArray(item?.spec?.template?.spec?.volumes) ? item.spec.template.spec.volumes : [],
|
||||||
__podContainers: Array.isArray(item?.spec?.template?.spec?.containers) ? item.spec.template.spec.containers : []
|
__podContainers: Array.isArray(item?.spec?.template?.spec?.containers) ? item.spec.template.spec.containers : [],
|
||||||
|
__initContainers: Array.isArray(item?.spec?.template?.spec?.initContainers) ? item.spec.template.spec.initContainers : [],
|
||||||
|
__deploymentStrategy: item?.spec?.strategy ?? null
|
||||||
},
|
},
|
||||||
path: `${workloadsPath}.${item?.kind ?? "Workload"}.${item?.metadata?.name ?? serviceId}.containers[${containerIndex}]`
|
path: `${workloadsPath}.${item?.kind ?? "Workload"}.${item?.metadata?.name ?? serviceId}.containers[${containerIndex}]`
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -179,6 +179,9 @@ const cloudApiWorkloadEnv = getWorkloadEnv(k8sWorkloads, "hwlab-cloud-api");
|
|||||||
cloudApiWorkloadEnv.__volumeMounts = cloudApiContainer?.volumeMounts ?? [];
|
cloudApiWorkloadEnv.__volumeMounts = cloudApiContainer?.volumeMounts ?? [];
|
||||||
cloudApiWorkloadEnv.__volumes = cloudApiWorkload?.spec?.template?.spec?.volumes ?? [];
|
cloudApiWorkloadEnv.__volumes = cloudApiWorkload?.spec?.template?.spec?.volumes ?? [];
|
||||||
cloudApiWorkloadEnv.__podContainers = cloudApiWorkload?.spec?.template?.spec?.containers ?? [];
|
cloudApiWorkloadEnv.__podContainers = cloudApiWorkload?.spec?.template?.spec?.containers ?? [];
|
||||||
|
cloudApiWorkloadEnv.__initContainers = cloudApiWorkload?.spec?.template?.spec?.initContainers ?? [];
|
||||||
|
cloudApiWorkloadEnv.__deploymentStrategy = cloudApiWorkload?.spec?.strategy ?? null;
|
||||||
|
cloudApiWorkloadEnv.__workspacePvc = getWorkload(k8sWorkloads, DEV_CODE_AGENT_PROVIDER_CONTRACT.workspace.claimName);
|
||||||
const patchPanelWorkloadEnv = getWorkloadEnv(k8sWorkloads, "hwlab-patch-panel");
|
const patchPanelWorkloadEnv = getWorkloadEnv(k8sWorkloads, "hwlab-patch-panel");
|
||||||
assert.equal(deployManifest.health.path, "/health/live", "deploy health source path");
|
assert.equal(deployManifest.health.path, "/health/live", "deploy health source path");
|
||||||
assert.equal(deployManifest.publicEndpoints.frontend.url, "http://74.48.78.17:16666", "deploy frontend endpoint");
|
assert.equal(deployManifest.publicEndpoints.frontend.url, "http://74.48.78.17:16666", "deploy frontend endpoint");
|
||||||
@@ -324,6 +327,7 @@ function assertCodeAgentProviderWorkloadContract(env) {
|
|||||||
assertNoProxyIncludes(env.NO_PROXY?.value, contract.noProxyRequired, "cloud-api workload NO_PROXY contract");
|
assertNoProxyIncludes(env.NO_PROXY?.value, contract.noProxyRequired, "cloud-api workload NO_PROXY contract");
|
||||||
assertNoProxyIncludes(env.no_proxy?.value, contract.noProxyRequired, "cloud-api workload no_proxy contract");
|
assertNoProxyIncludes(env.no_proxy?.value, contract.noProxyRequired, "cloud-api workload no_proxy contract");
|
||||||
assertCodexHomeMountContract(env, contract);
|
assertCodexHomeMountContract(env, contract);
|
||||||
|
assertCodeAgentWorkspaceContract(env, contract);
|
||||||
assertCodexApiForwarderContract(env, contract);
|
assertCodexApiForwarderContract(env, contract);
|
||||||
assert.notEqual(
|
assert.notEqual(
|
||||||
env.HWLAB_CODE_AGENT_OPENAI_BASE_URL?.value,
|
env.HWLAB_CODE_AGENT_OPENAI_BASE_URL?.value,
|
||||||
@@ -403,6 +407,38 @@ function assertCodexHomeMountContract(env, contract) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertCodeAgentWorkspaceContract(env, contract) {
|
||||||
|
const workspace = contract.workspace;
|
||||||
|
const volumeMounts = env.__volumeMounts ?? [];
|
||||||
|
const volumes = env.__volumes ?? [];
|
||||||
|
const initContainers = env.__initContainers ?? [];
|
||||||
|
const pvc = env.__workspacePvc;
|
||||||
|
assert.ok(
|
||||||
|
volumeMounts.some((entry) => entry.name === workspace.volumeName && entry.mountPath === workspace.mountPath && entry.readOnly !== true),
|
||||||
|
"cloud-api workload Code Agent workspace writable mount"
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
volumes.some((entry) => entry.name === workspace.volumeName && entry.persistentVolumeClaim?.claimName === workspace.claimName),
|
||||||
|
"cloud-api workload Code Agent workspace PVC volume"
|
||||||
|
);
|
||||||
|
assert.equal(pvc?.kind, "PersistentVolumeClaim", "Code Agent workspace PVC resource");
|
||||||
|
assert.equal(pvc?.metadata?.name, workspace.claimName, "Code Agent workspace PVC name");
|
||||||
|
assert.ok((pvc?.spec?.accessModes ?? []).includes(workspace.accessMode), "Code Agent workspace PVC uses ReadWriteOnce");
|
||||||
|
assert.equal(pvc?.spec?.resources?.requests?.storage, workspace.storage, "Code Agent workspace PVC requested storage");
|
||||||
|
const initContainer = initContainers.find((entry) => entry.name === workspace.initContainerName);
|
||||||
|
assert.ok(initContainer, "cloud-api workload initializes persistent Code Agent workspace");
|
||||||
|
assert.ok(
|
||||||
|
(initContainer?.volumeMounts ?? []).some((entry) => entry.name === workspace.volumeName && entry.mountPath === workspace.mountPath),
|
||||||
|
"Code Agent workspace init container mounts the persistent workspace"
|
||||||
|
);
|
||||||
|
const initCommand = [...(initContainer?.command ?? []), ...(initContainer?.args ?? [])].join("\n");
|
||||||
|
assert.ok(
|
||||||
|
initCommand.includes(workspace.sourcePath) && initCommand.includes(workspace.path) && initCommand.includes(workspace.initMarker),
|
||||||
|
"Code Agent workspace init container uses copy-once /app bootstrap"
|
||||||
|
);
|
||||||
|
assert.equal(env.__deploymentStrategy?.type, workspace.rolloutStrategy, "cloud-api uses Recreate rollout for single-writer workspace PVC");
|
||||||
|
}
|
||||||
|
|
||||||
function assertM3PatchPanelDeployContract(manifestService, workloadEnv) {
|
function assertM3PatchPanelDeployContract(manifestService, workloadEnv) {
|
||||||
const route = {
|
const route = {
|
||||||
fromResourceId: "res_boxsimu_1",
|
fromResourceId: "res_boxsimu_1",
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ description: Build and validate the HWLAB agent-mgr and agent-worker runtime ske
|
|||||||
|
|
||||||
Skill(cli-spec)
|
Skill(cli-spec)
|
||||||
|
|
||||||
|
Scope: this skill is only for HWLAB-internal Code Agent runtime work inside
|
||||||
|
the HWLAB repository and runtime, such as `/workspace/hwlab` agent sessions,
|
||||||
|
`/root/hwlab` source changes, and HWLAB-managed worker or skill services. It
|
||||||
|
does not apply to external developer workspaces such as `/root/unidesk`; those
|
||||||
|
workspaces must follow their own repository instructions and must not treat
|
||||||
|
this skill as a general UniDesk or third-party developer runtime contract.
|
||||||
|
|
||||||
Use this skill when working on HWLAB agent runtime skeletons.
|
Use this skill when working on HWLAB agent runtime skeletons.
|
||||||
|
|
||||||
- Keep the control plane long-lived and worker execution session-scoped.
|
- Keep the control plane long-lived and worker execution session-scoped.
|
||||||
|
|||||||
Reference in New Issue
Block a user