diff --git a/internal/dev-entrypoint/cloud-web-routes.mjs b/internal/dev-entrypoint/cloud-web-routes.mjs new file mode 100644 index 00000000..0f91eeda --- /dev/null +++ b/internal/dev-entrypoint/cloud-web-routes.mjs @@ -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, "") || "/"; +} diff --git a/internal/dev-entrypoint/http.test.mjs b/internal/dev-entrypoint/http.test.mjs index eb39249b..e34c5f83 100644 --- a/internal/dev-entrypoint/http.test.mjs +++ b/internal/dev-entrypoint/http.test.mjs @@ -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(); diff --git a/package.json b/package.json index bf2acda5..3311eeae 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "validate": "node scripts/repo-reports-guard.mjs && node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs", - "check": "node scripts/repo-reports-guard.mjs && node --check scripts/repo-reports-guard.mjs && node --check scripts/src/report-paths.mjs && node --check internal/protocol/index.mjs && node --check internal/build-metadata.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/code-agent-trace-store.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-cloud-api/provision.mjs && node --check cmd/hwlab-cloud-api/migrate.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.test.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.test.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/dev-runtime-base-image.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check tools/hwlab-cli/lib/cicd-jobs.mjs && node --check tools/hwlab-cli/lib/cli.test.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node cmd/hwlab-cloud-api/provision.mjs --check && node cmd/hwlab-cloud-api/migrate.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test web/hwlab-cloud-web/code-agent-facts.test.mjs web/hwlab-cloud-web/code-agent-status.test.mjs web/hwlab-cloud-web/wiring-status.test.mjs web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/validate-dev-m3-cardinality.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/refresh-artifact-catalog.test.mjs scripts/src/dev-cd-apply.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-provisioning.test.mjs scripts/src/dev-runtime-migration.test.mjs scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs scripts/src/dev-evidence-blocker-aggregator.test.mjs skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs tools/hwlab-cli/lib/cli.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/code-agent-session-registry.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", + "check": "node scripts/repo-reports-guard.mjs && node --check scripts/repo-reports-guard.mjs && node --check scripts/src/report-paths.mjs && node --check internal/protocol/index.mjs && node --check internal/build-metadata.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/cloud-web-routes.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/code-agent-trace-store.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-cloud-api/provision.mjs && node --check cmd/hwlab-cloud-api/migrate.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.test.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.test.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/dev-runtime-base-image.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check tools/hwlab-cli/lib/cicd-jobs.mjs && node --check tools/hwlab-cli/lib/cli.test.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node cmd/hwlab-cloud-api/provision.mjs --check && node cmd/hwlab-cloud-api/migrate.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test web/hwlab-cloud-web/code-agent-facts.test.mjs web/hwlab-cloud-web/code-agent-status.test.mjs web/hwlab-cloud-web/wiring-status.test.mjs web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/validate-dev-m3-cardinality.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/refresh-artifact-catalog.test.mjs scripts/src/dev-cd-apply.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-provisioning.test.mjs scripts/src/dev-runtime-migration.test.mjs scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs scripts/src/dev-evidence-blocker-aggregator.test.mjs skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs tools/hwlab-cli/lib/cli.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/code-agent-session-registry.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", "dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs", "dev-runtime-base:build": "node scripts/dev-runtime-base-image.mjs", "cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs", diff --git a/scripts/code-agent-chat-smoke.mjs b/scripts/code-agent-chat-smoke.mjs index 18da44d7..28483d11 100644 --- a/scripts/code-agent-chat-smoke.mjs +++ b/scripts/code-agent-chat-smoke.mjs @@ -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"); } diff --git a/scripts/dev-artifact-publish.mjs b/scripts/dev-artifact-publish.mjs index d21e64e1..604ee551 100644 --- a/scripts/dev-artifact-publish.mjs +++ b/scripts/dev-artifact-publish.mjs @@ -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", diff --git a/scripts/src/m3-io-control-e2e.mjs b/scripts/src/m3-io-control-e2e.mjs index 8fab0a25..a4a03558 100644 --- a/scripts/src/m3-io-control-e2e.mjs +++ b/scripts/src/m3-io-control-e2e.mjs @@ -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"); } diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 3c49fc0c..2b33fa60 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -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"/); diff --git a/web/hwlab-cloud-web/scripts/m3-control-panel-guard.mjs b/web/hwlab-cloud-web/scripts/m3-control-panel-guard.mjs index 0e50d013..dce8a59e 100644 --- a/web/hwlab-cloud-web/scripts/m3-control-panel-guard.mjs +++ b/web/hwlab-cloud-web/scripts/m3-control-panel-guard.mjs @@ -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(