Files
pikasTech-HWLAB/internal/cloud/server.mjs
T

2021 lines
66 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { existsSync, lstatSync, mkdirSync, symlinkSync } from "node:fs";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
buildMetadataFromEnv,
imageTagFromReference,
normalizeIsoTimestamp
} from "../build-metadata.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, ERROR_CODES, SERVICE_IDS } from "../protocol/index.mjs";
import {
AUDIT_FIELD_NAMES,
CLOUD_API_SERVICE_ID,
createAuditRecord
} from "../audit/index.mjs";
import {
SUPPORTED_RPC_METHODS,
createErrorEnvelope,
handleJsonRpcRequest
} from "./json-rpc.mjs";
import {
createCodeAgentErrorPayload,
describeCodeAgentAvailability,
handleCodeAgentChat
} from "./code-agent-chat.mjs";
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.mjs";
import { createCodeAgentSessionRegistry } from "./code-agent-session-registry.mjs";
import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.mjs";
import { createCodexStdioSessionManager } from "./codex-stdio-session.mjs";
import { buildGateDiagnosticsRows } from "./gate-diagnostics.mjs";
import {
applyRuntimeDbReadinessLayers,
buildDbRuntimeReadiness
} from "./db-contract.mjs";
import { buildCloudApiReadiness } from "./health-contract.mjs";
import {
M3_IO_CONTROL_ROUTE,
M3_STATUS_ROUTE,
describeM3IoControl,
describeM3IoControlLive,
describeM3StatusLive,
handleM3IoControl
} from "./m3-io-control.mjs";
import { createConfiguredCloudRuntimeStore } from "../db/runtime-store.mjs";
import { createGatewayDemoRegistry } from "./gateway-demo-registry.mjs";
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
const LIVE_BUILD_TIMEOUT_MS = 900;
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const LIVE_BUILD_METADATA_PATHS = Object.freeze({
deployManifest: "deploy/deploy.json",
artifactCatalog: "deploy/artifact-catalog.dev.json"
});
const LIVE_BUILD_SERVICE_DEFAULTS = Object.freeze({
"hwlab-cloud-api": Object.freeze({ urlEnv: null, defaultUrl: "http://127.0.0.1:6667" }),
"hwlab-cloud-web": Object.freeze({ urlEnv: "HWLAB_CLOUD_WEB_SERVICE_URL", defaultUrl: "http://hwlab-cloud-web.hwlab-dev.svc.cluster.local:8080" }),
"hwlab-agent-mgr": Object.freeze({ urlEnv: "HWLAB_AGENT_MGR_URL", defaultUrl: "http://hwlab-agent-mgr.hwlab-dev.svc.cluster.local:7410" }),
"hwlab-agent-worker": Object.freeze({ urlEnv: "HWLAB_AGENT_WORKER_URL", defaultUrl: "http://hwlab-agent-worker.hwlab-dev.svc.cluster.local:7411" }),
"hwlab-gateway": Object.freeze({ urlEnv: "HWLAB_GATEWAY_URL", defaultUrl: "http://hwlab-gateway.hwlab-dev.svc.cluster.local:7001" }),
"hwlab-gateway-simu": Object.freeze({ urlEnv: "HWLAB_GATEWAY_SIMU_URL", defaultUrl: "http://hwlab-gateway-simu.hwlab-dev.svc.cluster.local:7101" }),
"hwlab-box-simu": Object.freeze({ urlEnv: "HWLAB_BOX_SIMU_URL", defaultUrl: "http://hwlab-box-simu.hwlab-dev.svc.cluster.local:7201" }),
"hwlab-patch-panel": Object.freeze({ urlEnv: "HWLAB_PATCH_PANEL_URL", defaultUrl: "http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301" }),
"hwlab-router": Object.freeze({ urlEnv: "HWLAB_ROUTER_URL", defaultUrl: "http://hwlab-router.hwlab-dev.svc.cluster.local:7401" }),
"hwlab-tunnel-client": Object.freeze({ urlEnv: "HWLAB_TUNNEL_CLIENT_URL", defaultUrl: "http://hwlab-tunnel-client.hwlab-dev.svc.cluster.local:7402" }),
"hwlab-edge-proxy": Object.freeze({ urlEnv: "HWLAB_EDGE_PROXY_URL", defaultUrl: "http://hwlab-edge-proxy.hwlab-dev.svc.cluster.local:6667", healthPath: "/health" }),
"hwlab-cli": Object.freeze({ urlEnv: "HWLAB_CLI_URL", defaultUrl: "http://hwlab-cli.hwlab-dev.svc.cluster.local:7501" }),
"hwlab-agent-skills": Object.freeze({ urlEnv: "HWLAB_AGENT_SKILLS_URL", defaultUrl: "http://hwlab-agent-skills.hwlab-dev.svc.cluster.local:7430" })
});
const LIVE_BUILD_EXTERNAL_COMPONENTS = Object.freeze([
Object.freeze({
serviceId: "hwlab-frpc",
serviceName: "hwlab-frpc",
kind: "external",
externalImage: true,
defaultImage: "127.0.0.1:5000/hwlab/frpc:v0.68.1",
externalReason: "外部镜像或非 HWLAB 构建产物"
})
]);
export function createCloudApiServer(options = {}) {
const env = options.env ?? process.env;
ensureCodeAgentRuntimeBase(env);
const sessionRegistry = options.sessionRegistry || createCodeAgentSessionRegistry();
const traceStore = options.traceStore || defaultCodeAgentTraceStore;
const codexStdioManager = options.codexStdioManager || createCodexStdioSessionManager({ traceStore });
const codeAgentChatResults = options.codeAgentChatResults || createCodeAgentChatResultStore({
maxResults: parsePositiveInteger(env.HWLAB_CODE_AGENT_CHAT_RESULT_CACHE_SIZE, 256)
});
const runtimeStore = options.runtimeStore || createConfiguredCloudRuntimeStore({ ...options, env });
const gatewayRegistry = options.gatewayRegistry || createGatewayDemoRegistry({
staleMs: parsePositiveInteger(env.HWLAB_GATEWAY_DEMO_STALE_MS, 30000),
dispatchTimeoutMs: parsePositiveInteger(env.HWLAB_GATEWAY_DISPATCH_TIMEOUT_MS, 30000)
});
return createServer(async (request, response) => {
try {
await routeRequest(request, response, { ...options, env, runtimeStore, gatewayRegistry, sessionRegistry, traceStore, codexStdioManager, codeAgentChatResults });
} catch (error) {
sendJson(response, 500, {
error: {
code: "internal_error",
message: "hwlab-cloud-api request handling failed",
reason: error.message
}
});
}
});
}
export function ensureCodeAgentRuntimeBase(env = process.env) {
const workspace = env.HWLAB_CODE_AGENT_CODEX_WORKSPACE || env.HWLAB_CODE_AGENT_WORKSPACE || "/workspace/hwlab";
const codexHome = env.CODEX_HOME || env.HWLAB_CODE_AGENT_CODEX_HOME || "/codex-home";
const appCodexCommand = "/app/node_modules/.bin/codex";
const localCodexCommand = path.join(process.cwd(), "node_modules", ".bin", "codex");
const codexCommand = existsSync(appCodexCommand)
? appCodexCommand
: existsSync(localCodexCommand)
? localCodexCommand
: appCodexCommand;
env.HWLAB_CODE_AGENT_WORKSPACE ||= workspace;
env.HWLAB_CODE_AGENT_CODEX_WORKSPACE ||= workspace;
env.HWLAB_CODE_AGENT_CODEX_SANDBOX ||= "danger-full-access";
env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED ||= "1";
env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR ||= "repo-owned";
env.CODEX_HOME ||= codexHome;
env.HWLAB_CODE_AGENT_CODEX_COMMAND ||= codexCommand;
env.HWLAB_CODE_AGENT_SKILLS_DIRS ||= "/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills";
for (const target of [path.dirname(workspace), codexHome]) {
try {
mkdirSync(target, { recursive: true });
} catch {
// Runtime health reports the concrete missing/writable blocker.
}
}
try {
if (!existsSync(workspace)) {
if (workspace === "/workspace/hwlab" && existsSync("/app")) {
symlinkSync("/app", workspace, "dir");
} else {
mkdirSync(workspace, { recursive: true });
}
}
lstatSync(workspace);
} catch {
// Runtime health reports the concrete missing/writable blocker.
}
}
export async function buildHealthPayload(options = {}) {
const serviceId = CLOUD_API_SERVICE_ID;
const env = options.env ?? process.env;
ensureCodeAgentRuntimeBase(env);
const metadata = buildMetadataFromEnv(env, {
serviceId,
fallbackImageRepository: "ghcr.io/pikastech/hwlab-cloud-api"
});
const dbProbe = await buildDbRuntimeReadiness(env, options.dbProbe);
const codeAgent = await describeCodeAgentAvailability(env, options);
const runtime = await runtimeReadiness(options.runtimeStore ?? createConfiguredCloudRuntimeStore({ ...options, env }));
const db = applyRuntimeDbReadinessLayers(dbProbe, runtime);
const readiness = buildCloudApiReadiness({ db, codeAgent, runtime });
return {
serviceId,
environment: ENVIRONMENT_DEV,
status: readiness.status,
ready: readiness.ready,
revision: metadata.revision,
readiness,
blockers: readiness.blockers,
blockerCodes: readiness.blockerCodes,
service: {
id: serviceId,
role: "cloud-api",
healthPath: "/health",
livePath: "/health/live"
},
commit: metadata.commit,
image: metadata.image,
build: metadata.build,
endpoint: env.HWLAB_PUBLIC_ENDPOINT || DEV_ENDPOINT,
observedAt: new Date().toISOString(),
db,
codeAgent,
runtime
};
}
async function routeRequest(request, response, options) {
const url = new URL(request.url || "/", "http://hwlab-cloud-api.local");
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) {
sendJson(response, 200, await buildHealthPayload(options));
return;
}
if (request.method === "GET" && url.pathname === "/live") {
sendJson(response, 200, {
serviceId: CLOUD_API_SERVICE_ID,
status: "live"
});
return;
}
if (request.method === "POST" && (url.pathname === "/rpc" || url.pathname === "/json-rpc")) {
await handleRpcHttpRequest(request, response, options);
return;
}
if (url.pathname === "/v1" || url.pathname.startsWith("/v1/")) {
await handleRestAdapter(request, response, url, options);
return;
}
sendRestError(request, response, 404, "not_found", "Route is not part of the L1 cloud-api runtime", {
operation: `${request.method || "GET"} ${url.pathname}`,
target: {
type: "route",
id: url.pathname
}
});
}
async function handleRpcHttpRequest(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let envelope;
try {
envelope = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(
response,
400,
createErrorEnvelope({
id: "req_unassigned",
code: ERROR_CODES.parseError,
message: "Invalid JSON body",
data: {
reason: error.message
},
context: {
operation: "json_rpc.parse",
target: {
type: "http_route",
id: request.url || "/rpc"
},
result: "rejected"
}
})
);
return;
}
const rpcResponse = await handleJsonRpcRequest(envelope, {
adapter: "json-rpc",
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe,
m3IoRequestJson: options.m3IoRequestJson,
gatewayRegistry: options.gatewayRegistry
});
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
async function handleRestAdapter(request, response, url, options) {
if (request.method === "GET" && url.pathname === "/v1") {
const dbProbe = await buildDbRuntimeReadiness(options.env ?? process.env, options.dbProbe);
const codeAgent = await describeCodeAgentAvailability(options.env ?? process.env, options);
const runtime = await runtimeReadiness(options.runtimeStore);
const db = applyRuntimeDbReadinessLayers(dbProbe, runtime);
const readiness = buildCloudApiReadiness({ db, codeAgent, runtime });
sendJson(response, 200, {
serviceId: CLOUD_API_SERVICE_ID,
adapter: "rest",
status: readiness.status,
ready: readiness.ready,
rpcBridge: "POST /v1/rpc/{method}",
codeAgent,
methods: SUPPORTED_RPC_METHODS,
auditFields: AUDIT_FIELD_NAMES,
db,
runtime,
readiness,
blockers: readiness.blockers,
blockerCodes: readiness.blockerCodes,
m3IoControl: describeM3IoControl(options)
});
return;
}
if (request.method === "GET" && url.pathname === M3_IO_CONTROL_ROUTE) {
sendJson(response, 200, await describeM3IoControlLive(options));
return;
}
if (request.method === "GET" && url.pathname === M3_STATUS_ROUTE) {
sendJson(response, 200, await describeM3StatusLive(options));
return;
}
if (request.method === "GET" && url.pathname === "/v1/diagnostics/gate") {
sendJson(response, 200, await buildGateDiagnosticsRows(options));
return;
}
if (request.method === "GET" && url.pathname === "/v1/live-builds") {
sendJson(response, 200, await buildLiveBuildsPayload(options));
return;
}
if (request.method === "GET" && url.pathname === "/v1/gateway/sessions") {
sendJson(response, 200, options.gatewayRegistry.describe());
return;
}
if (request.method === "POST" && url.pathname === "/v1/gateway/poll") {
await handleGatewayPollHttp(request, response, options);
return;
}
if (request.method === "POST" && url.pathname === "/v1/gateway/result") {
await handleGatewayResultHttp(request, response, options);
return;
}
if (request.method === "POST" && url.pathname === M3_IO_CONTROL_ROUTE) {
await handleM3IoControlHttp(request, response, options);
return;
}
if (request.method === "POST" && url.pathname === "/v1/agent/chat") {
await handleCodeAgentChatHttp(request, response, options);
return;
}
if (request.method === "GET" && url.pathname.startsWith("/v1/agent/chat/result/")) {
await handleCodeAgentChatResultHttp(request, response, url, options);
return;
}
if (request.method === "POST" && url.pathname === "/v1/agent/chat/cancel") {
await handleCodeAgentCancelHttp(request, response, options);
return;
}
if (request.method === "GET" && url.pathname.startsWith("/v1/agent/chat/trace/")) {
await handleCodeAgentTraceHttp(request, response, url, options);
return;
}
if (request.method !== "POST" || !url.pathname.startsWith("/v1/rpc/")) {
sendRestError(request, response, 404, "not_found", "REST route is not implemented in the L1 cloud-api runtime", {
operation: `${request.method || "GET"} ${url.pathname}`,
target: {
type: "route",
id: url.pathname
}
});
return;
}
const method = decodeURIComponent(url.pathname.slice("/v1/rpc/".length));
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendRestError(request, response, 400, "parse_error", "Invalid JSON body", {
operation: method,
target: {
type: "rpc_method",
id: method
},
result: "rejected",
reason: error.message
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendRestError(request, response, 400, "invalid_params", "REST RPC bridge body must be a JSON object", {
operation: method,
target: {
type: "rpc_method",
id: method
},
result: "rejected"
});
return;
}
const rpcResponse = await handleJsonRpcRequest(
{
jsonrpc: "2.0",
id: getHeader(request, "x-request-id") || `req_${randomUUID()}`,
method,
params,
meta: {
traceId: getHeader(request, "x-trace-id") || `trc_${randomUUID()}`,
actorId: getHeader(request, "x-actor-id"),
serviceId: getHeader(request, "x-source-service-id") || CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV
}
},
{
adapter: "rest",
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe,
m3IoRequestJson: options.m3IoRequestJson,
gatewayRegistry: options.gatewayRegistry
}
);
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
async function buildLiveBuildsPayload(options = {}) {
const env = options.env ?? process.env;
const metadata = await loadLiveBuildMetadata(options);
const inventory = liveBuildServiceInventory(metadata);
const services = await Promise.all(
inventory.map((service) => observeLiveBuildService(service, { ...options, env }))
);
const hwlabServices = services.filter((service) => service.kind === "hwlab");
const latest = [...hwlabServices]
.filter((service) => service.build.createdAt)
.sort((left, right) => Date.parse(right.build.createdAt) - Date.parse(left.build.createdAt))[0] ?? null;
return {
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
status: "ok",
contractVersion: "live-builds-v1",
observedAt: new Date().toISOString(),
source: {
kind: "live-health+deploy-artifact-catalog",
route: "/v1/live-builds",
healthPath: "/health/live",
metadataPaths: LIVE_BUILD_METADATA_PATHS,
desiredStateSource: metadata.deployManifest.status === "ok" ? LIVE_BUILD_METADATA_PATHS.deployManifest : "unavailable",
artifactCatalogSource: metadata.artifactCatalog.status === "ok" ? LIVE_BUILD_METADATA_PATHS.artifactCatalog : "unavailable",
note: "Each row prefers current live service health build metadata and falls back only to deploy/artifact catalog metadata when the live tag/revision matches; observedAt is not used as build time."
},
latest,
counts: {
total: services.length,
hwlab: hwlabServices.length,
withBuildTime: hwlabServices.filter((service) => Boolean(service.build.createdAt)).length,
unavailable: hwlabServices.filter((service) => service.build.createdAt === null).length,
external: services.filter((service) => service.kind === "external").length
},
services
};
}
async function observeLiveBuildService(service, options) {
if (service.externalImage) {
return externalLiveBuildRecord(service, {
healthUrl: null,
httpStatus: null,
imageReference: service.defaultImage ?? "unknown",
reason: "外部镜像或非 HWLAB 构建产物"
});
}
const metadataRecords = liveBuildRecordsFromMetadata(service);
const metadataRecord = metadataRecords[0] ?? null;
if (service.serviceId === CLOUD_API_SERVICE_ID) {
const health = await buildHealthPayload(options);
return liveBuildRecordFromHealth(service, {
ok: true,
status: 200,
url: "/health/live",
payload: health
}, metadataRecords);
}
const baseUrl = String(options.env?.[service.urlEnv] || service.defaultUrl || "").replace(/\/+$/u, "");
if (!baseUrl) {
return metadataRecord
? metadataRecordWithHealthGap(metadataRecord, {
service,
healthUrl: null,
httpStatus: null,
reason: `${service.serviceId} 没有配置 live health URL`
})
: unavailableLiveBuildRecord(service, {
reasonCode: "service_url_unavailable",
reason: `${service.serviceId} 没有配置 live health URL`
});
}
const healthPath = service.healthPath ?? "/health/live";
return liveBuildRecordFromHealth(
service,
await fetchServiceHealth(`${baseUrl}${healthPath}`, options.fetchImpl ?? fetch),
metadataRecords
);
}
async function fetchServiceHealth(url, fetchImpl) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), LIVE_BUILD_TIMEOUT_MS);
try {
const response = await fetchImpl(url, {
headers: { Accept: "application/json" },
signal: controller.signal
});
const text = await response.text();
let payload = null;
try {
payload = text ? JSON.parse(text) : null;
} catch {
return {
ok: false,
status: response.status,
url,
reasonCode: "invalid_json",
reason: `${url} returned non-JSON health payload`
};
}
return {
ok: response.ok,
status: response.status,
url,
payload,
reasonCode: response.ok ? null : "http_error",
reason: response.ok ? null : `${url} returned HTTP ${response.status}`
};
} catch (error) {
const timedOut = error?.name === "AbortError";
return {
ok: false,
status: null,
url,
reasonCode: timedOut ? "timeout" : "request_failed",
reason: timedOut ? `${url} timed out after ${LIVE_BUILD_TIMEOUT_MS}ms` : error.message
};
} finally {
clearTimeout(timeout);
}
}
function liveBuildRecordFromHealth(service, result, metadataRecords = []) {
const candidates = Array.isArray(metadataRecords) ? metadataRecords : [metadataRecords].filter(Boolean);
const metadataRecord = candidates[0] ?? null;
if (!result.ok) {
const reason = healthUnavailableReason(service, result.reason ?? `${service.serviceId} health unavailable`);
return metadataRecord
? metadataRecordWithHealthGap(metadataRecord, {
service,
healthUrl: result.url,
httpStatus: result.status,
reason
})
: unavailableLiveBuildRecord(service, {
healthUrl: result.url,
httpStatus: result.status,
reasonCode: result.reasonCode ?? "health_unavailable",
reason
});
}
const payload = result.payload ?? {};
const payloadServiceId = payload.serviceId ?? service.serviceId;
if (!String(payloadServiceId).startsWith("hwlab-")) {
return externalLiveBuildRecord(service, {
healthUrl: result.url,
httpStatus: result.status,
imageReference: payload.image?.reference ?? payload.image ?? "unknown",
imageTag: payload.image?.tag ?? null,
imageDigest: payload.image?.digest ?? "unknown",
commitId: payload.commit?.id ?? payload.revision ?? payload.version ?? "unknown",
commitSource: payload.commit?.source ?? "external-health",
revision: payload.revision ?? payload.commit?.id ?? payload.version ?? "unknown",
reason: "外部镜像或非 HWLAB 构建产物"
});
}
const imageReference = payload.image?.reference ?? payload.image ?? "unknown";
const commitId = payload.commit?.id ?? payload.revision ?? "unknown";
const liveIdentity = liveBuildIdentityFromHealth({ payload, imageReference, commitId });
const createdAt = normalizeHealthBuildTime(payload);
if (createdAt) {
return liveBuildHealthPayloadRecord(service, result, {
payload,
imageReference,
commitId,
createdAt,
metadataSource: payload.build?.metadataSource ?? "health:build.createdAt",
unavailableReason: null
});
}
const matched = selectMatchingMetadataRecord(liveIdentity, candidates);
if (matched.record?.build?.createdAt) {
return mergeLiveHealthOntoMetadataRecord(matched.record, {
match: matched.match,
liveIdentity,
service,
result,
payload,
imageReference,
commitId
});
}
const mismatch = matched.match ?? liveBuildMetadataMatch(liveIdentity, metadataRecord);
const unavailableReason = matched.record
? "构建时间不可用:当前 live 镜像未提供 buildCreatedAt"
: metadataRecord
? `构建时间不可用:live tag 与 ${controlledMetadataLabel(metadataRecord)} 不匹配`
: "构建时间不可用:当前 live 镜像未提供 buildCreatedAt";
return liveBuildHealthPayloadRecord(service, result, {
payload,
imageReference,
commitId,
createdAt: null,
metadataSource: matched.record ? "live-health:matched-controlled-metadata-without-build-time" : metadataRecord ? "live-health:controlled-metadata-mismatch" : "live-health:build-created-at-missing",
unavailableReason,
metadataMismatch: mismatch,
unavailableDetail: matched.record
? `live tag ${liveIdentity.imageTag} / revision ${liveIdentity.revision} 已匹配 ${controlledMetadataLabel(matched.record)},但该 metadata 没有 buildCreatedAtmetadata tag ${matched.record.image.tag}metadata revision ${matched.record.revision},来源 ${matched.record.build.metadataSource}`
: metadataRecord
? `未使用可能过期的构建时间:live tag ${liveIdentity.imageTag}live revision ${liveIdentity.revision}metadata tag ${metadataRecord.image.tag}metadata revision ${metadataRecord.revision},来源 ${metadataRecord.build.metadataSource}`
: "health payload 缺少 build.createdAt / image.createdAt / buildCreatedAtdeploy/artifact catalog 也没有可匹配的 buildCreatedAt"
});
}
function liveBuildHealthPayloadRecord(service, result, {
payload,
imageReference,
commitId,
createdAt,
metadataSource,
unavailableReason,
metadataMismatch = null,
unavailableDetail = null
}) {
return {
serviceId: service.serviceId,
name: service.serviceName,
kind: "hwlab",
status: createdAt ? "ok" : "build_time_unavailable",
healthUrl: result.url,
httpStatus: result.status,
desiredState: desiredStateSummary(service),
build: {
createdAt,
source: payload.build?.source ?? payload.commit?.source ?? "health-payload",
metadataSource,
unavailableReason,
unavailableDetail,
metadataMismatch
},
image: {
reference: imageReference,
tag: payload.image?.tag ?? imageTagFromReference(imageReference) ?? "unknown",
digest: payload.image?.digest ?? "unknown"
},
commit: {
id: commitId,
source: payload.commit?.source ?? "health-payload"
},
revision: payload.revision ?? commitId
};
}
function metadataRecordWithHealthGap(metadataRecord, { service, healthUrl, httpStatus, reason }) {
const metadataTag = metadataRecord.image?.tag ?? "unknown";
const metadataRevision = metadataRecord.revision ?? metadataRecord.commit?.id ?? "unknown";
return {
...metadataRecord,
healthUrl,
httpStatus,
status: "build_time_unavailable",
build: {
...metadataRecord.build,
createdAt: null,
metadataSource: "live-health-unavailable",
unavailableReason: `构建时间不可用:当前 live health 不可用,无法确认 ${service.serviceId} live tag/revision 是否匹配 deploy/artifact catalog metadata,未使用可能过期的构建时间(metadata tag ${metadataTag}metadata revision ${metadataRevision});${reason}`
}
};
}
function mergeLiveHealthOntoMetadataRecord(metadataRecord, { match, liveIdentity, service, result, payload, imageReference, commitId }) {
return {
...metadataRecord,
serviceId: service.serviceId,
name: service.serviceName,
kind: "hwlab",
status: "ok",
healthUrl: result.url,
httpStatus: result.status,
build: {
...metadataRecord.build,
metadataSource: metadataRecord.build.metadataSource,
unavailableReason: null,
liveMetadataMatch: match,
liveHealthMissingReason: `health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用与 live tag/revision 匹配的 ${controlledMetadataLabel(metadataRecord)}live tag ${liveIdentity.imageTag}`
},
image: {
reference: imageReference !== "unknown" ? imageReference : metadataRecord.image.reference,
tag: payload.image?.tag ?? imageTagFromReference(imageReference) ?? metadataRecord.image.tag,
digest: payload.image?.digest ?? metadataRecord.image.digest
},
commit: {
id: commitId !== "unknown" ? commitId : metadataRecord.commit.id,
source: payload.commit?.source ?? metadataRecord.commit.source
},
revision: payload.revision ?? (commitId !== "unknown" ? commitId : metadataRecord.revision)
};
}
function liveBuildIdentityFromHealth({ payload, imageReference, commitId }) {
return {
imageReference,
imageTag: payload.image?.tag ?? imageTagFromReference(imageReference) ?? "unknown",
commitId,
revision: payload.revision ?? commitId,
digest: payload.image?.digest ?? "unknown"
};
}
function selectMatchingMetadataRecord(liveIdentity, metadataRecords) {
let firstMatched = null;
for (const record of metadataRecords) {
const match = liveBuildMetadataMatch(liveIdentity, record);
if (!match.matched) continue;
if (record.build?.createdAt) return { record, match };
firstMatched ??= { record, match };
}
if (firstMatched) return firstMatched;
return {
record: null,
match: metadataRecords.length ? liveBuildMetadataMatch(liveIdentity, metadataRecords[0]) : null
};
}
function liveBuildMetadataMatch(liveIdentity, metadataRecord) {
if (!metadataRecord) {
return {
matched: false,
matchedValues: [],
live: liveBuildIdentitySummary(liveIdentity),
metadata: null
};
}
const liveValues = uniqueStrings([
liveIdentity.imageTag,
imageTagFromReference(liveIdentity.imageReference),
liveIdentity.revision,
liveIdentity.commitId,
liveIdentity.digest
]).filter((value) => value !== "unknown");
const metadataValues = uniqueStrings([
metadataRecord.image?.tag,
imageTagFromReference(metadataRecord.image?.reference),
metadataRecord.revision,
metadataRecord.commit?.id,
metadataRecord.image?.digest
]).filter((value) => value !== "unknown" && value !== "not_published");
const matchedValues = liveValues.filter((liveValue) =>
metadataValues.some((metadataValue) => liveBuildIdentityEquivalent(liveValue, metadataValue))
);
return {
matched: matchedValues.length > 0,
matchedValues,
live: liveBuildIdentitySummary(liveIdentity),
metadata: {
imageTag: metadataRecord.image?.tag ?? "unknown",
revision: metadataRecord.revision ?? "unknown",
commitId: metadataRecord.commit?.id ?? "unknown",
digest: metadataRecord.image?.digest ?? "unknown"
}
};
}
function liveBuildIdentitySummary(identity) {
return {
imageTag: identity?.imageTag ?? "unknown",
revision: identity?.revision ?? "unknown",
commitId: identity?.commitId ?? "unknown",
digest: identity?.digest ?? "unknown"
};
}
function liveBuildIdentityEquivalent(left, right) {
const a = String(left ?? "").trim();
const b = String(right ?? "").trim();
if (!a || !b || a === "unknown" || b === "unknown") return false;
if (a === b) return true;
if (a.startsWith("sha256:") || b.startsWith("sha256:")) return false;
return a.length >= 7 && b.length >= 7 && (a.startsWith(b) || b.startsWith(a));
}
function liveBuildRecordsFromMetadata(service) {
return [
liveBuildRecordFromMetadataSource(service, "artifact"),
liveBuildRecordFromMetadataSource(service, "deploy")
].filter(Boolean);
}
function liveBuildRecordFromMetadataSource(service, sourceKind) {
const source = sourceKind === "artifact"
? service.artifact
: service.deploy;
if (!source) return null;
const createdAt = normalizeIsoTimestamp(
sourceKind === "deploy" ? source.env?.HWLAB_BUILD_CREATED_AT : source.buildCreatedAt
);
const imageReference = sourceKind === "deploy"
? source.env?.HWLAB_IMAGE ?? source.image ?? "unknown"
: source.image ?? service.deploy?.image ?? "unknown";
const commitId = sourceKind === "artifact"
? source.commitId ?? imageTagFromReference(imageReference) ?? "unknown"
: source.env?.HWLAB_COMMIT_ID ?? imageTagFromReference(imageReference) ?? "unknown";
const digest = sourceKind === "deploy"
? source.env?.HWLAB_IMAGE_DIGEST ?? "unknown"
: source.digest ?? "unknown";
const buildSource = sourceKind === "deploy"
? source.env?.HWLAB_BUILD_SOURCE ?? "deploy-desired-state-env"
: source.buildSource ?? "artifact-catalog-metadata";
const imageTag = sourceKind === "deploy"
? source.env?.HWLAB_IMAGE_TAG ?? imageTagFromReference(imageReference) ?? "unknown"
: source.imageTag ?? imageTagFromReference(imageReference) ?? "unknown";
const revision = sourceKind === "deploy"
? source.env?.HWLAB_REVISION ?? commitId
: commitId;
return {
serviceId: service.serviceId,
name: service.serviceName,
kind: "hwlab",
status: createdAt ? "ok" : "build_time_unavailable",
healthUrl: null,
httpStatus: null,
desiredState: desiredStateSummary(service),
build: {
createdAt,
source: buildSource,
metadataSource: artifactMetadataSource(sourceKind),
unavailableReason: createdAt
? null
: `构建时间不可用:${controlledMetadataLabel({ build: { metadataSource: artifactMetadataSource(sourceKind) } })} 缺少 buildCreatedAt`
},
image: {
reference: imageReference,
tag: imageTag,
digest
},
commit: {
id: commitId,
source: sourceKind === "artifact" ? "artifact-catalog" : "deploy-desired-state"
},
revision
};
}
function desiredStateSummary(service) {
return {
namespace: service.deploy?.namespace ?? service.artifact?.namespace ?? "hwlab-dev",
profile: service.deploy?.profile ?? service.artifact?.profile ?? ENVIRONMENT_DEV,
replicas: Number.isFinite(Number(service.deploy?.replicas)) ? Number(service.deploy.replicas) : null,
healthPath: service.healthPath ?? service.deploy?.healthPath ?? service.artifact?.healthPath ?? "/health/live",
source: service.deploy ? LIVE_BUILD_METADATA_PATHS.deployManifest : "service-defaults"
};
}
function artifactMetadataSource(sourceKind) {
if (sourceKind === "artifact") return "artifact-catalog:buildCreatedAt";
return "deploy-env:HWLAB_BUILD_CREATED_AT";
}
function controlledMetadataLabel(record) {
const source = String(record?.build?.metadataSource ?? "");
if (source.includes("artifact-catalog")) return "catalog metadata";
if (source.includes("deploy-env")) return "deploy metadata";
return "controlled metadata";
}
function unavailableLiveBuildRecord(service, { healthUrl = null, httpStatus = null, reasonCode, reason }) {
return {
serviceId: service.serviceId,
name: service.serviceName,
kind: "hwlab",
status: "unavailable",
healthUrl,
httpStatus,
desiredState: desiredStateSummary(service),
build: {
createdAt: null,
metadataSource: "unavailable",
unavailableReason: `构建时间不可用:${reason}`
},
image: {
reference: "unknown",
tag: "unknown",
digest: "unknown"
},
commit: {
id: "unknown",
source: "unavailable"
},
revision: "unknown",
error: {
code: reasonCode,
message: reason
}
};
}
function externalLiveBuildRecord(service, {
healthUrl = null,
httpStatus = null,
imageReference = "unknown",
imageTag = null,
imageDigest = "unknown",
commitId = "unknown",
commitSource = "external-image",
revision = commitId,
reason
}) {
return {
serviceId: service.serviceId,
name: service.serviceName,
kind: "external",
status: "external",
healthUrl,
httpStatus,
build: {
createdAt: null,
metadataSource: "external-image",
unavailableReason: reason
},
image: {
reference: imageReference,
tag: imageTag ?? imageTagFromReference(imageReference) ?? "unknown",
digest: imageDigest
},
commit: {
id: commitId,
source: commitSource
},
revision
};
}
function healthUnavailableReason(service, reason) {
if (Number(service.deploy?.replicas) === 0) {
return `${service.serviceId} 当前 hwlab-dev desired replicas=0,无可读取的常驻 /health/live${reason}`;
}
if (service.activation === "manual") {
return `${service.serviceId} 是手动激活服务,当前 live health 不可用:${reason}`;
}
return reason;
}
function normalizeHealthBuildTime(payload) {
return normalizeIsoTimestamp(
payload?.build?.createdAt ??
payload?.image?.createdAt ??
payload?.buildCreatedAt ??
payload?.metadata?.buildCreatedAt ??
null
);
}
async function loadLiveBuildMetadata(options = {}) {
if (options.liveBuildMetadata) return normalizeLiveBuildMetadata(options.liveBuildMetadata);
const root = options.repoRoot ?? process.env.HWLAB_REPO_ROOT ?? repoRoot;
const [deployManifest, artifactCatalog] = await Promise.all([
readJsonMetadata(root, LIVE_BUILD_METADATA_PATHS.deployManifest),
readJsonMetadata(root, LIVE_BUILD_METADATA_PATHS.artifactCatalog)
]);
return normalizeLiveBuildMetadata({ deployManifest, artifactCatalog });
}
function normalizeLiveBuildMetadata(metadata) {
return {
deployManifest: normalizeMetadataFile(metadata.deployManifest),
artifactCatalog: normalizeMetadataFile(metadata.artifactCatalog)
};
}
async function readJsonMetadata(root, relativePath) {
try {
const payload = JSON.parse(await readFile(path.join(root, relativePath), "utf8"));
return { status: "ok", path: relativePath, payload };
} catch (error) {
return {
status: "unavailable",
path: relativePath,
error: error.message,
payload: null
};
}
}
function normalizeMetadataFile(file) {
if (file?.status === "ok" || file?.status === "unavailable") {
return {
status: file.status,
path: file.path ?? null,
error: file.error ?? null,
payload: file.payload ?? null
};
}
return {
status: file ? "ok" : "unavailable",
path: null,
error: null,
payload: file ?? null
};
}
function liveBuildServiceInventory(metadata) {
const deployServices = Array.isArray(metadata.deployManifest.payload?.services)
? metadata.deployManifest.payload.services
: [];
const deployByServiceId = new Map(deployServices.map((service) => [service.serviceId, service]));
const artifactByServiceId = new Map(
(Array.isArray(metadata.artifactCatalog.payload?.services) ? metadata.artifactCatalog.payload.services : [])
.map((service) => [service.serviceId, service])
);
const serviceIds = uniqueStrings([
...SERVICE_IDS,
...deployServices.map((service) => service.serviceId),
...artifactByServiceId.keys()
]).filter((serviceId) => String(serviceId).startsWith("hwlab-"));
return [
...serviceIds.map((serviceId) => {
const deploy = deployByServiceId.get(serviceId) ?? null;
const defaults = LIVE_BUILD_SERVICE_DEFAULTS[serviceId] ?? {};
const healthPath = deploy?.healthPath ?? defaults.healthPath ?? "/health/live";
return Object.freeze({
serviceId,
serviceName: deploy?.name ?? serviceId,
urlEnv: defaults.urlEnv ?? serviceUrlEnvName(serviceId),
defaultUrl: defaults.defaultUrl ?? serviceDefaultUrl(serviceId),
healthPath,
activation: deploy?.replicas === 0 ? "zero-replica" : defaults.activation,
deploy,
artifact: artifactByServiceId.get(serviceId) ?? null
});
}),
...LIVE_BUILD_EXTERNAL_COMPONENTS
];
}
function uniqueStrings(values) {
return [...new Set(values.map((value) => String(value ?? "").trim()).filter(Boolean))];
}
function serviceUrlEnvName(serviceId) {
return `${serviceId.replace(/^hwlab-/u, "HWLAB_").replace(/-/gu, "_").toUpperCase()}_URL`;
}
function serviceDefaultUrl(serviceId) {
const port = {
"hwlab-cloud-api": 6667,
"hwlab-cloud-web": 8080,
"hwlab-agent-mgr": 7410,
"hwlab-agent-worker": 7411,
"hwlab-gateway": 7001,
"hwlab-gateway-simu": 7101,
"hwlab-box-simu": 7201,
"hwlab-patch-panel": 7301,
"hwlab-router": 7401,
"hwlab-tunnel-client": 7402,
"hwlab-edge-proxy": 6667,
"hwlab-cli": 7501,
"hwlab-agent-skills": 7430
}[serviceId] ?? 8080;
return `http://${serviceId}.hwlab-dev.svc.cluster.local:${port}`;
}
async function handleGatewayPollHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
accepted: false,
error: {
code: "parse_error",
message: "Invalid JSON body",
reason: error.message
}
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendJson(response, 400, {
accepted: false,
error: {
code: "invalid_params",
message: "gateway poll body must be a JSON object"
}
});
return;
}
const session = options.gatewayRegistry.updateSession(params);
await persistGatewayDemoSession({
runtimeStore: options.runtimeStore,
session,
request,
env: options.env
});
const outbound = options.gatewayRegistry.nextRequest(session.gatewaySessionId);
sendJson(response, 200, {
accepted: true,
serviceId: CLOUD_API_SERVICE_ID,
mode: "gateway-outbound-poll-demo",
gatewayId: session.gatewayId,
gatewaySessionId: session.gatewaySessionId,
queueDepth: session.queue.length,
request: outbound,
type: outbound ? "request" : "noop",
observedAt: new Date().toISOString()
});
}
async function handleGatewayResultHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
accepted: false,
error: {
code: "parse_error",
message: "Invalid JSON body",
reason: error.message
}
});
return;
}
const result = options.gatewayRegistry.complete(params);
sendJson(response, result.accepted ? 200 : 404, {
...result,
serviceId: CLOUD_API_SERVICE_ID,
mode: "gateway-outbound-poll-demo",
observedAt: new Date().toISOString()
});
}
async function persistGatewayDemoSession({ runtimeStore, session, request, env = process.env }) {
if (!runtimeStore) return;
const refreshMs = parsePositiveInteger(env.HWLAB_GATEWAY_REGISTRATION_REFRESH_MS, 60000);
if (session.runtimeRegisteredEpochMs && Date.now() - session.runtimeRegisteredEpochMs < refreshMs) {
return;
}
const meta = {
traceId: getHeader(request, "x-trace-id") || `trc_gateway_poll_${randomUUID()}`,
actorId: `svc_${session.serviceId}`,
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV
};
try {
await runtimeStore.registerGatewaySession?.({
projectId: session.projectId,
gatewaySessionId: session.gatewaySessionId,
serviceId: "hwlab-gateway",
gatewayId: session.gatewayId,
endpoint: session.endpoint,
status: "connected",
labels: {
demoOutboundPoll: "true",
os: String(session.system?.platform ?? "unknown")
}
}, meta);
await runtimeStore.registerBoxResource?.({
projectId: session.projectId,
gatewaySessionId: session.gatewaySessionId,
resourceId: session.resourceId,
boxId: session.boxId,
resourceType: "simulator_endpoint",
state: "available",
metadata: {
serviceId: "hwlab-gateway",
demoShellHost: true
}
}, meta);
if (session.capabilities.length > 0) {
await runtimeStore.reportBoxCapabilities?.({
capabilities: session.capabilities.map((capability) => ({
...capability,
resourceId: capability.resourceId ?? session.resourceId,
projectId: capability.projectId ?? session.projectId
}))
}, meta);
}
session.runtimeRegisteredAt = new Date().toISOString();
session.runtimeRegisteredEpochMs = Date.now();
} catch {
// Demo polling must keep the gateway online even when durable runtime writes are not ready.
}
}
async function handleCodeAgentChatHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
...createCodeAgentErrorPayload({
code: "parse_error",
message: "Invalid JSON body",
reason: error.message,
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
model: options.env?.HWLAB_CODE_AGENT_MODEL || options.env?.OPENAI_MODEL || process.env.HWLAB_CODE_AGENT_MODEL || process.env.OPENAI_MODEL || "unknown",
layer: "api",
retryable: true
})
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendJson(response, 400, {
...createCodeAgentErrorPayload({
code: "invalid_params",
message: "Code Agent chat body must be a JSON object",
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
model: options.env?.HWLAB_CODE_AGENT_MODEL || options.env?.OPENAI_MODEL || process.env.HWLAB_CODE_AGENT_MODEL || process.env.OPENAI_MODEL || "unknown",
layer: "api",
retryable: true
})
});
return;
}
const traceId = safeTraceId(getHeader(request, "x-trace-id") || params.traceId) || `trc_${randomUUID()}`;
const chatParams = {
...params,
traceId
};
if (codeAgentChatShortConnectionRequested(request, params, options)) {
submitCodeAgentChatTurn({
params: chatParams,
options,
traceId
});
const traceUrl = `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`;
sendJson(response, 202, {
accepted: true,
status: "running",
shortConnection: true,
controlSemantics: "submit-and-poll",
traceId,
conversationId: safeConversationId(params.conversationId) || null,
sessionId: safeSessionId(params.sessionId) || null,
traceUrl,
resultUrl: `/v1/agent/chat/result/${encodeURIComponent(traceId)}`,
streamUrl: `${traceUrl}/stream`,
cancelUrl: "/v1/agent/chat/cancel",
polling: {
resultIntervalMs: parsePositiveInteger(options.env?.HWLAB_CODE_AGENT_RESULT_POLL_INTERVAL_MS, 1000),
traceIntervalMs: parsePositiveInteger(options.env?.HWLAB_CODE_AGENT_TRACE_POLL_INTERVAL_MS, 1000)
},
runnerTrace: (options.traceStore ?? defaultCodeAgentTraceStore).snapshot(traceId)
});
return;
}
const payload = await runCodeAgentChat(chatParams, options);
sendJson(response, payload.status === "failed" && payload.error?.code === "invalid_params" ? 400 : 200, payload);
}
function runCodeAgentChat(params, options) {
return handleCodeAgentChat(params, codeAgentChatExecutionOptions(options));
}
function codeAgentChatExecutionOptions(options = {}) {
return {
callProvider: options.callCodeAgentProvider,
timeoutMs: options.codeAgentTimeoutMs,
hardTimeoutMs: options.codeAgentHardTimeoutMs,
env: options.env,
workspace: options.workspace,
skillsDirs: options.skillsDirs,
skillsDirsExact: options.skillsDirsExact,
skillsDiscoveryDelayMs: options.skillsDiscoveryDelayMs,
sessionRegistry: options.sessionRegistry,
m3IoSkillRequestJson: options.m3IoSkillRequestJson ?? createCodeAgentM3HwlabApiRequestJson(options),
codexStdioManager: options.codexStdioManager,
traceStore: options.traceStore,
gatewayRegistry: options.gatewayRegistry
};
}
function submitCodeAgentChatTurn({ params, options, traceId }) {
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
const results = options.codeAgentChatResults ?? createCodeAgentChatResultStore();
results.set(traceId, {
accepted: true,
status: "running",
traceId,
conversationId: safeConversationId(params.conversationId) || null,
sessionId: safeSessionId(params.sessionId) || null,
updatedAt: new Date().toISOString()
});
traceStore.ensure(traceId, {
runnerKind: "codex-app-server-stdio-runner",
workspace: options.workspace ?? options.env?.HWLAB_CODE_AGENT_CODEX_WORKSPACE ?? options.env?.HWLAB_CODE_AGENT_WORKSPACE ?? null,
sandbox: options.env?.HWLAB_CODE_AGENT_CODEX_SANDBOX ?? null,
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session"
});
traceStore.append(traceId, {
type: "request",
status: "accepted",
label: "request:accepted-short-connection",
message: "Code Agent request accepted; client must poll trace/result with short HTTP requests.",
waitingFor: "codex-stdio"
});
const run = async () => {
try {
const payload = await runCodeAgentChat(params, options);
if (isCodeAgentResultCanceled(results.get(traceId))) return;
results.set(traceId, payload);
traceStore.append(traceId, {
type: "result",
status: payload.status === "completed" ? "completed" : "failed",
label: `result:${payload.status}`,
message: payload.status === "completed"
? "Code Agent result is ready for short-connection polling."
: payload.error?.message ?? "Code Agent completed without a successful reply.",
sessionId: payload.sessionId ?? payload.session?.sessionId,
sessionStatus: payload.session?.status,
terminal: true
});
} catch (error) {
if (isCodeAgentResultCanceled(results.get(traceId))) return;
const payload = {
status: "failed",
traceId,
conversationId: safeConversationId(params.conversationId) || null,
sessionId: safeSessionId(params.sessionId) || null,
error: {
code: "code_agent_background_failed",
layer: "api",
retryable: true,
message: error?.message ?? "Code Agent background turn failed",
userMessage: "Code Agent 后台任务失败;trace/result 轮询已保留错误。"
},
updatedAt: new Date().toISOString()
};
results.set(traceId, payload);
traceStore.append(traceId, {
type: "result",
status: "failed",
label: "result:failed",
errorCode: payload.error.code,
message: payload.error.message,
terminal: true
});
}
};
setImmediate(() => {
run();
});
}
function codeAgentChatShortConnectionRequested(request, params, options = {}) {
const header = firstHeaderValue(request, "prefer");
const explicit = firstHeaderValue(request, "x-hwlab-short-connection");
const envValue = options.env?.HWLAB_CODE_AGENT_CHAT_SHORT_CONNECTION;
return /(?:^|,)\s*respond-async\s*(?:,|$)/iu.test(String(header ?? "")) ||
truthyFlag(explicit) ||
params?.shortConnection === true ||
truthyFlag(envValue);
}
async function handleCodeAgentChatResultHttp(request, response, url, options) {
const parts = url.pathname.split("/").filter(Boolean);
const traceId = decodeURIComponent(parts[4] ?? "");
if (!safeTraceId(traceId)) {
sendJson(response, 400, {
error: {
code: "invalid_trace_id",
message: "traceId must start with trc_ and contain only safe identifier characters"
}
});
return;
}
const results = options.codeAgentChatResults;
const result = results?.get(traceId) ?? null;
if (result && result.status !== "running") {
sendJson(response, 200, result);
return;
}
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
const runnerTrace = traceStore.snapshot(traceId);
if (result || runnerTrace.status !== "missing") {
sendJson(response, 202, {
accepted: true,
status: "running",
shortConnection: true,
traceId,
runnerTrace,
waitingFor: runnerTrace.waitingFor ?? "codex-stdio"
});
return;
}
sendJson(response, 404, {
error: {
code: "code_agent_result_not_found",
message: `No Code Agent result is registered for ${traceId}`
}
});
}
async function handleCodeAgentCancelHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
...createCodeAgentErrorPayload({
code: "parse_error",
message: "Invalid JSON body",
reason: error.message,
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
layer: "api",
retryable: true
})
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendJson(response, 400, {
...createCodeAgentErrorPayload({
code: "invalid_params",
message: "Code Agent cancel body must be a JSON object",
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
layer: "api",
retryable: true
})
});
return;
}
const traceId = safeTraceId(getHeader(request, "x-trace-id") || params.traceId);
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
const snapshot = traceId ? traceStore.snapshot(traceId) : null;
const sessionId = safeSessionId(params.sessionId) || safeSessionId(snapshot?.sessionId);
const conversationId = safeConversationId(params.conversationId);
const manager = options.codexStdioManager;
if (!traceId) {
sendJson(response, 400, cancelBlockedPayload({
code: "cancel_trace_missing",
message: "traceId is required to cancel the current Code Agent request.",
traceId: "trc_unassigned",
conversationId,
sessionId
}));
return;
}
if (!manager || typeof manager.get !== "function" || typeof manager.cancel !== "function") {
traceStore.append(traceId, {
type: "cancel",
status: "unsupported",
label: "cancel:unsupported",
errorCode: "cancel_unsupported",
message: "Codex stdio cancel/interrupt backend is not available on this runtime.",
waitingFor: "session-control"
});
const runnerTrace = traceStore.snapshot(traceId);
sendJson(response, 501, cancelBlockedPayload({
code: "cancel_unsupported",
message: "当前后端不支持 Code Agent interrupt/cancel;本次按 unsupported/degraded 返回,不会静默假装已中断。",
traceId,
conversationId,
sessionId,
runnerTrace,
status: "degraded",
unsupported: true,
degraded: true
}));
return;
}
if (!sessionId) {
traceStore.append(traceId, {
type: "cancel",
status: "blocked",
label: "cancel:not_cancelable",
errorCode: "cancel_session_missing",
message: "Cancel request did not include a bound Codex stdio sessionId.",
waitingFor: "session-binding"
});
sendJson(response, 409, cancelBlockedPayload({
code: "cancel_session_missing",
message: "当前请求尚未暴露可取消的 Codex stdio sessionId;页面不能只隐藏 UI,已保留输入和 trace。",
traceId,
conversationId,
sessionId: null,
runnerTrace: traceStore.snapshot(traceId)
}));
return;
}
const currentSession = manager.get(sessionId, { conversationId }) ?? null;
if (!currentSession || !["busy", "creating"].includes(currentSession.status)) {
traceStore.append(traceId, {
type: "cancel",
status: "blocked",
label: "cancel:not_in_flight",
errorCode: "cancel_not_in_flight",
message: `Session ${sessionId} is not an in-flight Codex stdio request.`,
sessionId,
sessionStatus: currentSession?.status ?? "missing"
});
sendJson(response, 409, cancelBlockedPayload({
code: currentSession ? "cancel_not_in_flight" : "cancel_session_not_found",
message: currentSession
? `当前 session 状态为 ${currentSession.status},没有可取消的 in-flight Codex stdio 请求。`
: `没有找到 sessionId=${sessionId} 的 Codex stdio session。`,
traceId,
conversationId,
sessionId,
session: currentSession,
runnerTrace: traceStore.snapshot(traceId)
}));
return;
}
const canceledSession = manager.cancel(sessionId, {
traceId,
conversationId,
reason: "user_cancel"
});
traceStore.append(traceId, {
type: "cancel",
status: "canceled",
label: "cancel:canceled",
message: "User canceled the current Codex stdio request.",
sessionId,
sessionStatus: canceledSession?.status ?? "canceled",
sessionLifecycleStatus: codeAgentSessionLifecycleSummary({ session: canceledSession, status: "canceled" }).status,
waitingFor: "user-retry",
terminal: true
});
const runnerTraceSnapshot = traceStore.snapshot(traceId);
const sessionSummary = codeAgentSessionLifecycleSummary({
session: canceledSession,
runnerTrace: runnerTraceSnapshot,
status: "canceled"
});
const runnerTrace = {
...runnerTraceSnapshot,
sessionLifecycleStatus: sessionSummary.status
};
const payload = {
accepted: true,
canceled: true,
status: "canceled",
conversationId: conversationId ?? canceledSession?.conversationId ?? null,
sessionId,
traceId,
session: canceledSession,
sessionLifecycleStatus: sessionSummary.status,
sessionLifecycle: sessionSummary,
sessionSummary,
runnerTrace,
lastTraceEvent: runnerTrace.lastEvent,
retryable: true,
error: {
code: "codex_stdio_canceled",
layer: "session",
category: "canceled",
retryable: true,
message: "user canceled current Code Agent request",
userMessage: "当前 Codex stdio 请求已取消;输入、sessionId、traceId 和最后 trace event 已保留,可重试上一条消息。",
traceId,
route: "/v1/agent/chat/cancel",
toolName: "codex-stdio.cancel"
},
userMessage: "当前 Codex stdio 请求已取消;输入、sessionId、traceId 和最后 trace event 已保留,可重试上一条消息。",
updatedAt: new Date().toISOString()
};
options.codeAgentChatResults?.set(traceId, payload);
sendJson(response, 200, payload);
}
function isCodeAgentResultCanceled(result) {
return result?.status === "canceled" || result?.canceled === true || result?.error?.code === "codex_stdio_canceled";
}
function cancelBlockedPayload({
code,
message,
traceId,
conversationId = null,
sessionId = null,
session = null,
runnerTrace = null,
status = "failed",
unsupported = false,
degraded = false
}) {
const sessionSummary = codeAgentSessionLifecycleSummary({
session,
runnerTrace,
status,
unsupported,
degraded,
error: { code, userMessage: message, message }
});
return {
accepted: false,
canceled: false,
status,
conversationId,
sessionId,
traceId,
session,
sessionLifecycleStatus: sessionSummary.status,
sessionLifecycle: sessionSummary,
sessionSummary,
unsupported,
degraded,
runnerTrace,
lastTraceEvent: runnerTrace?.lastEvent ?? null,
error: {
code,
layer: "session",
category: "cancel_blocked",
retryable: true,
userMessage: message,
message,
traceId,
route: "/v1/agent/chat/cancel",
toolName: "codex-stdio.cancel"
},
blocker: {
code,
layer: "session",
category: "cancel_blocked",
retryable: true,
summary: message,
userMessage: message,
traceId,
route: "/v1/agent/chat/cancel",
toolName: "codex-stdio.cancel"
}
};
}
async function handleCodeAgentTraceHttp(request, response, url, options) {
const parts = url.pathname.split("/").filter(Boolean);
const traceId = decodeURIComponent(parts[4] ?? "");
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
if (!/^trc_[A-Za-z0-9_.:-]+$/u.test(traceId)) {
sendJson(response, 400, {
error: {
code: "invalid_trace_id",
message: "traceId must start with trc_ and contain only safe identifier characters"
}
});
return;
}
if (parts[5] === "stream") {
sendTraceSse(response, traceStore, traceId, options);
return;
}
sendJson(response, 200, traceStore.snapshot(traceId));
}
function sendTraceSse(response, traceStore, traceId, options = {}) {
response.writeHead(200, {
"content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-store",
connection: "keep-alive",
"x-accel-buffering": "no"
});
const writeEvent = (eventName, payload) => {
response.write(`event: ${eventName}\n`);
response.write(`data: ${JSON.stringify(payload)}\n\n`);
};
writeEvent("snapshot", traceStore.snapshot(traceId));
const unsubscribe = traceStore.subscribe(traceId, (event, snapshot) => {
writeEvent("runnerTrace", { event, snapshot });
});
const heartbeatMs = parsePositiveInteger(options.env?.HWLAB_CODE_AGENT_TRACE_HEARTBEAT_MS, 15000);
const heartbeat = setInterval(() => {
writeEvent("heartbeat", traceStore.snapshot(traceId));
}, heartbeatMs);
response.on("close", () => {
clearInterval(heartbeat);
unsubscribe();
});
}
function createCodeAgentChatResultStore({ maxResults = 256 } = {}) {
const limit = positiveInteger(maxResults, 256);
const results = new Map();
function set(traceId, payload) {
const id = safeTraceId(traceId);
if (!id) return;
results.set(id, {
...payload,
traceId: id,
cachedAt: new Date().toISOString()
});
while (results.size > limit) {
const oldest = results.keys().next().value;
if (!oldest) break;
results.delete(oldest);
}
}
return {
set,
get: (traceId) => results.get(safeTraceId(traceId)) ?? null,
clear: () => results.clear()
};
}
function createCodeAgentM3HwlabApiRequestJson(options = {}) {
return async (targetUrl, request = {}) => {
const url = new URL(targetUrl);
const route = url.pathname;
if (route === M3_IO_CONTROL_ROUTE && request.method === "POST") {
return {
ok: true,
status: 200,
body: await handleM3IoControl(request.body ?? {}, {
runtimeStore: options.runtimeStore,
now: options.now,
env: options.env,
requestJson: options.m3IoRequestJson
})
};
}
if (route === M3_STATUS_ROUTE && request.method === "GET") {
return {
ok: true,
status: 200,
body: await describeM3StatusLive(options)
};
}
return {
ok: false,
status: 404,
body: {
error: {
code: "not_found",
message: `Unsupported Code Agent M3 HWLAB API route ${route}`
}
},
error: `Unsupported Code Agent M3 HWLAB API route ${route}`
};
};
}
async function handleM3IoControlHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "m3-io-control-v1",
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED"
},
readback: null,
error: {
code: "parse_error",
message: "Invalid JSON body",
reason: error.message
}
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendJson(response, 400, {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "m3-io-control-v1",
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED"
},
readback: null,
error: {
code: "invalid_params",
message: "M3 IO control body must be a JSON object"
}
});
return;
}
try {
const payload = await handleM3IoControl(
{
...params,
traceId: getHeader(request, "x-trace-id") || params.traceId,
requestId: getHeader(request, "x-request-id") || params.requestId,
actorId: getHeader(request, "x-actor-id") || params.actorId
},
{
runtimeStore: options.runtimeStore,
env: options.env,
requestJson: options.m3IoRequestJson,
traceId: getHeader(request, "x-trace-id"),
requestId: getHeader(request, "x-request-id"),
actorId: getHeader(request, "x-actor-id")
}
);
sendJson(response, payload.httpStatus ?? 200, payload);
} catch (error) {
const protocolCode = Number.isInteger(error?.code) ? error.code : ERROR_CODES.internalError;
sendJson(response, protocolCode === ERROR_CODES.invalidParams ? 400 : 200, {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "m3-io-control-v1",
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
traceId: getHeader(request, "x-trace-id") || params.traceId || "trc_unassigned",
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED"
},
readback: null,
error: {
code: protocolCode,
message: error.message,
data: error.data ?? {}
}
});
}
}
function sendRestError(request, response, statusCode, code, message, context = {}) {
const requestId = getHeader(request, "x-request-id") || "req_unassigned";
sendJson(response, statusCode, {
error: {
code,
message,
reason: context.reason,
audit: createAuditRecord({
requestId,
actor: {
type: "user",
id: getHeader(request, "x-actor-id") || `system_${CLOUD_API_SERVICE_ID}`
},
source: {
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
traceId: getHeader(request, "x-trace-id"),
adapter: "rest"
},
operation: context.operation || `${request.method || "GET"} ${request.url || "/"}`,
target: context.target || {
type: "route",
id: request.url || "/"
},
result: context.result || "failed"
})
}
});
}
function statusForRpcResponse(rpcResponse) {
if (!rpcResponse.error) {
return 200;
}
if (rpcResponse.error.code === ERROR_CODES.invalidRequest || rpcResponse.error.code === ERROR_CODES.parseError) {
return 400;
}
if (rpcResponse.error.code === ERROR_CODES.methodNotFound) {
return 404;
}
return 200;
}
function readBody(request, limitBytes = DEFAULT_BODY_LIMIT_BYTES) {
const limit = Number.isInteger(limitBytes) && limitBytes > 0 ? limitBytes : DEFAULT_BODY_LIMIT_BYTES;
return new Promise((resolve, reject) => {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
if (Buffer.byteLength(body, "utf8") > limit) {
request.destroy(new Error(`request body exceeds ${limit} bytes`));
}
});
request.on("end", () => resolve(body));
request.on("error", reject);
});
}
function sendJson(response, statusCode, body) {
response.writeHead(statusCode, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store"
});
response.end(`${JSON.stringify(body)}\n`);
}
function getHeader(request, name) {
const value = request.headers[name.toLowerCase()];
if (Array.isArray(value)) {
return value[0];
}
return value;
}
function firstHeaderValue(request, name) {
return getHeader(request, name);
}
function truthyFlag(value) {
return /^(?:1|true|yes|on|enabled)$/iu.test(String(value ?? "").trim());
}
function safeTraceId(value) {
const text = String(value ?? "").trim();
return /^trc_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
function safeSessionId(value) {
const text = String(value ?? "").trim();
return /^ses_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
function safeConversationId(value) {
const text = String(value ?? "").trim();
return /^cnv_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
function parsePositiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
function positiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
async function runtimeReadiness(runtimeStore) {
if (typeof runtimeStore?.readiness === "function") {
return runtimeStore.readiness();
}
return runtimeStore.summary();
}