fix: allow public code agent chat proxy

This commit is contained in:
Code Queue Review
2026-05-24 09:20:02 +00:00
parent 13e589641f
commit b63dd948dc
8 changed files with 160 additions and 19 deletions
@@ -0,0 +1,48 @@
const GET_PROXY_PREFIXES = Object.freeze(["/v1/"]);
const GET_PROXY_ROUTES = new Set(["/v1"]);
const POST_PROXY_ROUTES = new Set([
"/json-rpc",
"/v1/agent/chat",
"/v1/agent/chat/cancel",
"/v1/m3/io"
]);
const PUBLIC_PROXY_ROUTES = new Set([
"POST /v1/agent/chat"
]);
export function cloudWebProxyRoutePolicy(method, pathname) {
const normalizedMethod = normalizeMethod(method);
const normalizedPath = normalizePathname(pathname);
const routeKey = `${normalizedMethod} ${normalizedPath}`;
const proxy = isCloudApiProxyRoute(normalizedMethod, normalizedPath);
const publicRoute = proxy && PUBLIC_PROXY_ROUTES.has(routeKey);
return {
proxy,
authRequired: proxy && !publicRoute,
publicRoute,
routeKey
};
}
export function isCloudApiProxyRoute(method, pathname) {
const normalizedMethod = normalizeMethod(method);
const normalizedPath = normalizePathname(pathname);
if (normalizedMethod === "GET") {
return GET_PROXY_ROUTES.has(normalizedPath) ||
GET_PROXY_PREFIXES.some((prefix) => normalizedPath.startsWith(prefix));
}
if (normalizedMethod === "POST") {
return POST_PROXY_ROUTES.has(normalizedPath);
}
return false;
}
function normalizeMethod(method) {
return String(method || "GET").trim().toUpperCase() || "GET";
}
function normalizePathname(pathname) {
const value = String(pathname || "/").trim();
if (!value || value[0] !== "/") return "/";
return value.replace(/\/+$/u, "") || "/";
}
+40
View File
@@ -2,8 +2,48 @@ import assert from "node:assert/strict";
import { createServer } from "node:http";
import test from "node:test";
import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs";
import { proxyHttpRequest } from "./http.mjs";
test("cloud web route policy proxies public Code Agent chat without gating other writes", () => {
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/agent/chat"), {
proxy: true,
authRequired: false,
publicRoute: true,
routeKey: "POST /v1/agent/chat"
});
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/agent/chat/cancel"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "POST /v1/agent/chat/cancel"
});
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/m3/io"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "POST /v1/m3/io"
});
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/json-rpc"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "POST /json-rpc"
});
assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/agent/chat/trace/trc_live"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "GET /v1/agent/chat/trace/trc_live"
});
assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/app.mjs"), {
proxy: false,
authRequired: false,
publicRoute: false,
routeKey: "GET /app.mjs"
});
});
test("dev entrypoint proxy allows slow first response beyond legacy 4500ms", async () => {
const upstream = createServer((request, response) => {
request.resume();
+1 -1
View File
File diff suppressed because one or more lines are too long
+40 -1
View File
@@ -10,7 +10,11 @@ import {
handleCodeAgentChat,
validateCodeAgentChatSchema
} from "../internal/cloud/code-agent-chat.mjs";
import { createCodexStdioSessionManager } from "../internal/cloud/codex-stdio-session.mjs";
import {
codexAppServerArgs,
codexAppServerProviderBaseUrl,
createCodexStdioSessionManager
} from "../internal/cloud/codex-stdio-session.mjs";
import {
classifyCodexRunnerCapability,
classifyCodeAgentChatReadiness,
@@ -281,6 +285,41 @@ async function runLocalContractSmoke() {
}
logOk("non-2xx completion-looking payloads are blocked");
const failedProvider401Payload = {
...completedCodexPayload,
status: "failed",
reply: undefined,
error: {
code: "codex_stdio_failed",
providerStatus: 401,
layer: "runner",
message: "provider returned HTTP 401"
},
availability: {
status: "codex-stdio-feasible",
ready: true
}
};
const provider401Readiness = classifyCodeAgentChatReadiness(failedProvider401Payload, { realDevLive: true, httpStatus: 200 });
assert.equal(provider401Readiness.status, "blocked");
assert.equal(provider401Readiness.level, "BLOCKED/provider");
assert.equal(provider401Readiness.blocker, "provider-upstream");
assert.equal(provider401Readiness.providerStatus, 401);
assert.equal(provider401Readiness.devLiveReplyPass, false);
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",
OPENAI_API_KEY: "test-openai-key-material"
};
assert.equal(codexAppServerProviderBaseUrl(providerEnv), "http://172.26.26.227:17680/v1");
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\.wire_api="responses"/u);
assert.match(providerArgs, /model_providers\.OpenAI\.requires_openai_auth=true/u);
assert.equal(providerArgs.includes("test-openai-key-material"), false);
logOk("Codex app-server provider override keeps auth material out of argv");
process.stdout.write("[code-agent-chat-smoke] passed\n");
}
+4 -5
View File
@@ -325,6 +325,7 @@ import { randomBytes, timingSafeEqual } from "node:crypto";
import { chmodSync, existsSync, lstatSync, mkdirSync, symlinkSync } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import { cloudWebProxyRoutePolicy } from "/app/internal/dev-entrypoint/cloud-web-routes.mjs";
const serviceId = process.env.HWLAB_SERVICE_ID || "hwlab-unknown";
const environment = process.env.HWLAB_ENVIRONMENT || "dev";
@@ -740,11 +741,9 @@ async function serveCloudWeb() {
return;
}
if (
(request.method === "GET" && (url.pathname === "/v1" || url.pathname === "/v1/m3/status" || url.pathname.startsWith("/v1/"))) ||
(request.method === "POST" && (url.pathname === "/json-rpc" || url.pathname === "/v1/agent/chat" || url.pathname === "/v1/m3/io"))
) {
if (!activeAuthSession(request)) {
const proxyPolicy = cloudWebProxyRoutePolicy(request.method, url.pathname);
if (proxyPolicy.proxy) {
if (proxyPolicy.authRequired && !activeAuthSession(request)) {
sendJson(response, 401, {
status: "failed",
error: "auth_required",
+13 -6
View File
@@ -224,13 +224,14 @@ export async function buildM3IoControlSourceReport(options = {}) {
}
export async function runM3IoControlSourceChecks({ repoRoot: root = repoRoot } = {}) {
const [serverSource, jsonRpcSource, m3Source, appSource, htmlSource, artifactPublisherSource] = await Promise.all([
const [serverSource, jsonRpcSource, m3Source, appSource, htmlSource, artifactPublisherSource, cloudWebRouteSource] = await Promise.all([
readRepoText(root, "internal/cloud/server.mjs"),
readRepoText(root, "internal/cloud/json-rpc.mjs"),
readRepoText(root, "internal/cloud/m3-io-control.mjs"),
readRepoText(root, "web/hwlab-cloud-web/app.mjs"),
readRepoText(root, "web/hwlab-cloud-web/index.html"),
readRepoText(root, "scripts/dev-artifact-publish.mjs")
readRepoText(root, "scripts/dev-artifact-publish.mjs"),
readRepoText(root, "internal/dev-entrypoint/cloud-web-routes.mjs")
]);
const contract = describeM3IoControl({
@@ -241,7 +242,8 @@ export async function runM3IoControlSourceChecks({ repoRoot: root = repoRoot } =
const frontendGuardrails = checkFrontendNoDirectRuntimeCalls({
appSource,
htmlSource,
artifactPublisherSource
artifactPublisherSource,
cloudWebRouteSource
});
const requestSchema = validateExpectedRequestSchemas(expectedM3IoLiveSequence());
const responseSchema = validateM3IoSourceResponseContract(m3Source);
@@ -323,7 +325,7 @@ export async function runM3IoControlSourceChecks({ repoRoot: root = repoRoot } =
];
}
export function checkFrontendNoDirectRuntimeCalls({ appSource, htmlSource = "", artifactPublisherSource = "" }) {
export function checkFrontendNoDirectRuntimeCalls({ appSource, htmlSource = "", artifactPublisherSource = "", cloudWebRouteSource = "" }) {
const source = `${appSource}\n${htmlSource}`;
const fetchTargets = literalFetchTargets(appSource);
const issues = [];
@@ -382,10 +384,15 @@ export function checkFrontendNoDirectRuntimeCalls({ appSource, htmlSource = "",
issues.push(`m3ControlCanOperate must require ${label}`);
}
}
if (!/url\.pathname === "\/v1\/m3\/io"/u.test(artifactPublisherSource)) {
const artifactUsesCloudWebRoutePolicy = /cloudWebProxyRoutePolicy\(request\.method,\s*url\.pathname\)/u.test(artifactPublisherSource);
const routePolicyProxiesM3Io = artifactUsesCloudWebRoutePolicy &&
new RegExp(`"${escapeRegExp(M3_IO_CONTROL_ROUTE)}"`, "u").test(cloudWebRouteSource);
const routePolicyProxiesV1Status = artifactUsesCloudWebRoutePolicy &&
/const GET_PROXY_PREFIXES = Object\.freeze\(\["\/v1\/"\]\)/u.test(cloudWebRouteSource);
if (!routePolicyProxiesM3Io && !/url\.pathname === "\/v1\/m3\/io"/u.test(artifactPublisherSource)) {
issues.push("cloud-web artifact server must proxy same-origin /v1/m3/io to cloud-api");
}
if (!/\/v1\/m3\/status/u.test(artifactPublisherSource)) {
if (!routePolicyProxiesV1Status && !/\/v1\/m3\/status/u.test(artifactPublisherSource)) {
issues.push("cloud-web artifact server must proxy same-origin /v1/m3/status to cloud-api");
}
+9 -3
View File
@@ -64,6 +64,7 @@ const buildScript = fs.readFileSync(path.resolve(rootDir, "scripts/build.mjs"),
const distContractScript = fs.readFileSync(path.resolve(rootDir, "scripts/dist-contract.mjs"), "utf8");
const markedLicense = fs.readFileSync(path.resolve(rootDir, "third_party/marked/LICENSE"), "utf8");
const artifactPublisher = fs.readFileSync(path.resolve(repoRoot, "scripts/dev-artifact-publish.mjs"), "utf8");
const cloudWebRoutes = fs.readFileSync(path.resolve(repoRoot, "internal/dev-entrypoint/cloud-web-routes.mjs"), "utf8");
const frontendSource = `${html}\n${auth}\n${app}\n${artifactPublisher}`;
const requiredTrustedRecordTerms = Object.freeze([
"Code Agent 对话记录",
@@ -980,9 +981,14 @@ assert.match(artifactPublisher, /readOnlyRpcMethods/);
assert.match(artifactPublisher, /requestUpstream/);
assert.match(artifactPublisher, /from "node:http"/);
assert.doesNotMatch(artifactPublisher, /await fetch\(target/);
assert.match(artifactPublisher, /url\.pathname === "\/v1\/agent\/chat"/);
assert.match(artifactPublisher, /url\.pathname === "\/v1\/m3\/io"/);
assert.match(artifactPublisher, /url\.pathname === "\/v1\/m3\/status"/);
assert.match(artifactPublisher, /cloudWebProxyRoutePolicy\(request\.method,\s*url\.pathname\)/);
assert.match(cloudWebRoutes, /"\/v1\/agent\/chat"/);
assert.match(cloudWebRoutes, /"\/v1\/agent\/chat\/cancel"/);
assert.match(cloudWebRoutes, /"\/v1\/m3\/io"/);
assert.match(cloudWebRoutes, /"POST \/v1\/agent\/chat"/);
assert.match(cloudWebRoutes, /const GET_PROXY_PREFIXES = Object\.freeze\(\["\/v1\/"\]\)/);
assert.match(cloudWebRoutes, /const GET_PROXY_ROUTES = new Set\(\["\/v1"\]\)/);
assert.match(cloudWebRoutes, /authRequired: proxy && !publicRoute/);
assert.match(artifactPublisher, /"system\.health"/);
assert.match(artifactPublisher, /"cloud\.adapter\.describe"/);
assert.match(artifactPublisher, /"audit\.event\.query"/);
@@ -29,6 +29,7 @@ export function runM3ControlPanelGuard() {
const app = readText("web/hwlab-cloud-web/app.mjs");
const styles = readText("web/hwlab-cloud-web/styles.css");
const artifactPublisher = readText("scripts/dev-artifact-publish.mjs");
const cloudWebRouteSource = readText("internal/dev-entrypoint/cloud-web-routes.mjs");
const browserSource = `${html}\n${app}\n${styles}`;
assert.equal(M3_IO_CONTROL_ROUTE, "/v1/m3/io", "M3 IO control route must remain exact");
@@ -41,7 +42,7 @@ export function runM3ControlPanelGuard() {
assertControlPanelDom(html);
assertControlPanelSource(app);
assertBrowserWriteBoundaries({ app, html, artifactPublisher });
assertBrowserWriteBoundaries({ app, html, artifactPublisher, cloudWebRouteSource });
assertNoStaticDevLiveClaim({ html, app });
for (const pattern of [
@@ -203,13 +204,14 @@ function assertControlPanelSource(app) {
assert.match(operationRecordBody, /frontendBypass=false/u, "operation record must show no frontend bypass");
}
function assertBrowserWriteBoundaries({ app, html, artifactPublisher }) {
function assertBrowserWriteBoundaries({ app, html, artifactPublisher, cloudWebRouteSource }) {
const browserSource = `${html}\n${app}`;
const sanitizedBrowserSource = browserSource.replace(/data-static-source-fallback="SOURCE"/gu, "");
const frontendGuard = checkFrontendNoDirectRuntimeCalls({
appSource: app,
htmlSource: html,
artifactPublisherSource: artifactPublisher
artifactPublisherSource: artifactPublisher,
cloudWebRouteSource
});
assert.equal(frontendGuard.status, "pass", frontendGuard.issues.join("\n"));
assert.deepEqual(