fix: restore codex-api via pod-local forwarder
This commit is contained in:
@@ -15,6 +15,10 @@
|
||||
"name": "g14-artifact-publish-static-check",
|
||||
"run": "node --check scripts/artifact-publish.mjs && node --check scripts/g14-artifact-publish.mjs"
|
||||
},
|
||||
{
|
||||
"name": "codex-api-forwarder-check",
|
||||
"run": "node --check cmd/hwlab-codex-api-responses-forwarder/main.mjs && node --check cmd/hwlab-codex-api-responses-forwarder/main.test.mjs && node --test cmd/hwlab-codex-api-responses-forwarder/main.test.mjs"
|
||||
},
|
||||
{
|
||||
"name": "parse-deploy-json",
|
||||
"run": "node -e \"JSON.parse(require('node:fs').readFileSync('deploy/deploy.json','utf8'))\""
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env node
|
||||
import { createServer, request as httpRequest } from "node:http";
|
||||
import { request as httpsRequest } from "node:https";
|
||||
|
||||
const serviceId = "hwlab-codex-api-responses-forwarder";
|
||||
const defaultPort = 49280;
|
||||
const port = parsePositiveInteger(process.env.HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT || process.env.PORT, defaultPort);
|
||||
const host = firstNonEmpty(process.env.HWLAB_CODE_AGENT_CODEX_API_FORWARDER_HOST, "127.0.0.1");
|
||||
const upstreamBaseUrl = new URL(firstNonEmpty(process.env.HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL, "https://hyueapi.com"));
|
||||
const upstreamNoProxyEntries = noProxyEntriesForHost(upstreamBaseUrl.hostname);
|
||||
const maxBodyBytes = parsePositiveInteger(process.env.HWLAB_CODE_AGENT_CODEX_API_FORWARDER_MAX_BODY_BYTES, 64 * 1024 * 1024);
|
||||
|
||||
const hopByHopHeaders = new Set([
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailer",
|
||||
"transfer-encoding",
|
||||
"upgrade"
|
||||
]);
|
||||
|
||||
ensureNoProxyEntries(upstreamNoProxyEntries);
|
||||
|
||||
const server = createServer(async (clientReq, clientRes) => {
|
||||
try {
|
||||
const url = new URL(clientReq.url || "/", "http://hwlab-codex-api-forwarder.local");
|
||||
if (url.pathname === "/health" || url.pathname === "/health/live" || url.pathname === "/health/liveliness") {
|
||||
sendJson(clientRes, 200, healthPayload("ok"));
|
||||
return;
|
||||
}
|
||||
if (url.pathname === "/health/readiness") {
|
||||
sendJson(clientRes, 200, healthPayload("ready"));
|
||||
return;
|
||||
}
|
||||
await proxyRequest(clientReq, clientRes, url);
|
||||
} catch (error) {
|
||||
sendJson(clientRes, 502, {
|
||||
error: {
|
||||
code: "codex_api_forwarder_failed",
|
||||
message: "Codex API Responses forwarder failed to proxy request",
|
||||
reason: error.message
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(port, host, () => {
|
||||
process.stdout.write(JSON.stringify({
|
||||
serviceId,
|
||||
status: "listening",
|
||||
host,
|
||||
port,
|
||||
upstreamHost: upstreamBaseUrl.hostname,
|
||||
noProxyRequired: upstreamNoProxyEntries
|
||||
}) + "\n");
|
||||
});
|
||||
|
||||
async function proxyRequest(clientReq, clientRes, url) {
|
||||
const startedAt = Date.now();
|
||||
const targetUrl = targetUrlForRequest(url);
|
||||
const headers = filterRequestHeaders(clientReq.headers, targetUrl);
|
||||
const requestImpl = targetUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
||||
|
||||
const upstreamReq = requestImpl({
|
||||
protocol: targetUrl.protocol,
|
||||
hostname: targetUrl.hostname,
|
||||
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
||||
method: clientReq.method,
|
||||
path: `${targetUrl.pathname}${targetUrl.search}`,
|
||||
headers
|
||||
}, (upstreamRes) => {
|
||||
const responseHeaders = filterResponseHeaders(upstreamRes.headers);
|
||||
clientRes.writeHead(upstreamRes.statusCode || 502, responseHeaders);
|
||||
let responseBytes = 0;
|
||||
upstreamRes.on("data", (chunk) => {
|
||||
responseBytes += chunk.length;
|
||||
});
|
||||
upstreamRes.on("end", () => {
|
||||
process.stderr.write(JSON.stringify({
|
||||
serviceId,
|
||||
method: clientReq.method,
|
||||
path: url.pathname,
|
||||
upstreamPath: targetUrl.pathname,
|
||||
status: upstreamRes.statusCode || null,
|
||||
responseBytes,
|
||||
elapsedMs: Date.now() - startedAt
|
||||
}) + "\n");
|
||||
});
|
||||
upstreamRes.pipe(clientRes);
|
||||
});
|
||||
|
||||
upstreamReq.on("error", (error) => {
|
||||
if (!clientRes.headersSent) {
|
||||
sendJson(clientRes, 502, {
|
||||
error: {
|
||||
code: "codex_api_forwarder_upstream_error",
|
||||
message: "Codex API forwarder upstream request failed",
|
||||
reason: error.message
|
||||
}
|
||||
});
|
||||
} else {
|
||||
clientRes.end();
|
||||
}
|
||||
});
|
||||
|
||||
await pipeRequestBody(clientReq, upstreamReq);
|
||||
}
|
||||
|
||||
async function pipeRequestBody(clientReq, upstreamReq) {
|
||||
let bytes = 0;
|
||||
for await (const chunk of clientReq) {
|
||||
bytes += chunk.length;
|
||||
if (bytes > maxBodyBytes) {
|
||||
upstreamReq.destroy(new Error(`request body exceeded ${maxBodyBytes} bytes`));
|
||||
return;
|
||||
}
|
||||
if (!upstreamReq.write(chunk)) {
|
||||
await new Promise((resolve) => upstreamReq.once("drain", resolve));
|
||||
}
|
||||
}
|
||||
upstreamReq.end();
|
||||
}
|
||||
|
||||
function targetUrlForRequest(url) {
|
||||
const target = new URL(upstreamBaseUrl.toString());
|
||||
target.pathname = upstreamPathFor(url.pathname);
|
||||
target.search = url.search;
|
||||
target.hash = "";
|
||||
return target;
|
||||
}
|
||||
|
||||
function upstreamPathFor(inboundPath) {
|
||||
const basePath = upstreamBaseUrl.pathname.replace(/\/+$/u, "");
|
||||
const requestPath = normalizeCodexResponsesPath(inboundPath || "/");
|
||||
if (!basePath) return requestPath;
|
||||
if (basePath === "/v1" && requestPath.startsWith("/v1/")) return requestPath;
|
||||
if (basePath === "/v1" && requestPath === "/v1") return requestPath;
|
||||
if (basePath.endsWith("/responses") && (requestPath === "/responses" || requestPath === "/v1/responses")) return basePath;
|
||||
if (basePath.endsWith("/models") && (requestPath === "/models" || requestPath === "/v1/models")) return basePath;
|
||||
if (requestPath.startsWith(`${basePath}/`) || requestPath === basePath) return requestPath;
|
||||
return `${basePath}${requestPath}`;
|
||||
}
|
||||
|
||||
function normalizeCodexResponsesPath(pathname) {
|
||||
if (upstreamBaseUrl.pathname.replace(/\/+$/u, "") === "/v1") return pathname;
|
||||
if (pathname === "/v1/responses") return "/responses";
|
||||
if (pathname === "/v1/models") return "/models";
|
||||
return pathname;
|
||||
}
|
||||
|
||||
function filterRequestHeaders(headers, targetUrl) {
|
||||
const filtered = {};
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
const lower = name.toLowerCase();
|
||||
if (hopByHopHeaders.has(lower) || lower === "host") continue;
|
||||
filtered[name] = value;
|
||||
}
|
||||
filtered.host = targetUrl.host;
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function filterResponseHeaders(headers) {
|
||||
const filtered = {};
|
||||
for (const [name, value] of Object.entries(headers)) {
|
||||
if (hopByHopHeaders.has(name.toLowerCase())) continue;
|
||||
filtered[name] = value;
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
function sendJson(res, status, payload) {
|
||||
const body = JSON.stringify(payload);
|
||||
res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(body) });
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function healthPayload(status) {
|
||||
return {
|
||||
serviceId,
|
||||
status,
|
||||
listen: { host, port },
|
||||
upstream: {
|
||||
origin: upstreamBaseUrl.origin,
|
||||
host: upstreamBaseUrl.hostname,
|
||||
directNoProxyRequired: true,
|
||||
noProxyRequired: upstreamNoProxyEntries
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ensureNoProxyEntries(entries) {
|
||||
for (const name of ["NO_PROXY", "no_proxy"]) {
|
||||
process.env[name] = mergeNoProxyEntries(process.env[name], entries);
|
||||
}
|
||||
}
|
||||
|
||||
function noProxyEntriesForHost(hostname) {
|
||||
const hostText = String(hostname || "").trim().toLowerCase();
|
||||
if (!hostText) return ["127.0.0.1", "localhost", "::1"];
|
||||
const entries = [hostText, "127.0.0.1", "localhost", "::1"];
|
||||
if (!/^\d+(?:\.\d+){3}$/u.test(hostText) && !hostText.includes(":")) entries.push(`.${hostText}`);
|
||||
return uniqueStrings(entries);
|
||||
}
|
||||
|
||||
function mergeNoProxyEntries(current, additions = []) {
|
||||
return uniqueStrings([...String(current ?? "").split(/[,;\s]+/u), ...additions]).join(",");
|
||||
}
|
||||
|
||||
function uniqueStrings(values) {
|
||||
const seen = new Set();
|
||||
const result = [];
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (!text) continue;
|
||||
const key = text.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push(text);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function firstNonEmpty(...values) {
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text) return text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function parsePositiveInteger(value, fallback) {
|
||||
const parsed = Number.parseInt(value || "", 10);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import { createServer } from "node:http";
|
||||
import test from "node:test";
|
||||
|
||||
test("Codex API Responses forwarder maps loopback Codex paths to configurable upstream and preserves stream body", async () => {
|
||||
const upstream = await startUpstream();
|
||||
const forwarder = await startForwarder(upstream.port, { upstreamPath: "/" });
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${forwarder.port}/v1/responses`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: "Bearer test-secret"
|
||||
},
|
||||
body: JSON.stringify({ model: "gpt-5.5", input: "OK" })
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
const payload = await response.json();
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(upstream.captured.url, "/responses");
|
||||
assert.equal(upstream.captured.authorization, "Bearer test-secret");
|
||||
assert.deepEqual(JSON.parse(upstream.captured.body), { model: "gpt-5.5", input: "OK" });
|
||||
|
||||
const health = await fetch(`http://127.0.0.1:${forwarder.port}/health/live`).then((item) => item.json());
|
||||
assert.equal(health.upstream.host, "127.0.0.1");
|
||||
assert.equal(health.upstream.directNoProxyRequired, true);
|
||||
} finally {
|
||||
await forwarder.stop();
|
||||
await upstream.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("Codex API Responses forwarder keeps /v1 prefix when upstream base is /v1", async () => {
|
||||
const upstream = await startUpstream();
|
||||
const forwarder = await startForwarder(upstream.port, { upstreamPath: "/v1" });
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${forwarder.port}/responses`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ input: "OK" })
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
assert.equal(upstream.captured.url, "/v1/responses");
|
||||
} finally {
|
||||
await forwarder.stop();
|
||||
await upstream.stop();
|
||||
}
|
||||
});
|
||||
|
||||
async function startUpstream() {
|
||||
let captured = null;
|
||||
const server = createServer(async (request, response) => {
|
||||
const chunks = [];
|
||||
for await (const chunk of request) chunks.push(chunk);
|
||||
captured = {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
authorization: request.headers.authorization || null,
|
||||
body: Buffer.concat(chunks).toString("utf8")
|
||||
};
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({ ok: true }));
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
return {
|
||||
port: server.address().port,
|
||||
get captured() {
|
||||
return captured;
|
||||
},
|
||||
stop: () => new Promise((resolve) => server.close(resolve))
|
||||
};
|
||||
}
|
||||
|
||||
async function startForwarder(upstreamPort, { upstreamPath }) {
|
||||
const port = await freePort();
|
||||
const child = spawn(process.execPath, ["cmd/hwlab-codex-api-responses-forwarder/main.mjs"], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT: String(port),
|
||||
HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL: `http://127.0.0.1:${upstreamPort}${upstreamPath}`
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"]
|
||||
});
|
||||
let stderr = "";
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error(`forwarder did not start: ${stderr}`)), 5000);
|
||||
child.stdout.on("data", (chunk) => {
|
||||
if (chunk.toString().includes("listening")) {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
child.on("exit", (code) => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`forwarder exited early code=${code}: ${stderr}`));
|
||||
});
|
||||
});
|
||||
return {
|
||||
port,
|
||||
stop: async () => {
|
||||
child.kill("SIGTERM");
|
||||
await new Promise((resolve) => child.once("exit", resolve));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function freePort() {
|
||||
const server = createServer();
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const port = server.address().port;
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
return port;
|
||||
}
|
||||
+9
-2
@@ -206,7 +206,7 @@
|
||||
"HWLAB_CODE_AGENT_PROVIDER": "codex-stdio",
|
||||
"HWLAB_CODE_AGENT_MODEL": "gpt-5.5",
|
||||
"HWLAB_CODE_AGENT_TIMEOUT_MS": "600000",
|
||||
"HWLAB_CODE_AGENT_OPENAI_BASE_URL": "http://172.26.26.227:17680/v1/responses",
|
||||
"HWLAB_CODE_AGENT_OPENAI_BASE_URL": "http://127.0.0.1:49280/responses",
|
||||
"HWLAB_CODE_AGENT_HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED": "1",
|
||||
"HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR": "repo-owned",
|
||||
@@ -218,7 +218,14 @@
|
||||
"NO_PROXY": "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com",
|
||||
"no_proxy": "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com",
|
||||
"OPENAI_API_KEY": "secretRef:hwlab-code-agent-provider/openai-api-key",
|
||||
"HWLAB_CODE_AGENT_SKILLS_DIRS": "/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills"
|
||||
"HWLAB_CODE_AGENT_SKILLS_DIRS": "/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills",
|
||||
"HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE": "deepseek",
|
||||
"HWLAB_CODE_AGENT_DEEPSEEK_MODEL": "deepseek-chat",
|
||||
"HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL": "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_MODEL": "gpt-5.5",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_BASE_URL": "http://127.0.0.1:49280/responses",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL": "https://hyueapi.com",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT": "49280"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -414,9 +414,37 @@
|
||||
"type": "string",
|
||||
"const": "gpt-5.5"
|
||||
},
|
||||
"HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE": {
|
||||
"type": "string",
|
||||
"const": "deepseek"
|
||||
},
|
||||
"HWLAB_CODE_AGENT_DEEPSEEK_MODEL": {
|
||||
"type": "string",
|
||||
"const": "deepseek-chat"
|
||||
},
|
||||
"HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL": {
|
||||
"type": "string",
|
||||
"const": "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses"
|
||||
},
|
||||
"HWLAB_CODE_AGENT_CODEX_API_MODEL": {
|
||||
"type": "string",
|
||||
"const": "gpt-5.5"
|
||||
},
|
||||
"HWLAB_CODE_AGENT_CODEX_API_BASE_URL": {
|
||||
"type": "string",
|
||||
"const": "http://127.0.0.1:49280/responses"
|
||||
},
|
||||
"HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL": {
|
||||
"type": "string",
|
||||
"const": "https://hyueapi.com"
|
||||
},
|
||||
"HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT": {
|
||||
"type": "string",
|
||||
"const": "49280"
|
||||
},
|
||||
"HWLAB_CODE_AGENT_OPENAI_BASE_URL": {
|
||||
"type": "string",
|
||||
"const": "http://172.26.26.227:17680/v1/responses"
|
||||
"const": "http://127.0.0.1:49280/responses"
|
||||
},
|
||||
"HWLAB_CODE_AGENT_HWLAB_API_BASE_URL": {
|
||||
"type": "string",
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
||||
"value": "http://172.26.26.227:17680/v1/responses"
|
||||
"value": "http://127.0.0.1:49280/responses"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_HWLAB_API_BASE_URL",
|
||||
@@ -178,6 +178,34 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE",
|
||||
"value": "deepseek"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_DEEPSEEK_MODEL",
|
||||
"value": "deepseek-chat"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL",
|
||||
"value": "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_CODEX_API_MODEL",
|
||||
"value": "gpt-5.5"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_CODEX_API_BASE_URL",
|
||||
"value": "http://127.0.0.1:49280/responses"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL",
|
||||
"value": "https://hyueapi.com"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT",
|
||||
"value": "49280"
|
||||
}
|
||||
],
|
||||
"readinessProbe": {
|
||||
@@ -214,6 +242,54 @@
|
||||
"readOnly": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "hwlab-codex-api-forwarder",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:6a84959",
|
||||
"command": [
|
||||
"node",
|
||||
"/app/cmd/hwlab-codex-api-responses-forwarder/main.mjs"
|
||||
],
|
||||
"ports": [
|
||||
{
|
||||
"name": "codex-api",
|
||||
"containerPort": 49280
|
||||
}
|
||||
],
|
||||
"env": [
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_HOST",
|
||||
"value": "127.0.0.1"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT",
|
||||
"value": "49280"
|
||||
},
|
||||
{
|
||||
"name": "HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL",
|
||||
"value": "https://hyueapi.com"
|
||||
},
|
||||
{
|
||||
"name": "NO_PROXY",
|
||||
"value": "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com"
|
||||
},
|
||||
{
|
||||
"name": "no_proxy",
|
||||
"value": "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com"
|
||||
}
|
||||
],
|
||||
"readinessProbe": {
|
||||
"httpGet": {
|
||||
"path": "/health/readiness",
|
||||
"port": "codex-api"
|
||||
}
|
||||
},
|
||||
"livenessProbe": {
|
||||
"httpGet": {
|
||||
"path": "/health/live",
|
||||
"port": "codex-api"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"volumes": [
|
||||
|
||||
@@ -10,7 +10,14 @@ export const DEV_CODE_AGENT_PROVIDER_CONTRACT = Object.freeze({
|
||||
model: "gpt-5.5",
|
||||
requiredEnv: Object.freeze([
|
||||
"OPENAI_API_KEY",
|
||||
"HWLAB_CODE_AGENT_OPENAI_BASE_URL"
|
||||
"HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE",
|
||||
"HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
||||
"HWLAB_CODE_AGENT_DEEPSEEK_MODEL",
|
||||
"HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_MODEL",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_BASE_URL",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL",
|
||||
"HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT"
|
||||
]),
|
||||
secretRefs: Object.freeze([
|
||||
Object.freeze({
|
||||
@@ -21,10 +28,32 @@ export const DEV_CODE_AGENT_PROVIDER_CONTRACT = Object.freeze({
|
||||
]),
|
||||
egress: Object.freeze({
|
||||
env: "HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
||||
target: "dev-egress-proxy",
|
||||
defaultBaseUrl: "http://172.26.26.227:17680/v1/responses",
|
||||
target: "pod-local-codex-api-forwarder",
|
||||
defaultBaseUrl: "http://127.0.0.1:49280/responses",
|
||||
upstreamEnv: "HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL",
|
||||
upstreamBaseUrl: "https://hyueapi.com",
|
||||
forbiddenDirectBaseUrl: "https://api.openai.com/v1/responses",
|
||||
reason: "DEV cloud-api pods must use the D601 egress/proxy path for OpenAI Responses."
|
||||
reason: "DEV cloud-api pods must send codex-api traffic to a same-Pod loopback forwarder; the forwarder direct-connects to hyueapi with NO_PROXY."
|
||||
}),
|
||||
profiles: Object.freeze({
|
||||
defaultProfile: "deepseek",
|
||||
deepSeekProfile: "deepseek",
|
||||
deepSeekModelEnv: "HWLAB_CODE_AGENT_DEEPSEEK_MODEL",
|
||||
deepSeekBaseUrlEnv: "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL",
|
||||
deepSeekBaseUrl: "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses",
|
||||
codexApiProfile: "codex-api",
|
||||
codexApiModelEnv: "HWLAB_CODE_AGENT_CODEX_API_MODEL",
|
||||
codexApiBaseUrlEnv: "HWLAB_CODE_AGENT_CODEX_API_BASE_URL"
|
||||
}),
|
||||
forwarder: Object.freeze({
|
||||
containerName: "hwlab-codex-api-forwarder",
|
||||
command: Object.freeze(["node", "/app/cmd/hwlab-codex-api-responses-forwarder/main.mjs"]),
|
||||
port: 49280,
|
||||
portName: "codex-api",
|
||||
listenHostEnv: "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_HOST",
|
||||
portEnv: "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT",
|
||||
upstreamEnv: "HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL",
|
||||
upstreamBaseUrl: "https://hyueapi.com"
|
||||
}),
|
||||
codexHome: Object.freeze({
|
||||
path: "/codex-home",
|
||||
@@ -65,6 +94,13 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
|
||||
const expectedValues = {
|
||||
HWLAB_CODE_AGENT_PROVIDER: contract.codeAgentProvider,
|
||||
HWLAB_CODE_AGENT_MODEL: contract.model,
|
||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: contract.profiles.defaultProfile,
|
||||
[contract.profiles.deepSeekModelEnv]: "deepseek-chat",
|
||||
[contract.profiles.deepSeekBaseUrlEnv]: contract.profiles.deepSeekBaseUrl,
|
||||
[contract.profiles.codexApiModelEnv]: contract.model,
|
||||
[contract.profiles.codexApiBaseUrlEnv]: contract.egress.defaultBaseUrl,
|
||||
[contract.forwarder.upstreamEnv]: contract.forwarder.upstreamBaseUrl,
|
||||
[contract.forwarder.portEnv]: String(contract.forwarder.port),
|
||||
[contract.egress.env]: contract.egress.defaultBaseUrl,
|
||||
CODEX_HOME: contract.codexHome.path,
|
||||
NO_PROXY: contract.noProxyRequired.join(","),
|
||||
@@ -115,6 +151,7 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
|
||||
...noProxyMissing(deployEnv?.no_proxy, contract.noProxyRequired),
|
||||
...workloadInspection.noProxy.missing
|
||||
])];
|
||||
const missingForwarderContract = workloadInspection.forwarder.present ? [] : [contract.forwarder.containerName];
|
||||
|
||||
const ready =
|
||||
missingDeployEnv.length === 0 &&
|
||||
@@ -123,7 +160,8 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
|
||||
missingSecretRefs.length === 0 &&
|
||||
missingEgressContract.length === 0 &&
|
||||
missingCodexHomeContract.length === 0 &&
|
||||
missingNoProxyContract.length === 0;
|
||||
missingNoProxyContract.length === 0 &&
|
||||
missingForwarderContract.length === 0;
|
||||
|
||||
return {
|
||||
contractVersion: contract.contractVersion,
|
||||
@@ -160,6 +198,12 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
|
||||
directPublicOpenAi:
|
||||
isForbiddenDirectOpenAiBaseUrl(deployEnv?.[contract.egress.env]) ||
|
||||
workloadInspection.egress.directPublicOpenAi === true,
|
||||
upstream: {
|
||||
env: contract.egress.upstreamEnv,
|
||||
deployMatchesDirectHyueapi: deployEnv?.[contract.egress.upstreamEnv] === contract.egress.upstreamBaseUrl,
|
||||
workloadMatchesDirectHyueapi: workloadInspection.egress.upstreamMatchesDirectHyueapi,
|
||||
valueRedacted: true
|
||||
},
|
||||
valueRedacted: true
|
||||
},
|
||||
missingDeployEnv,
|
||||
@@ -170,7 +214,9 @@ export function inspectCodeAgentProviderManifestRefs({ deployEnv = {}, workloadE
|
||||
missingEgressContract,
|
||||
missingCodexHomeContract,
|
||||
missingNoProxyContract,
|
||||
missingForwarderContract,
|
||||
codexHome: workloadInspection.codexHome,
|
||||
forwarder: workloadInspection.forwarder,
|
||||
noProxy: workloadInspection.noProxy,
|
||||
secretMaterialRead: false,
|
||||
valuesRedacted: true,
|
||||
@@ -186,6 +232,13 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
|
||||
const expectedValues = {
|
||||
HWLAB_CODE_AGENT_PROVIDER: contract.codeAgentProvider,
|
||||
HWLAB_CODE_AGENT_MODEL: contract.model,
|
||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: contract.profiles.defaultProfile,
|
||||
[contract.profiles.deepSeekModelEnv]: "deepseek-chat",
|
||||
[contract.profiles.deepSeekBaseUrlEnv]: contract.profiles.deepSeekBaseUrl,
|
||||
[contract.profiles.codexApiModelEnv]: contract.model,
|
||||
[contract.profiles.codexApiBaseUrlEnv]: contract.egress.defaultBaseUrl,
|
||||
[contract.forwarder.upstreamEnv]: contract.forwarder.upstreamBaseUrl,
|
||||
[contract.forwarder.portEnv]: String(contract.forwarder.port),
|
||||
[contract.egress.env]: contract.egress.defaultBaseUrl,
|
||||
CODEX_HOME: contract.codexHome.path,
|
||||
NO_PROXY: contract.noProxyRequired.join(","),
|
||||
@@ -218,24 +271,29 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
|
||||
secretKeyRef?.name === secretRef.secretName &&
|
||||
secretKeyRef?.key === secretRef.secretKey;
|
||||
const baseUrl = envEntryValue(getEnvEntry(workloadEnv, contract.egress.env));
|
||||
const upstreamBaseUrl = envEntryValue(getEnvEntry(workloadEnv, contract.egress.upstreamEnv));
|
||||
const directPublicOpenAi = isForbiddenDirectOpenAiBaseUrl(baseUrl);
|
||||
const matchesDevProxy = baseUrl === contract.egress.defaultBaseUrl;
|
||||
const upstreamMatchesDirectHyueapi = upstreamBaseUrl === contract.egress.upstreamBaseUrl;
|
||||
const codexHomePresent = envEntryValue(getEnvEntry(workloadEnv, "CODEX_HOME")) === contract.codexHome.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 forwarder = inspectCodexApiForwarderWorkload(workloadEnv);
|
||||
const ready =
|
||||
missingEnv.length === 0 &&
|
||||
mismatchedEnv.length === 0 &&
|
||||
secretPresent &&
|
||||
matchesDevProxy &&
|
||||
upstreamMatchesDirectHyueapi &&
|
||||
!directPublicOpenAi &&
|
||||
codexHome.present === true &&
|
||||
codexHome.configMapPresent === true &&
|
||||
codexHome.authSecretPresent === true &&
|
||||
noProxyMissingEntries.length === 0;
|
||||
noProxyMissingEntries.length === 0 &&
|
||||
forwarder.present === true;
|
||||
|
||||
return {
|
||||
contractVersion: contract.contractVersion,
|
||||
@@ -261,6 +319,7 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
|
||||
target: contract.egress.target,
|
||||
present: Boolean(baseUrl),
|
||||
matchesDevProxy,
|
||||
upstreamMatchesDirectHyueapi,
|
||||
directPublicOpenAi,
|
||||
valueRedacted: true
|
||||
},
|
||||
@@ -268,6 +327,7 @@ export function inspectCodeAgentProviderWorkloadEnv(workloadEnv = {}) {
|
||||
...codexHome,
|
||||
present: codexHomePresent && codexHome.present === true
|
||||
},
|
||||
forwarder,
|
||||
noProxy: {
|
||||
required: [...contract.noProxyRequired],
|
||||
missing: noProxyMissingEntries,
|
||||
@@ -291,6 +351,11 @@ export function buildCodeAgentProviderManifestPlaceholder() {
|
||||
requiredEnv: [...contract.requiredEnv],
|
||||
secretRefs: contract.secretRefs.map((item) => ({ ...item })),
|
||||
egress: { ...contract.egress },
|
||||
profiles: { ...contract.profiles },
|
||||
forwarder: {
|
||||
...contract.forwarder,
|
||||
command: [...contract.forwarder.command]
|
||||
},
|
||||
codexHome: { ...contract.codexHome },
|
||||
noProxyRequired: [...contract.noProxyRequired],
|
||||
fixtureEvidence: {
|
||||
@@ -305,10 +370,17 @@ export function inspectCodeAgentProviderEnv(env = {}) {
|
||||
const missingEnv = contract.requiredEnv.filter((name) => !hasEnvValue(env, name));
|
||||
const secretRef = contract.secretRefs[0];
|
||||
const baseUrl = firstNonEmpty(env.HWLAB_CODE_AGENT_OPENAI_BASE_URL);
|
||||
const codexApiBaseUrl = firstNonEmpty(env[contract.profiles.codexApiBaseUrlEnv]);
|
||||
const upstreamBaseUrl = firstNonEmpty(env[contract.forwarder.upstreamEnv]);
|
||||
const forwarderPort = firstNonEmpty(env[contract.forwarder.portEnv]);
|
||||
const directPublicOpenAi =
|
||||
baseUrl === contract.egress.forbiddenDirectBaseUrl ||
|
||||
/^https:\/\/api\.openai\.com\/v1\/responses\/?$/u.test(baseUrl);
|
||||
const egressReady = Boolean(baseUrl) && !directPublicOpenAi;
|
||||
const egressMatchesForwarder = baseUrl === contract.egress.defaultBaseUrl;
|
||||
const codexApiProfileReady = codexApiBaseUrl === contract.egress.defaultBaseUrl;
|
||||
const upstreamReady = upstreamBaseUrl === contract.forwarder.upstreamBaseUrl;
|
||||
const forwarderPortReady = forwarderPort === String(contract.forwarder.port);
|
||||
const egressReady = Boolean(baseUrl) && !directPublicOpenAi && egressMatchesForwarder && codexApiProfileReady && upstreamReady && forwarderPortReady;
|
||||
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;
|
||||
@@ -347,9 +419,22 @@ export function inspectCodeAgentProviderEnv(env = {}) {
|
||||
target: contract.egress.target,
|
||||
baseUrlConfigured: Boolean(baseUrl),
|
||||
directPublicOpenAi,
|
||||
matchesForwarder: egressMatchesForwarder,
|
||||
codexApiProfileReady,
|
||||
upstreamReady,
|
||||
forwarderPortReady,
|
||||
ready: egressReady,
|
||||
valueRedacted: true
|
||||
},
|
||||
forwarder: {
|
||||
containerName: contract.forwarder.containerName,
|
||||
expectedBaseUrl: contract.egress.defaultBaseUrl,
|
||||
upstreamEnv: contract.forwarder.upstreamEnv,
|
||||
expectedUpstreamHost: new URL(contract.forwarder.upstreamBaseUrl).hostname,
|
||||
managedByKubernetes: true,
|
||||
podLocalOnly: true,
|
||||
serviceRequired: false
|
||||
},
|
||||
codexHome: {
|
||||
path: contract.codexHome.path,
|
||||
present: codexHomeReady,
|
||||
@@ -374,16 +459,19 @@ export function inspectCodeAgentProviderEnv(env = {}) {
|
||||
valuesRedacted: true,
|
||||
directPublicOpenAiFromDevPod: directPublicOpenAi
|
||||
},
|
||||
blocker: ready ? null : codeAgentProviderBlocker({ missingEnv, directPublicOpenAi, baseUrl })
|
||||
blocker: ready ? null : codeAgentProviderBlocker({ missingEnv, directPublicOpenAi, baseUrl, egressMatchesForwarder, codexApiProfileReady, upstreamReady, forwarderPortReady })
|
||||
};
|
||||
}
|
||||
|
||||
function codeAgentProviderBlocker({ missingEnv, directPublicOpenAi, baseUrl }) {
|
||||
function codeAgentProviderBlocker({ missingEnv, directPublicOpenAi, baseUrl, egressMatchesForwarder, codexApiProfileReady, upstreamReady, forwarderPortReady }) {
|
||||
if (directPublicOpenAi) {
|
||||
return "DEV Code Agent OpenAI base URL points at public api.openai.com instead of the DEV egress/proxy path";
|
||||
return "DEV Code Agent OpenAI base URL points at public api.openai.com instead of the pod-local Codex API forwarder";
|
||||
}
|
||||
if (!baseUrl) {
|
||||
return "DEV Code Agent OpenAI base URL is missing; provider calls must use the DEV egress/proxy path";
|
||||
return "DEV Code Agent OpenAI base URL is missing; provider calls must use the pod-local Codex API forwarder";
|
||||
}
|
||||
if (!egressMatchesForwarder || !codexApiProfileReady || !upstreamReady || !forwarderPortReady) {
|
||||
return "DEV Code Agent codex-api profile must use the pod-local loopback forwarder with direct hyueapi upstream";
|
||||
}
|
||||
return `Code Agent provider runtime config is missing ${missingEnv.join(", ")}`;
|
||||
}
|
||||
@@ -482,6 +570,44 @@ function noProxyMissing(value, required) {
|
||||
return required.filter((item) => !entries.has(String(item).toLowerCase()));
|
||||
}
|
||||
|
||||
function inspectCodexApiForwarderWorkload(workloadEnv = {}) {
|
||||
const contract = DEV_CODE_AGENT_PROVIDER_CONTRACT;
|
||||
const containers = Array.isArray(workloadEnv.__podContainers) ? workloadEnv.__podContainers : [];
|
||||
const sidecar = containers.find((entry) => entry?.name === contract.forwarder.containerName);
|
||||
const env = new Map((sidecar?.env ?? []).map((entry) => [entry?.name, envEntryValue(entry)]));
|
||||
const commandMatches = arrayEqual(sidecar?.command, contract.forwarder.command);
|
||||
const portMatches = (sidecar?.ports ?? []).some((entry) =>
|
||||
entry?.name === contract.forwarder.portName && entry?.containerPort === contract.forwarder.port
|
||||
);
|
||||
const upstreamMatches = env.get(contract.forwarder.upstreamEnv) === contract.forwarder.upstreamBaseUrl;
|
||||
const listenHostMatches = env.get(contract.forwarder.listenHostEnv) === "127.0.0.1";
|
||||
const portEnvMatches = env.get(contract.forwarder.portEnv) === String(contract.forwarder.port);
|
||||
const noProxyReady = noProxyMissing(env.get("NO_PROXY"), contract.noProxyRequired).length === 0 &&
|
||||
noProxyMissing(env.get("no_proxy"), contract.noProxyRequired).length === 0;
|
||||
const present = Boolean(sidecar && commandMatches && portMatches && upstreamMatches && listenHostMatches && portEnvMatches && noProxyReady);
|
||||
return {
|
||||
containerName: contract.forwarder.containerName,
|
||||
present,
|
||||
sidecarPresent: Boolean(sidecar),
|
||||
commandMatches,
|
||||
portMatches,
|
||||
upstreamMatches,
|
||||
listenHostMatches,
|
||||
portEnvMatches,
|
||||
noProxyReady,
|
||||
podLocalOnly: true,
|
||||
managedByKubernetes: true,
|
||||
serviceRequired: false,
|
||||
valueRedacted: true
|
||||
};
|
||||
}
|
||||
|
||||
function arrayEqual(actual, expected) {
|
||||
if (!Array.isArray(actual) || !Array.isArray(expected)) return false;
|
||||
if (actual.length !== expected.length) return false;
|
||||
return actual.every((item, index) => Object.is(item, expected[index]));
|
||||
}
|
||||
|
||||
function isForbiddenDirectOpenAiBaseUrl(value) {
|
||||
const baseUrl = firstNonEmpty(value);
|
||||
return (
|
||||
|
||||
@@ -768,7 +768,7 @@ test("Code Agent PC gateway prompt reaches Codex stdio instead of internal hardw
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
},
|
||||
sessionRegistry: registry,
|
||||
codexStdioManager: manager,
|
||||
@@ -850,7 +850,7 @@ test("Codex stdio manager rebuilds app-server client when provider profile confi
|
||||
env: {
|
||||
...baseEnv,
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-5.5",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -899,7 +899,7 @@ test("Code Agent Keil gateway prompt is not blocked by M3 IO intent guard", asyn
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
},
|
||||
sessionRegistry: registry,
|
||||
codexStdioManager: manager,
|
||||
@@ -1207,7 +1207,7 @@ test("Codex stdio feasibility verifies writable workspace, CODEX_HOME, and versi
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||||
HWLAB_CODE_AGENT_CODEX_SANDBOX: "workspace-write",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1247,17 +1247,17 @@ test("Codex stdio feasibility verifies writable workspace, CODEX_HOME, and versi
|
||||
|
||||
test("Codex app-server args pin repo-owned DEV responses egress without leaking provider env", () => {
|
||||
const env = {
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses",
|
||||
HWLAB_CODE_AGENT_MODEL: "deepseek-chat",
|
||||
OPENAI_API_KEY: "test-openai-key-material"
|
||||
};
|
||||
|
||||
assert.equal(codexAppServerProviderBaseUrl(env), "http://172.26.26.227:17680/v1");
|
||||
assert.equal(codexAppServerProviderBaseUrl(env), "http://127.0.0.1:49280");
|
||||
const args = codexAppServerArgs(env);
|
||||
assert.ok(args.indexOf("app-server") > args.indexOf("review_model=\"deepseek-chat\""));
|
||||
assert.deepEqual(args.slice(-2), ["--listen", "stdio://"]);
|
||||
assert.ok(args.includes("model_provider=\"OpenAI\""));
|
||||
assert.ok(args.includes("model_providers.OpenAI.base_url=\"http://172.26.26.227:17680/v1\""));
|
||||
assert.ok(args.includes("model_providers.OpenAI.base_url=\"http://127.0.0.1:49280\""));
|
||||
assert.ok(args.includes("model_providers.OpenAI.name=\"OpenAI\""));
|
||||
assert.ok(args.includes("model_providers.OpenAI.wire_api=\"responses\""));
|
||||
assert.ok(args.includes("model=\"deepseek-chat\""));
|
||||
@@ -1315,7 +1315,7 @@ test("Codex stdio skills discovery returns bounded manifest facts instead of gen
|
||||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||||
HWLAB_CODE_AGENT_SKILLS_DIRS: skillsDir,
|
||||
HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1389,7 +1389,7 @@ test("Codex stdio skills discovery returns structured blocker when manifests are
|
||||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||||
HWLAB_CODE_AGENT_SKILLS_DIRS: path.join(root, "missing-skills"),
|
||||
HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1450,7 +1450,7 @@ test("Codex stdio runner startup failure marks session failed and blocks long-li
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
};
|
||||
|
||||
const payload = await handleCodeAgentChat(
|
||||
@@ -1510,7 +1510,7 @@ test("Codex app-server retrying error notification waits for completed turn", as
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
};
|
||||
|
||||
const payload = await handleCodeAgentChat(
|
||||
@@ -1565,7 +1565,7 @@ test("Codex app-server activity timeout does not fail an active turn", async ()
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -1620,7 +1620,7 @@ test("Codex app-server activity timeout fails only after no new events", async (
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -1683,7 +1683,7 @@ test("Codex app-server partial assistant output without turn completion is a tim
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -1740,7 +1740,7 @@ test("Codex app-server failed turn is not wrapped as completed assistant output"
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||||
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses"
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
|
||||
};
|
||||
|
||||
const payload = await handleCodeAgentChat(
|
||||
|
||||
@@ -58,7 +58,7 @@ const CODE_AGENT_PROVIDER_PROFILE_LABELS = Object.freeze({
|
||||
"runtime-default": "运行默认"
|
||||
});
|
||||
const DEFAULT_CODE_AGENT_CODEX_API_MODEL = "gpt-5.5";
|
||||
const DEFAULT_CODE_AGENT_CODEX_API_BASE_URL = "http://172.26.26.227:17680/v1/responses";
|
||||
const DEFAULT_CODE_AGENT_CODEX_API_BASE_URL = "http://127.0.0.1:49280/responses";
|
||||
const DEFAULT_CODE_AGENT_DEEPSEEK_MODEL = "deepseek-chat";
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const LIVE_BUILD_METADATA_PATHS = Object.freeze({
|
||||
@@ -1355,7 +1355,23 @@ function defaultDeepSeekBaseUrlForEnv(env = process.env) {
|
||||
|
||||
function codeAgentClusterNoProxyEntries(env = process.env) {
|
||||
const namespace = env.HWLAB_GITOPS_PROFILE === "prod" || env.HWLAB_ENVIRONMENT === "prod" ? "hwlab-prod" : "hwlab-dev";
|
||||
return [`${namespace}.svc.cluster.local`, ".svc", ".cluster.local", "127.0.0.1", "localhost", "::1", "10.0.0.0/8", "10.42.0.0/16", "10.43.0.0/16", "172.26.26.227", "172.26.0.0/16"];
|
||||
return [
|
||||
`${namespace}.svc.cluster.local`,
|
||||
".svc",
|
||||
".cluster.local",
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
"::1",
|
||||
"10.0.0.0/8",
|
||||
"10.42.0.0/16",
|
||||
"10.43.0.0/16",
|
||||
"hyueapi.com",
|
||||
".hyueapi.com",
|
||||
"hyui.com",
|
||||
".hyui.com",
|
||||
"api.minimaxi.com",
|
||||
".minimaxi.com"
|
||||
];
|
||||
}
|
||||
|
||||
function mergeNoProxyEntries(current, additions = []) {
|
||||
|
||||
@@ -309,12 +309,12 @@ async function runLocalContractSmoke() {
|
||||
logOk("providerStatus=401 blocks even when readiness fields look feasible");
|
||||
|
||||
const providerEnv = {
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://172.26.26.227:17680/v1/responses",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses",
|
||||
OPENAI_API_KEY: "test-openai-key-material"
|
||||
};
|
||||
assert.equal(codexAppServerProviderBaseUrl(providerEnv), "http://172.26.26.227:17680/v1");
|
||||
assert.equal(codexAppServerProviderBaseUrl(providerEnv), "http://127.0.0.1:49280");
|
||||
const providerArgs = codexAppServerArgs(providerEnv).join(" ");
|
||||
assert.match(providerArgs, /model_providers\.OpenAI\.base_url="http:\/\/172\.26\.26\.227:17680\/v1"/u);
|
||||
assert.match(providerArgs, /model_providers\.OpenAI\.base_url="http:\/\/127\.0\.0\.1:49280"/u);
|
||||
assert.match(providerArgs, /model_providers\.OpenAI\.name="OpenAI"/u);
|
||||
assert.match(providerArgs, /model_providers\.OpenAI\.wire_api="responses"/u);
|
||||
assert.match(providerArgs, /model_providers\.OpenAI\.requires_openai_auth=true/u);
|
||||
|
||||
@@ -21,6 +21,7 @@ async function git(repoRoot, args) {
|
||||
async function makeFixture({
|
||||
serviceId = "hwlab-cloud-api",
|
||||
commitId = "abc1234",
|
||||
deployImageTag = commitId,
|
||||
catalogCommitId = commitId,
|
||||
catalogServiceCommitId = catalogCommitId,
|
||||
catalogImageTag = catalogServiceCommitId.slice(0, 7),
|
||||
@@ -46,19 +47,27 @@ async function makeFixture({
|
||||
} = {}) {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-desired-state-"));
|
||||
await mkdir(path.join(root, "deploy/k8s/base"), { recursive: true });
|
||||
const image = `127.0.0.1:5000/hwlab/${serviceId}:${commitId}`;
|
||||
const image = `127.0.0.1:5000/hwlab/${serviceId}:${deployImageTag}`;
|
||||
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 providerBaseUrl = "http://127.0.0.1:49280/responses";
|
||||
const providerNoProxy = "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com";
|
||||
const providerEnv =
|
||||
serviceId === "hwlab-cloud-api" && deployCodeAgentProviderEnv
|
||||
? {
|
||||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||||
HWLAB_CODE_AGENT_MODEL: "gpt-5.5",
|
||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
||||
HWLAB_CODE_AGENT_DEEPSEEK_MODEL: "deepseek-chat",
|
||||
HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL: "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses",
|
||||
HWLAB_CODE_AGENT_CODEX_API_MODEL: "gpt-5.5",
|
||||
HWLAB_CODE_AGENT_CODEX_API_BASE_URL: providerBaseUrl,
|
||||
HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL: "https://hyueapi.com",
|
||||
HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT: "49280",
|
||||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: providerBaseUrl,
|
||||
CODEX_HOME: "/codex-home",
|
||||
NO_PROXY: "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com",
|
||||
no_proxy: "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com",
|
||||
NO_PROXY: providerNoProxy,
|
||||
no_proxy: providerNoProxy,
|
||||
OPENAI_API_KEY: "secretRef:hwlab-code-agent-provider/openai-api-key"
|
||||
}
|
||||
: {};
|
||||
@@ -67,10 +76,17 @@ async function makeFixture({
|
||||
? [
|
||||
{ name: "HWLAB_CODE_AGENT_PROVIDER", value: "codex-stdio" },
|
||||
{ name: "HWLAB_CODE_AGENT_MODEL", value: "gpt-5.5" },
|
||||
{ name: "HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", value: "deepseek" },
|
||||
{ name: "HWLAB_CODE_AGENT_DEEPSEEK_MODEL", value: "deepseek-chat" },
|
||||
{ name: "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL", value: "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses" },
|
||||
{ name: "HWLAB_CODE_AGENT_CODEX_API_MODEL", value: "gpt-5.5" },
|
||||
{ name: "HWLAB_CODE_AGENT_CODEX_API_BASE_URL", value: providerBaseUrl },
|
||||
{ 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_OPENAI_BASE_URL", value: providerBaseUrl },
|
||||
{ name: "CODEX_HOME", value: "/codex-home" },
|
||||
{ name: "NO_PROXY", value: "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com" },
|
||||
{ name: "no_proxy", value: "hyueapi.com,.hyueapi.com,hyui.com,.hyui.com,127.0.0.1,localhost,::1,api.minimaxi.com,.minimaxi.com" },
|
||||
{ name: "NO_PROXY", value: providerNoProxy },
|
||||
{ name: "no_proxy", value: providerNoProxy },
|
||||
{
|
||||
name: "OPENAI_API_KEY",
|
||||
valueFrom: {
|
||||
@@ -195,7 +211,24 @@ async function makeFixture({
|
||||
]
|
||||
}
|
||||
: {})
|
||||
}
|
||||
},
|
||||
...(serviceId === "hwlab-cloud-api" && workloadCodeAgentProviderEnv
|
||||
? [
|
||||
{
|
||||
name: "hwlab-codex-api-forwarder",
|
||||
image: workloadImage,
|
||||
command: ["node", "/app/cmd/hwlab-codex-api-responses-forwarder/main.mjs"],
|
||||
ports: [{ name: "codex-api", containerPort: 49280 }],
|
||||
env: [
|
||||
{ name: "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_HOST", value: "127.0.0.1" },
|
||||
{ name: "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT", value: "49280" },
|
||||
{ name: "HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL", value: "https://hyueapi.com" },
|
||||
{ name: "NO_PROXY", value: providerNoProxy },
|
||||
{ name: "no_proxy", value: providerNoProxy }
|
||||
]
|
||||
}
|
||||
]
|
||||
: [])
|
||||
],
|
||||
...(serviceId === "hwlab-cloud-api" && workloadCodeAgentProviderEnv && workloadCodexHomeVolumes
|
||||
? {
|
||||
@@ -227,6 +260,29 @@ test("passes internally consistent desired-state", async () => {
|
||||
assert.deepEqual(plan.diagnostics, []);
|
||||
});
|
||||
|
||||
test("passes mixed desired-state when unchanged services reuse service-level artifact tags", async () => {
|
||||
const repoRoot = await makeFixture({
|
||||
commitId: "803ba22",
|
||||
deployImageTag: "6a84959",
|
||||
catalogCommitId: "803ba22",
|
||||
catalogServiceCommitId: "6a84959",
|
||||
catalogImageTag: "6a84959",
|
||||
deployEnvCommitId: "6a84959",
|
||||
deployEnvImageTag: "6a84959",
|
||||
workloadTag: "6a84959",
|
||||
workloadEnvCommitId: "6a84959",
|
||||
workloadEnvImageTag: "6a84959"
|
||||
});
|
||||
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
|
||||
assert.equal(plan.status, "pass");
|
||||
assert.equal(plan.summary.desiredCommitId, "803ba22");
|
||||
assert.equal(plan.services[0].deploy.imageTag, "6a84959");
|
||||
assert.equal(plan.services[0].catalog.commitId, "6a84959");
|
||||
assert.deepEqual(plan.diagnostics, []);
|
||||
});
|
||||
|
||||
test("passes when cloud-web owns runtime identity mirrors", async () => {
|
||||
const repoRoot = await makeFixture({ serviceId: "hwlab-cloud-web" });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
|
||||
@@ -20,7 +20,7 @@ const defaultProdRuntimeEndpoint = process.env.HWLAB_G14_PROD_RUNTIME_ENDPOINT |
|
||||
const defaultProdWebEndpoint = process.env.HWLAB_G14_PROD_WEB_ENDPOINT || "http://74.48.78.17:18666";
|
||||
const defaultProxyUrl = process.env.HWLAB_G14_PROXY_URL || "http://127.0.0.1:10808";
|
||||
const defaultAllProxyUrl = process.env.HWLAB_G14_ALL_PROXY_URL || "socks5h://127.0.0.1:10808";
|
||||
const defaultNoProxy = process.env.HWLAB_G14_NO_PROXY || "127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc";
|
||||
const defaultNoProxy = process.env.HWLAB_G14_NO_PROXY || "hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,192.168.0.0/16,.svc,.cluster.local,kubernetes.default.svc,registry.hwlab-ci.svc,hwlab-registry.hwlab-ci.svc";
|
||||
const ciJsonRunnerImage = process.env.HWLAB_G14_CI_JSON_IMAGE || "mcr.microsoft.com/playwright@sha256:b0ab6f3cb99aa7803adbc14d9027ec1785fc6e433b97e134e0f8fe61683b6b53";
|
||||
const ciToolsRunnerImage = process.env.HWLAB_G14_CI_TOOLS_IMAGE || "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-v1";
|
||||
const buildkitRunnerImage = process.env.HWLAB_G14_BUILDKIT_IMAGE || "moby/buildkit:rootless";
|
||||
@@ -46,7 +46,9 @@ const moonBridgeSourceRef = process.env.HWLAB_G14_MOONBRIDGE_REF || "1b99888d3da
|
||||
const moonBridgeImage = process.env.HWLAB_G14_MOONBRIDGE_IMAGE || `${defaultRegistryPrefix}/moonbridge:${moonBridgeSourceRef.slice(0, 12)}`;
|
||||
const deepSeekProfileModel = process.env.HWLAB_G14_DEEPSEEK_PROFILE_MODEL || "deepseek-chat";
|
||||
const codexApiProfileModel = process.env.HWLAB_G14_CODEX_API_PROFILE_MODEL || "gpt-5.5";
|
||||
const codexApiProfileBaseUrl = process.env.HWLAB_G14_CODEX_API_PROFILE_BASE_URL || "http://172.26.26.227:17680/v1/responses";
|
||||
const codexApiProfileBaseUrl = process.env.HWLAB_G14_CODEX_API_PROFILE_BASE_URL || "http://127.0.0.1:49280/responses";
|
||||
const codexApiForwarderPort = process.env.HWLAB_G14_CODEX_API_FORWARDER_PORT || "49280";
|
||||
const codexApiForwarderUpstreamBaseUrl = process.env.HWLAB_G14_CODEX_API_UPSTREAM_BASE_URL || "https://hyueapi.com";
|
||||
const sourceCommitPattern = /^[a-f0-9]{7,40}$/u;
|
||||
|
||||
function proxyEnv() {
|
||||
@@ -286,6 +288,26 @@ function deepSeekBaseUrl(namespace) {
|
||||
return `http://hwlab-deepseek-proxy.${namespace}.svc.cluster.local:4000/v1/responses`;
|
||||
}
|
||||
|
||||
function codeAgentNoProxy(namespace) {
|
||||
return [
|
||||
`${namespace}.svc.cluster.local`,
|
||||
".svc",
|
||||
".cluster.local",
|
||||
"hyueapi.com",
|
||||
".hyueapi.com",
|
||||
"hyui.com",
|
||||
".hyui.com",
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
"::1",
|
||||
"10.0.0.0/8",
|
||||
"10.42.0.0/16",
|
||||
"10.43.0.0/16",
|
||||
"api.minimaxi.com",
|
||||
".minimaxi.com"
|
||||
].join(",");
|
||||
}
|
||||
|
||||
function namespaceScopedHost(value, namespace) {
|
||||
if (typeof value !== "string") return value;
|
||||
return value.replace(/\.hwlab-dev\.svc\.cluster\.local/gu, `.${namespace}.svc.cluster.local`);
|
||||
@@ -380,8 +402,10 @@ function transformWorkloads({ workloads, deploy, source, registryPrefix, runtime
|
||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL", deepSeekBaseUrl(namespace));
|
||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_MODEL", codexApiProfileModel);
|
||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_BASE_URL", codexApiProfileBaseUrl);
|
||||
upsertEnv(container.env, "NO_PROXY", `${namespace}.svc.cluster.local,.svc,.cluster.local,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.26.26.227,172.26.0.0/16`);
|
||||
upsertEnv(container.env, "no_proxy", `${namespace}.svc.cluster.local,.svc,.cluster.local,127.0.0.1,localhost,::1,10.0.0.0/8,10.42.0.0/16,10.43.0.0/16,172.26.26.227,172.26.0.0/16`);
|
||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_UPSTREAM_BASE_URL", codexApiForwarderUpstreamBaseUrl);
|
||||
upsertEnv(container.env, "HWLAB_CODE_AGENT_CODEX_API_FORWARDER_PORT", codexApiForwarderPort);
|
||||
upsertEnv(container.env, "NO_PROXY", codeAgentNoProxy(namespace));
|
||||
upsertEnv(container.env, "no_proxy", codeAgentNoProxy(namespace));
|
||||
}
|
||||
if (serviceId === "hwlab-cloud-web") {
|
||||
upsertEnv(container.env, "HWLAB_API_BASE_URL", `http://hwlab-cloud-api.${namespace}.svc.cluster.local:6667`);
|
||||
|
||||
@@ -379,13 +379,20 @@ function validateCloudApiCodeAgentSource(ctx, env) {
|
||||
const contract = DEV_CODE_AGENT_PROVIDER_CONTRACT;
|
||||
expectEqual(ctx, env.HWLAB_CODE_AGENT_PROVIDER, contract.codeAgentProvider, "$.services.hwlab-cloud-api.env.HWLAB_CODE_AGENT_PROVIDER", "cloud API Code Agent provider");
|
||||
expectEqual(ctx, env.HWLAB_CODE_AGENT_MODEL, contract.model, "$.services.hwlab-cloud-api.env.HWLAB_CODE_AGENT_MODEL", "cloud API Code Agent model");
|
||||
expectEqual(ctx, env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE, contract.profiles.defaultProfile, "$.services.hwlab-cloud-api.env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE", "cloud API default provider profile");
|
||||
expectEqual(ctx, env[contract.profiles.deepSeekModelEnv], "deepseek-chat", `$.services.hwlab-cloud-api.env.${contract.profiles.deepSeekModelEnv}`, "cloud API DeepSeek profile model");
|
||||
expectEqual(ctx, env[contract.profiles.deepSeekBaseUrlEnv], contract.profiles.deepSeekBaseUrl, `$.services.hwlab-cloud-api.env.${contract.profiles.deepSeekBaseUrlEnv}`, "cloud API DeepSeek profile base URL");
|
||||
expectEqual(ctx, env[contract.profiles.codexApiModelEnv], contract.model, `$.services.hwlab-cloud-api.env.${contract.profiles.codexApiModelEnv}`, "cloud API codex-api profile model");
|
||||
expectEqual(ctx, env[contract.profiles.codexApiBaseUrlEnv], contract.egress.defaultBaseUrl, `$.services.hwlab-cloud-api.env.${contract.profiles.codexApiBaseUrlEnv}`, "cloud API codex-api profile base URL");
|
||||
expectEqual(ctx, env[contract.forwarder.upstreamEnv], contract.forwarder.upstreamBaseUrl, `$.services.hwlab-cloud-api.env.${contract.forwarder.upstreamEnv}`, "cloud API codex-api upstream base URL");
|
||||
expectEqual(ctx, env[contract.forwarder.portEnv], String(contract.forwarder.port), `$.services.hwlab-cloud-api.env.${contract.forwarder.portEnv}`, "cloud API codex-api forwarder port");
|
||||
expectEqual(ctx, env.HWLAB_CODE_AGENT_TIMEOUT_MS, codeAgentBackendTimeoutMs, "$.services.hwlab-cloud-api.env.HWLAB_CODE_AGENT_TIMEOUT_MS", "cloud API Code Agent hard timeout budget");
|
||||
expectEqual(
|
||||
ctx,
|
||||
env.HWLAB_CODE_AGENT_OPENAI_BASE_URL,
|
||||
contract.egress.defaultBaseUrl,
|
||||
"$.services.hwlab-cloud-api.env.HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
||||
"cloud API Code Agent DEV egress/proxy base URL"
|
||||
"cloud API Code Agent pod-local Codex API forwarder base URL"
|
||||
);
|
||||
expectEqual(
|
||||
ctx,
|
||||
@@ -572,16 +579,24 @@ function validateCloudApiCodeAgentArtifacts(ctx, workloads) {
|
||||
const secretRef = contract.secretRefs[0];
|
||||
const container = mapByServiceId(listItems(workloads)).get("hwlab-cloud-api")?.spec?.template?.spec?.containers?.[0];
|
||||
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 volumes = mapByServiceId(listItems(workloads)).get("hwlab-cloud-api")?.spec?.template?.spec?.volumes ?? [];
|
||||
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_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(contract.profiles.deepSeekModelEnv)?.value, "deepseek-chat", `deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.${contract.profiles.deepSeekModelEnv}`, "cloud API workload DeepSeek profile model");
|
||||
expectEqual(ctx, env.get(contract.profiles.deepSeekBaseUrlEnv)?.value, contract.profiles.deepSeekBaseUrl, `deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.${contract.profiles.deepSeekBaseUrlEnv}`, "cloud API workload DeepSeek profile base URL");
|
||||
expectEqual(ctx, env.get(contract.profiles.codexApiModelEnv)?.value, contract.model, `deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.${contract.profiles.codexApiModelEnv}`, "cloud API workload codex-api profile model");
|
||||
expectEqual(ctx, env.get(contract.profiles.codexApiBaseUrlEnv)?.value, contract.egress.defaultBaseUrl, `deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.${contract.profiles.codexApiBaseUrlEnv}`, "cloud API workload codex-api profile base URL");
|
||||
expectEqual(ctx, env.get(contract.forwarder.upstreamEnv)?.value, contract.forwarder.upstreamBaseUrl, `deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.${contract.forwarder.upstreamEnv}`, "cloud API workload codex-api upstream base URL");
|
||||
expectEqual(ctx, env.get(contract.forwarder.portEnv)?.value, String(contract.forwarder.port), `deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.${contract.forwarder.portEnv}`, "cloud API workload codex-api forwarder port");
|
||||
expectEqual(ctx, env.get("HWLAB_CODE_AGENT_TIMEOUT_MS")?.value, codeAgentBackendTimeoutMs, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_TIMEOUT_MS", "cloud API workload Code Agent hard timeout budget");
|
||||
expectEqual(
|
||||
ctx,
|
||||
env.get("HWLAB_CODE_AGENT_OPENAI_BASE_URL")?.value,
|
||||
contract.egress.defaultBaseUrl,
|
||||
"deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.HWLAB_CODE_AGENT_OPENAI_BASE_URL",
|
||||
"cloud API workload Code Agent DEV egress/proxy base URL"
|
||||
"cloud API workload Code Agent pod-local Codex API forwarder base URL"
|
||||
);
|
||||
expectEqual(
|
||||
ctx,
|
||||
@@ -599,6 +614,7 @@ function validateCloudApiCodeAgentArtifacts(ctx, workloads) {
|
||||
expectEqual(ctx, env.get("CODEX_HOME")?.value, "/codex-home", "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.env.CODEX_HOME", "cloud API workload CODEX_HOME contract");
|
||||
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);
|
||||
validateCodexHomeMountArtifacts(ctx, container?.volumeMounts ?? [], volumes, contract);
|
||||
expect(
|
||||
ctx,
|
||||
@@ -622,6 +638,26 @@ function expectNoProxyIncludes(ctx, value, required, path, label = "NO_PROXY con
|
||||
}
|
||||
}
|
||||
|
||||
function validateCodexApiForwarderArtifacts(ctx, containers, contract) {
|
||||
const sidecar = (containers ?? []).find((entry) => entry.name === contract.forwarder.containerName);
|
||||
expect(ctx, Boolean(sidecar), "codex_api_forwarder_missing", "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.containers.hwlab-codex-api-forwarder", "cloud API pod includes codex-api loopback forwarder sidecar");
|
||||
if (!sidecar) return;
|
||||
expect(ctx, JSON.stringify(sidecar.command) === JSON.stringify(contract.forwarder.command), "codex_api_forwarder_command_mismatch", "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.containers.hwlab-codex-api-forwarder.command", "codex-api forwarder command");
|
||||
expect(
|
||||
ctx,
|
||||
(sidecar.ports ?? []).some((entry) => entry.name === contract.forwarder.portName && entry.containerPort === contract.forwarder.port),
|
||||
"codex_api_forwarder_port_missing",
|
||||
"deploy/k8s/base/workloads.yaml.hwlab-cloud-api.containers.hwlab-codex-api-forwarder.ports",
|
||||
"codex-api forwarder exposes only a Pod-local container port"
|
||||
);
|
||||
const env = new Map((sidecar.env ?? []).map((entry) => [entry.name, entry.value]));
|
||||
expectEqual(ctx, env.get(contract.forwarder.listenHostEnv), "127.0.0.1", `deploy/k8s/base/workloads.yaml.hwlab-cloud-api.containers.hwlab-codex-api-forwarder.env.${contract.forwarder.listenHostEnv}`, "codex-api forwarder loopback listen host env");
|
||||
expectEqual(ctx, env.get(contract.forwarder.upstreamEnv), contract.forwarder.upstreamBaseUrl, `deploy/k8s/base/workloads.yaml.hwlab-cloud-api.containers.hwlab-codex-api-forwarder.env.${contract.forwarder.upstreamEnv}`, "codex-api forwarder upstream env");
|
||||
expectEqual(ctx, env.get(contract.forwarder.portEnv), String(contract.forwarder.port), `deploy/k8s/base/workloads.yaml.hwlab-cloud-api.containers.hwlab-codex-api-forwarder.env.${contract.forwarder.portEnv}`, "codex-api forwarder port env");
|
||||
expectNoProxyIncludes(ctx, env.get("NO_PROXY"), contract.noProxyRequired, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.containers.hwlab-codex-api-forwarder.env.NO_PROXY", "codex-api forwarder NO_PROXY contract");
|
||||
expectNoProxyIncludes(ctx, env.get("no_proxy"), contract.noProxyRequired, "deploy/k8s/base/workloads.yaml.hwlab-cloud-api.containers.hwlab-codex-api-forwarder.env.no_proxy", "codex-api forwarder no_proxy contract");
|
||||
}
|
||||
|
||||
function validateCodexHomeMountArtifacts(ctx, volumeMounts, volumes, contract) {
|
||||
expect(
|
||||
ctx,
|
||||
|
||||
@@ -201,7 +201,7 @@ function recordObservation(ctx, observation) {
|
||||
}
|
||||
|
||||
function expectedEnvValue(name, service, desiredCommitId) {
|
||||
if (name === "HWLAB_COMMIT_ID" || name === "HWLAB_SKILLS_COMMIT_ID") return desiredCommitId;
|
||||
if (name === "HWLAB_COMMIT_ID" || name === "HWLAB_SKILLS_COMMIT_ID") return service?.commitId ?? service?.imageTag ?? desiredCommitId;
|
||||
if (name === "HWLAB_IMAGE") return service?.image ?? null;
|
||||
if (name === "HWLAB_IMAGE_TAG") return service?.imageTag ?? null;
|
||||
if (name === "HWLAB_IMAGE_DIGEST") return service?.digest ?? "not_published";
|
||||
@@ -306,7 +306,8 @@ function inspectCodeAgentProviderDesiredState(ctx, deployService, workloadRecord
|
||||
missingSecretRefs: inspection.missingSecretRefs,
|
||||
missingEgressContract: inspection.missingEgressContract,
|
||||
missingCodexHomeContract: inspection.missingCodexHomeContract,
|
||||
missingNoProxyContract: inspection.missingNoProxyContract
|
||||
missingNoProxyContract: inspection.missingNoProxyContract,
|
||||
missingForwarderContract: inspection.missingForwarderContract
|
||||
},
|
||||
secretMaterialRead: false,
|
||||
valuesRedacted: true
|
||||
@@ -326,8 +327,8 @@ function inspectCodeAgentProviderDesiredState(ctx, deployService, workloadRecord
|
||||
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",
|
||||
kind: "codex-api-forwarder",
|
||||
value: inspection.egress.workloadMatchesDevProxy ? "pod-local-codex-api-forwarder" : "missing_or_mismatched",
|
||||
expected: DEV_CODE_AGENT_PROVIDER_CONTRACT.egress.target
|
||||
});
|
||||
return inspection;
|
||||
@@ -443,7 +444,8 @@ function indexWorkloads(workloads) {
|
||||
env: {
|
||||
...envListToObject(container?.env),
|
||||
__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 : []
|
||||
},
|
||||
path: `${workloadsPath}.${item?.kind ?? "Workload"}.${item?.metadata?.name ?? serviceId}.containers[${containerIndex}]`
|
||||
};
|
||||
@@ -796,10 +798,12 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
|
||||
const deployImage = parseImageRef(deployService?.image);
|
||||
const catalogImage = parseImageRef(catalogService?.image);
|
||||
const serviceImageTag = catalogService?.imageTag ?? deployImage.tag;
|
||||
const serviceImageTag = catalogService?.imageTag ?? deployImage.tag ?? catalogImage.tag ?? desiredImageTag;
|
||||
const serviceCommitId = catalogService?.commitId ?? serviceImageTag;
|
||||
const expectedService = {
|
||||
image: deployService?.image ?? catalogService?.image ?? null,
|
||||
imageTag: desiredImageTag,
|
||||
imageTag: serviceImageTag,
|
||||
commitId: serviceCommitId,
|
||||
digest: catalogService?.digest ?? "not_published"
|
||||
};
|
||||
|
||||
@@ -816,8 +820,8 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
});
|
||||
if (!deployImage.valid) {
|
||||
addMismatch(ctx, "invalid_image", `${deployPathBase}.image`, deployImage.reason, "tagged image ref", deployService.image, { serviceId });
|
||||
} else if (deployImage.tag !== desiredImageTag) {
|
||||
addMismatch(ctx, "image_tag_mismatch", `${deployPathBase}.image`, `${serviceId} deploy image tag must match deploy commit tag`, desiredImageTag, deployImage.tag, { serviceId });
|
||||
} else if (deployImage.tag !== serviceImageTag) {
|
||||
addMismatch(ctx, "image_tag_mismatch", `${deployPathBase}.image`, `${serviceId} deploy image tag must match service artifact tag`, serviceImageTag, deployImage.tag, { serviceId });
|
||||
}
|
||||
service.deploy = {
|
||||
image: deployService.image,
|
||||
@@ -843,7 +847,7 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
field: "commitId",
|
||||
kind: "commit",
|
||||
value: catalogService.commitId,
|
||||
expected: desiredCommitId
|
||||
expected: serviceCommitId
|
||||
});
|
||||
recordObservation(ctx, {
|
||||
source: "catalog",
|
||||
@@ -861,16 +865,18 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
field: "imageTag",
|
||||
kind: "tag",
|
||||
value: catalogService.imageTag,
|
||||
expected: desiredImageTag
|
||||
expected: serviceImageTag
|
||||
});
|
||||
if (!commitEquivalent(catalogService.commitId, desiredCommitId)) {
|
||||
addMismatch(ctx, "commit_mismatch", `${catalogPathBase}.commitId`, `${serviceId} catalog service commitId must match deploy commitId`, desiredCommitId, catalogService.commitId, { serviceId });
|
||||
if (!commitPattern.test(String(catalogService.commitId ?? ""))) {
|
||||
addMismatch(ctx, "invalid_commit", `${catalogPathBase}.commitId`, `${serviceId} catalog service commitId must be a short or full lowercase Git SHA`, "7-40 lowercase hex", catalogService.commitId, { serviceId });
|
||||
} else if (!commitEquivalent(catalogService.commitId, serviceImageTag)) {
|
||||
addMismatch(ctx, "commit_mismatch", `${catalogPathBase}.commitId`, `${serviceId} catalog service commitId must match service artifact tag`, serviceImageTag, catalogService.commitId, { serviceId });
|
||||
}
|
||||
if (!catalogImage.valid) {
|
||||
addMismatch(ctx, "invalid_image", `${catalogPathBase}.image`, catalogImage.reason, "tagged image ref", catalogService.image, { serviceId });
|
||||
}
|
||||
if (catalogService.imageTag !== desiredImageTag) {
|
||||
addMismatch(ctx, "image_tag_mismatch", `${catalogPathBase}.imageTag`, `${serviceId} catalog imageTag must match deploy commit tag`, desiredImageTag, catalogService.imageTag, { serviceId });
|
||||
if (catalogService.imageTag !== serviceImageTag) {
|
||||
addMismatch(ctx, "image_tag_mismatch", `${catalogPathBase}.imageTag`, `${serviceId} catalog imageTag must match service artifact tag`, serviceImageTag, catalogService.imageTag, { serviceId });
|
||||
}
|
||||
if (catalogImage.valid && catalogImage.tag !== catalogService.imageTag) {
|
||||
addMismatch(ctx, "image_tag_mismatch", `${catalogPathBase}.image`, `${serviceId} catalog image tag must match catalog imageTag`, catalogService.imageTag, catalogImage.tag, { serviceId });
|
||||
@@ -901,8 +907,8 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
});
|
||||
if (!workloadImage.valid) {
|
||||
addMismatch(ctx, "invalid_image", `${workload.path}.image`, workloadImage.reason, "tagged image ref", workload.image, { serviceId });
|
||||
} else if (workloadImage.tag !== desiredImageTag) {
|
||||
addMismatch(ctx, "image_tag_mismatch", `${workload.path}.image`, `${serviceId} workload image tag must match deploy commit tag`, desiredImageTag, workloadImage.tag, { serviceId });
|
||||
} else if (workloadImage.tag !== serviceImageTag) {
|
||||
addMismatch(ctx, "image_tag_mismatch", `${workload.path}.image`, `${serviceId} workload image tag must match service artifact tag`, serviceImageTag, workloadImage.tag, { serviceId });
|
||||
}
|
||||
if (deployService?.image && workload.image !== deployService.image) {
|
||||
addMismatch(ctx, "image_mismatch", `${workload.path}.image`, `${serviceId} workload image must match deploy image`, deployService.image, workload.image, { serviceId });
|
||||
|
||||
@@ -41,6 +41,7 @@ export const DEFAULT_GITOPS_ONLY_PATHS = Object.freeze([
|
||||
const serviceSpecificPaths = Object.freeze({
|
||||
"hwlab-cloud-api": [
|
||||
"cmd/hwlab-cloud-api/",
|
||||
"cmd/hwlab-codex-api-responses-forwarder/",
|
||||
"cmd/hwlab-deepseek-responses-bridge/",
|
||||
"internal/cloud/",
|
||||
"internal/db/",
|
||||
|
||||
@@ -77,6 +77,16 @@ function assertUnique(name, values) {
|
||||
assert.equal(new Set(values).size, values.length, `${name} must be unique`);
|
||||
}
|
||||
|
||||
function imageTagFromReference(image) {
|
||||
if (typeof image !== "string" || image.length === 0) return null;
|
||||
const digestIndex = image.indexOf("@");
|
||||
const withoutDigest = digestIndex >= 0 ? image.slice(0, digestIndex) : image;
|
||||
const slashIndex = withoutDigest.lastIndexOf("/");
|
||||
const colonIndex = withoutDigest.lastIndexOf(":");
|
||||
if (colonIndex <= slashIndex) return null;
|
||||
return withoutDigest.slice(colonIndex + 1) || null;
|
||||
}
|
||||
|
||||
function assertCommonSchema(commonSchema) {
|
||||
const schemaServiceIds = commonSchema.$defs.serviceId.enum;
|
||||
assert.deepEqual(schemaServiceIds, SERVICE_IDS, "common service ids must match runtime constants");
|
||||
@@ -165,6 +175,7 @@ const cloudApiContainer = cloudApiWorkload?.spec?.template?.spec?.containers?.fi
|
||||
const cloudApiWorkloadEnv = getWorkloadEnv(k8sWorkloads, "hwlab-cloud-api");
|
||||
cloudApiWorkloadEnv.__volumeMounts = cloudApiContainer?.volumeMounts ?? [];
|
||||
cloudApiWorkloadEnv.__volumes = cloudApiWorkload?.spec?.template?.spec?.volumes ?? [];
|
||||
cloudApiWorkloadEnv.__podContainers = cloudApiWorkload?.spec?.template?.spec?.containers ?? [];
|
||||
const patchPanelWorkloadEnv = getWorkloadEnv(k8sWorkloads, "hwlab-patch-panel");
|
||||
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");
|
||||
@@ -176,9 +187,10 @@ assert.ok(
|
||||
"deploy edge proxy k3s mapping"
|
||||
);
|
||||
assert.equal(cloudApi.healthPath, "/health/live", "cloud-api health path");
|
||||
assert.equal(cloudApi.env.HWLAB_COMMIT_ID, deployManifest.commitId, "cloud-api health commit evidence");
|
||||
const cloudApiImageTag = imageTagFromReference(cloudApi.image) ?? deployManifest.commitId.slice(0, 7);
|
||||
assert.equal(cloudApi.env.HWLAB_COMMIT_ID, cloudApiImageTag, "cloud-api health commit evidence");
|
||||
assert.equal(cloudApi.env.HWLAB_IMAGE, cloudApi.image, "cloud-api health image evidence");
|
||||
assert.equal(cloudApi.env.HWLAB_IMAGE_TAG, deployManifest.commitId.slice(0, 7), "cloud-api health image tag evidence");
|
||||
assert.equal(cloudApi.env.HWLAB_IMAGE_TAG, cloudApiImageTag, "cloud-api health image tag evidence");
|
||||
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_URL, "secretRef:hwlab-cloud-api-dev-db/database-url", "cloud-api DB URL must be a Secret reference placeholder");
|
||||
assert.equal(
|
||||
cloudApi.env.HWLAB_CLOUD_DB_SSL_MODE,
|
||||
@@ -206,10 +218,17 @@ assert.equal(cloudApiWorkloadEnv.HWLAB_CLOUD_DB_PORT, undefined, "cloud-api work
|
||||
assertCloudApiDbOptionalAlias(k8sServices);
|
||||
assert.equal(cloudApi.env.HWLAB_CODE_AGENT_PROVIDER, DEV_CODE_AGENT_PROVIDER_CONTRACT.codeAgentProvider, "cloud-api Code Agent provider");
|
||||
assert.equal(cloudApi.env.HWLAB_CODE_AGENT_MODEL, DEV_CODE_AGENT_PROVIDER_CONTRACT.model, "cloud-api Code Agent model");
|
||||
assert.equal(cloudApi.env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE, DEV_CODE_AGENT_PROVIDER_CONTRACT.profiles.defaultProfile, "cloud-api default provider profile");
|
||||
assert.equal(cloudApi.env[DEV_CODE_AGENT_PROVIDER_CONTRACT.profiles.deepSeekModelEnv], "deepseek-chat", "cloud-api DeepSeek profile model");
|
||||
assert.equal(cloudApi.env[DEV_CODE_AGENT_PROVIDER_CONTRACT.profiles.deepSeekBaseUrlEnv], DEV_CODE_AGENT_PROVIDER_CONTRACT.profiles.deepSeekBaseUrl, "cloud-api DeepSeek profile base URL");
|
||||
assert.equal(cloudApi.env[DEV_CODE_AGENT_PROVIDER_CONTRACT.profiles.codexApiModelEnv], DEV_CODE_AGENT_PROVIDER_CONTRACT.model, "cloud-api codex-api profile model");
|
||||
assert.equal(cloudApi.env[DEV_CODE_AGENT_PROVIDER_CONTRACT.profiles.codexApiBaseUrlEnv], DEV_CODE_AGENT_PROVIDER_CONTRACT.egress.defaultBaseUrl, "cloud-api codex-api profile base URL");
|
||||
assert.equal(cloudApi.env[DEV_CODE_AGENT_PROVIDER_CONTRACT.forwarder.upstreamEnv], DEV_CODE_AGENT_PROVIDER_CONTRACT.forwarder.upstreamBaseUrl, "cloud-api codex-api upstream base URL");
|
||||
assert.equal(cloudApi.env[DEV_CODE_AGENT_PROVIDER_CONTRACT.forwarder.portEnv], String(DEV_CODE_AGENT_PROVIDER_CONTRACT.forwarder.port), "cloud-api codex-api forwarder port");
|
||||
assert.equal(
|
||||
cloudApi.env.HWLAB_CODE_AGENT_OPENAI_BASE_URL,
|
||||
DEV_CODE_AGENT_PROVIDER_CONTRACT.egress.defaultBaseUrl,
|
||||
"cloud-api Code Agent OpenAI base URL must use DEV egress/proxy"
|
||||
"cloud-api Code Agent OpenAI base URL must use pod-local Codex API forwarder"
|
||||
);
|
||||
assert.equal(
|
||||
cloudApi.env[HWLAB_M3_IO_API_BASE_URL_ENV],
|
||||
@@ -278,10 +297,17 @@ function assertCodeAgentProviderWorkloadContract(env) {
|
||||
const apiKey = env[secretRef.env]?.valueFrom?.secretKeyRef;
|
||||
assert.equal(env.HWLAB_CODE_AGENT_PROVIDER?.value, contract.codeAgentProvider, "cloud-api workload Code Agent provider");
|
||||
assert.equal(env.HWLAB_CODE_AGENT_MODEL?.value, contract.model, "cloud-api workload Code Agent model");
|
||||
assert.equal(env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE?.value, contract.profiles.defaultProfile, "cloud-api workload default provider profile");
|
||||
assert.equal(env[contract.profiles.deepSeekModelEnv]?.value, "deepseek-chat", "cloud-api workload DeepSeek profile model");
|
||||
assert.equal(env[contract.profiles.deepSeekBaseUrlEnv]?.value, contract.profiles.deepSeekBaseUrl, "cloud-api workload DeepSeek profile base URL");
|
||||
assert.equal(env[contract.profiles.codexApiModelEnv]?.value, contract.model, "cloud-api workload codex-api profile model");
|
||||
assert.equal(env[contract.profiles.codexApiBaseUrlEnv]?.value, contract.egress.defaultBaseUrl, "cloud-api workload codex-api profile base URL must use pod-local forwarder");
|
||||
assert.equal(env[contract.forwarder.upstreamEnv]?.value, contract.forwarder.upstreamBaseUrl, "cloud-api workload codex-api upstream must direct-connect to hyueapi");
|
||||
assert.equal(env[contract.forwarder.portEnv]?.value, String(contract.forwarder.port), "cloud-api workload codex-api forwarder port");
|
||||
assert.equal(
|
||||
env.HWLAB_CODE_AGENT_OPENAI_BASE_URL?.value,
|
||||
contract.egress.defaultBaseUrl,
|
||||
"cloud-api workload Code Agent OpenAI base URL must use DEV egress/proxy"
|
||||
"cloud-api workload Code Agent OpenAI base URL must use pod-local Codex API forwarder"
|
||||
);
|
||||
assert.equal(
|
||||
env[HWLAB_M3_IO_API_BASE_URL_ENV]?.value,
|
||||
@@ -298,6 +324,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");
|
||||
assertCodexHomeMountContract(env, contract);
|
||||
assertCodexApiForwarderContract(env, contract);
|
||||
assert.notEqual(
|
||||
env.HWLAB_CODE_AGENT_OPENAI_BASE_URL?.value,
|
||||
contract.egress.forbiddenDirectBaseUrl,
|
||||
@@ -317,6 +344,22 @@ function assertNoProxyIncludes(value, required, label) {
|
||||
}
|
||||
}
|
||||
|
||||
function assertCodexApiForwarderContract(env, contract) {
|
||||
const sidecar = (env.__podContainers ?? []).find((entry) => entry.name === contract.forwarder.containerName);
|
||||
assert.ok(sidecar, "cloud-api pod includes codex-api loopback forwarder sidecar");
|
||||
assert.deepEqual(sidecar.command, contract.forwarder.command, "codex-api forwarder command");
|
||||
assert.ok(
|
||||
(sidecar.ports ?? []).some((entry) => entry.name === contract.forwarder.portName && entry.containerPort === contract.forwarder.port),
|
||||
"codex-api forwarder exposes only Pod-local container port"
|
||||
);
|
||||
const sidecarEnv = Object.fromEntries((sidecar.env ?? []).map((entry) => [entry.name, entry.value]));
|
||||
assert.equal(sidecarEnv[contract.forwarder.listenHostEnv], "127.0.0.1", "codex-api forwarder loopback listen host env");
|
||||
assert.equal(sidecarEnv[contract.forwarder.upstreamEnv], contract.forwarder.upstreamBaseUrl, "codex-api forwarder upstream env");
|
||||
assert.equal(sidecarEnv[contract.forwarder.portEnv], String(contract.forwarder.port), "codex-api forwarder port env");
|
||||
assertNoProxyIncludes(sidecarEnv.NO_PROXY, contract.noProxyRequired, "codex-api forwarder NO_PROXY contract");
|
||||
assertNoProxyIncludes(sidecarEnv.no_proxy, contract.noProxyRequired, "codex-api forwarder no_proxy contract");
|
||||
}
|
||||
|
||||
function assertCodexHomeMountContract(env, contract) {
|
||||
const volumeMounts = env.__volumeMounts ?? [];
|
||||
const volumes = env.__volumes ?? [];
|
||||
|
||||
Reference in New Issue
Block a user