refactor: migrate v02 cloud api to TypeScript

This commit is contained in:
Codex
2026-05-29 00:33:01 +08:00
parent 44851d8eb3
commit a0869c5afa
79 changed files with 10696 additions and 10065 deletions
@@ -1,9 +1,9 @@
#!/usr/bin/env node
import { createCloudApiServer } from "../../internal/cloud/server.mjs";
#!/usr/bin/env bun
import { createCloudApiServer } from "../../internal/cloud/server.ts";
import { applyCloudApiImageEnv, parsePort, parseTimeout } from "./runtime-options.ts";
const host = process.env.HWLAB_CLOUD_API_HOST || "0.0.0.0";
const port = parsePort(process.env.HWLAB_CLOUD_API_PORT, parsePort(process.env.PORT, 6667));
const commitId = process.env.HWLAB_COMMIT_ID || process.env.HWLAB_GIT_SHA;
const codeAgentTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_TIMEOUT_MS, 1200000, {
min: 1000,
max: 2400000
@@ -12,32 +12,13 @@ const codeAgentHardTimeoutMs = parseTimeout(process.env.HWLAB_CODE_AGENT_HARD_TI
min: codeAgentTimeoutMs,
max: 3600000
});
if (!process.env.HWLAB_IMAGE && commitId) {
process.env.HWLAB_IMAGE = `ghcr.io/pikastech/hwlab-cloud-api:${commitId.slice(0, 7)}`;
}
if (!process.env.HWLAB_IMAGE_TAG && commitId) {
process.env.HWLAB_IMAGE_TAG = commitId.slice(0, 7);
}
applyCloudApiImageEnv(process.env);
const server = createCloudApiServer({
codeAgentTimeoutMs,
codeAgentHardTimeoutMs
});
function parseTimeout(value, fallback, { min, max }) {
const parsed = Number.parseInt(value || "", 10);
if (!Number.isInteger(parsed)) return fallback;
return Math.min(Math.max(parsed, min), max);
}
function parsePort(value, fallback) {
const parsed = Number.parseInt(value || "", 10);
if (!Number.isInteger(parsed) || parsed < 0 || parsed >= 65536) return fallback;
return parsed;
}
server.listen(port, host, () => {
const address = server.address();
const resolvedPort = typeof address === "object" && address ? address.port : port;
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import {
formatDevRuntimeMigrationFailure,
runDevRuntimeMigrationCli
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import {
formatDevRuntimeProvisioningFailure,
runDevRuntimeProvisioningCli
@@ -0,0 +1,38 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import { applyCloudApiImageEnv, parsePort, parseTimeout } from "./runtime-options.ts";
test("parsePort preserves current hwlab-cloud-api port fallback behavior", () => {
assert.equal(parsePort(undefined, 6667), 6667);
assert.equal(parsePort("", 6667), 6667);
assert.equal(parsePort("abc", 6667), 6667);
assert.equal(parsePort("-1", 6667), 6667);
assert.equal(parsePort("65536", 6667), 6667);
assert.equal(parsePort("0", 6667), 0);
assert.equal(parsePort("19667", 6667), 19667);
});
test("parseTimeout clamps parsed integer values to the configured range", () => {
assert.equal(parseTimeout(undefined, 1200000, { min: 1000, max: 2400000 }), 1200000);
assert.equal(parseTimeout("abc", 1200000, { min: 1000, max: 2400000 }), 1200000);
assert.equal(parseTimeout("1", 1200000, { min: 1000, max: 2400000 }), 1000);
assert.equal(parseTimeout("99999999", 1200000, { min: 1000, max: 2400000 }), 2400000);
assert.equal(parseTimeout("1500", 1200000, { min: 1000, max: 2400000 }), 1500);
});
test("applyCloudApiImageEnv derives image identity from commit without overriding explicit values", () => {
const env = { HWLAB_COMMIT_ID: "abcdef1234567890" };
applyCloudApiImageEnv(env);
assert.equal(env.HWLAB_IMAGE, "ghcr.io/pikastech/hwlab-cloud-api:abcdef1");
assert.equal(env.HWLAB_IMAGE_TAG, "abcdef1");
const explicit = {
HWLAB_GIT_SHA: "11111112222222",
HWLAB_IMAGE: "registry.local/custom:tag",
HWLAB_IMAGE_TAG: "custom"
};
applyCloudApiImageEnv(explicit);
assert.equal(explicit.HWLAB_IMAGE, "registry.local/custom:tag");
assert.equal(explicit.HWLAB_IMAGE_TAG, "custom");
});
+22
View File
@@ -0,0 +1,22 @@
export function parseTimeout(value, fallback, { min, max }) {
const parsed = Number.parseInt(value || "", 10);
if (!Number.isInteger(parsed)) return fallback;
return Math.min(Math.max(parsed, min), max);
}
export function parsePort(value, fallback) {
const parsed = Number.parseInt(value || "", 10);
if (!Number.isInteger(parsed) || parsed < 0 || parsed >= 65536) return fallback;
return parsed;
}
export function applyCloudApiImageEnv(env = process.env) {
const commitId = env.HWLAB_COMMIT_ID || env.HWLAB_GIT_SHA;
if (!env.HWLAB_IMAGE && commitId) {
env.HWLAB_IMAGE = `ghcr.io/pikastech/hwlab-cloud-api:${commitId.slice(0, 7)}`;
}
if (!env.HWLAB_IMAGE_TAG && commitId) {
env.HWLAB_IMAGE_TAG = commitId.slice(0, 7);
}
}
+5 -3
View File
@@ -97,7 +97,9 @@
"digest": "sha256:fe724e72e7cea3c93abe17085c4d885c78d34d18eb2a3c01f987d1971e2f4425",
"reusedFrom": "7498160c36db86db76d3c1b103e397f2b2304248136d3b79eafe1ab69d4abeff"
},
"reusedFrom": "7498160c36db86db76d3c1b103e397f2b2304248136d3b79eafe1ab69d4abeff"
"reusedFrom": "7498160c36db86db76d3c1b103e397f2b2304248136d3b79eafe1ab69d4abeff",
"runtimeKind": "bun-command",
"entrypoint": "cmd/hwlab-cloud-api/main.ts"
},
{
"serviceId": "hwlab-cloud-web",
@@ -578,10 +580,10 @@
"publishEnabled": true,
"artifactRequired": true,
"artifactScope": "required",
"runtimeKind": "node-command",
"runtimeKind": "bun-command",
"implementationState": "repo-entrypoint",
"sourceState": "source-present",
"entrypoint": "cmd/hwlab-cloud-api/main.mjs",
"entrypoint": "cmd/hwlab-cloud-api/main.ts",
"disabledReason": null
},
{
+1 -1
View File
@@ -10,7 +10,7 @@ Secret 或 token。
`/v1/agent/chat`
- DEV API/edge 入口是 `http://74.48.78.17:17667/`;它不能替代 Workbench
同源聊天入口的真实回复证据。
- `internal/cloud/code-agent-chat.mjs``/v1/agent/chat` 的后端处理入口。
- `internal/cloud/code-agent-chat.ts``/v1/agent/chat` 的后端处理入口。
- `scripts/code-agent-chat-smoke.mjs` 是 Code Agent chat schema 与 readiness
合同检查。
- `scripts/dev-cloud-workbench-smoke.mjs --static` 只验证 Workbench 源码合同和
+8 -8
View File
@@ -193,7 +193,7 @@ When health reports:
- `runtime.status: "degraded"`
the Cloud API is using the process-local runtime store in
`internal/db/runtime-store.mjs`. Gateway sessions, box resources, operations,
`internal/db/runtime-store.ts`. Gateway sessions, box resources, operations,
audit events, and evidence records accepted through the runtime can be lost on
pod restart, redeploy, or scale-out, and cannot be treated as durable M3/M4/M5
evidence. Users may see accepted operations or evidence disappear after a
@@ -238,9 +238,9 @@ below are true.
The cloud-api image owns a stable DEV DB provisioning entrypoint:
```sh
node cmd/hwlab-cloud-api/provision.mjs --check
node cmd/hwlab-cloud-api/provision.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json
node cmd/hwlab-cloud-api/provision.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json
bun cmd/hwlab-cloud-api/provision.ts --check
bun cmd/hwlab-cloud-api/provision.ts --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json
bun cmd/hwlab-cloud-api/provision.ts --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json
```
This is the preferred image-internal operator command for `hwlab-cloud-api`.
@@ -288,9 +288,9 @@ untracked manual DB write.
The cloud-api image owns a stable runtime migration entrypoint:
```sh
node cmd/hwlab-cloud-api/migrate.mjs --check
node cmd/hwlab-cloud-api/migrate.mjs --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json
node cmd/hwlab-cloud-api/migrate.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json
bun cmd/hwlab-cloud-api/migrate.ts --check
bun cmd/hwlab-cloud-api/migrate.ts --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json
bun cmd/hwlab-cloud-api/migrate.ts --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json
```
This is the preferred image-internal operator command for `hwlab-cloud-api`.
@@ -298,7 +298,7 @@ It delegates to the same repo-owned migration implementation as the root
script, so there is only one migration contract and one output format. The
cloud-api artifact build copies `cmd/`, `scripts/`, `internal/`, `package.json`,
and runtime dependencies into the image; the DEV CD runtime migration Job uses
`node cmd/hwlab-cloud-api/migrate.mjs` from the current cloud-api image and
`bun cmd/hwlab-cloud-api/migrate.ts` from the current cloud-api image and
injects DB inputs only through SecretRef-backed env.
The repo-level compatibility entrypoint remains:
+3 -3
View File
@@ -94,7 +94,7 @@ SCRIPT
| --- | --- |
| Deployment | `hwlab-dev/hwlab-cloud-api` |
| ConfigMap | `hwlab-cloud-api-code-agent-hotfix` |
| 覆盖文件 | `/app/internal/cloud/code-agent-chat.mjs` |
| 覆盖文件 | `/app/internal/cloud/code-agent-chat.ts` |
| subPath | `code-agent-chat.mjs` |
| marker | `runtime-hotfix-pc-gateway-shell` |
| annotation | `hwlab.pikastech.local/pc-gateway-shell-hotfix` |
@@ -125,7 +125,7 @@ node scripts/dev-runtime-hotfix-audit.mjs --collect-readonly --pretty
## 回滚口径
优先回滚方式是用源码化 artifact/CD 覆盖 runtime hotfix#460/#461 合并并发布后,Deployment 应消费正式镜像,不再通过 ConfigMap 覆盖 `/app/internal/cloud/code-agent-chat.mjs`。注意 `kubectl apply -k` 可能保留运行面 patch 写入、且源码 desired-state 不拥有的 hotfix `volumes``volumeMounts` 或 template annotations;正式 DEV CD apply 应先识别这种 unmanaged hotfix 覆盖,删除对应 desired Deployment,再由同一次 apply 从源码重新创建。
优先回滚方式是用源码化 artifact/CD 覆盖 runtime hotfix#460/#461 合并并发布后,Deployment 应消费正式镜像,不再通过 ConfigMap 覆盖 `/app/internal/cloud/code-agent-chat.ts`。注意 `kubectl apply -k` 可能保留运行面 patch 写入、且源码 desired-state 不拥有的 hotfix `volumes``volumeMounts` 或 template annotations;正式 DEV CD apply 应先识别这种 unmanaged hotfix 覆盖,删除对应 desired Deployment,再由同一次 apply 从源码重新创建。
只读确认口径是通过当前 G14 k3s route 读取目标对象;不要从 master server 裸跑 `kubectl`,也不要把 D601 kubeconfig 当作当前 DEV/v0.2 控制面:
@@ -139,7 +139,7 @@ node scripts/dev-runtime-hotfix-audit.mjs --collect-readonly --pretty
如果必须直接移除运行态覆盖,授权操作者应先从 `kubectl -n hwlab-dev get deployment hwlab-cloud-api -o json` 中定位精确 indexes,再移除:
- 指向 hotfix ConfigMap 的 `spec.template.spec.volumes[]`
- 指向 `/app/internal/cloud/code-agent-chat.mjs``containers[].volumeMounts[]`
- 指向 `/app/internal/cloud/code-agent-chat.ts``containers[].volumeMounts[]`
- `hwlab.pikastech.local/pc-gateway-shell-hotfix` 等 hotfix annotation
- 不再需要的 `hwlab-cloud-api-code-agent-hotfix` ConfigMap。
+4 -4
View File
@@ -10,10 +10,10 @@
## 内部架构
- `cmd/hwlab-cloud-api/main.mjs` 负责启动 HTTP server、解析端口和 Code Agent timeout。
- `internal/cloud/server.mjs` 负责 HTTP route、REST/RPC bridge、health、live-builds、device-pod proxy、gateway poll/result 和 Code Agent chat。
- `internal/db/runtime-store.mjs``internal/cloud/db-contract.mjs` 负责 Postgres runtime store 与 readiness 分层。
- `internal/cloud/code-agent-*.mjs` 负责 Codex stdio session、trace store、result cache、provider profile 和取消/轮询。
- `cmd/hwlab-cloud-api/main.ts` 负责启动 HTTP server、解析端口和 Code Agent timeout。
- `internal/cloud/server.ts` 负责 HTTP route、REST/RPC bridge、health、live-builds、device-pod proxy、gateway poll/result 和 Code Agent chat。
- `internal/db/runtime-store.ts``internal/cloud/db-contract.ts` 负责 Postgres runtime store 与 readiness 分层。
- `internal/cloud/code-agent-*.ts` 负责 Codex stdio session、trace store、result cache、provider profile 和取消/轮询。
- 同 Pod sidecar `hwlab-codex-api-forwarder` 监听 `127.0.0.1:49280/responses`,用于 `codex-api` profile 直连 hyueapi,并保持 hyueapi 在 `NO_PROXY` 中。
- `hwlab-code-agent-workspace` PVC 挂载到 `/workspace/hwlab`,用于长会话 workspace;它是 cloud-api 运行资源,不是独立用户入口。
+1 -1
View File
@@ -11,7 +11,7 @@
## 内部架构
- `cmd/hwlab-gateway/main.mjs` 维护 gateway state、outbound poll loop、inflight request map 和 command execution。
- `internal/cloud/gateway-demo-registry.mjs` 在 cloud-api 内保存 gateway session、队列和 pending result。
- `internal/cloud/gateway-demo-registry.ts` 在 cloud-api 内保存 gateway session、队列和 pending result。
- command execution 默认关闭;只有显式 `HWLAB_GATEWAY_CMD_EXEC_ENABLED=1` 或 demo open 时才执行 shell。
## API 接口说明
@@ -8,7 +8,7 @@ import {
DEV_CODE_AGENT_PROVIDER_CONTRACT,
codeAgentSecretRefPlaceholder,
inspectCodeAgentProviderEnv
} from "./code-agent-contract.mjs";
} from "./code-agent-contract.ts";
import {
CODEX_STDIO_BACKEND,
CODEX_STDIO_CAPABILITY_LEVEL,
@@ -18,19 +18,19 @@ import {
CODEX_STDIO_SESSION_MODE,
createCodexStdioSessionManager,
longLivedSessionGate as codexLongLivedSessionGate
} from "./codex-stdio-session.mjs";
} from "./codex-stdio-session.ts";
import {
createCodeAgentTraceRecorder,
defaultCodeAgentTraceStore
} from "./code-agent-trace-store.mjs";
} from "./code-agent-trace-store.ts";
import {
DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS,
createCodeAgentSessionRegistry
} from "./code-agent-session-registry.mjs";
} from "./code-agent-session-registry.ts";
import {
codeAgentSessionLifecycleSummary,
decorateCodeAgentSession
} from "./code-agent-session-lifecycle.mjs";
} from "./code-agent-session-lifecycle.ts";
const DEFAULT_MODEL = DEV_CODE_AGENT_PROVIDER_CONTRACT.model;
const DEFAULT_PROJECT_ID = "prj_hwlab-cloud-workbench";
const READONLY_RUNNER_PROVIDER = "codex-readonly-runner";
@@ -3,15 +3,15 @@ import { existsSync } from "node:fs";
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { test } from "bun:test";
import { createCodeAgentSessionRegistry } from "./code-agent-session-registry.mjs";
import { handleCodeAgentChat, validateCodeAgentChatSchema } from "./code-agent-chat.mjs";
import { createCodeAgentSessionRegistry } from "./code-agent-session-registry.ts";
import { handleCodeAgentChat, validateCodeAgentChatSchema } from "./code-agent-chat.ts";
import {
codexAppServerArgs,
codexAppServerProviderBaseUrl,
createCodexStdioSessionManager
} from "./codex-stdio-session.mjs";
} from "./codex-stdio-session.ts";
import {
classifyCodeAgentChatReadiness,
classifyCodexRunnerCapability
@@ -5,7 +5,7 @@ import {
CODE_AGENT_SESSION_STATUS_ALIASES,
codeAgentSessionLifecycleSummary,
decorateCodeAgentSession
} from "./code-agent-session-lifecycle.mjs";
} from "./code-agent-session-lifecycle.ts";
export const DEFAULT_CODE_AGENT_SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
export const DEFAULT_CODE_AGENT_MAX_SESSIONS = 200;
@@ -0,0 +1,37 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import { createCodeAgentTraceStore } from "./code-agent-trace-store.ts";
test("code agent trace store keeps assistant deltas outside event count", () => {
const traceStore = createCodeAgentTraceStore({ maxEvents: 6000 });
const traceId = "trc_trace-store-assistant-stream";
traceStore.append(traceId, { type: "request", status: "accepted", label: "request:accepted" });
for (let index = 0; index < 6200; index += 1) {
traceStore.appendAssistantDelta(traceId, {
itemId: "item_assistant_stream",
chunk: `chunk-${index} `,
waitingFor: "turn/completed"
});
}
const snapshot = traceStore.snapshot(traceId);
assert.equal(snapshot.eventCount, 1);
assert.equal(snapshot.events.length, 1);
assert.equal(snapshot.events[0].label, "request:accepted");
assert.equal(snapshot.assistantStreams.length, 1);
assert.equal(snapshot.assistantStreams[0].chunkCount, 6200);
assert.equal(snapshot.waitingFor, "turn/completed");
});
test("code agent trace store retains six thousand regular events", () => {
const traceStore = createCodeAgentTraceStore({ maxEvents: 6000 });
const traceId = "trc_trace-store-regular-events";
for (let index = 0; index < 6001; index += 1) {
traceStore.append(traceId, { type: "trace", status: "observed", label: `event:${index}` });
}
const snapshot = traceStore.snapshot(traceId);
assert.equal(snapshot.eventCount, 6000);
assert.equal(snapshot.events.length, 6000);
assert.equal(snapshot.events[0].label, "event:1");
assert.equal(snapshot.events.at(-1).label, "event:6000");
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,419 @@
import { CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS } from "./codex-stdio-session.ts";
import {
appServerActivityTimeoutError,
appServerCommandStatus,
appServerHardTimeoutError,
appServerTerminalStatus,
commandExecutionCommandText,
commandExecutionErrorText,
commandExecutionOutputText,
extractAppServerRecord,
extractAppServerString,
firstNonEmpty,
optionalId,
redactText,
tailText
} from "./codex-stdio-session-helpers.ts";
export function createAppServerTurnState({ traceRecorder, session } = {}) {
let threadId = optionalId(session?.threadId);
let turnId = null;
let assistantText = "";
let finalResponse = "";
let terminal = null;
let terminalResolve;
const terminalPromise = new Promise((resolve) => {
terminalResolve = resolve;
});
let lastActivityAt = Date.now();
let lastActivityLabel = "turn-state:created";
let lastWaitingFor = "app-server-turn";
function setThreadId(value) {
threadId = optionalId(value) ?? threadId;
}
function setTurnId(value) {
turnId = optionalId(value) ?? turnId;
}
function activitySnapshot(referenceNow = Date.now()) {
return {
lastActivityAt: new Date(lastActivityAt).toISOString(),
lastActivityLabel,
idleMs: Math.max(0, referenceNow - lastActivityAt),
waitingFor: lastWaitingFor
};
}
function observeActivity({ label = "app-server:activity", waitingFor = null } = {}) {
lastActivityAt = Date.now();
lastActivityLabel = label;
if (waitingFor) lastWaitingFor = waitingFor;
return activitySnapshot(lastActivityAt);
}
function appendTrace(event = {}) {
const appended = traceRecorder?.append(event);
observeActivity({
label: appended?.label ?? event.label ?? event.type ?? "trace:event",
waitingFor: appended?.waitingFor ?? event.waitingFor ?? null
});
return appended;
}
function appendProgressTrace(event = {}) {
return traceRecorder?.append(event);
}
function hasAssistantOutput() {
return Boolean(firstNonEmpty(finalResponse, assistantText));
}
function appendFirstTokenProgressTrace(referenceNow = Date.now()) {
const activity = activitySnapshot(referenceNow);
if (hasAssistantOutput() || !turnId || terminal) return null;
if (!["assistant-message", "turn/completed"].includes(activity.waitingFor)) return null;
return appendProgressTrace({
type: "turn",
status: "running",
label: "turn:waiting:first_assistant_token",
message: "Codex app-server is waiting for the first assistant token; this progress trace does not reset the backend idle timer.",
progressOnly: true,
idleMs: activity.idleMs,
lastActivityAt: activity.lastActivityAt,
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
waitingFor: "assistant-message"
});
}
function finish(status, error = null, extra = {}) {
if (terminal) return;
terminal = {
terminalStatus: appServerTerminalStatus(status),
terminalError: error ? redactText(error) : null,
threadId,
turnId,
...activitySnapshot(),
...extra
};
terminalResolve(terminal);
}
function handle(message) {
const method = typeof message?.method === "string" ? message.method : "unknown";
observeActivity({
label: `app-server:${method}`,
waitingFor: appServerWaitingForMethod(method)
});
const params = extractAppServerRecord(message?.params);
const item = extractAppServerRecord(params?.item);
const turn = extractAppServerRecord(params?.turn);
if (method === "thread/started") {
setThreadId(extractAppServerString(extractAppServerRecord(params?.thread), "id"));
appendTrace({
type: "thread",
status: "completed",
label: "thread:started",
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
waitingFor: "turn/start"
});
return;
}
if (method === "turn/started") {
setTurnId(extractAppServerString(turn, "id"));
appendTrace({
type: "turn",
status: "started",
label: "turn:started",
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
waitingFor: "assistant-message"
});
return;
}
if (method === "item/agentMessage/delta") {
const delta = String(params?.delta ?? "");
assistantText += delta;
if (delta) {
traceRecorder?.appendAssistantDelta?.({
itemId: optionalId(params?.itemId),
chunk: delta,
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
waitingFor: "turn/completed"
});
}
return;
}
if (method === "item/completed" && item?.type === "agentMessage") {
if (typeof item.text === "string") finalResponse = item.text;
appendTrace({
type: "assistant_message",
status: "completed",
label: "assistant:item_completed",
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
waitingFor: "turn/completed"
});
return;
}
if (method === "item/started" && item?.type === "commandExecution") {
appendCommandExecutionTrace(item, {
status: "started",
label: "item/commandExecution:started"
});
return;
}
if (method === "item/completed" && item?.type === "commandExecution") {
appendCommandExecutionTrace(item, {
status: appServerCommandStatus(item),
label: "item/commandExecution:completed"
});
return;
}
if (method === "item/commandExecution/outputDelta" || method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") {
appendTrace({
type: method.startsWith("item/reasoning/") ? "reasoning" : "tool_call",
status: "output_chunk",
label: method,
outputSummary: String(params?.delta ?? "").slice(0, 400),
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
waitingFor: "turn/completed"
});
return;
}
if (method === "error") {
const error = extractAppServerRecord(params?.error);
const message = typeof error?.message === "string" ? error.message : "Codex app-server error";
const willRetry = params?.willRetry === true;
setThreadId(extractAppServerString(params, "threadId"));
setTurnId(extractAppServerString(params, "turnId"));
appendTrace({
type: willRetry ? "provider_retry" : "error",
status: willRetry ? "retrying" : "failed",
label: willRetry ? "app-server:retrying" : "app-server:error",
errorCode: willRetry ? "codex_stdio_provider_retry" : "codex_stdio_failed",
message: redactText(message),
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
waitingFor: willRetry ? "turn/completed" : "user-retry",
terminal: !willRetry
});
if (!willRetry) finish("failed", message);
return;
}
if (method === "turn/completed") {
const status = appServerTerminalStatus(turn?.status);
const error = extractAppServerRecord(turn?.error);
const message = typeof error?.message === "string" ? error.message : null;
appendTrace({
type: "turn",
status,
label: `turn:completed:${status ?? "unknown"}`,
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
errorCode: status === "completed" ? null : "codex_stdio_failed",
message: message ? redactText(message) : undefined,
terminal: status !== "completed"
});
finish(status, message);
}
}
function appendCommandExecutionTrace(item, { status, label }) {
const output = commandExecutionOutputText(item);
appendTrace({
type: "tool_call",
stage: "tool_call",
status,
label,
itemId: optionalId(item.id),
toolName: "commandExecution",
command: redactText(commandExecutionCommandText(item)),
exitCode: Number.isInteger(item.exitCode) ? item.exitCode : undefined,
durationMs: typeof item.durationMs === "number" ? item.durationMs : undefined,
outputBytes: output.length,
stdoutSummary: output ? tailText(redactText(output), 600) : undefined,
stderrSummary: commandExecutionErrorText(item),
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
waitingFor: status === "completed" ? "turn/completed" : "commandExecution/completed"
});
}
async function wait(timeoutMs, closedPromise, { hardTimeoutMs = null } = {}) {
let activityTimer;
let hardTimer;
let progressTimer;
const timeoutPromise = new Promise((_, reject) => {
const checkActivity = () => {
const now = Date.now();
const activity = activitySnapshot(now);
if (activity.idleMs >= timeoutMs) {
const error = appServerActivityTimeoutError({
timeoutMs,
idleMs: activity.idleMs,
lastActivityAt: activity.lastActivityAt,
waitingFor: activity.waitingFor,
threadId,
turnId,
partialAssistant: hasAssistantOutput()
});
appendTrace({
type: "timeout",
status: "failed",
label: "timeout:no_activity",
errorCode: error.code,
message: error.message,
timeoutMs,
idleMs: activity.idleMs,
lastActivityAt: activity.lastActivityAt,
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
waitingFor: activity.waitingFor,
terminal: true
});
reject(error);
return;
}
activityTimer = setTimeout(checkActivity, Math.max(1, timeoutMs - activity.idleMs));
};
activityTimer = setTimeout(checkActivity, timeoutMs);
});
const progressPromise = new Promise(() => {
const tick = () => {
appendFirstTokenProgressTrace();
progressTimer = setTimeout(tick, CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS);
};
progressTimer = setTimeout(tick, CODEX_STDIO_FIRST_TOKEN_PROGRESS_MS);
});
const hardTimeoutPromise = hardTimeoutMs
? new Promise((_, reject) => {
hardTimer = setTimeout(() => {
const activity = activitySnapshot();
const error = appServerHardTimeoutError({
hardTimeoutMs,
timeoutMs,
idleMs: activity.idleMs,
lastActivityAt: activity.lastActivityAt,
waitingFor: activity.waitingFor,
threadId,
turnId
});
appendTrace({
type: "timeout",
status: "failed",
label: "timeout:hard_cap",
errorCode: error.code,
message: error.message,
timeoutMs,
hardTimeoutMs,
idleMs: activity.idleMs,
lastActivityAt: activity.lastActivityAt,
sessionId: session?.sessionId,
sessionStatus: session?.status,
turn: session?.turn,
threadId,
turnId,
waitingFor: activity.waitingFor,
terminal: true
});
reject(error);
}, hardTimeoutMs);
})
: null;
const candidates = [terminalPromise];
if (closedPromise && typeof closedPromise.then === "function") {
candidates.push(closedPromise.then((exit) => {
if (terminal) return terminal;
return {
terminalStatus: "failed",
terminalError: hasAssistantOutput()
? "Codex app-server transport closed after partial assistant output but before turn/completed."
: "Codex app-server transport closed before turn/completed.",
threadId,
turnId,
...activitySnapshot(),
appServerExit: exit
};
}));
}
try {
return await Promise.race([...candidates, timeoutPromise, hardTimeoutPromise, progressPromise].filter(Boolean));
} finally {
clearTimeout(activityTimer);
clearTimeout(hardTimer);
clearTimeout(progressTimer);
}
}
function snapshot() {
const activity = activitySnapshot();
return {
threadId,
turnId,
assistantText: redactText(assistantText),
finalResponse: redactText(firstNonEmpty(finalResponse, assistantText)),
terminalStatus: terminal?.terminalStatus ?? null,
terminalError: terminal?.terminalError ?? null,
lastActivityAt: terminal?.lastActivityAt ?? activity.lastActivityAt,
idleMs: terminal?.idleMs ?? activity.idleMs,
waitingFor: terminal?.waitingFor ?? activity.waitingFor
};
}
return {
appendTrace,
handle,
setThreadId,
setTurnId,
wait,
snapshot
};
}
function appServerWaitingForMethod(method) {
if (method === "thread/started") return "turn/start";
if (method === "turn/started") return "assistant-message";
if (method === "item/agentMessage/delta") return "turn/completed";
if (method === "item/completed") return "turn/completed";
if (method === "item/commandExecution/outputDelta") return "turn/completed";
if (method === "item/reasoning/summaryTextDelta" || method === "item/reasoning/textDelta") return "turn/completed";
if (method === "error") return "turn/completed";
if (method === "turn/completed") return "turn/completed";
return "app-server-notification";
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@ import {
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
RUNTIME_STORE_KIND_POSTGRES
} from "../db/runtime-store.mjs";
} from "../db/runtime-store.ts";
import { ENVIRONMENT_DEV } from "../protocol/index.mjs";
export const DEV_DB_ENV_CONTRACT = Object.freeze({
@@ -1,14 +1,14 @@
import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs";
import { describeCodeAgentAvailability } from "./code-agent-chat.mjs";
import { describeCodeAgentAvailability } from "./code-agent-chat.ts";
import {
applyRuntimeDbReadinessLayers,
buildDbRuntimeReadiness
} from "./db-contract.mjs";
import { buildCloudApiReadiness } from "./health-contract.mjs";
} from "./db-contract.ts";
import { buildCloudApiReadiness } from "./health-contract.ts";
import {
M3_IO_CONTROL_ROUTE,
describeM3IoControlLive
} from "./m3-io-control.mjs";
} from "./m3-io-control.ts";
const STATUS = Object.freeze({
pass: "通过",
@@ -9,7 +9,7 @@ import {
RUNTIME_DURABLE_ADAPTER_UNCONFIGURED,
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
RUNTIME_STORE_KIND_POSTGRES
} from "../db/runtime-store.mjs";
} from "../db/runtime-store.ts";
export const CLOUD_API_HEALTH_CONTRACT_VERSION = "v3";
@@ -1,10 +1,10 @@
import assert from "node:assert/strict";
import test from "node:test";
import { test } from "bun:test";
import { AUDIT_FIELD_NAMES } from "../audit/index.mjs";
import { ERROR_CODES, validateResponse } from "../protocol/index.mjs";
import { createCloudRuntimeStore } from "../db/runtime-store.mjs";
import { handleJsonRpcRequest } from "./json-rpc.mjs";
import { createCloudRuntimeStore } from "../db/runtime-store.ts";
import { handleJsonRpcRequest } from "./json-rpc.ts";
test("unknown JSON-RPC method returns an error envelope with audit fields", async () => {
const response = await handleJsonRpcRequest({
@@ -14,12 +14,12 @@ import {
createAuditRecord,
deriveActorFromMeta
} from "../audit/index.mjs";
import { buildDbRuntimeReadiness } from "./db-contract.mjs";
import { describeCodeAgentAvailability } from "./code-agent-chat.mjs";
import { buildCloudApiReadiness } from "./health-contract.mjs";
import { createCloudRuntimeStore } from "../db/runtime-store.mjs";
import { M3_IO_RPC_METHODS, handleM3IoRpc } from "./m3-io-control.mjs";
import { createGatewayShellRequest } from "./gateway-demo-registry.mjs";
import { buildDbRuntimeReadiness } from "./db-contract.ts";
import { describeCodeAgentAvailability } from "./code-agent-chat.ts";
import { buildCloudApiReadiness } from "./health-contract.ts";
import { createCloudRuntimeStore } from "../db/runtime-store.ts";
import { M3_IO_RPC_METHODS, handleM3IoRpc } from "./m3-io-control.ts";
import { createGatewayShellRequest } from "./gateway-demo-registry.ts";
export const SUPPORTED_RPC_METHODS = Object.freeze([
"system.health",
+771
View File
@@ -0,0 +1,771 @@
import { createHash } from "node:crypto";
import {
CLOUD_API_SERVICE_ID,
createProtocolAuditEvent
} from "../audit/index.mjs";
import { ENVIRONMENT_DEV, assertProtocolRecord } from "../protocol/index.mjs";
import { HWLAB_M3_IO_CAPABILITY_LEVELS } from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
const DEFAULT_TIMEOUT_MS = 2200;
const M3_IO_CONTROL_CONTRACT_VERSION = "m3-io-control-v1";
const M3_IO_CONTROL_ROUTE = "/v1/m3/io";
const M3_IO_CHAIN = Object.freeze({
projectId: "prj_m3_hardware_loop",
sourceGatewayId: "gwsimu_1",
sourceGatewaySessionId: "gws_gwsimu_1",
sourceResourceId: "res_boxsimu_1",
sourceBoxId: "boxsimu_1",
sourcePort: "DO1",
targetGatewayId: "gwsimu_2",
targetGatewaySessionId: "gws_gwsimu_2",
targetResourceId: "res_boxsimu_2",
targetBoxId: "boxsimu_2",
targetPort: "DI1",
patchPanelServiceId: "hwlab-patch-panel"
});
const M3_IO_BLOCKER_CODES = Object.freeze({
gatewayUnavailable: "m3_gateway_session_unavailable",
boxUnavailable: "m3_box_resource_unavailable",
portDirectionInvalid: "m3_port_direction_invalid",
wiringMissing: "m3_wiring_missing",
patchPanelUnavailable: "m3_patch_panel_unavailable",
dispatchFailed: "m3_gateway_dispatch_failed",
runtimeDurableBlocked: "runtime_durable_not_green"
});
export async function requestRuntimeJson(url, { method = "GET", body, requestJson, timeoutMs = DEFAULT_TIMEOUT_MS }) {
if (requestJson) {
try {
const result = await requestJson(url, { method, body, timeoutMs });
return normalizeRuntimeJsonResponse(result);
} catch (error) {
return {
ok: false,
status: 0,
error: error instanceof Error ? error.message : String(error)
};
}
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method,
headers: body === undefined
? { accept: "application/json" }
: {
accept: "application/json",
"content-type": "application/json"
},
body: body === undefined ? undefined : JSON.stringify(body),
signal: controller.signal
});
const text = await response.text();
let parsed = null;
try {
parsed = text ? JSON.parse(text) : null;
} catch {
parsed = null;
}
return {
ok: response.ok,
status: response.status,
body: parsed,
error: response.ok ? null : parsed?.error?.message ?? parsed?.message ?? response.statusText
};
} catch (error) {
return {
ok: false,
status: 0,
error: error.name === "AbortError" ? `request timed out after ${timeoutMs}ms` : error.message
};
} finally {
clearTimeout(timer);
}
}
export function normalizeRuntimeJsonResponse(result = {}) {
if (Object.hasOwn(result, "ok") || Object.hasOwn(result, "body")) {
return {
ok: result.ok ?? (Number(result.status ?? 200) >= 200 && Number(result.status ?? 200) < 300),
status: result.status ?? 200,
body: result.body ?? result.json ?? null,
error: result.error ?? result.body?.error?.message ?? null
};
}
if (Object.hasOwn(result, "status") || Object.hasOwn(result, "json")) {
const status = result.status ?? 200;
return {
ok: status >= 200 && status < 300,
status,
body: result.json ?? null,
error: result.error ?? null
};
}
return {
ok: true,
status: 200,
body: result
};
}
export async function persistRuntimeRegistrations({ runtimeStore, gatewayStatus, box, capability, command, meta }) {
const gatewaySessionId = gatewayStatus.session?.gatewaySessionId ?? gatewayStatus.gatewaySessionId ?? gatewayStatus.registry?.gatewaySessionId ?? command.gatewaySessionId;
const gatewayId = gatewayStatus.gatewayId ?? gatewayStatus.session?.gatewayId ?? gatewayStatus.registry?.gatewayId ?? command.gatewayId;
const gatewaySession = {
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId,
gatewayId,
serviceId: "hwlab-gateway-simu",
endpoint: gatewayStatus.registry?.endpoint ?? gatewayStatus.session?.endpoint,
status: "connected",
labels: {
m3ControlSurface: "true"
}
};
const resource = {
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId,
resourceId: command.resourceId,
boxId: box.boxId ?? command.boxId,
resourceType: "simulator_endpoint",
state: "available",
metadata: {
serviceId: "hwlab-box-simu",
patchPanelOnlyPropagation: true,
frontendBypassAllowed: false
}
};
const writes = [];
writes.push(await tryRuntimeWrite("gateway.session.register", () => runtimeStore?.registerGatewaySession?.(gatewaySession, meta)));
writes.push(await tryRuntimeWrite("box.resource.register", () => runtimeStore?.registerBoxResource?.(resource, meta)));
writes.push(await tryRuntimeWrite("box.capability.report", () => runtimeStore?.reportBoxCapabilities?.({ capability }, meta)));
return writes;
}
export async function tryRuntimeWrite(label, write) {
if (typeof write !== "function") {
return {
label,
written: false,
skipped: true,
reason: "runtimeStore method unavailable"
};
}
try {
const result = await write();
if (result === undefined) {
return {
label,
written: false,
skipped: true,
reason: "runtimeStore method unavailable"
};
}
return {
label,
written: true,
result
};
} catch (error) {
return {
label,
written: false,
reason: error instanceof Error ? error.message : String(error),
code: error?.code ?? null,
data: error?.data ?? null
};
}
}
export async function successControlResult(input) {
const records = await writeOutcomeRecords({
...input,
outcome: "succeeded",
reason: null
});
const target = controlTarget(input.command, input.operation.gatewaySessionId);
const audit = auditState(records.auditWrite, records.runtimeAfter);
const evidence = evidenceState(records.runtimeAfter, records);
const readback = input.command.action === "di.read"
? {
status: "succeeded",
value: readValueFromGatewayResult(input.dispatch.body),
resourceId: input.command.resourceId,
port: input.command.port
}
: input.targetReadback ?? null;
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "succeeded",
accepted: true,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.ready,
controlReady: true,
action: input.action,
chain: M3_IO_CHAIN,
command: input.command,
target,
gatewayId: target.gatewayId,
gatewaySessionId: target.gatewaySessionId,
resourceId: target.resourceId,
boxId: target.boxId,
port: target.port,
value: target.value,
operationId: input.operation.operationId,
traceId: input.meta.traceId,
auditId: records.auditEvent.auditId,
evidenceId: records.evidenceRecord.evidenceId,
audit: {
auditId: records.auditEvent.auditId,
...audit
},
evidence: {
evidenceId: records.evidenceRecord.evidenceId,
...evidence
},
operationState: {
status: "succeeded",
reachable: true
},
auditState: audit,
operation: {
...input.operation,
status: "succeeded",
completedAt: input.now,
updatedAt: input.now
},
result: {
value: input.command.action === "di.read"
? readValueFromGatewayResult(input.dispatch.body)
: input.command.value,
dispatch: gatewayResultSummary(input.gateway),
patchPanel: input.patchPanel?.body ?? null,
targetReadback: input.targetReadback ?? null
},
readback,
controlPath: {
status: "succeeded",
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: input.action === "do.write",
frontendBypass: false
},
auditEvent: records.auditEvent,
evidenceRecord: records.evidenceRecord,
evidenceState: evidence,
durableStatus: durableStatus(records.runtimeAfter),
trustBlocker: trustBlocker(records.runtimeAfter),
blockerClassification: redactedBlockerClassification({
code: records.runtimeAfter?.blocker,
reason: records.runtimeAfter?.reason,
runtime: records.runtimeAfter
}),
persistence: persistenceSummary(records.runtimeAfter, input.registration, input.operationWrite, records),
observedAt: input.now
};
}
export async function blockedControlResult(input) {
const records = await writeOutcomeRecords({
...input,
outcome: "failed",
reason: input.reason
});
const target = controlTarget(input.command, input.operation.gatewaySessionId);
const audit = auditState(records.auditWrite, records.runtimeAfter);
const evidence = evidenceState(records.runtimeAfter, records);
const readback = input.command.action === "di.read"
? null
: input.targetReadback ?? null;
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
controlReady: false,
action: input.action,
chain: M3_IO_CHAIN,
command: input.command,
target,
gatewayId: target.gatewayId,
gatewaySessionId: target.gatewaySessionId,
resourceId: target.resourceId,
boxId: target.boxId,
port: target.port,
value: target.value,
operationId: input.operation.operationId,
traceId: input.meta.traceId,
auditId: records.auditEvent.auditId,
evidenceId: records.evidenceRecord.evidenceId,
audit: {
auditId: records.auditEvent.auditId,
...audit
},
evidence: {
evidenceId: records.evidenceRecord.evidenceId,
...evidence
},
blocker: {
code: input.code,
message: input.reason,
zh: input.reason
},
blockerClassification: redactedBlockerClassification({
code: input.code,
reason: input.reason,
runtime: records.runtimeAfter
}),
operationState: {
status: "blocked",
reachable: false,
blocker: input.code
},
auditState: audit,
operation: {
...input.operation,
status: "failed",
completedAt: input.now,
updatedAt: input.now,
error: {
code: input.code,
message: input.reason
}
},
result: {
dispatch: input.gateway ? gatewayResultSummary(input.gateway) : null,
patchPanel: input.patchPanel?.body ?? null,
targetReadback: input.targetReadback ?? null
},
readback,
controlPath: {
status: "blocked",
cloudApi: true,
gatewaySimu: Boolean(input.gateway),
boxSimu: input.code !== M3_IO_BLOCKER_CODES.gatewayUnavailable,
patchPanel: Boolean(input.patchPanel?.ok),
frontendBypass: false
},
auditEvent: records.auditEvent,
evidenceRecord: records.evidenceRecord,
evidenceState: evidence,
durableStatus: durableStatus(records.runtimeAfter),
trustBlocker: trustBlocker(records.runtimeAfter),
persistence: persistenceSummary(records.runtimeAfter, input.registration, input.operationWrite, records),
observedAt: input.now
};
}
export function blockedResult({ action, command = null, meta, runtime, code, reason, httpStatus, now, details = {} }) {
const target = controlTarget(command, command?.gatewaySessionId);
const audit = {
auditId: null,
status: "not_written",
durableStatus: durableStatus(runtime),
reason: "operation was blocked before audit write"
};
const evidence = evidenceState(runtime, {
auditWrite: { written: false, reason: "operation was blocked before audit write" },
evidenceWrite: { written: false, reason: "operation was blocked before evidence write" }
});
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
controlReady: false,
action: action || "unknown",
chain: M3_IO_CHAIN,
command,
target,
gatewayId: target.gatewayId,
gatewaySessionId: target.gatewaySessionId,
resourceId: target.resourceId,
boxId: target.boxId,
port: target.port,
value: target.value,
operationId: null,
traceId: meta.traceId,
auditId: null,
evidenceId: null,
audit,
evidence: {
evidenceId: null,
...evidence
},
blocker: {
code,
message: reason,
zh: reason
},
blockerClassification: redactedBlockerClassification({
code,
reason,
runtime
}),
operationState: {
status: "blocked",
reachable: false,
blocker: code
},
auditState: audit,
result: {
value: null,
targetReadback: null
},
readback: null,
controlPath: {
status: "blocked",
cloudApi: true,
gatewaySimu: false,
boxSimu: false,
patchPanel: false,
frontendBypass: false
},
evidenceState: evidence,
durableStatus: durableStatus(runtime),
trustBlocker: trustBlocker(runtime),
persistence: {
runtime,
writes: []
},
observedAt: now,
httpStatus,
...details
};
}
export async function writeOutcomeRecords({
action,
command,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
tracePayload,
outcome,
reason,
now
}) {
const auditEvent = createProtocolAuditEvent({
auditId: `aud_${operation.operationId.slice(3)}_${outcome}`,
traceId: meta.traceId,
actorType: "user",
actorId: meta.actorId,
action: action === "do.write" ? "m3.io.do.write" : "m3.io.di.read",
targetType: "hardware_operation",
targetId: operation.operationId,
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId: operation.gatewaySessionId,
operationId: operation.operationId,
serviceId: CLOUD_API_SERVICE_ID,
outcome,
reason: reason ?? undefined,
metadata: {
route: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort}->${M3_IO_CHAIN.patchPanelServiceId}->${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`,
command,
frontendBypass: false,
runtimeDurableBefore: runtimeBefore?.status ?? "unknown"
},
occurredAt: now
});
const evidenceRecord = createEvidenceRecord({
evidenceId: `evd_${operation.operationId.slice(3)}_${outcome}`,
projectId: M3_IO_CHAIN.projectId,
operationId: operation.operationId,
traceId: meta.traceId,
createdAt: now,
payload: {
outcome,
reason,
tracePayload,
command,
frontendBypass: false,
patchPanelOnlyPropagation: true
}
});
const auditWrite = await tryRuntimeWrite("audit.event.write", () => runtimeStore?.writeAuditEvent?.({ event: auditEvent }, meta));
const evidenceWrite = await tryRuntimeWrite("evidence.record.write", () => runtimeStore?.writeEvidenceRecord?.({ record: evidenceRecord }, meta));
const runtimeAfter = await safeRuntimeSummary(runtimeStore);
return {
auditEvent,
evidenceRecord,
auditWrite,
evidenceWrite,
runtimeAfter,
registration,
operationWrite
};
}
export function createEvidenceRecord({ evidenceId, projectId, operationId, traceId, payload, createdAt }) {
const metadata = {
traceId,
serviceId: CLOUD_API_SERVICE_ID,
route: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort}->${M3_IO_CHAIN.patchPanelServiceId}->${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`,
frontendBypass: false,
patchPanelOnlyPropagation: true
};
const serialized = JSON.stringify({
serviceId: CLOUD_API_SERVICE_ID,
projectId,
operationId,
traceId,
payload,
metadata,
createdAt
});
const record = {
evidenceId,
projectId,
operationId,
kind: "trace",
uri: `memory://hwlab-cloud-api/${operationId}/m3-io-control.json`,
mimeType: "application/json",
sha256: createHash("sha256").update(serialized).digest("hex"),
sizeBytes: Buffer.byteLength(serialized),
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
metadata,
createdAt
};
assertProtocolRecord("evidenceRecord", record);
return record;
}
export function evidenceState(runtime, records) {
const durableReady = isDurableRuntimeReady(runtime);
const recordsWritten = records.auditWrite?.written === true && records.evidenceWrite?.written === true;
const evidenceWriteState = recordWriteState(records.evidenceWrite, runtime);
if (durableReady && recordsWritten) {
return {
status: "green",
sourceKind: "DEV-LIVE",
durable: true,
writeStatus: evidenceWriteState.status,
durableStatus: durableStatus(runtime),
reason: "operation/audit/evidence 已通过 durable runtime 写入;仍需 DEV 浏览器 E2E 才能宣称验收通过。"
};
}
const blocker = runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked;
return {
status: "blocked",
sourceKind: "BLOCKED",
durable: runtime?.durable === true,
blocker,
writeStatus: evidenceWriteState.status,
durableStatus: durableStatus(runtime),
reason: `操作链路可达性与可信持久化分离:runtime durable 未 greenstatus=${runtime?.status ?? "unknown"}blocker=${blocker}),不能宣称可信闭环完全 green。`
};
}
export function auditState(auditWrite, runtime) {
return {
...recordWriteState(auditWrite, runtime),
durableStatus: durableStatus(runtime)
};
}
export function recordWriteState(write, runtime) {
if (write?.written === true && isDurableRuntimeReady(runtime)) {
return {
status: "persisted",
durable: true,
sourceKind: "DEV-LIVE"
};
}
if (write?.written === true) {
return {
status: "written_non_durable",
durable: false,
sourceKind: "BLOCKED",
blocker: runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked
};
}
return {
status: "not_written",
durable: false,
sourceKind: "BLOCKED",
blocker: runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked,
reason: write?.reason ?? "runtime write did not complete"
};
}
export function durableStatus(runtime) {
const ready = isDurableRuntimeReady(runtime);
return {
status: runtime?.status ?? "unknown",
durable: runtime?.durable === true,
ready: runtime?.ready === true,
liveRuntimeEvidence: runtime?.liveRuntimeEvidence === true,
blocker: ready ? null : runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked,
redacted: true
};
}
export function trustBlocker(runtime) {
if (isDurableRuntimeReady(runtime)) return null;
const durable = durableStatus(runtime);
return {
code: durable.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked,
layer: "runtime-durable",
zh: `runtime durable 未 green${durable.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked}`,
category: "runtime_durable",
durableStatus: durable
};
}
export function redactedBlockerClassification({ code, reason, runtime } = {}) {
const blockerCode = code ?? runtime?.blocker ?? null;
if (!blockerCode) {
return null;
}
return {
code: blockerCode,
category: blockerCategory(blockerCode),
runtimeStatus: runtime?.status ?? "unknown",
redacted: true,
message: reason ? redactBlockerMessage(reason) : null
};
}
export function blockerCategory(code) {
if (String(code).startsWith("runtime_durable")) return "runtime_durable";
if (String(code).includes("gateway")) return "gateway_session";
if (String(code).includes("patch_panel")) return "patch_panel";
if (String(code).includes("wiring")) return "patch_panel_wiring";
if (String(code).includes("box")) return "box_resource";
if (String(code).includes("port")) return "port_direction";
return "m3_io_control";
}
export function redactBlockerMessage(message) {
return String(message)
.replace(/https?:\/\/[^\s,;]+/giu, "[redacted-url]")
.replace(/postgres(?:ql)?:\/\/[^\s,;]+/giu, "[redacted-db-url]");
}
export function controlTarget(command, gatewaySessionId) {
return {
gatewayId: command?.gatewayId ?? null,
gatewaySessionId: gatewaySessionId ?? command?.gatewaySessionId ?? null,
resourceId: command?.resourceId ?? null,
boxId: command?.boxId ?? null,
port: command?.port ?? null,
value: Object.hasOwn(command ?? {}, "value") ? command.value : null
};
}
export function persistenceSummary(runtime, registration, operationWrite, records) {
return {
runtime,
writes: [
...(registration ?? []),
operationWrite,
records.auditWrite,
records.evidenceWrite
].filter(Boolean)
};
}
export async function safeRuntimeSummary(runtimeStore) {
try {
if (typeof runtimeStore?.readiness === "function") {
return await runtimeStore.readiness();
}
if (typeof runtimeStore?.summary === "function") {
return runtimeStore.summary();
}
} catch (error) {
return {
adapter: "unknown",
durable: false,
ready: false,
status: "blocked",
blocker: "runtime_summary_unavailable",
reason: error instanceof Error ? error.message : String(error),
liveRuntimeEvidence: false
};
}
return {
adapter: "none",
durable: false,
ready: false,
status: "blocked",
blocker: "runtime_store_missing",
reason: "runtime store is not available",
liveRuntimeEvidence: false
};
}
export function isDurableRuntimeReady(runtime) {
if (!runtime || typeof runtime !== "object") return false;
if (runtime.durable !== true) return false;
if (runtime.ready === false) return false;
if (runtime.liveRuntimeEvidence !== true) return false;
return !["blocked", "degraded", "failed"].includes(String(runtime.status ?? "").toLowerCase());
}
export function readValueFromGatewayResult(result) {
return result?.result?.value ?? result?.value ?? null;
}
export function gatewayResultSummary(gateway) {
return {
gatewayId: gateway.gatewayId,
gatewaySessionId: gateway.gatewaySessionId,
url: redactUrl(gateway.url),
role: gateway.role
};
}
export function publicGatewayStatus(status) {
return {
gatewayId: status?.gatewayId ?? status?.session?.gatewayId ?? null,
gatewaySessionId: status?.session?.gatewaySessionId ?? status?.gatewaySessionId ?? null,
boxCount: Array.isArray(status?.registry?.boxes) ? status.registry.boxes.length : null
};
}
export function finalizeControlResult(result) {
return result;
}
export function ensureTrailingSlash(url) {
return url.endsWith("/") ? url : `${url}/`;
}
export function trimUrl(value) {
const text = String(value ?? "").trim();
return text ? text : null;
}
export function parseTimeout(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
}
export function redactUrl(url) {
try {
const parsed = new URL(url);
parsed.username = parsed.username ? "***" : "";
parsed.password = parsed.password ? "***" : "";
return parsed.toString();
} catch {
return "invalid-url";
}
}
@@ -1,11 +1,11 @@
import assert from "node:assert/strict";
import test from "node:test";
import { test } from "bun:test";
import { createCloudRuntimeStore } from "../db/runtime-store.mjs";
import { createCloudRuntimeStore } from "../db/runtime-store.ts";
import { createBoxState, createGatewayState } from "../sim/model.mjs";
import { writeBoxPort } from "../sim/l2-runtime.mjs";
import { validateResponse } from "../protocol/index.mjs";
import { handleJsonRpcRequest, SUPPORTED_RPC_METHODS } from "./json-rpc.mjs";
import { handleJsonRpcRequest, SUPPORTED_RPC_METHODS } from "./json-rpc.ts";
import { HWLAB_M3_IO_CAPABILITY_LEVELS } from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
import {
M3_IO_BLOCKER_CODES,
@@ -16,7 +16,7 @@ import {
describeM3IoControlLive,
describeM3StatusLive,
handleM3IoControl
} from "./m3-io-control.mjs";
} from "./m3-io-control.ts";
const fixedNow = "2026-05-23T00:00:00.000Z";
@@ -10,6 +10,37 @@ import {
CLOUD_API_SERVICE_ID,
createProtocolAuditEvent
} from "../audit/index.mjs";
import {
requestRuntimeJson,
normalizeRuntimeJsonResponse,
persistRuntimeRegistrations,
tryRuntimeWrite,
successControlResult,
blockedControlResult,
blockedResult,
writeOutcomeRecords,
createEvidenceRecord,
evidenceState,
auditState,
recordWriteState,
durableStatus,
trustBlocker,
redactedBlockerClassification,
blockerCategory,
redactBlockerMessage,
controlTarget,
persistenceSummary,
safeRuntimeSummary,
isDurableRuntimeReady,
readValueFromGatewayResult,
gatewayResultSummary,
publicGatewayStatus,
finalizeControlResult,
ensureTrailingSlash,
trimUrl,
parseTimeout,
redactUrl
} from "./m3-io-control-runtime.ts";
import { HWLAB_M3_IO_CAPABILITY_LEVELS } from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
export const M3_IO_CONTROL_CONTRACT_VERSION = "m3-io-control-v1";
@@ -46,7 +77,7 @@ export const M3_IO_BLOCKER_CODES = Object.freeze({
runtimeDurableBlocked: "runtime_durable_not_green"
});
const DEFAULT_TIMEOUT_MS = 2200;
export const DEFAULT_TIMEOUT_MS = 2200;
const DEV_SERVICE_ENDPOINTS = Object.freeze({
gateway1: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101",
gateway2: "http://hwlab-gateway-simu-2.hwlab-dev.svc.cluster.local:7101",
@@ -1704,738 +1735,3 @@ function firstDiagnostic(body) {
...(Array.isArray(body?.routes) ? body.routes.flatMap((route) => route.diagnostics ?? []) : [])
][0] ?? null;
}
async function requestRuntimeJson(url, { method = "GET", body, requestJson, timeoutMs = DEFAULT_TIMEOUT_MS }) {
if (requestJson) {
try {
const result = await requestJson(url, { method, body, timeoutMs });
return normalizeRuntimeJsonResponse(result);
} catch (error) {
return {
ok: false,
status: 0,
error: error instanceof Error ? error.message : String(error)
};
}
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method,
headers: body === undefined
? { accept: "application/json" }
: {
accept: "application/json",
"content-type": "application/json"
},
body: body === undefined ? undefined : JSON.stringify(body),
signal: controller.signal
});
const text = await response.text();
let parsed = null;
try {
parsed = text ? JSON.parse(text) : null;
} catch {
parsed = null;
}
return {
ok: response.ok,
status: response.status,
body: parsed,
error: response.ok ? null : parsed?.error?.message ?? parsed?.message ?? response.statusText
};
} catch (error) {
return {
ok: false,
status: 0,
error: error.name === "AbortError" ? `request timed out after ${timeoutMs}ms` : error.message
};
} finally {
clearTimeout(timer);
}
}
function normalizeRuntimeJsonResponse(result = {}) {
if (Object.hasOwn(result, "ok") || Object.hasOwn(result, "body")) {
return {
ok: result.ok ?? (Number(result.status ?? 200) >= 200 && Number(result.status ?? 200) < 300),
status: result.status ?? 200,
body: result.body ?? result.json ?? null,
error: result.error ?? result.body?.error?.message ?? null
};
}
if (Object.hasOwn(result, "status") || Object.hasOwn(result, "json")) {
const status = result.status ?? 200;
return {
ok: status >= 200 && status < 300,
status,
body: result.json ?? null,
error: result.error ?? null
};
}
return {
ok: true,
status: 200,
body: result
};
}
async function persistRuntimeRegistrations({ runtimeStore, gatewayStatus, box, capability, command, meta }) {
const gatewaySessionId = gatewayStatus.session?.gatewaySessionId ?? gatewayStatus.gatewaySessionId ?? gatewayStatus.registry?.gatewaySessionId ?? command.gatewaySessionId;
const gatewayId = gatewayStatus.gatewayId ?? gatewayStatus.session?.gatewayId ?? gatewayStatus.registry?.gatewayId ?? command.gatewayId;
const gatewaySession = {
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId,
gatewayId,
serviceId: "hwlab-gateway-simu",
endpoint: gatewayStatus.registry?.endpoint ?? gatewayStatus.session?.endpoint,
status: "connected",
labels: {
m3ControlSurface: "true"
}
};
const resource = {
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId,
resourceId: command.resourceId,
boxId: box.boxId ?? command.boxId,
resourceType: "simulator_endpoint",
state: "available",
metadata: {
serviceId: "hwlab-box-simu",
patchPanelOnlyPropagation: true,
frontendBypassAllowed: false
}
};
const writes = [];
writes.push(await tryRuntimeWrite("gateway.session.register", () => runtimeStore?.registerGatewaySession?.(gatewaySession, meta)));
writes.push(await tryRuntimeWrite("box.resource.register", () => runtimeStore?.registerBoxResource?.(resource, meta)));
writes.push(await tryRuntimeWrite("box.capability.report", () => runtimeStore?.reportBoxCapabilities?.({ capability }, meta)));
return writes;
}
async function tryRuntimeWrite(label, write) {
if (typeof write !== "function") {
return {
label,
written: false,
skipped: true,
reason: "runtimeStore method unavailable"
};
}
try {
const result = await write();
if (result === undefined) {
return {
label,
written: false,
skipped: true,
reason: "runtimeStore method unavailable"
};
}
return {
label,
written: true,
result
};
} catch (error) {
return {
label,
written: false,
reason: error instanceof Error ? error.message : String(error),
code: error?.code ?? null,
data: error?.data ?? null
};
}
}
async function successControlResult(input) {
const records = await writeOutcomeRecords({
...input,
outcome: "succeeded",
reason: null
});
const target = controlTarget(input.command, input.operation.gatewaySessionId);
const audit = auditState(records.auditWrite, records.runtimeAfter);
const evidence = evidenceState(records.runtimeAfter, records);
const readback = input.command.action === "di.read"
? {
status: "succeeded",
value: readValueFromGatewayResult(input.dispatch.body),
resourceId: input.command.resourceId,
port: input.command.port
}
: input.targetReadback ?? null;
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "succeeded",
accepted: true,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.ready,
controlReady: true,
action: input.action,
chain: M3_IO_CHAIN,
command: input.command,
target,
gatewayId: target.gatewayId,
gatewaySessionId: target.gatewaySessionId,
resourceId: target.resourceId,
boxId: target.boxId,
port: target.port,
value: target.value,
operationId: input.operation.operationId,
traceId: input.meta.traceId,
auditId: records.auditEvent.auditId,
evidenceId: records.evidenceRecord.evidenceId,
audit: {
auditId: records.auditEvent.auditId,
...audit
},
evidence: {
evidenceId: records.evidenceRecord.evidenceId,
...evidence
},
operationState: {
status: "succeeded",
reachable: true
},
auditState: audit,
operation: {
...input.operation,
status: "succeeded",
completedAt: input.now,
updatedAt: input.now
},
result: {
value: input.command.action === "di.read"
? readValueFromGatewayResult(input.dispatch.body)
: input.command.value,
dispatch: gatewayResultSummary(input.gateway),
patchPanel: input.patchPanel?.body ?? null,
targetReadback: input.targetReadback ?? null
},
readback,
controlPath: {
status: "succeeded",
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: input.action === "do.write",
frontendBypass: false
},
auditEvent: records.auditEvent,
evidenceRecord: records.evidenceRecord,
evidenceState: evidence,
durableStatus: durableStatus(records.runtimeAfter),
trustBlocker: trustBlocker(records.runtimeAfter),
blockerClassification: redactedBlockerClassification({
code: records.runtimeAfter?.blocker,
reason: records.runtimeAfter?.reason,
runtime: records.runtimeAfter
}),
persistence: persistenceSummary(records.runtimeAfter, input.registration, input.operationWrite, records),
observedAt: input.now
};
}
async function blockedControlResult(input) {
const records = await writeOutcomeRecords({
...input,
outcome: "failed",
reason: input.reason
});
const target = controlTarget(input.command, input.operation.gatewaySessionId);
const audit = auditState(records.auditWrite, records.runtimeAfter);
const evidence = evidenceState(records.runtimeAfter, records);
const readback = input.command.action === "di.read"
? null
: input.targetReadback ?? null;
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
controlReady: false,
action: input.action,
chain: M3_IO_CHAIN,
command: input.command,
target,
gatewayId: target.gatewayId,
gatewaySessionId: target.gatewaySessionId,
resourceId: target.resourceId,
boxId: target.boxId,
port: target.port,
value: target.value,
operationId: input.operation.operationId,
traceId: input.meta.traceId,
auditId: records.auditEvent.auditId,
evidenceId: records.evidenceRecord.evidenceId,
audit: {
auditId: records.auditEvent.auditId,
...audit
},
evidence: {
evidenceId: records.evidenceRecord.evidenceId,
...evidence
},
blocker: {
code: input.code,
message: input.reason,
zh: input.reason
},
blockerClassification: redactedBlockerClassification({
code: input.code,
reason: input.reason,
runtime: records.runtimeAfter
}),
operationState: {
status: "blocked",
reachable: false,
blocker: input.code
},
auditState: audit,
operation: {
...input.operation,
status: "failed",
completedAt: input.now,
updatedAt: input.now,
error: {
code: input.code,
message: input.reason
}
},
result: {
dispatch: input.gateway ? gatewayResultSummary(input.gateway) : null,
patchPanel: input.patchPanel?.body ?? null,
targetReadback: input.targetReadback ?? null
},
readback,
controlPath: {
status: "blocked",
cloudApi: true,
gatewaySimu: Boolean(input.gateway),
boxSimu: input.code !== M3_IO_BLOCKER_CODES.gatewayUnavailable,
patchPanel: Boolean(input.patchPanel?.ok),
frontendBypass: false
},
auditEvent: records.auditEvent,
evidenceRecord: records.evidenceRecord,
evidenceState: evidence,
durableStatus: durableStatus(records.runtimeAfter),
trustBlocker: trustBlocker(records.runtimeAfter),
persistence: persistenceSummary(records.runtimeAfter, input.registration, input.operationWrite, records),
observedAt: input.now
};
}
function blockedResult({ action, command = null, meta, runtime, code, reason, httpStatus, now, details = {} }) {
const target = controlTarget(command, command?.gatewaySessionId);
const audit = {
auditId: null,
status: "not_written",
durableStatus: durableStatus(runtime),
reason: "operation was blocked before audit write"
};
const evidence = evidenceState(runtime, {
auditWrite: { written: false, reason: "operation was blocked before audit write" },
evidenceWrite: { written: false, reason: "operation was blocked before evidence write" }
});
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked,
controlReady: false,
action: action || "unknown",
chain: M3_IO_CHAIN,
command,
target,
gatewayId: target.gatewayId,
gatewaySessionId: target.gatewaySessionId,
resourceId: target.resourceId,
boxId: target.boxId,
port: target.port,
value: target.value,
operationId: null,
traceId: meta.traceId,
auditId: null,
evidenceId: null,
audit,
evidence: {
evidenceId: null,
...evidence
},
blocker: {
code,
message: reason,
zh: reason
},
blockerClassification: redactedBlockerClassification({
code,
reason,
runtime
}),
operationState: {
status: "blocked",
reachable: false,
blocker: code
},
auditState: audit,
result: {
value: null,
targetReadback: null
},
readback: null,
controlPath: {
status: "blocked",
cloudApi: true,
gatewaySimu: false,
boxSimu: false,
patchPanel: false,
frontendBypass: false
},
evidenceState: evidence,
durableStatus: durableStatus(runtime),
trustBlocker: trustBlocker(runtime),
persistence: {
runtime,
writes: []
},
observedAt: now,
httpStatus,
...details
};
}
async function writeOutcomeRecords({
action,
command,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
tracePayload,
outcome,
reason,
now
}) {
const auditEvent = createProtocolAuditEvent({
auditId: `aud_${operation.operationId.slice(3)}_${outcome}`,
traceId: meta.traceId,
actorType: "user",
actorId: meta.actorId,
action: action === "do.write" ? "m3.io.do.write" : "m3.io.di.read",
targetType: "hardware_operation",
targetId: operation.operationId,
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId: operation.gatewaySessionId,
operationId: operation.operationId,
serviceId: CLOUD_API_SERVICE_ID,
outcome,
reason: reason ?? undefined,
metadata: {
route: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort}->${M3_IO_CHAIN.patchPanelServiceId}->${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`,
command,
frontendBypass: false,
runtimeDurableBefore: runtimeBefore?.status ?? "unknown"
},
occurredAt: now
});
const evidenceRecord = createEvidenceRecord({
evidenceId: `evd_${operation.operationId.slice(3)}_${outcome}`,
projectId: M3_IO_CHAIN.projectId,
operationId: operation.operationId,
traceId: meta.traceId,
createdAt: now,
payload: {
outcome,
reason,
tracePayload,
command,
frontendBypass: false,
patchPanelOnlyPropagation: true
}
});
const auditWrite = await tryRuntimeWrite("audit.event.write", () => runtimeStore?.writeAuditEvent?.({ event: auditEvent }, meta));
const evidenceWrite = await tryRuntimeWrite("evidence.record.write", () => runtimeStore?.writeEvidenceRecord?.({ record: evidenceRecord }, meta));
const runtimeAfter = await safeRuntimeSummary(runtimeStore);
return {
auditEvent,
evidenceRecord,
auditWrite,
evidenceWrite,
runtimeAfter,
registration,
operationWrite
};
}
function createEvidenceRecord({ evidenceId, projectId, operationId, traceId, payload, createdAt }) {
const metadata = {
traceId,
serviceId: CLOUD_API_SERVICE_ID,
route: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort}->${M3_IO_CHAIN.patchPanelServiceId}->${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`,
frontendBypass: false,
patchPanelOnlyPropagation: true
};
const serialized = JSON.stringify({
serviceId: CLOUD_API_SERVICE_ID,
projectId,
operationId,
traceId,
payload,
metadata,
createdAt
});
const record = {
evidenceId,
projectId,
operationId,
kind: "trace",
uri: `memory://hwlab-cloud-api/${operationId}/m3-io-control.json`,
mimeType: "application/json",
sha256: createHash("sha256").update(serialized).digest("hex"),
sizeBytes: Buffer.byteLength(serialized),
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
metadata,
createdAt
};
assertProtocolRecord("evidenceRecord", record);
return record;
}
function evidenceState(runtime, records) {
const durableReady = isDurableRuntimeReady(runtime);
const recordsWritten = records.auditWrite?.written === true && records.evidenceWrite?.written === true;
const evidenceWriteState = recordWriteState(records.evidenceWrite, runtime);
if (durableReady && recordsWritten) {
return {
status: "green",
sourceKind: "DEV-LIVE",
durable: true,
writeStatus: evidenceWriteState.status,
durableStatus: durableStatus(runtime),
reason: "operation/audit/evidence 已通过 durable runtime 写入;仍需 DEV 浏览器 E2E 才能宣称验收通过。"
};
}
const blocker = runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked;
return {
status: "blocked",
sourceKind: "BLOCKED",
durable: runtime?.durable === true,
blocker,
writeStatus: evidenceWriteState.status,
durableStatus: durableStatus(runtime),
reason: `操作链路可达性与可信持久化分离:runtime durable 未 greenstatus=${runtime?.status ?? "unknown"}blocker=${blocker}),不能宣称可信闭环完全 green。`
};
}
function auditState(auditWrite, runtime) {
return {
...recordWriteState(auditWrite, runtime),
durableStatus: durableStatus(runtime)
};
}
function recordWriteState(write, runtime) {
if (write?.written === true && isDurableRuntimeReady(runtime)) {
return {
status: "persisted",
durable: true,
sourceKind: "DEV-LIVE"
};
}
if (write?.written === true) {
return {
status: "written_non_durable",
durable: false,
sourceKind: "BLOCKED",
blocker: runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked
};
}
return {
status: "not_written",
durable: false,
sourceKind: "BLOCKED",
blocker: runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked,
reason: write?.reason ?? "runtime write did not complete"
};
}
function durableStatus(runtime) {
const ready = isDurableRuntimeReady(runtime);
return {
status: runtime?.status ?? "unknown",
durable: runtime?.durable === true,
ready: runtime?.ready === true,
liveRuntimeEvidence: runtime?.liveRuntimeEvidence === true,
blocker: ready ? null : runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked,
redacted: true
};
}
function trustBlocker(runtime) {
if (isDurableRuntimeReady(runtime)) return null;
const durable = durableStatus(runtime);
return {
code: durable.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked,
layer: "runtime-durable",
zh: `runtime durable 未 green${durable.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked}`,
category: "runtime_durable",
durableStatus: durable
};
}
function redactedBlockerClassification({ code, reason, runtime } = {}) {
const blockerCode = code ?? runtime?.blocker ?? null;
if (!blockerCode) {
return null;
}
return {
code: blockerCode,
category: blockerCategory(blockerCode),
runtimeStatus: runtime?.status ?? "unknown",
redacted: true,
message: reason ? redactBlockerMessage(reason) : null
};
}
function blockerCategory(code) {
if (String(code).startsWith("runtime_durable")) return "runtime_durable";
if (String(code).includes("gateway")) return "gateway_session";
if (String(code).includes("patch_panel")) return "patch_panel";
if (String(code).includes("wiring")) return "patch_panel_wiring";
if (String(code).includes("box")) return "box_resource";
if (String(code).includes("port")) return "port_direction";
return "m3_io_control";
}
function redactBlockerMessage(message) {
return String(message)
.replace(/https?:\/\/[^\s,;]+/giu, "[redacted-url]")
.replace(/postgres(?:ql)?:\/\/[^\s,;]+/giu, "[redacted-db-url]");
}
function controlTarget(command, gatewaySessionId) {
return {
gatewayId: command?.gatewayId ?? null,
gatewaySessionId: gatewaySessionId ?? command?.gatewaySessionId ?? null,
resourceId: command?.resourceId ?? null,
boxId: command?.boxId ?? null,
port: command?.port ?? null,
value: Object.hasOwn(command ?? {}, "value") ? command.value : null
};
}
function persistenceSummary(runtime, registration, operationWrite, records) {
return {
runtime,
writes: [
...(registration ?? []),
operationWrite,
records.auditWrite,
records.evidenceWrite
].filter(Boolean)
};
}
async function safeRuntimeSummary(runtimeStore) {
try {
if (typeof runtimeStore?.readiness === "function") {
return await runtimeStore.readiness();
}
if (typeof runtimeStore?.summary === "function") {
return runtimeStore.summary();
}
} catch (error) {
return {
adapter: "unknown",
durable: false,
ready: false,
status: "blocked",
blocker: "runtime_summary_unavailable",
reason: error instanceof Error ? error.message : String(error),
liveRuntimeEvidence: false
};
}
return {
adapter: "none",
durable: false,
ready: false,
status: "blocked",
blocker: "runtime_store_missing",
reason: "runtime store is not available",
liveRuntimeEvidence: false
};
}
function isDurableRuntimeReady(runtime) {
if (!runtime || typeof runtime !== "object") return false;
if (runtime.durable !== true) return false;
if (runtime.ready === false) return false;
if (runtime.liveRuntimeEvidence !== true) return false;
return !["blocked", "degraded", "failed"].includes(String(runtime.status ?? "").toLowerCase());
}
function readValueFromGatewayResult(result) {
return result?.result?.value ?? result?.value ?? null;
}
function gatewayResultSummary(gateway) {
return {
gatewayId: gateway.gatewayId,
gatewaySessionId: gateway.gatewaySessionId,
url: redactUrl(gateway.url),
role: gateway.role
};
}
function publicGatewayStatus(status) {
return {
gatewayId: status?.gatewayId ?? status?.session?.gatewayId ?? null,
gatewaySessionId: status?.session?.gatewaySessionId ?? status?.gatewaySessionId ?? null,
boxCount: Array.isArray(status?.registry?.boxes) ? status.registry.boxes.length : null
};
}
function finalizeControlResult(result) {
return result;
}
function ensureTrailingSlash(url) {
return url.endsWith("/") ? url : `${url}/`;
}
function trimUrl(value) {
const text = String(value ?? "").trim();
return text ? text : null;
}
function parseTimeout(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
}
function redactUrl(url) {
try {
const parsed = new URL(url);
parsed.username = parsed.username ? "***" : "";
parsed.password = parsed.password ? "***" : "";
return parsed.toString();
} catch {
return "invalid-url";
}
}
+833
View File
@@ -0,0 +1,833 @@
import assert from "node:assert/strict";
import { createServer as createHttpServer } from "node:http";
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "bun:test";
import { createCloudApiServer } from "./server.ts";
import { validateCodeAgentChatSchema } from "./code-agent-chat.ts";
import { createCodexStdioSessionManager } from "./codex-stdio-session.ts";
import { createCodeAgentTraceStore } from "./code-agent-trace-store.ts";
import {
codexStdioChatFixture,
codexStdioReadyFixture,
createFakeAppServerClient,
createFakeCodexCommand,
delay,
pollAgentResult,
postAgent,
postAgentRaw,
prepareFakeCodexHome
} from "./server-test-helpers.ts";
test("cloud api /v1/agent/chat parse and params errors use structured blocker envelope", async () => {
const server = createCloudApiServer({
env: {
HWLAB_CODE_AGENT_MODEL: "gpt-test"
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const parse = await postAgentRaw(port, "{", { "x-trace-id": "trc_parse_error" });
assert.equal(parse.status, 400);
assert.equal(parse.body.status, "failed");
assert.equal(parse.body.error.code, "parse_error");
assert.equal(parse.body.error.layer, "api");
assert.equal(parse.body.error.retryable, true);
assert.equal(parse.body.error.traceId, "trc_parse_error");
assert.equal(parse.body.error.route, "/v1/agent/chat");
assert.match(parse.body.error.userMessage, /JSON/u);
assert.equal(Object.hasOwn(parse.body, "reply"), false);
assert.equal(JSON.stringify(parse.body).includes("sk-"), false);
const invalid = await postAgentRaw(port, JSON.stringify([]), { "x-trace-id": "trc_invalid_params" });
assert.equal(invalid.status, 400);
assert.equal(invalid.body.error.code, "invalid_params");
assert.equal(invalid.body.error.layer, "api");
assert.equal(invalid.body.error.retryable, true);
assert.equal(invalid.body.error.blocker.code, "invalid_params");
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat supports short submit and result polling", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-short-"));
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-short-codex-home-"));
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
CODEX_HOME: codexHome,
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
OPENAI_API_KEY: "test-openai-key-material",
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
},
codexStdioManager: {
describe() {
return {
...codexStdioReadyFixture({ workspace, codexHome }),
sandbox: "danger-full-access"
};
},
async probe() {
return {
...codexStdioReadyFixture({ workspace, codexHome }),
sandbox: "danger-full-access"
};
},
async chat(params = {}) {
return {
...codexStdioChatFixture({ workspace, codexHome, params }),
sandbox: "danger-full-access",
session: {
...codexStdioChatFixture({ workspace, codexHome, params }).session,
sandbox: "danger-full-access"
}
};
},
cancel() {},
reapIdle() {}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const traceId = "trc_server-test-short-submit";
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": traceId,
"prefer": "respond-async",
"x-hwlab-short-connection": "1"
},
body: JSON.stringify({
conversationId: "cnv_server-test-short-submit",
message: "用pwd列出你当前的工作目录"
})
});
assert.equal(submit.status, 202);
const accepted = await submit.json();
assert.equal(accepted.accepted, true);
assert.equal(accepted.shortConnection, true);
assert.equal(accepted.traceId, traceId);
assert.equal(accepted.resultUrl, `/v1/agent/chat/result/${traceId}`);
const payload = await pollAgentResult(port, traceId);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.traceId, traceId);
assert.equal(payload.sandbox, "danger-full-access");
assert.match(payload.reply.content, new RegExp(workspace.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")));
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await rm(workspace, { recursive: true, force: true });
await rm(codexHome, { recursive: true, force: true });
}
});
test("cloud api /v1/agent/chat/inspect maps conversation session and thread details to trace evidence", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-inspect-"));
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-inspect-codex-home-"));
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
CODEX_HOME: codexHome,
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
OPENAI_API_KEY: "test-openai-key-material",
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
},
codexStdioManager: {
describe() {
return {
...codexStdioReadyFixture({ workspace, codexHome }),
sandbox: "danger-full-access",
recentSessions: [{
sessionId: "ses_server_stdio_pwd",
conversationId: "cnv_server-test-inspect",
threadId: "019e6c72-2373-75e3-9856-eff286db7163",
status: "idle",
currentTraceId: null,
lastTraceId: "trc_server-test-inspect",
workspace,
sandbox: "danger-full-access",
secretMaterialStored: false,
valuesRedacted: true
}]
};
},
async probe() {
return {
...codexStdioReadyFixture({ workspace, codexHome }),
sandbox: "danger-full-access"
};
},
async chat(params = {}) {
const base = codexStdioChatFixture({ workspace, codexHome, params });
return {
...base,
sandbox: "danger-full-access",
session: {
...base.session,
threadId: "019e6c72-2373-75e3-9856-eff286db7163",
sandbox: "danger-full-access"
},
sessionReuse: {
...base.sessionReuse,
threadId: "019e6c72-2373-75e3-9856-eff286db7163"
}
};
},
get(sessionId) {
if (sessionId !== "ses_server_stdio_pwd") return null;
return {
sessionId,
conversationId: "cnv_server-test-inspect",
threadId: "019e6c72-2373-75e3-9856-eff286db7163",
status: "idle",
lastTraceId: "trc_server-test-inspect",
workspace,
sandbox: "danger-full-access",
secretMaterialStored: false,
valuesRedacted: true
};
},
cancel() {},
reapIdle() {}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const traceId = "trc_server-test-inspect";
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": traceId,
"prefer": "respond-async",
"x-hwlab-short-connection": "1"
},
body: JSON.stringify({
conversationId: "cnv_server-test-inspect",
message: "inspect mapping smoke"
})
});
assert.equal(submit.status, 202);
const payload = await pollAgentResult(port, traceId);
assert.equal(payload.status, "completed");
const inspect = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/inspect?conversationId=cnv_server-test-inspect&sessionId=ses_server_stdio_pwd&threadId=019e6c72-2373-75e3-9856-eff286db7163`);
assert.equal(inspect.status, 200);
const body = await inspect.json();
assert.equal(body.ok, true);
assert.equal(body.status, "found");
assert.equal(body.latestTraceId, traceId);
assert.equal(body.traceUrl, `/v1/agent/chat/trace/${traceId}`);
assert.equal(body.resultUrl, `/v1/agent/chat/result/${traceId}`);
assert.equal(body.query.threadId, "019e6c72-2373-75e3-9856-eff286db7163");
assert.equal(body.session.sessionId, "ses_server_stdio_pwd");
assert.equal(body.conversationFacts.conversationId, "cnv_server-test-inspect");
assert.equal(body.runnerTrace.traceId, traceId);
assert.equal(body.valuesRedacted, true);
assert.equal(body.secretMaterialStored, false);
assert.equal(JSON.stringify(body).includes("test-openai-key-material"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await rm(workspace, { recursive: true, force: true });
await rm(codexHome, { recursive: true, force: true });
}
});
test("cloud api /v1/agent/chat/inspect returns not_found for empty conversation facts", async () => {
const server = createCloudApiServer();
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/inspect?conversationId=cnv_server-test-inspect-missing&sessionId=ses_server_test_missing&threadId=019e6c72-2373-75e3-9856-eff286db7163&traceId=trc_server_test_missing`);
assert.equal(response.status, 404);
const body = await response.json();
assert.equal(body.ok, false);
assert.equal(body.status, "not_found");
assert.equal(body.latestTraceId, null);
assert.deepEqual(body.traceIds, []);
assert.equal(body.traceUrl, null);
assert.equal(body.resultUrl, null);
assert.equal(body.conversationFacts.turnCount, 0);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api result polling compacts large runnerTrace while preserving providerTrace", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-result-compact-"));
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-result-compact-codex-home-"));
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
CODEX_HOME: codexHome,
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT: "32",
OPENAI_API_KEY: "test-openai-key-material",
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
},
codexStdioManager: {
describe() {
return {
...codexStdioReadyFixture({ workspace, codexHome }),
sandbox: "danger-full-access"
};
},
async probe() {
return {
...codexStdioReadyFixture({ workspace, codexHome }),
sandbox: "danger-full-access"
};
},
async chat(params = {}) {
const base = codexStdioChatFixture({ workspace, codexHome, params });
const events = Array.from({ length: 160 }, (_, index) => ({
seq: index + 1,
traceId: params.traceId,
type: "assistant_message",
status: "chunk",
label: index === 159 ? "assistant:completed" : "assistant:chunk",
createdAt: "2026-05-23T00:00:00.000Z",
waitingFor: index === 159 ? null : "turn/completed",
valuesPrinted: false
}));
return {
...base,
sandbox: "danger-full-access",
session: {
...base.session,
sandbox: "danger-full-access"
},
runnerTrace: {
...base.runnerTrace,
events,
eventLabels: events.map((event) => event.label),
eventCount: events.length,
lastEvent: events.at(-1)
}
};
},
cancel() {},
reapIdle() {}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const traceId = "trc_server-test-result-compact";
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": traceId,
"prefer": "respond-async",
"x-hwlab-short-connection": "1"
},
body: JSON.stringify({
conversationId: "cnv_server-test-result-compact",
message: "生成大量 trace 后返回"
})
});
assert.equal(submit.status, 202);
const payload = await pollAgentResult(port, traceId);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.traceId, traceId);
assert.equal(payload.providerTrace.protocol, "codex-app-server-jsonrpc-stdio");
assert.equal(payload.providerTrace.command, "codex app-server --listen stdio://");
assert.equal(payload.runnerTrace.eventCount, 160);
assert.equal(payload.runnerTrace.eventsCompacted, true);
assert.equal(payload.runnerTrace.events.length, 32);
assert.equal(payload.runnerTrace.events.some((event) => event.label === "trace:compacted"), true);
assert.equal(payload.runnerTrace.lastEvent.label, "assistant:completed");
assert.equal(payload.runnerTrace.eventLabels.includes("trace:compacted"), true);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await rm(workspace, { recursive: true, force: true });
await rm(codexHome, { recursive: true, force: true });
}
});
test("cloud api /v1/agent/chat answers skills prompt through real Codex stdio and retains trace", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-skills-"));
const codexHome = path.join(workspace, "codex-home");
const skillsDir = path.join(workspace, "skills");
const fakeCodex = await createFakeCodexCommand();
await mkdir(path.join(skillsDir, "hwlab-test-skill"), { recursive: true });
await prepareFakeCodexHome(codexHome);
await writeFile(path.join(skillsDir, "hwlab-test-skill", "SKILL.md"), [
"---",
"name: hwlab-test-skill",
"description: Test skill mounted for Codex stdio discovery.",
"version: v-test",
"---",
"",
"# HWLAB Test Skill"
].join("\n"));
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
CODEX_HOME: codexHome,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
HWLAB_CODE_AGENT_SKILLS_DIRS: skillsDir,
HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
},
codexStdioManager: createCodexStdioSessionManager({
idFactory: () => "ses_server_stdio_skills",
createRpcClient: async () => createFakeAppServerClient({
text: "模型泛化回答不应成为 skills 最终回复。"
})
})
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const payload = await postAgent(port, {
conversationId: "cnv_server_stdio_skills",
traceId: "trc_server_stdio_skills",
message: "列出你可用的skills"
});
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session");
assert.equal(payload.providerTrace.sidecarOnly, undefined);
assert.equal(payload.providerTrace.toolName, "codex-app-server.thread/start+turn/start");
assert.equal(payload.skills.status, "ready");
const discovered = payload.skills.items.find((skill) => skill.name === "hwlab-test-skill");
assert.ok(discovered);
assert.equal(discovered.source, path.join(skillsDir, "hwlab-test-skill", "SKILL.md"));
assert.equal(discovered.manifest.path, path.join(skillsDir, "hwlab-test-skill", "SKILL.md"));
assert.equal(discovered.version, "v-test");
assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "skills.discover" && toolCall.status === "completed"));
assert.match(payload.reply.content, /hwlab-test-skill/u);
assert.match(payload.reply.content, / skill manifest /u);
assert.doesNotMatch(payload.reply.content, //u);
assert.ok(payload.runnerTrace.events.some((event) => event.label === "prompt:sent"));
assert.ok(payload.runnerTrace.events.some((event) => event.label === "tool:codex-app-server.thread/start+turn/start:started"));
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await rm(workspace, { recursive: true, force: true });
await rm(fakeCodex.root, { recursive: true, force: true });
}
});
test("cloud api /v1/agent/chat returns structured skills blocker without local skills manifest", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-skills-missing-"));
const codexHome = path.join(workspace, "codex-home");
const fakeCodex = await createFakeCodexCommand();
await prepareFakeCodexHome(codexHome);
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
CODEX_HOME: codexHome,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
HWLAB_CODE_AGENT_SKILLS_DIRS: path.join(workspace, "missing-skills"),
HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
},
codexStdioManager: createCodexStdioSessionManager({
idFactory: () => "ses_server_stdio_skills_missing",
createRpcClient: async () => createFakeAppServerClient({
text: "模型泛化回答:alpha-skill"
})
})
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const payload = await postAgent(port, {
conversationId: "cnv_server_stdio_skills_missing",
traceId: "trc_server_stdio_skills_missing",
message: "列出你可用的skills"
});
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "failed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
assert.equal(payload.capabilityLevel, "blocked");
assert.equal(payload.error.code, "skills_unavailable");
assert.equal(payload.skills.status, "blocked");
assert.equal(payload.skills.blockers[0].code, "skills_unavailable");
assert.deepEqual(payload.skills.sourceIssues, ["pikasTech/HWLAB#136", "pikasTech/HWLAB#237"]);
assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "skills.discover" && toolCall.status === "blocked"));
assert.equal(payload.toolCalls.some((toolCall) => toolCall.name === "codex-app-server.thread/start+turn/start"), false);
assert.equal(payload.session.status, "idle");
assert.equal(Object.hasOwn(payload, "reply"), false);
assert.equal(JSON.stringify(payload).includes("alpha-skill"), false);
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await rm(workspace, { recursive: true, force: true });
await rm(fakeCodex.root, { recursive: true, force: true });
}
});
test("cloud api /v1/agent/chat blocks forbidden file paths without leaking values", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-file-block-"));
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: workspace
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const payload = await postAgent(port, {
conversationId: "cnv_server-test-security-block",
traceId: "trc_server-test-security-block",
message: "cat ../outside.txt"
});
assert.equal(payload.status, "failed");
assert.match(payload.error.code, /^(codex_cli_binary_missing|stdio_protocol_not_wired)$/u);
assert.deepEqual(payload.toolCalls, []);
assert.equal(payload.capabilityLevel, "blocked");
assert.ok(payload.runnerTrace.events.some((event) => event.label === "codex-stdio:blocked"));
assert.equal(Object.hasOwn(payload, "reply"), false);
assert.equal(JSON.stringify(payload).includes("sk-"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat/cancel reports unsupported lifecycle degradation", async () => {
const traceStore = createCodeAgentTraceStore();
traceStore.append("trc_cancel_unsupported", {
type: "request",
status: "running",
label: "request:running",
sessionId: "ses_cancel_unsupported",
sessionStatus: "busy",
sessionLifecycleStatus: "busy"
});
const server = createCloudApiServer({
traceStore,
codexStdioManager: {
get() {
return null;
}
},
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio"
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/cancel`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": "trc_cancel_unsupported"
},
body: JSON.stringify({
conversationId: "cnv_cancel_unsupported",
sessionId: "ses_cancel_unsupported",
traceId: "trc_cancel_unsupported"
})
});
assert.equal(response.status, 501);
const payload = await response.json();
assert.equal(payload.status, "degraded");
assert.equal(payload.unsupported, true);
assert.equal(payload.degraded, true);
assert.equal(payload.error.code, "cancel_unsupported");
assert.equal(payload.sessionLifecycleStatus, "failed");
assert.equal(payload.sessionSummary.unsupported, true);
assert.match(payload.sessionSummary.userMessage, /unsupported\/degraded/u);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat reports Codex stdio blocker instead of structured skills fallback", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-no-skills-"));
const skillsDir = path.join(root, "missing-skills");
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: root
},
skillsDirs: [skillsDir],
skillsDirsExact: true
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
conversationId: "cnv_server-test-skills-missing",
message: "列出你可用的skills"
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.status, "failed");
assert.equal(payload.provider, "codex-stdio");
assert.notEqual(payload.error.code, "skills_unavailable");
assert.equal(payload.capabilityLevel, "blocked");
assert.equal(payload.skills.status, "not_requested");
assert.deepEqual(payload.toolCalls, []);
assert.ok(payload.runnerLimitations.includes("no-controlled-readonly-fallback"));
assert.equal(Object.hasOwn(payload, "reply"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat does not run unsupported local grep fallback", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-tool-blocked-"));
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: workspace
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
conversationId: "cnv_server-test-tool-unavailable",
message: "请用grep搜索 package.json"
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.status, "failed");
assert.notEqual(payload.error.code, "tool_unavailable");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.capabilityLevel, "blocked");
assert.deepEqual(payload.toolCalls, []);
assert.ok(payload.runnerLimitations.includes("no-controlled-readonly-fallback"));
assert.equal(Object.hasOwn(payload, "reply"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat does not call delayed provider fallback beyond legacy 4500ms UI timeout", async () => {
let providerCalled = false;
const server = createCloudApiServer({
callCodeAgentProvider: async ({ providerPlan, message, traceId }) => {
providerCalled = true;
await delay(4700);
return {
provider: providerPlan.provider,
model: providerPlan.model,
backend: providerPlan.backend,
content: `延迟真实 provider stub: ${message} / ${traceId}`,
usage: null,
providerTrace: {
source: "delayed-test-provider"
}
};
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const startedAt = Date.now();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": "trc_server-test-agent-chat-delayed"
},
body: JSON.stringify({
conversationId: "cnv_server-test-agent-chat-delayed",
message: "请用一句话说明当前 HWLAB 工作台可以做什么,稍后回答"
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(Date.now() - startedAt < 4500, true);
assert.equal(payload.status, "failed");
assert.equal(payload.conversationId, "cnv_server-test-agent-chat-delayed");
assert.equal(payload.traceId, "trc_server-test-agent-chat-delayed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.capabilityLevel, "blocked");
assert.equal(providerCalled, false);
assert.equal(Object.hasOwn(payload, "reply"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat does not call OpenAI provider 502/503 fallback", async () => {
for (const status of [502, 503]) {
const providerServer = createHttpServer((request, response) => {
request.resume();
response.writeHead(status, { "content-type": "application/json" });
response.end(JSON.stringify({
error: {
message: `upstream ${status}`
}
}));
});
await new Promise((resolve) => providerServer.listen(0, "127.0.0.1", resolve));
const providerPort = providerServer.address().port;
const server = createCloudApiServer({
env: {
OPENAI_API_KEY: "test-openai-key-material",
HWLAB_CODE_AGENT_PROVIDER: "openai",
HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "true",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_OPENAI_BASE_URL: `http://127.0.0.1:${providerPort}/v1/responses`
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": `trc_server-test-agent-chat-provider-${status}`
},
body: JSON.stringify({
conversationId: `cnv_server-test-agent-chat-provider-${status}`,
message: `provider ${status}`
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.status, "failed");
assert.equal(payload.traceId, `trc_server-test-agent-chat-provider-${status}`);
assert.match(payload.error.code, /^(codex_cli_binary_missing|stdio_protocol_not_wired)$/u);
assert.match(payload.error.layer, /^(runner|api)$/u);
assert.equal(typeof payload.error.retryable, "boolean");
assert.equal(payload.error.blocker.traceId, `trc_server-test-agent-chat-provider-${status}`);
assert.equal(typeof payload.error.blocker.retryable, "boolean");
assert.equal(payload.error.blocker.capabilityLevel, "blocked");
assert.equal(payload.provider, "codex-stdio");
assert.equal(Object.hasOwn(payload, "reply"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise((resolve, reject) => {
providerServer.close((error) => (error ? reject(error) : resolve()));
});
}
}
});
test("cloud api /v1/agent/chat does not call empty provider text fallback", async () => {
let providerCalled = false;
const server = createCloudApiServer({
callCodeAgentProvider: async () => {
providerCalled = true;
throw new Error("empty provider fallback must not be used");
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json"
},
body: JSON.stringify({
conversationId: "cnv_empty-provider-text",
message: "你好"
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.conversationId, "cnv_empty-provider-text");
assert.equal(payload.status, "failed");
assert.match(payload.error.code, /^(codex_cli_binary_missing|stdio_protocol_not_wired)$/u);
assert.equal(payload.provider, "codex-stdio");
assert.equal(Object.hasOwn(payload, "reply"), false);
assert.equal(providerCalled, false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
+801
View File
@@ -0,0 +1,801 @@
import { randomUUID } from "node:crypto";
import { M3_IO_CONTROL_ROUTE, M3_STATUS_ROUTE, describeM3StatusLive, handleM3IoControl } from "./m3-io-control.ts";
import { createCodeAgentErrorPayload, handleCodeAgentChat } from "./code-agent-chat.ts";
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.ts";
import {
firstHeaderValue,
getHeader,
parsePositiveInteger,
positiveInteger,
readBody,
safeConversationId,
safeOpaqueId,
safeSessionId,
safeTraceId,
sendJson,
truthyFlag
} from "./server-http-utils.ts";
const DEFAULT_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT = 120;
const DEFAULT_CODE_AGENT_PROVIDER_PROFILE = "deepseek";
const CODE_AGENT_PROVIDER_PROFILE_IDS = Object.freeze(["deepseek", "codex-api", "runtime-default"]);
const CODE_AGENT_PROVIDER_PROFILE_LABELS = Object.freeze({
"deepseek": "DeepSeek",
"codex-api": "Codex API",
"runtime-default": "运行默认"
});
const DEFAULT_CODE_AGENT_CODEX_API_MODEL = "gpt-5.5";
const DEFAULT_CODE_AGENT_CODEX_API_BASE_URL = "http://127.0.0.1:49280/responses";
const DEFAULT_CODE_AGENT_DEEPSEEK_MODEL = "deepseek-chat";
export 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, params));
}
function codeAgentChatExecutionOptions(options = {}, params = {}) {
return {
callProvider: options.callCodeAgentProvider,
timeoutMs: options.codeAgentTimeoutMs,
hardTimeoutMs: options.codeAgentHardTimeoutMs,
env: codeAgentProviderProfileEnv(options.env ?? process.env, params),
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 codeAgentProviderProfileEnv(env = process.env, params = {}) {
const profile = normalizeCodeAgentProviderProfile(params.providerProfile ?? params.codeAgentProviderProfile ?? env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE);
if (profile === "runtime-default") return env;
const overlay = { ...env };
overlay.HWLAB_CODE_AGENT_SELECTED_PROVIDER_PROFILE = profile;
overlay.HWLAB_CODE_AGENT_PROVIDER_PROFILE_LABEL = CODE_AGENT_PROVIDER_PROFILE_LABELS[profile] ?? profile;
overlay.HWLAB_CODE_AGENT_PROVIDER = "codex-stdio";
if (profile === "codex-api") {
overlay.HWLAB_CODE_AGENT_MODEL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_CODEX_API_MODEL, env.HWLAB_CODE_AGENT_LEGACY_CODEX_MODEL, DEFAULT_CODE_AGENT_CODEX_API_MODEL);
overlay.HWLAB_CODE_AGENT_OPENAI_BASE_URL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_CODEX_API_BASE_URL, env.HWLAB_CODE_AGENT_LEGACY_OPENAI_BASE_URL, DEFAULT_CODE_AGENT_CODEX_API_BASE_URL);
} else {
overlay.HWLAB_CODE_AGENT_MODEL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_DEEPSEEK_MODEL, DEFAULT_CODE_AGENT_DEEPSEEK_MODEL);
overlay.HWLAB_CODE_AGENT_OPENAI_BASE_URL = firstNonEmptyValue(env.HWLAB_CODE_AGENT_DEEPSEEK_BASE_URL, defaultDeepSeekBaseUrlForEnv(env));
}
overlay.NO_PROXY = mergeNoProxyEntries(env.NO_PROXY, codeAgentClusterNoProxyEntries(env));
overlay.no_proxy = mergeNoProxyEntries(env.no_proxy ?? env.NO_PROXY, codeAgentClusterNoProxyEntries(env));
return overlay;
}
function normalizeCodeAgentProviderProfile(value) {
const text = String(value ?? DEFAULT_CODE_AGENT_PROVIDER_PROFILE).trim().toLowerCase();
return CODE_AGENT_PROVIDER_PROFILE_IDS.includes(text) ? text : DEFAULT_CODE_AGENT_PROVIDER_PROFILE;
}
function defaultDeepSeekBaseUrlForEnv(env = process.env) {
const namespace = env.HWLAB_GITOPS_PROFILE === "prod" || env.HWLAB_ENVIRONMENT === "prod" ? "hwlab-prod" : "hwlab-dev";
return `http://hwlab-deepseek-proxy.${namespace}.svc.cluster.local:4000/v1/responses`;
}
function codeAgentClusterNoProxyEntries(env = process.env) {
const namespace = env.HWLAB_GITOPS_PROFILE === "prod" || env.HWLAB_ENVIRONMENT === "prod" ? "hwlab-prod" : "hwlab-dev";
return [
`${namespace}.svc.cluster.local`,
".svc",
".cluster.local",
"127.0.0.1",
"localhost",
"::1",
"10.0.0.0/8",
"10.42.0.0/16",
"10.43.0.0/16",
"hyueapi.com",
".hyueapi.com",
"hyui.com",
".hyui.com",
"api.minimaxi.com",
".minimaxi.com"
];
}
function mergeNoProxyEntries(current, additions = []) {
return uniqueStrings([...String(current ?? "").split(","), ...additions]).join(",");
}
function firstNonEmptyValue(...values) {
for (const value of values) {
const text = String(value ?? "").trim();
if (text) return text;
}
return null;
}
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);
}
export 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, compactCodeAgentChatResultPayload(result, options));
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: compactRunnerTraceForResult(runnerTrace, resultTraceEventLimit(options)),
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}`
}
});
}
export async function handleCodeAgentInspectHttp(request, response, url, options) {
const query = {
conversationId: safeConversationId(url.searchParams.get("conversationId")),
sessionId: safeSessionId(url.searchParams.get("sessionId")),
threadId: safeOpaqueId(url.searchParams.get("threadId")),
traceId: safeTraceId(url.searchParams.get("traceId"))
};
const sessionRegistry = options.sessionRegistry;
const traceStore = options.traceStore ?? defaultCodeAgentTraceStore;
const manager = options.codexStdioManager;
const managerDescription = manager && typeof manager.describe === "function" ? manager.describe() : null;
const managerRecentSessions = Array.isArray(managerDescription?.recentSessions) ? managerDescription.recentSessions : [];
const directManagerSession = query.sessionId && manager && typeof manager.get === "function"
? manager.get(query.sessionId, { conversationId: query.conversationId })
: null;
const matchedManagerSession = directManagerSession ?? managerRecentSessions.find((session) => sessionMatchesInspectQuery(session, query)) ?? null;
const registryInspect = sessionRegistry && typeof sessionRegistry.inspect === "function"
? sessionRegistry.inspect(query)
: { ok: false, status: "unavailable", traceIds: [], session: null, conversationFacts: null };
const session = matchedManagerSession ?? registryInspect.session ?? null;
const conversationFacts = registryInspect.conversationFacts ?? null;
const requestedRunnerTrace = query.traceId ? traceStore.snapshot(query.traceId) : null;
const requestedTraceFound = Boolean(requestedRunnerTrace && requestedRunnerTrace.status !== "missing");
const traceIds = uniqueStrings([
requestedTraceFound ? query.traceId : null,
session?.currentTraceId,
session?.lastTraceId,
conversationFacts?.latestTraceId,
...(Array.isArray(conversationFacts?.traceIds) ? conversationFacts.traceIds : []),
...(Array.isArray(registryInspect.traceIds) ? registryInspect.traceIds : [])
]);
const latestTraceId = traceIds[0] ?? null;
const runnerTrace = latestTraceId
? latestTraceId === query.traceId && requestedRunnerTrace
? requestedRunnerTrace
: traceStore.snapshot(latestTraceId)
: null;
const found = Boolean(registryInspect.ok || session || latestTraceId);
sendJson(response, found ? 200 : 404, {
ok: found,
action: "code-agent.chat.inspect",
status: found ? "found" : "not_found",
query,
session,
conversationFacts,
traceIds,
latestTraceId,
traceUrl: latestTraceId ? `/v1/agent/chat/trace/${encodeURIComponent(latestTraceId)}` : null,
resultUrl: latestTraceId ? `/v1/agent/chat/result/${encodeURIComponent(latestTraceId)}` : null,
runnerTrace,
valuesRedacted: true,
secretMaterialStored: false
});
}
function sessionMatchesInspectQuery(session, query) {
if (!session || typeof session !== "object") return false;
if (query.sessionId && session.sessionId === query.sessionId) return true;
if (query.threadId && session.threadId === query.threadId) return true;
if (query.conversationId && session.conversationId === query.conversationId) return true;
if (query.traceId && (session.currentTraceId === query.traceId || session.lastTraceId === query.traceId)) return true;
return false;
}
export 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"
}
};
}
export 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();
});
}
export 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 compactCodeAgentChatResultPayload(payload, options = {}) {
if (!payload || typeof payload !== "object") return payload;
const limit = resultTraceEventLimit(options);
return {
...payload,
...(payload.runnerTrace && typeof payload.runnerTrace === "object"
? { runnerTrace: compactRunnerTraceForResult(payload.runnerTrace, limit) }
: {})
};
}
function compactRunnerTraceForResult(runnerTrace, limit = DEFAULT_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT) {
if (!runnerTrace || typeof runnerTrace !== "object") return runnerTrace;
const events = Array.isArray(runnerTrace.events) ? runnerTrace.events : [];
const total = Number.isInteger(runnerTrace.eventCount) ? runnerTrace.eventCount : events.length;
const effectiveLimit = Math.max(8, positiveInteger(limit, DEFAULT_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT));
if (events.length <= effectiveLimit) {
return {
...runnerTrace,
eventCount: total,
eventsCompacted: false
};
}
const headCount = Math.min(24, Math.max(1, Math.floor(effectiveLimit / 4)));
const tailCount = Math.max(1, effectiveLimit - headCount - 1);
const omitted = Math.max(0, events.length - headCount - tailCount);
const head = events.slice(0, headCount);
const tail = events.slice(events.length - tailCount);
const marker = {
traceId: runnerTrace.traceId ?? head[0]?.traceId ?? tail[0]?.traceId ?? null,
type: "trace",
stage: "trace",
status: "truncated",
label: "trace:compacted",
createdAt: tail[0]?.createdAt ?? runnerTrace.updatedAt ?? new Date().toISOString(),
message: `Result polling response compacted ${omitted} trace events; fetch /v1/agent/chat/trace/${encodeURIComponent(runnerTrace.traceId ?? "")} for the full trace.`,
omittedEventCount: omitted,
retainedEventCount: head.length + tail.length,
totalEventCount: total,
valuesPrinted: false
};
const compactEvents = [...head, marker, ...tail];
return {
...runnerTrace,
eventCount: total,
events: compactEvents,
eventLabels: compactEvents.map(traceEventLabel).filter(Boolean),
eventsCompacted: true,
eventWindow: {
mode: "head-tail",
retained: compactEvents.length,
omitted,
total
}
};
}
function traceEventLabel(event) {
return typeof event === "string" ? event : event?.label;
}
function resultTraceEventLimit(options = {}) {
return positiveInteger(
options.env?.HWLAB_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT,
DEFAULT_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT
);
}
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}`
};
};
}
function uniqueStrings(values) {
return [...new Set(values.map((value) => String(value ?? "").trim()).filter(Boolean))];
}
+847
View File
@@ -0,0 +1,847 @@
import assert from "node:assert/strict";
import { createServer as createTcpServer } from "node:net";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "bun:test";
import { createCloudApiServer } from "./server.ts";
import { createCodexStdioSessionManager } from "./codex-stdio-session.ts";
import {
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
} from "../db/runtime-store.ts";
import {
createFakeCodexCommand,
durableBlockedRuntime,
durableReadyRuntime,
layerForBlockedCase,
layerStatus,
prepareFakeCodexHome
} from "./server-test-helpers.ts";
test("cloud api health reports DB env presence and live connection classification without leaking values", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
const originalAliasEnv = {
HWLAB_CLOUD_DB_SERVICE_NAME: process.env.HWLAB_CLOUD_DB_SERVICE_NAME,
HWLAB_CLOUD_DB_SERVICE_NAMESPACE: process.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE,
HWLAB_CLOUD_DB_HOST: process.env.HWLAB_CLOUD_DB_HOST,
HWLAB_CLOUD_DB_PORT: process.env.HWLAB_CLOUD_DB_PORT
};
const fakeDb = createTcpServer((socket) => socket.end());
await new Promise((resolve) => fakeDb.listen(0, "127.0.0.1", resolve));
const dbPort = fakeDb.address().port;
process.env.HWLAB_CLOUD_DB_URL = `postgres://hwlab_test@127.0.0.1:${dbPort}/hwlab`;
process.env.HWLAB_CLOUD_DB_SSL_MODE = "disable";
delete process.env.HWLAB_CLOUD_DB_SERVICE_NAME;
delete process.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE;
delete process.env.HWLAB_CLOUD_DB_HOST;
delete process.env.HWLAB_CLOUD_DB_PORT;
const server = createCloudApiServer();
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health`);
const payload = await response.json();
assert.equal(payload.status, "degraded");
assert.equal(payload.db.status, "ready");
assert.equal(payload.db.connected, true);
assert.equal(payload.db.liveConnected, true);
assert.equal(payload.db.liveDbEvidence, true);
assert.equal(payload.db.connectionChecked, true);
assert.equal(payload.db.connectionAttempted, true);
assert.equal(payload.db.connectionResult, "connected");
assert.equal(payload.db.endpointSource, "secret-url-host");
assert.equal(payload.db.connection.endpointSource, "secret-url-host");
assert.equal(payload.db.endpoint.authoritative.source, "secret-url-host");
assert.equal(payload.db.endpoint.authoritative.env, "HWLAB_CLOUD_DB_URL");
assert.equal(payload.db.optionalPublicDnsAlias.source, "optional-public-dns-alias");
assert.equal(payload.db.optionalPublicDnsAlias.requiredForReadiness, false);
assert.equal(payload.db.optionalPublicDnsAlias.usedForProbe, false);
assert.equal(payload.db.optionalPublicDnsAlias.envPresent, false);
assert.equal(payload.db.configReady, true);
assert.equal(payload.db.ready, true);
assert.equal(payload.db.evidence, "live_db_tcp_connection_ready");
assert.equal(payload.db.safety.liveDbEvidence, true);
assert.equal(payload.db.safety.liveDbConnectedEvidence, true);
assert.equal(payload.runtime.adapter, "memory");
assert.equal(payload.runtime.durable, false);
assert.equal(payload.runtime.status, "degraded");
assert.match(payload.runtime.reason, /process-local/u);
assert.equal(payload.runtime.durabilityContract.blockedLayer, "adapter");
assert.equal(payload.runtime.durabilityContract.dbLiveEvidenceIsDurabilityEvidence, false);
assert.equal(payload.readiness.components.db, "ready");
assert.equal(payload.readiness.components.runtime, "blocked");
assert.equal(payload.readiness.durability.status, "blocked");
assert.equal(payload.readiness.durability.dbLiveEvidenceObserved, true);
assert.equal(payload.readiness.durability.dbLiveEvidenceIsDurabilityEvidence, false);
assert.equal(payload.readiness.durability.blockedLayer, "adapter");
assert.equal(payload.readiness.durability.liveRuntimeEvidence, false);
assert.equal(payload.readiness.provider.status, "blocked");
assert.equal(payload.readiness.dbDurable.status, "blocked");
assert.equal(payload.readiness.dbDurable.dbLiveEvidenceObserved, true);
assert.equal(payload.readiness.dbDurable.blocker, "runtime_durable_adapter_missing");
assert.equal(payload.readiness.sessionRunner.status, "blocked");
assert.equal(payload.readiness.codexStdio.status, "blocked");
assert.equal(payload.db.redaction.valuesRedacted, true);
assert.equal(payload.db.redaction.endpointRedacted, true);
assert.equal(payload.db.redaction.secretMaterialRead, false);
assert.deepEqual(payload.db.missingEnv, []);
assert.equal(JSON.stringify(payload.db).includes("password"), false);
assert.equal(JSON.stringify(payload.db).includes("127.0.0.1"), false);
assert.equal(JSON.stringify(payload.db).includes(String(dbPort)), false);
assert.equal(JSON.stringify(payload.db).includes("cloud-api-db.hwlab-dev.svc.cluster.local"), false);
assert.equal(payload.db.fields.find((field) => field.name === "HWLAB_CLOUD_DB_URL").redacted, true);
assert.equal(payload.db.connection.endpointRedacted, true);
} finally {
if (originalUrl === undefined) {
delete process.env.HWLAB_CLOUD_DB_URL;
} else {
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
}
if (originalSslMode === undefined) {
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
} else {
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
}
for (const [name, value] of Object.entries(originalAliasEnv)) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise((resolve, reject) => {
fakeDb.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api health reports the v02 DB contract under the v02 profile", async () => {
const originalEnv = {
HWLAB_ENVIRONMENT: process.env.HWLAB_ENVIRONMENT,
HWLAB_GITOPS_PROFILE: process.env.HWLAB_GITOPS_PROFILE,
HWLAB_CLOUD_DB_URL: process.env.HWLAB_CLOUD_DB_URL,
HWLAB_CLOUD_DB_SSL_MODE: process.env.HWLAB_CLOUD_DB_SSL_MODE,
HWLAB_CLOUD_DB_SERVICE_NAME: process.env.HWLAB_CLOUD_DB_SERVICE_NAME,
HWLAB_CLOUD_DB_SERVICE_NAMESPACE: process.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE,
HWLAB_CLOUD_DB_HOST: process.env.HWLAB_CLOUD_DB_HOST,
HWLAB_CLOUD_DB_PORT: process.env.HWLAB_CLOUD_DB_PORT
};
const fakeDb = createTcpServer((socket) => socket.end());
await new Promise((resolve) => fakeDb.listen(0, "127.0.0.1", resolve));
const dbPort = fakeDb.address().port;
process.env.HWLAB_ENVIRONMENT = "v02";
process.env.HWLAB_GITOPS_PROFILE = "v02";
process.env.HWLAB_CLOUD_DB_URL = `postgres://hwlab_test@127.0.0.1:${dbPort}/hwlab_v02`;
process.env.HWLAB_CLOUD_DB_SSL_MODE = "disable";
delete process.env.HWLAB_CLOUD_DB_SERVICE_NAME;
delete process.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE;
delete process.env.HWLAB_CLOUD_DB_HOST;
delete process.env.HWLAB_CLOUD_DB_PORT;
const server = createCloudApiServer();
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health`);
const payload = await response.json();
const dbJson = JSON.stringify(payload.db);
assert.equal(payload.environment, "v02");
assert.equal(payload.db.environment, "v02");
assert.equal(payload.db.secretRefs[0].secretName, "hwlab-cloud-api-v02-db");
assert.equal(payload.db.secretRefs[0].secretKey, "database-url");
assert.equal(payload.db.optionalPublicDnsAlias.serviceName, "hwlab-v02-postgres");
assert.equal(payload.db.optionalPublicDnsAlias.namespace, "hwlab-v02");
assert.equal(payload.db.optionalPublicDnsAlias.requiredForReadiness, false);
assert.equal(payload.db.optionalPublicDnsAlias.usedForProbe, false);
assert.equal(payload.db.safety.environment, "v02");
assert.equal(payload.db.safety.devOnly, false);
assert.equal(payload.db.redaction.secretMaterialRead, false);
assert.equal(dbJson.includes("hwlab-cloud-api-dev-db"), false);
assert.equal(dbJson.includes("hwlab-dev"), false);
assert.equal(dbJson.includes("127.0.0.1"), false);
assert.equal(dbJson.includes(String(dbPort)), false);
} finally {
for (const [name, value] of Object.entries(originalEnv)) {
if (value === undefined) {
delete process.env[name];
} else {
process.env[name] = value;
}
}
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise((resolve, reject) => {
fakeDb.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api health separates DB connected from durable runtime schema readiness", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
const fakeDb = createTcpServer((socket) => socket.end());
await new Promise((resolve) => fakeDb.listen(0, "127.0.0.1", resolve));
const dbPort = fakeDb.address().port;
process.env.HWLAB_CLOUD_DB_URL = `postgres://hwlab_test@127.0.0.1:${dbPort}/hwlab`;
process.env.HWLAB_CLOUD_DB_SSL_MODE = "disable";
const server = createCloudApiServer({
runtimeStore: {
async readiness() {
return {
adapter: "postgres",
durable: false,
durableRequested: true,
durableCapable: false,
ready: false,
status: "blocked",
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
reason: "test runtime schema is missing",
liveRuntimeEvidence: false,
fixtureEvidence: false,
connection: {
queryAttempted: true,
queryResult: "schema_blocked",
endpointRedacted: true,
valueRedacted: true
},
schema: {
checked: true,
ready: false,
missingTables: ["gateway_sessions"],
missingColumns: ["gateway_sessions.gateway_session_json"]
},
migration: {
checked: false,
ready: false,
missing: true
},
counts: {}
};
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
const payload = await response.json();
assert.equal(response.status, 200);
assert.equal(payload.status, "degraded");
assert.equal(payload.db.connected, true);
assert.equal(payload.db.liveDbEvidence, true);
assert.equal(payload.runtime.durable, false);
assert.equal(payload.runtime.durableRequested, true);
assert.equal(payload.runtime.ready, false);
assert.equal(payload.runtime.blocker, RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED);
assert.equal(payload.readiness.components.db, "ready");
assert.equal(payload.readiness.components.runtime, "blocked");
assert.equal(payload.readiness.durability.status, "blocked");
assert.equal(payload.readiness.durability.dbLiveEvidenceObserved, true);
assert.equal(payload.readiness.durability.dbLiveEvidenceIsDurabilityEvidence, false);
assert.equal(payload.readiness.durability.blockedLayer, "schema");
assert.equal(payload.readiness.durability.queryAttempted, true);
assert.equal(payload.readiness.durability.queryResult, "schema_blocked");
assert.ok(payload.blockerCodes.includes(RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED));
assert.equal(JSON.stringify(payload).includes("password"), false);
} finally {
if (originalUrl === undefined) {
delete process.env.HWLAB_CLOUD_DB_URL;
} else {
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
}
if (originalSslMode === undefined) {
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
} else {
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
}
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise((resolve, reject) => {
fakeDb.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api health keeps DB live evidence separate when durable adapter query is blocked", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
const fakeDb = createTcpServer((socket) => socket.end());
await new Promise((resolve) => fakeDb.listen(0, "127.0.0.1", resolve));
const dbPort = fakeDb.address().port;
process.env.HWLAB_CLOUD_DB_URL = `postgres://hwlab_test:password@127.0.0.1:${dbPort}/hwlab`;
process.env.HWLAB_CLOUD_DB_SSL_MODE = "disable";
const server = createCloudApiServer({
runtimeStore: {
async readiness() {
return {
adapter: "postgres",
durable: false,
durableRequested: true,
durableCapable: false,
ready: false,
status: "blocked",
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
reason: "test runtime durable read query is blocked",
liveRuntimeEvidence: false,
fixtureEvidence: false,
connection: {
queryAttempted: true,
queryResult: "query_blocked",
endpointRedacted: true,
valueRedacted: true,
errorCode: "57014"
},
schema: {
checked: true,
ready: true,
missingTables: [],
missingColumns: []
},
migration: {
checked: true,
ready: true,
missing: false
},
gates: {
auth: { checked: true, ready: true, status: "ready", blocker: null },
schema: { checked: true, ready: true, status: "ready", blocker: null },
migration: { checked: true, ready: true, status: "ready", blocker: null },
durability: {
checked: true,
ready: false,
status: "blocked",
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED
}
},
counts: {}
};
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
const payload = await response.json();
assert.equal(response.status, 200);
assert.equal(payload.status, "degraded");
assert.equal(payload.db.ready, true);
assert.equal(payload.db.connected, true);
assert.equal(payload.db.liveDbEvidence, true);
assert.equal(payload.runtime.durable, false);
assert.equal(payload.runtime.ready, false);
assert.equal(payload.runtime.blocker, RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED);
assert.equal(payload.readiness.components.db, "ready");
assert.equal(payload.readiness.components.runtime, "blocked");
assert.equal(payload.readiness.durability.status, "blocked");
assert.equal(payload.readiness.durability.dbLiveEvidenceObserved, true);
assert.equal(payload.readiness.durability.dbLiveEvidenceIsDurabilityEvidence, false);
assert.equal(payload.readiness.durability.blockedLayer, "durability_query");
assert.equal(payload.readiness.durability.queryAttempted, true);
assert.equal(payload.readiness.durability.queryResult, "query_blocked");
assert.equal(payload.readiness.durability.liveRuntimeEvidence, false);
assert.ok(payload.blockerCodes.includes(RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED));
assert.equal(payload.readiness.dbDurable.status, "blocked");
assert.equal(payload.readiness.dbDurable.blocker, RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED);
assert.equal(payload.readiness.codeAgent.currentBlockers.includes(RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED), true);
assert.equal(JSON.stringify(payload.db).includes("password"), false);
assert.equal(JSON.stringify(payload.db).includes("127.0.0.1"), false);
assert.equal(JSON.stringify(payload.db).includes(String(dbPort)), false);
} finally {
if (originalUrl === undefined) {
delete process.env.HWLAB_CLOUD_DB_URL;
} else {
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
}
if (originalSslMode === undefined) {
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
} else {
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
}
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await new Promise((resolve, reject) => {
fakeDb.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api health contract distinguishes durable SSL, auth, schema, migration, and query blockers", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
process.env.HWLAB_CLOUD_DB_URL = "redacted";
process.env.HWLAB_CLOUD_DB_SSL_MODE = "require";
const cases = [
{
blocker: RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
blockedLayer: "ssl",
queryResult: "ssl_negotiation_blocked",
gates: {
ssl: "blocked",
auth: "not_checked",
schema: "not_checked",
migration: "not_checked",
durability: "blocked"
}
},
{
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
blockedLayer: "auth",
queryResult: "auth_blocked",
gates: {
ssl: "ready",
auth: "blocked",
schema: "not_checked",
migration: "not_checked",
durability: "blocked"
}
},
{
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
blockedLayer: "schema",
queryResult: "schema_blocked",
gates: {
ssl: "ready",
auth: "ready",
schema: "blocked",
migration: "not_checked",
durability: "blocked"
}
},
{
blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
blockedLayer: "migration",
queryResult: "migration_blocked",
gates: {
ssl: "ready",
auth: "ready",
schema: "ready",
migration: "blocked",
durability: "blocked"
}
},
{
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
blockedLayer: "durability_query",
queryResult: "query_blocked",
gates: {
ssl: "ready",
auth: "ready",
schema: "ready",
migration: "ready",
durability: "blocked"
}
}
];
try {
for (const testCase of cases) {
const server = createCloudApiServer({
dbProbe: {
probe: async ({ endpointSource, timeoutMs }) => ({
attempted: true,
networkAttempted: true,
endpointSource,
probeType: "tcp-connect",
endpointRedacted: true,
valueRedacted: true,
timeoutMs,
durationMs: 0,
result: "connected",
classification: "tcp_connected",
errorCode: null,
missingEnv: []
})
},
runtimeStore: {
async readiness() {
return durableBlockedRuntime(testCase);
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
const payload = await response.json();
assert.equal(response.status, 200);
assert.equal(payload.status, "degraded");
assert.equal(payload.db.ready, true);
assert.equal(payload.db.liveDbEvidence, true);
assert.equal(payload.runtime.blocker, testCase.blocker);
assert.equal(payload.runtime.connection.queryResult, testCase.queryResult);
assert.equal(payload.runtime.gates.ssl.status, testCase.gates.ssl);
assert.equal(payload.runtime.gates.auth.status, testCase.gates.auth);
assert.equal(payload.runtime.gates.schema.status, testCase.gates.schema);
assert.equal(payload.runtime.gates.migration.status, testCase.gates.migration);
assert.equal(payload.runtime.gates.durability.status, testCase.gates.durability);
assert.equal(payload.runtime.durabilityContract.blockedLayer, testCase.blockedLayer);
assert.equal(payload.db.runtimeReadiness.blockedLayer, testCase.blockedLayer);
assert.equal(payload.db.runtimeReadiness.queryResult, testCase.queryResult);
assert.equal(payload.db.runtimeReadiness.dbLiveEvidenceIsDurabilityEvidence, false);
assert.equal(payload.db.readinessLayers.ssl.status, layerStatus(testCase.gates.ssl));
assert.equal(payload.db.readinessLayers.auth.status, layerStatus(testCase.gates.auth));
assert.equal(payload.db.readinessLayers.schema.status, layerStatus(testCase.gates.schema));
assert.equal(payload.db.readinessLayers.migration.status, layerStatus(testCase.gates.migration));
assert.equal(payload.db.readinessLayers.durability.status, layerStatus(testCase.gates.durability));
assert.equal(payload.db.readinessLayers[layerForBlockedCase(testCase.blockedLayer)].result, testCase.queryResult);
assert.equal(payload.readiness.durability.blockedLayer, testCase.blockedLayer);
assert.equal(payload.readiness.durability.queryResult, testCase.queryResult);
assert.equal(payload.readiness.durability.secretMaterialRead, false);
assert.ok(payload.blockerCodes.includes(testCase.blocker));
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
}
} finally {
if (originalUrl === undefined) {
delete process.env.HWLAB_CLOUD_DB_URL;
} else {
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
}
if (originalSslMode === undefined) {
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
} else {
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
}
}
});
test("cloud api health does not treat DB env presence-only as live readiness", async () => {
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
const originalProbeDisabled = process.env.HWLAB_CLOUD_DB_PROBE_DISABLED;
process.env.HWLAB_CLOUD_DB_URL = "postgres://user:password@db.example.invalid:5432/hwlab";
process.env.HWLAB_CLOUD_DB_SSL_MODE = "disable";
delete process.env.HWLAB_CLOUD_DB_PROBE_DISABLED;
const server = createCloudApiServer();
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
const payload = await response.json();
assert.equal(response.status, 200);
assert.equal(payload.db.configReady, true);
assert.equal(payload.db.connected, false);
assert.equal(payload.db.liveConnected, false);
assert.equal(payload.db.liveDbEvidence, false);
assert.equal(payload.db.safety.liveDbEvidence, false);
assert.equal(payload.db.ready, false);
assert.equal(payload.db.connectionAttempted, true);
assert.equal(payload.db.connectionResult, "forbidden_runtime_host");
assert.equal(payload.db.evidence, "live_db_tcp_connection_blocked");
assert.equal(payload.db.endpointSource, "secret-url-host");
assert.equal(payload.db.connection.endpointSource, "secret-url-host");
assert.equal(payload.db.connection.networkAttempted, false);
assert.equal(payload.db.connection.classification, "db_url_forbidden_invalid_host");
assert.equal(payload.db.readinessLayers.dns.status, "not_proven");
assert.equal(JSON.stringify(payload.db).includes("password"), false);
assert.equal(JSON.stringify(payload.db).includes("db.example.invalid"), false);
} finally {
if (originalUrl === undefined) {
delete process.env.HWLAB_CLOUD_DB_URL;
} else {
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
}
if (originalSslMode === undefined) {
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
} else {
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
}
if (originalProbeDisabled === undefined) {
delete process.env.HWLAB_CLOUD_DB_PROBE_DISABLED;
} else {
process.env.HWLAB_CLOUD_DB_PROBE_DISABLED = originalProbeDisabled;
}
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1 describes Code Agent egress blocker without leaking API key", async () => {
const server = createCloudApiServer({
env: {
OPENAI_API_KEY: "test-openai-key-material",
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "https://api.openai.com/v1/responses"
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1`);
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.codeAgent.status, "blocked");
assert.equal(payload.codeAgent.egress.directPublicOpenAi, true);
assert.match(payload.codeAgent.blocker, /Codex stdio|Codex app-server stdio|long-lived|Codex CLI command|stdio protocol/u);
assert.equal(payload.codeAgent.runner.ready, false);
assert.equal(payload.codeAgent.safety.secretMaterialRead, false);
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api health reports provider, durable DB, and codex stdio ready when runtime contract passes", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-health-codex-stdio-"));
const codexHome = path.join(workspace, "codex-home");
const fakeCodex = await createFakeCodexCommand();
await prepareFakeCodexHome(codexHome);
const manager = createCodexStdioSessionManager({
createRpcClient: async () => ({
async initialize() {
return { tools: ["codex", "codex-reply"] };
},
async listTools() {
return ["codex", "codex-reply"];
},
close() {}
})
});
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
CODEX_HOME: codexHome,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
HWLAB_CODE_AGENT_WORKSPACE: workspace,
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:17680/v1/responses",
HWLAB_CLOUD_DB_URL: "postgres://hwlab_test:password@db.internal.local:5432/hwlab",
HWLAB_CLOUD_DB_SSL_MODE: "disable"
},
dbProbe: {
probe: async ({ endpointSource, timeoutMs }) => ({
attempted: true,
networkAttempted: true,
endpointSource,
probeType: "tcp-connect",
endpointRedacted: true,
valueRedacted: true,
timeoutMs,
durationMs: 1,
result: "connected",
classification: "tcp_connected",
errorCode: null,
missingEnv: []
})
},
runtimeStore: {
async readiness() {
return durableReadyRuntime();
}
},
codexStdioManager: manager
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.status, "ok");
assert.equal(payload.db.ready, true);
assert.equal(payload.db.liveDbEvidence, true);
assert.equal(payload.runtime.ready, true);
assert.equal(payload.runtime.durable, true);
assert.equal(payload.readiness.provider.status, "ready");
assert.equal(payload.readiness.provider.ready, true);
assert.equal(payload.readiness.provider.blocker, null);
assert.equal(payload.readiness.dbDurable.status, "ready");
assert.equal(payload.readiness.dbDurable.ready, true);
assert.equal(payload.readiness.sessionRunner.status, "codex_stdio_ready");
assert.equal(payload.readiness.sessionRunner.ready, true);
assert.equal(payload.readiness.sessionRunner.codexStdio, true);
assert.equal(payload.readiness.sessionRunner.writeCapable, true);
assert.equal(payload.readiness.codexStdio.status, "ready");
assert.equal(payload.readiness.codexStdio.ready, true);
assert.deepEqual(payload.readiness.codeAgent.currentBlockers, []);
assert.equal(payload.readiness.codeAgent.providerReady, true);
assert.equal(payload.readiness.codeAgent.durableDbReady, true);
assert.equal(payload.readiness.codeAgent.sessionRunnerReady, true);
assert.equal(payload.readiness.codeAgent.codexStdioFeasible, true);
assert.equal(payload.codeAgent.ready, true);
assert.equal(payload.codeAgent.status, "codex-stdio-feasible");
assert.equal(payload.codeAgent.longLivedSessionGate.status, "pass");
assert.equal(payload.codeAgent.codexStdio.runtimeContract.binary.status, "present");
assert.deepEqual(payload.codeAgent.codexStdio.protocol.toolsObserved, []);
assert.equal(payload.codeAgent.codexStdio.commandProbe.ready, true);
assert.equal(payload.codeAgent.codexStdio.runtimeContract.commandProbe.ready, true);
assert.equal(payload.readiness.codexStdio.commandProbeReady, true);
assert.equal(JSON.stringify(payload).includes("provider_unavailable"), false);
assert.equal(JSON.stringify(payload).includes("dns_resolution_failed"), false);
assert.equal(JSON.stringify(payload).includes("runtime_durable_adapter_auth_blocked"), false);
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
assert.equal(JSON.stringify(payload).includes("password"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await rm(workspace, { recursive: true, force: true });
await rm(fakeCodex.root, { recursive: true, force: true });
}
});
test("cloud api health blocks full Code Agent readiness when Codex stdio command probe fails", async () => {
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-health-codex-stdio-probe-fail-"));
const codexHome = path.join(workspace, "codex-home");
const workspaceFile = path.join(workspace, "workspace-file");
const fakeCodex = await createFakeCodexCommand();
await prepareFakeCodexHome(codexHome);
await writeFile(workspaceFile, "not a directory\n", "utf8");
const manager = createCodexStdioSessionManager({
createRpcClient: async () => ({
async initialize() {
return { tools: ["codex", "codex-reply"] };
},
async listTools() {
return ["codex", "codex-reply"];
},
close() {}
})
});
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
OPENAI_API_KEY: "test-openai-key-material",
CODEX_HOME: codexHome,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_MODEL: "gpt-test",
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspaceFile,
HWLAB_CODE_AGENT_WORKSPACE: workspaceFile,
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:17680/v1/responses",
HWLAB_CLOUD_DB_URL: "postgres://hwlab_test:password@db.internal.local:5432/hwlab",
HWLAB_CLOUD_DB_SSL_MODE: "disable"
},
dbProbe: {
probe: async ({ endpointSource, timeoutMs }) => ({
attempted: true,
networkAttempted: true,
endpointSource,
probeType: "tcp-connect",
endpointRedacted: true,
valueRedacted: true,
timeoutMs,
durationMs: 1,
result: "connected",
classification: "tcp_connected",
errorCode: null,
missingEnv: []
})
},
runtimeStore: {
async readiness() {
return durableReadyRuntime();
}
},
codexStdioManager: manager
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.status, "degraded");
assert.equal(payload.ready, false);
assert.equal(payload.readiness.components.codeAgent, "blocked");
assert.equal(payload.readiness.codexStdio.status, "blocked");
assert.equal(payload.readiness.codexStdio.ready, false);
assert.equal(payload.readiness.codexStdio.commandProbeReady, false);
assert.ok(payload.readiness.codexStdio.blockerCodes.includes("codex_stdio_command_probe_failed"));
assert.ok(payload.blockerCodes.includes("codex_stdio_command_probe_failed"));
assert.equal(payload.codeAgent.ready, false);
assert.equal(payload.codeAgent.codexStdio.commandProbe.ready, false);
assert.equal(payload.codeAgent.codexStdio.commandProbe.status, "blocked");
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
assert.equal(JSON.stringify(payload).includes("password"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
await rm(workspace, { recursive: true, force: true });
await rm(fakeCodex.root, { recursive: true, force: true });
}
});
test("cloud api REST JSON-RPC bridge exposes unknown serviceId reason for Code Agent internal calls", async () => {
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio"
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/rpc/hardware.invoke.shell`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": "trc_server-test-invalid-service-id",
"x-source-service-id": "hwlab-code-agent-hotfix"
},
body: JSON.stringify({
projectId: "prj_mvp_topology",
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
resourceId: "res_windows_host",
capabilityId: "cap_windows_cmd_exec",
input: {
command: "cmd /c echo should-not-dispatch"
}
})
});
assert.equal(response.status, 400);
const payload = await response.json();
assert.equal(payload.error.code, -32600);
assert.equal(payload.error.message, "Invalid JSON-RPC request");
assert.match(payload.error.data.reason, /unknown serviceId "hwlab-code-agent-hotfix"/u);
assert.equal(JSON.stringify(payload).includes("should-not-dispatch"), false);
assert.equal(JSON.stringify(payload).includes("sk-"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
+73
View File
@@ -0,0 +1,73 @@
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
export 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);
});
}
export 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");
}
export function getHeader(request, name) {
const value = request.headers[name.toLowerCase()];
if (Array.isArray(value)) return value[0];
return value;
}
export function firstHeaderValue(request, name) {
return getHeader(request, name);
}
export function truthyFlag(value) {
return /^(?:1|true|yes|on|enabled)$/iu.test(String(value ?? "").trim());
}
export function safeTraceId(value) {
const text = String(value ?? "").trim();
return /^trc_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
export function safeSessionId(value) {
const text = String(value ?? "").trim();
return /^ses_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
export function safeConversationId(value) {
const text = String(value ?? "").trim();
return /^cnv_[A-Za-z0-9_.:-]+$/u.test(text) ? text : null;
}
export function safeOpaqueId(value) {
const text = String(value ?? "").trim();
return /^[A-Za-z0-9_.:-]{3,180}$/u.test(text) ? text : null;
}
export function parsePositiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
export function positiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
+375
View File
@@ -0,0 +1,375 @@
import assert from "node:assert/strict";
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "bun:test";
import { createCloudApiServer } from "./server.ts";
test("cloud api aggregates live HWLAB build times from health and controlled deploy/catalog metadata", async () => {
const healthByPath = new Map([
["/hwlab-cloud-web/health/live", {
serviceId: "hwlab-cloud-web",
status: "ok",
revision: "webabcdef123456",
commit: { id: "webabcdef123456", source: "test" },
image: {
reference: "127.0.0.1:5000/hwlab/hwlab-cloud-web:webabcd",
tag: "webabcd",
digest: "sha256:" + "1".repeat(64)
},
build: {
createdAt: "2026-05-23T02:10:00.000Z",
metadataSource: "runtime-env:HWLAB_BUILD_CREATED_AT"
}
}],
["/hwlab-agent-mgr/health/live", {
serviceId: "hwlab-agent-mgr",
status: "ok",
revision: "48dfbf9",
commit: { id: "48dfbf9", source: "test" },
image: {
reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
tag: "48dfbf9",
digest: "sha256:" + "2".repeat(64)
}
}],
["/hwlab-agent-skills/health/live", {
serviceId: "hwlab-agent-skills",
status: "ok",
revision: "skillsabcdef123456",
commit: { id: "skillsabcdef123456", source: "test" },
image: {
reference: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd",
tag: "skillsabcd",
digest: "sha256:" + "5".repeat(64)
}
}],
["/external/health", {
serviceId: "postgres",
status: "ok",
image: {
reference: "postgres:16",
tag: "16"
}
}]
]);
const liveBuildMetadata = {
deployManifest: {
services: [
{
serviceId: "hwlab-cloud-web",
image: "127.0.0.1:5000/hwlab/hwlab-cloud-web:webcat",
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health/live",
replicas: 1
},
{
serviceId: "hwlab-agent-mgr",
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3df89fe",
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health/live",
replicas: 1
},
{
serviceId: "hwlab-agent-skills",
image: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd",
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health/live",
replicas: 1,
env: {
HWLAB_COMMIT_ID: "skillsabcdef123456",
HWLAB_IMAGE: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd",
HWLAB_IMAGE_TAG: "skillsabcd",
HWLAB_BUILD_CREATED_AT: "2026-05-23T05:30:00.000Z",
HWLAB_BUILD_SOURCE: "deploy-env-test"
}
},
{
serviceId: "hwlab-agent-worker",
image: "127.0.0.1:5000/hwlab/hwlab-agent-worker:workcat",
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health/live",
replicas: 0
},
{
serviceId: "hwlab-edge-proxy",
image: "127.0.0.1:5000/hwlab/hwlab-edge-proxy:edgecat",
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health",
replicas: 1
},
{
serviceId: "hwlab-extra-lab",
image: "127.0.0.1:5000/hwlab/hwlab-extra-lab:extracat",
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health/live",
replicas: 1
}
]
},
artifactCatalog: {
services: [
{
serviceId: "hwlab-agent-mgr",
commitId: "3df89fe",
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3df89fe",
imageTag: "3df89fe",
digest: "sha256:" + "3".repeat(64),
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health/live",
buildCreatedAt: "2026-05-23T04:20:00.000Z",
buildSource: "artifact-catalog-test"
},
{
serviceId: "hwlab-agent-skills",
commitId: "skillsabcdef123456",
image: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd",
imageTag: "skillsabcd",
digest: "sha256:" + "5".repeat(64),
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health/live",
buildCreatedAt: null,
buildSource: null
},
{
serviceId: "hwlab-extra-lab",
commitId: "extracatabcdef123456",
image: "127.0.0.1:5000/hwlab/hwlab-extra-lab:extracat",
imageTag: "extracat",
digest: "sha256:" + "4".repeat(64),
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health/live",
buildCreatedAt: "2026-05-23T03:00:00.000Z",
buildSource: "artifact-catalog-test"
}
]
}
};
const server = createCloudApiServer({
env: {
PATH: "",
HWLAB_COMMIT_ID: "apiabcdef123456",
HWLAB_IMAGE: "127.0.0.1:5000/hwlab/hwlab-cloud-api:apiabcd",
HWLAB_IMAGE_TAG: "apiabcd",
HWLAB_BUILD_CREATED_AT: "2026-05-23T01:00:00.000Z",
HWLAB_CLOUD_WEB_SERVICE_URL: "http://live.test/hwlab-cloud-web",
HWLAB_AGENT_MGR_URL: "http://live.test/hwlab-agent-mgr",
HWLAB_AGENT_WORKER_URL: "http://live.test/missing-agent-worker",
HWLAB_GATEWAY_URL: "http://live.test/missing-gateway",
HWLAB_GATEWAY_SIMU_URL: "http://live.test/missing-gateway-simu",
HWLAB_BOX_SIMU_URL: "http://live.test/missing-box-simu",
HWLAB_PATCH_PANEL_URL: "http://live.test/missing-patch-panel",
HWLAB_ROUTER_URL: "http://live.test/missing-router",
HWLAB_TUNNEL_CLIENT_URL: "http://live.test/missing-tunnel-client",
HWLAB_EDGE_PROXY_URL: "http://live.test/external",
HWLAB_CLI_URL: "http://live.test/missing-cli",
HWLAB_AGENT_SKILLS_URL: "http://live.test/hwlab-agent-skills",
HWLAB_EXTRA_LAB_URL: "http://live.test/missing-extra-lab"
},
liveBuildMetadata,
fetchImpl: async (url) => {
const path = new URL(url).pathname;
const payload = healthByPath.get(path);
if (!payload) {
return {
ok: false,
status: 503,
async text() {
return JSON.stringify({ error: "offline" });
}
};
}
return {
ok: true,
status: 200,
async text() {
return JSON.stringify(payload);
}
};
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/live-builds`);
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.contractVersion, "live-builds-v1");
assert.equal(payload.latest.serviceId, "hwlab-agent-skills");
assert.equal(payload.latest.build.createdAt, "2026-05-23T05:30:00.000Z");
assert.equal(payload.latest.image.tag, "skillsabcd");
assert.equal(payload.latest.commit.id, "skillsabcdef123456");
assert.equal(payload.latest.build.metadataSource, "deploy-env:HWLAB_BUILD_CREATED_AT");
assert.equal(payload.latest.build.liveMetadataMatch.metadata.imageTag, "skillsabcd");
assert.match(payload.latest.build.liveHealthMissingReason, / deploy metadata/u);
assert.equal(payload.counts.total, payload.services.length);
assert.equal(payload.counts.external, 2);
assert.equal(payload.counts.withBuildTime, 3);
const manager = payload.services.find((service) => service.serviceId === "hwlab-agent-mgr");
assert.equal(manager.build.createdAt, null);
assert.equal(manager.image.tag, "48dfbf9");
assert.equal(manager.revision, "48dfbf9");
assert.equal(manager.build.metadataSource, "live-health:controlled-metadata-mismatch");
assert.equal(manager.build.unavailableReason, "构建时间不可用:live tag 与 catalog metadata 不匹配");
assert.match(manager.build.unavailableDetail, /live tag 48dfbf9/u);
assert.match(manager.build.unavailableDetail, /metadata tag 3df89fe/u);
const extra = payload.services.find((service) => service.serviceId === "hwlab-extra-lab");
assert.equal(extra.build.createdAt, null);
assert.match(extra.build.unavailableReason, / live health /u);
const worker = payload.services.find((service) => service.serviceId === "hwlab-agent-worker");
assert.equal(worker.build.createdAt, null);
assert.match(worker.build.unavailableReason, / live health /u);
assert.match(worker.build.unavailableReason, /hwlab-agent-worker hwlab-dev desired replicas=0/u);
assert.ok(payload.services.some((service) => service.kind === "external" && service.serviceId === "hwlab-edge-proxy" && service.healthUrl.endsWith("/health")));
assert.ok(payload.services.some((service) => service.kind === "external" && service.serviceId === "hwlab-frpc" && service.image.tag === "v0.68.1"));
assert.ok(payload.services.some((service) => service.serviceId === "hwlab-device-pod"));
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api ignores old repository reports and keeps missing buildCreatedAt unavailable", async () => {
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "hwlab-live-builds-"));
const deployDir = path.join(tempRoot, "deploy");
const oldReportDir = path.join(tempRoot, "reports", "dev-gate");
await mkdir(deployDir, { recursive: true });
await mkdir(oldReportDir, { recursive: true });
await writeFile(path.join(deployDir, "deploy.json"), `${JSON.stringify({
services: [
{
serviceId: "hwlab-agent-mgr",
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health/live",
replicas: 1
}
]
}, null, 2)}\n`);
await writeFile(path.join(deployDir, "artifact-catalog.dev.json"), `${JSON.stringify({
services: [
{
serviceId: "hwlab-agent-mgr",
commitId: "48dfbf9abcdef",
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
imageTag: "48dfbf9",
digest: "sha256:" + "6".repeat(64),
namespace: "hwlab-dev",
profile: "dev",
healthPath: "/health/live",
buildCreatedAt: null,
buildSource: null
}
]
}, null, 2)}\n`);
await writeFile(path.join(oldReportDir, "dev-artifacts.json"), `${JSON.stringify({
artifactPublish: {
services: [
{
serviceId: "hwlab-agent-mgr",
sourceCommitId: "48dfbf9abcdef",
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
imageTag: "48dfbf9",
digest: "sha256:" + "6".repeat(64),
buildCreatedAt: "2099-01-01T00:00:00.000Z",
buildSource: "old-report-must-not-be-read"
}
]
}
}, null, 2)}\n`);
const observe = async () => {
const server = createCloudApiServer({
env: {
PATH: "",
HWLAB_AGENT_MGR_URL: "http://live.test/hwlab-agent-mgr"
},
repoRoot: tempRoot,
fetchImpl: async (url) => {
if (new URL(url).pathname === "/hwlab-agent-mgr/health/live") {
return {
ok: true,
status: 200,
async text() {
return JSON.stringify({
serviceId: "hwlab-agent-mgr",
status: "ok",
revision: "48dfbf9abcdef",
commit: { id: "48dfbf9abcdef", source: "test" },
image: {
reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
tag: "48dfbf9",
digest: "sha256:" + "6".repeat(64)
}
});
}
};
}
return {
ok: false,
status: 503,
async text() {
return JSON.stringify({ error: "offline" });
}
};
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/live-builds`);
assert.equal(response.status, 200);
return response.json();
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
};
try {
const withOldReport = await observe();
await rm(path.join(tempRoot, "reports"), { recursive: true, force: true });
const withoutOldReport = await observe();
const summarize = (payload) => {
const manager = payload.services.find((service) => service.serviceId === "hwlab-agent-mgr");
return {
latest: payload.latest,
source: payload.source,
createdAt: manager.build.createdAt,
metadataSource: manager.build.metadataSource,
unavailableReason: manager.build.unavailableReason,
unavailableDetail: manager.build.unavailableDetail,
liveMetadataMatch: manager.build.liveMetadataMatch,
counts: payload.counts
};
};
assert.deepEqual(summarize(withOldReport), summarize(withoutOldReport));
const manager = withOldReport.services.find((service) => service.serviceId === "hwlab-agent-mgr");
assert.equal(withOldReport.latest, null);
assert.equal(manager.build.createdAt, null);
assert.equal(manager.build.metadataSource, "live-health:matched-controlled-metadata-without-build-time");
assert.equal(manager.build.unavailableReason, "构建时间不可用:当前 live 镜像未提供 buildCreatedAt");
assert.match(manager.build.unavailableDetail, /catalog metadata/u);
assert.equal(Object.hasOwn(withOldReport.source, "artifactReportSource"), false);
assert.equal(JSON.stringify(withOldReport).includes("2099-01-01T00:00:00.000Z"), false);
assert.equal(JSON.stringify(withOldReport).includes("artifact-report"), false);
} finally {
await rm(tempRoot, { recursive: true, force: true });
}
});
+263
View File
@@ -0,0 +1,263 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import { createCloudApiServer } from "./server.ts";
import { createCloudRuntimeStore } from "../db/runtime-store.ts";
import { createGreenM3StatusRuntimeStore, m3ReadinessRequestJson } from "./server-test-helpers.ts";
test("cloud api /v1 exposes M3 IO control contract without generic frontend hardware RPC", async () => {
const server = createCloudApiServer({
env: {
HWLAB_M3_IO_CONTROL_ENABLED: "true",
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
m3IoRequestJson: m3ReadinessRequestJson
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const index = await fetch(`http://127.0.0.1:${port}/v1`);
assert.equal(index.status, 200);
const indexPayload = await index.json();
assert.equal(indexPayload.m3IoControl.route, "/v1/m3/io");
assert.equal(indexPayload.m3IoControl.boundaries.directFrontendGatewayOrBoxAccess, false);
assert.ok(indexPayload.methods.includes("m3.io.do.write"));
assert.ok(indexPayload.methods.includes("m3.io.di.read"));
const contract = await fetch(`http://127.0.0.1:${port}/v1/m3/io`);
assert.equal(contract.status, 200);
const payload = await contract.json();
assert.equal(payload.status, "available");
assert.equal(payload.controlReady, true);
assert.equal(payload.readiness.status, "ready");
assert.equal(payload.chain.sourceResourceId, "res_boxsimu_1");
assert.equal(payload.chain.targetResourceId, "res_boxsimu_2");
assert.equal(payload.boundaries.frontendCallsOnly, "/v1/m3/io");
assert.equal(payload.boundaries.patchPanelOwnsPropagation, true);
assert.equal(payload.boundaries.genericHardwareRpcExposedToFrontend, false);
const statusResponse = await fetch(`http://127.0.0.1:${port}/v1/m3/status`);
assert.equal(statusResponse.status, 200);
const statusPayload = await statusResponse.json();
assert.equal(statusPayload.route, "/v1/m3/status");
assert.equal(statusPayload.chain.sourceResourceId, "res_boxsimu_1");
assert.equal(statusPayload.chain.targetResourceId, "res_boxsimu_2");
assert.equal(statusPayload.gateways.length, 2);
assert.equal(statusPayload.gateways.every((gateway) => gateway.online === true), true);
assert.equal(statusPayload.boxes.find((box) => box.resourceId === "res_boxsimu_1").ports.DO1.value, false);
assert.equal(statusPayload.boxes.find((box) => box.resourceId === "res_boxsimu_2").ports.DI1.value, false);
assert.equal(statusPayload.patchPanel.connectionActive, true);
assert.equal(statusPayload.io.status, "live");
assert.equal(statusPayload.io.do1.resourceId, "res_boxsimu_1");
assert.equal(statusPayload.io.do1.port, "DO1");
assert.equal(statusPayload.io.do1.value, false);
assert.equal(statusPayload.io.do1.sourceKind, "DEV-LIVE");
assert.equal(statusPayload.io.di1.resourceId, "res_boxsimu_2");
assert.equal(statusPayload.io.di1.port, "DI1");
assert.equal(statusPayload.io.di1.value, false);
assert.equal(statusPayload.io.di1.sourceKind, "DEV-LIVE");
assert.equal(statusPayload.io.patchPanel.serviceId, "hwlab-patch-panel");
assert.equal(statusPayload.io.patchPanel.connectionActive, true);
assert.equal(statusPayload.operation.status, "blocked");
assert.equal(statusPayload.operation.operationId, null);
assert.equal(statusPayload.operation.blocker, "runtime_durable_adapter_missing");
assert.equal(statusPayload.trace.status, "blocked");
assert.equal(statusPayload.trace.traceId, null);
assert.equal(statusPayload.trace.blocker, "runtime_durable_adapter_missing");
assert.equal(statusPayload.audit.status, "blocked");
assert.equal(statusPayload.audit.auditId, null);
assert.equal(statusPayload.audit.blocker, "runtime_durable_adapter_missing");
assert.equal(statusPayload.runtimeDurable.status, "blocked");
assert.equal(statusPayload.runtimeDurable.green, false);
assert.equal(statusPayload.runtimeDurable.blocker, "runtime_durable_adapter_missing");
assert.equal(statusPayload.runtimeDurable.readStatus.audit, "read");
assert.equal(statusPayload.runtimeDurable.readStatus.evidence, "read");
assert.equal(statusPayload.runtimeDurable.auditReady, true);
assert.equal(statusPayload.runtimeDurable.evidenceReady, true);
assert.equal(statusPayload.trust.durableStatus, "blocked");
assert.notEqual(statusPayload.trust.durableStatus, "green");
assert.equal(statusPayload.trust.runtime.blocker, "runtime_durable_adapter_missing");
assert.equal(statusPayload.boundaries.frontendCallsOnly, "/v1/m3/status");
assert.equal(statusPayload.boundaries.directFrontendGatewayOrBoxAccess, false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/m3/status clears nested runtime blocker when trusted durable evidence is green", async () => {
const server = createCloudApiServer({
runtimeStore: createGreenM3StatusRuntimeStore({ blocker: "runtime_durable_not_green" }),
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
m3IoRequestJson: m3ReadinessRequestJson
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const statusResponse = await fetch(`http://127.0.0.1:${port}/v1/m3/status`);
assert.equal(statusResponse.status, 200);
const statusPayload = await statusResponse.json();
assert.equal(statusPayload.contractVersion, "m3-status-v1");
assert.equal(statusPayload.status, "live");
assert.equal(statusPayload.sourceKind, "DEV-LIVE");
assert.equal(statusPayload.patchPanel.connectionActive, true);
assert.equal(statusPayload.trust.durableStatus, "green");
assert.equal(statusPayload.trust.blocker, null);
assert.equal(statusPayload.trust.runtime.status, "ready");
assert.equal(statusPayload.trust.runtime.blocker, null);
assert.equal(statusPayload.trust.operationId, "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f");
assert.equal(statusPayload.trust.traceId, "trc_hotfix_307_verify_1779527747043_read_false");
assert.equal(statusPayload.trust.auditId, "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded");
assert.equal(statusPayload.trust.evidenceId, "evd_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f_succeeded");
assert.equal(statusPayload.operation.status, "read");
assert.equal(statusPayload.operation.operationId, "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f");
assert.equal(statusPayload.operation.blocker, null);
assert.equal(statusPayload.trace.status, "read");
assert.equal(statusPayload.trace.traceId, "trc_hotfix_307_verify_1779527747043_read_false");
assert.equal(statusPayload.trace.blocker, null);
assert.equal(statusPayload.audit.status, "read");
assert.equal(statusPayload.audit.auditId, "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded");
assert.equal(statusPayload.audit.blocker, null);
assert.equal(statusPayload.runtimeDurable.status, "green");
assert.equal(statusPayload.runtimeDurable.green, true);
assert.equal(statusPayload.runtimeDurable.blocker, null);
assert.equal(statusPayload.runtimeDurable.auditReady, true);
assert.equal(statusPayload.runtimeDurable.evidenceReady, true);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/diagnostics/gate exposes Chinese single-table rows from live backend aggregation", async () => {
const runtimeStore = createCloudRuntimeStore({
now: () => "2026-05-23T00:00:00.000Z"
});
const server = createCloudApiServer({
runtimeStore,
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel",
HWLAB_COMMIT_ID: "abc123456789",
HWLAB_IMAGE_TAG: "abc1234"
},
m3IoRequestJson: async () => ({
ok: false,
status: 503,
body: {
error: {
message: "gateway offline"
}
}
})
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/diagnostics/gate`);
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.route, "/v1/diagnostics/gate");
assert.equal(payload.contractVersion, "gate-diagnostics-table-v1");
assert.equal(payload.sourceKind, "LIVE-BACKEND");
assert.equal(payload.liveBackend, true);
assert.deepEqual(payload.columns, ["类别", "检查项", "状态", "归因对象", "关键细节", "证据/trace", "更新时间", "下一步"]);
assert.equal(payload.safety.frontendHardcodedRows, false);
assert.equal(payload.safety.sourceFixturePromoted, false);
assert.equal(payload.safety.blockedIsGreen, false);
assert.equal(payload.safety.hardwareControlSurface, false);
assert.equal(payload.safety.codeAgentConversationSurface, false);
assert.ok(payload.rows.length >= 6);
assert.ok(payload.rows.every((row) => row.sourceKind === "LIVE-BACKEND"));
assert.ok(payload.rows.every((row) => ["通过", "阻塞", "失败", "待验证", "信息"].includes(row.status)));
assert.ok(payload.rows.some((row) => row.category === "M3" && row.status === "阻塞"));
assert.ok(payload.rows.some((row) => row.check === "/health/live readiness"));
assert.ok(payload.rows.some((row) => row.check === "audit.event.query"));
assert.ok(payload.rows.some((row) => row.check === "evidence.record.query"));
assert.ok(payload.rows.some((row) => row.check === "Cloud API 实况身份" && row.status === "通过"));
assert.equal(
payload.rows.some((row) => row.status === "阻塞" && row.statusKey === "pass"),
false,
"blocked rows must not be mapped to pass"
);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/m3/io returns structured blocked payload when gateway is unavailable", async () => {
const runtimeStore = createCloudRuntimeStore({
now: () => "2026-05-23T00:00:00.000Z"
});
const server = createCloudApiServer({
runtimeStore,
env: {
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
},
m3IoRequestJson: async () => ({
ok: false,
status: 503,
body: {
error: {
message: "gateway offline"
}
}
})
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const response = await fetch(`http://127.0.0.1:${port}/v1/m3/io`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-trace-id": "trc_http_m3_gateway_missing",
"x-actor-id": "usr_http_m3"
},
body: JSON.stringify({
action: "do.write",
value: true
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.status, "blocked");
assert.equal(payload.accepted, false);
assert.equal(payload.route, "/v1/m3/io");
assert.equal(payload.method, "POST");
assert.equal(payload.traceId, "trc_http_m3_gateway_missing");
assert.equal(payload.operationId, null);
assert.equal(payload.auditId, null);
assert.equal(payload.evidenceId, null);
assert.equal(payload.audit.status, "not_written");
assert.equal(payload.evidence.status, "blocked");
assert.equal(payload.readback, null);
assert.match(payload.blocker.zh, /gateway \//u);
assert.equal(payload.controlPath.cloudApi, true);
assert.equal(payload.controlPath.frontendBypass, false);
assert.equal(payload.evidenceState.sourceKind, "BLOCKED");
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
+122
View File
@@ -0,0 +1,122 @@
import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs";
import { ERROR_CODES } from "../protocol/index.mjs";
import { M3_IO_CONTROL_ROUTE, handleM3IoControl } from "./m3-io-control.ts";
import { getHeader, readBody, sendJson } from "./server-http-utils.ts";
export 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 ?? {}
}
});
}
}
+760
View File
@@ -0,0 +1,760 @@
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 { ENVIRONMENT_DEV, SERVICE_IDS } from "../protocol/index.mjs";
import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs";
import { buildDevicePodRestPayload, buildDevicePodStatus } from "../device-pod/fake-data.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const LIVE_BUILD_TIMEOUT_MS = 900;
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-device-pod": Object.freeze({ urlEnv: "HWLAB_DEVICE_POD_URL", defaultUrl: "http://hwlab-device-pod.hwlab-dev.svc.cluster.local:7601" }),
"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 构建产物"
})
]);
function defaultRuntimeEnvironment(env = process.env) {
const value = env.HWLAB_GITOPS_PROFILE || env.HWLAB_ENVIRONMENT || ENVIRONMENT_DEV;
return typeof value === "string" && value.trim() ? value.trim() : ENVIRONMENT_DEV;
}
export async function buildLiveBuildsPayload(options = {}, dependencies = {}) {
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, buildHealthPayload: dependencies.buildHealthPayload }))
);
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: (dependencies.runtimeEnvironment ?? defaultRuntimeEnvironment)(env),
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
};
}
export async function buildDevicePodCloudApiPayload(url, options = {}) {
const upstreamPayload = await fetchDevicePodUpstreamPayload(url, options);
if (upstreamPayload) {
return {
...upstreamPayload,
cloudApiProxy: {
route: url.pathname,
upstream: configuredDevicePodUrl(options.env ?? process.env),
status: "proxied"
}
};
}
const fallback = buildDevicePodRestPayload(url.pathname, url.searchParams, {
sourceKind: "FAKE-CLOUD-API"
}) ?? buildDevicePodStatus(undefined, { sourceKind: "FAKE-CLOUD-API" });
return {
...fallback,
cloudApiProxy: {
route: url.pathname,
upstream: configuredDevicePodUrl(options.env ?? process.env),
status: "fake-fallback",
reason: "hwlab-device-pod upstream is unavailable or not configured"
}
};
}
async function fetchDevicePodUpstreamPayload(url, options = {}) {
const baseUrl = configuredDevicePodUrl(options.env ?? process.env);
if (!baseUrl) return null;
const target = `${baseUrl}${url.pathname}${url.search}`;
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 900);
try {
const response = await (options.fetchImpl ?? fetch)(target, {
headers: { Accept: "application/json" },
signal: controller.signal
});
if (!response.ok) return null;
return await response.json();
} catch {
return null;
} finally {
clearTimeout(timeout);
}
}
function configuredDevicePodUrl(env = process.env) {
const value = String(env.HWLAB_DEVICE_POD_URL ?? "").trim();
return value ? value.replace(/\/+$/u, "") : "";
}
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 options.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}`;
}
+748
View File
@@ -0,0 +1,748 @@
import assert from "node:assert/strict";
import { chmod, mkdtemp, mkdir, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
export function durableBlockedRuntime({ blocker, blockedLayer, queryResult, gates }) {
return {
adapter: "postgres",
durable: false,
durableRequested: true,
durableCapable: false,
ready: false,
status: "blocked",
blocker,
reason: `test durable runtime blocker ${blocker}`,
liveRuntimeEvidence: false,
fixtureEvidence: false,
connection: {
queryAttempted: true,
queryResult,
endpointRedacted: true,
valueRedacted: true,
errorCode: "TEST_BLOCKER"
},
schema: {
checked: gates.schema !== "not_checked",
ready: gates.schema === "ready",
missingTables: gates.schema === "blocked" ? ["gateway_sessions"] : [],
missingColumns: gates.schema === "blocked" ? ["gateway_sessions.gateway_session_json"] : []
},
migration: {
checked: gates.migration !== "not_checked",
ready: gates.migration === "ready",
missing: gates.migration !== "ready"
},
gates: Object.fromEntries(Object.entries(gates).map(([name, status]) => [
name,
{
checked: status !== "not_checked",
ready: status === "ready",
status,
blocker: status === "blocked" ? blocker : null
}
])),
durabilityContract: {
ready: false,
status: "blocked",
requiredEvidence: "runtime_adapter_schema_migration_read_query",
dbLiveEvidenceIsDurabilityEvidence: false,
liveRuntimeEvidence: false,
adapterQueryRequired: true,
blockedLayer,
blocker,
secretMaterialRead: false
},
safety: {
secretMaterialRead: false,
valuesRedacted: true,
endpointRedacted: true
},
counts: {}
};
}
export function durableReadyRuntime() {
return {
adapter: "postgres",
durable: true,
durableRequested: true,
durableCapable: true,
ready: true,
status: "ready",
blocker: null,
reason: "test durable runtime ready",
liveRuntimeEvidence: true,
fixtureEvidence: false,
connection: {
queryAttempted: true,
queryResult: "durable_readiness_ready",
endpointRedacted: true,
valueRedacted: true
},
schema: {
checked: true,
ready: true,
missingTables: [],
missingColumns: []
},
migration: {
checked: true,
ready: true,
missing: false
},
gates: Object.fromEntries(["ssl", "auth", "schema", "migration", "durability"].map((name) => [
name,
{
checked: true,
ready: true,
status: "ready",
blocker: null
}
])),
durabilityContract: {
ready: true,
status: "ready",
requiredEvidence: "runtime_adapter_schema_migration_read_query",
dbLiveEvidenceIsDurabilityEvidence: false,
liveRuntimeEvidence: true,
adapterQueryRequired: true,
blockedLayer: null,
blocker: null,
secretMaterialRead: false
},
safety: {
secretMaterialRead: false,
valuesRedacted: true,
endpointRedacted: true
},
counts: {}
};
}
export function layerStatus(gateStatus) {
if (gateStatus === "ready") return "pass";
if (gateStatus === "blocked") return "blocked";
return "not_proven";
}
export function layerForBlockedCase(blockedLayer) {
if (blockedLayer === "durability_query") return "durability";
return blockedLayer;
}
export async function m3ReadinessRequestJson(url, request = {}) {
const parsed = new URL(url);
const body = request.body ?? {};
if (url.startsWith("http://gateway-1") && parsed.pathname === "/status") {
return {
ok: true,
status: 200,
body: m3GatewayStatus({
gatewayId: "gwsimu_1",
gatewaySessionId: "gws_gwsimu_1",
boxId: "boxsimu_1",
resourceId: "res_boxsimu_1"
})
};
}
if (url.startsWith("http://gateway-1") && parsed.pathname === "/invoke") {
return {
ok: true,
status: 200,
body: {
accepted: true,
operationId: "op_m3_status_do1",
traceId: "trc_m3_status_do1",
boxId: "boxsimu_1",
resourceId: "res_boxsimu_1",
port: "DO1",
value: false,
state: {
port: "DO1",
direction: "output",
value: false,
source: "gateway-simu",
updatedAt: "2026-05-23T00:00:00.000Z"
},
auditId: "aud_m3_status_do1",
evidenceId: "evd_m3_status_do1"
}
};
}
if (url.startsWith("http://gateway-2") && parsed.pathname === "/status") {
return {
ok: true,
status: 200,
body: m3GatewayStatus({
gatewayId: "gwsimu_2",
gatewaySessionId: "gws_gwsimu_2",
boxId: "boxsimu_2",
resourceId: "res_boxsimu_2"
})
};
}
if (url.startsWith("http://gateway-2") && parsed.pathname === "/invoke") {
const value = request.body?.operationId?.includes("do_write") || request.body?.operationId?.includes("do-write")
? true
: false;
return {
ok: true,
status: 200,
body: {
accepted: true,
operationId: "op_m3_status_di1",
traceId: "trc_m3_status_di1",
boxId: "boxsimu_2",
resourceId: "res_boxsimu_2",
port: "DI1",
value,
state: {
port: "DI1",
direction: "input",
value,
source: "patch-panel",
sourceResourceId: "res_boxsimu_1",
sourcePort: "DO1",
propagatedBy: "hwlab-patch-panel",
updatedAt: "2026-05-23T00:00:00.000Z"
},
auditId: "aud_m3_status_di1",
evidenceId: "evd_m3_status_di1"
}
};
}
if (url.startsWith("http://patch-panel") && parsed.pathname === "/status") {
return {
ok: true,
status: 200,
body: {
serviceId: "hwlab-patch-panel",
state: "active",
activeConnections: [
{
fromResourceId: "res_boxsimu_1",
fromPort: "DO1",
toResourceId: "res_boxsimu_2",
toPort: "DI1"
}
]
}
};
}
if (url.startsWith("http://patch-panel") && parsed.pathname === "/wiring") {
return {
ok: true,
status: 200,
body: {
wiringConfigId: "wir_m3_do1_di1",
status: "active",
connections: [
{
from: {
resourceId: "res_boxsimu_1",
port: "DO1"
},
to: {
resourceId: "res_boxsimu_2",
port: "DI1"
},
mode: "exclusive"
}
]
}
};
}
if (url.startsWith("http://patch-panel") && parsed.pathname === "/sync/tick") {
return {
ok: true,
status: 200,
body: {
accepted: true,
propagatedBy: "hwlab-patch-panel",
routeCount: 1,
deliveryCount: 1,
routes: [
{
accepted: true,
deliveryCount: 1,
deliveries: [
{
resourceId: "res_boxsimu_2",
port: "DI1",
value: body.signals?.[0]?.value ?? false,
sourceResourceId: "res_boxsimu_1",
sourcePort: "DO1",
deliveryStatus: "applied"
}
]
}
]
}
};
}
throw new Error(`unexpected M3 readiness request ${url}`);
}
export function m3GatewayStatus({ gatewayId, gatewaySessionId, boxId, resourceId }) {
return {
serviceId: "hwlab-gateway-simu",
gatewayId,
gatewaySessionId,
session: {
gatewayId,
gatewaySessionId
},
registry: {
gatewayId,
gatewaySessionId,
boxes: [
{
boxId,
resourceId,
state: "registered"
}
]
}
};
}
export function codexStdioReadyFixture({ workspace, codexHome }) {
return {
kind: "codex-app-server-stdio-runner",
provider: "codex-stdio",
backend: "hwlab-cloud-api/codex-app-server-stdio",
status: "feasible",
ready: true,
startupReady: true,
canStartLongLivedCodexStdio: true,
command: path.join(process.cwd(), "node_modules", ".bin", "codex"),
binaryOnPath: true,
binary: {
present: true,
executable: true,
nativeDependencyPresent: true,
versionDetected: true
},
workspace,
workspaceState: { exists: true, readable: true, writable: true, writeRequired: true },
codexHome,
codexHomeState: { exists: true, readable: true, writable: true, writeRequired: true },
sandbox: "workspace-write",
enabled: true,
supervisor: { configured: true, mode: "repo-owned-node-supervisor" },
tokenBoundary: { present: true, secretMaterialRead: false, valuesRedacted: true },
egress: { configured: true, directPublicOpenAi: false, valueRedacted: true },
protocol: {
status: "wired",
wired: true,
requiredTools: ["codex", "codex-reply"],
toolsObserved: ["codex", "codex-reply"],
missingTools: [],
command: "codex app-server --listen stdio://"
},
commandProbe: {
status: "ready",
ready: true,
probe: "workspace.pwd",
traceId: "trc_codex_stdio_command_probe",
toolCalls: [{
name: "pwd",
status: "completed",
cwd: workspace,
command: "pwd",
exitCode: 0,
stdoutSummary: "workspace path matched",
outputTruncated: false
}],
secretMaterialRead: false,
valuesRedacted: true
},
stdioProtocol: {
status: "wired",
wired: true,
requiredTools: ["codex", "codex-reply"],
toolsObserved: ["codex", "codex-reply"],
missingTools: [],
command: "codex app-server --listen stdio://"
},
sessionLifecycle: {
status: "present",
present: true,
create: true,
reuse: true,
cancel: true,
reap: true,
traceCapture: true,
idleTimeoutMs: 1800000
},
lifecycleSupervisor: {
status: "present",
present: true,
create: true,
reuse: true,
cancel: true,
reap: true,
traceCapture: true,
idleTimeoutMs: 1800000
},
runtimeContract: {
status: "ready",
ready: true,
binary: {
status: "present",
present: true,
executable: true,
nativeDependencyPresent: true,
versionDetected: true
},
stdioProtocol: {
status: "wired",
wired: true,
requiredTools: ["codex", "codex-reply"],
toolsObserved: ["codex", "codex-reply"],
missingTools: []
},
commandProbe: {
status: "ready",
ready: true,
probe: "workspace.pwd",
traceId: "trc_codex_stdio_command_probe",
toolCalls: [{
name: "pwd",
status: "completed",
cwd: workspace,
command: "pwd",
exitCode: 0,
stdoutSummary: "workspace path matched",
outputTruncated: false
}],
secretMaterialRead: false,
valuesRedacted: true
},
lifecycleSupervisor: {
status: "present",
present: true,
create: true,
reuse: true,
cancel: true,
reap: true,
traceCapture: true,
idleTimeoutMs: 1800000
},
workspaceMount: {
path: workspace,
status: "ready",
mounted: true,
readable: true,
writable: true,
sandbox: "workspace-write"
},
codexHome: {
path: codexHome,
status: "ready",
exists: true,
readable: true,
writable: true
},
cancelReapTraceReadiness: {
status: "ready",
cancel: true,
reap: true,
traceCapture: true,
idleTimeout: true
}
},
blockers: [],
blockerCodes: [],
safety: { secretMaterialRead: false, valuesRedacted: true }
};
}
export function codexStdioChatFixture({ workspace, codexHome, params }) {
const traceId = params.traceId;
const session = {
sessionId: "ses_server_stdio_pwd",
conversationId: params.conversationId,
status: "idle",
workspace,
sandbox: "workspace-write",
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
capabilityLevel: "long-lived-codex-stdio-session",
implementationType: "repo-owned-codex-app-server-stdio-session",
createdAt: "2026-05-23T00:00:00.000Z",
updatedAt: "2026-05-23T00:00:00.000Z",
idleTimeoutMs: 1800000,
expiresAt: "2026-05-23T00:30:00.000Z",
lastTraceId: traceId,
turn: 1,
threadId: "thread_server_stdio_pwd",
reused: false,
durable: true,
longLivedSession: true,
codexStdio: true,
writeCapable: true,
secretMaterialStored: false,
valuesRedacted: true
};
const feasibility = codexStdioReadyFixture({ workspace, codexHome });
return {
provider: "codex-stdio",
model: "gpt-test",
backend: "hwlab-cloud-api/codex-app-server-stdio",
content: `stdio pwd\n${workspace}`,
workspace,
sandbox: "workspace-write",
session,
sessionMode: "codex-app-server-stdio-long-lived",
sessionReuse: {
conversationId: params.conversationId,
sessionId: session.sessionId,
threadId: session.threadId,
mapped: true,
reused: false,
turn: 1,
previousTurns: 0,
workspace,
status: "idle",
idleTimeoutMs: 1800000
},
implementationType: "repo-owned-codex-app-server-stdio-session",
runnerLimitations: ["secret-values-redacted"],
codexStdioFeasibility: feasibility,
longLivedSessionGate: {
status: "pass",
pass: true,
provider: "codex-stdio",
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
blockers: []
},
toolCalls: [{
id: "tool_server_stdio_pwd",
type: "workspace-read",
name: "pwd",
status: "completed",
cwd: workspace,
command: "pwd",
exitCode: 0,
stdout: workspace,
stderrSummary: "",
outputTruncated: false,
traceId
}],
skills: {
status: "not_requested",
items: [],
count: 0,
blockers: []
},
runner: {
kind: "codex-app-server-stdio-runner",
provider: "codex-stdio",
backend: "hwlab-cloud-api/codex-app-server-stdio",
workspace,
sandbox: "workspace-write",
session: "codex-app-server-stdio-long-lived",
sessionMode: "codex-app-server-stdio-long-lived",
sessionId: session.sessionId,
implementationType: "repo-owned-codex-app-server-stdio-session",
codexStdio: true,
longLivedSession: true,
durableSession: true,
writeCapable: true,
readOnly: false,
capabilityLevel: "long-lived-codex-stdio-session"
},
runnerTrace: {
traceId,
runnerKind: "codex-app-server-stdio-runner",
workspace,
sandbox: "workspace-write",
sessionMode: "codex-app-server-stdio-long-lived",
sessionId: session.sessionId,
sessionStatus: "idle",
turn: 1,
implementationType: "repo-owned-codex-app-server-stdio-session",
events: ["stdio:acquire", "session:created", "stdio:ready", "tool:pwd:completed"],
outputTruncated: false,
valuesPrinted: false
},
capabilityLevel: "long-lived-codex-stdio-session",
providerTrace: {
transport: "stdio",
protocol: "codex-app-server-jsonrpc-stdio",
command: "codex app-server --listen stdio://",
toolName: "codex",
threadId: "thread_server_stdio_pwd",
valuesPrinted: false
}
};
}
export function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function postAgent(port, body) {
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json",
...(body.traceId ? { "x-trace-id": body.traceId } : {})
},
body: JSON.stringify(body)
});
assert.equal(response.status, 200);
return response.json();
}
export async function pollAgentResult(port, traceId) {
let last = null;
for (let attempt = 0; attempt < 20; attempt += 1) {
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/result/${encodeURIComponent(traceId)}`);
last = {
status: response.status,
body: await response.json()
};
if (response.status === 200) return last.body;
await delay(10);
}
throw new Error(`Code Agent result did not complete: ${JSON.stringify(last)}`);
}
export async function createFakeCodexCommand() {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-codex-"));
const packageRoot = path.join(root, "node_modules", "@openai", "codex");
const command = path.join(packageRoot, "bin", "codex.js");
const native = path.join(root, "node_modules", "@openai", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex");
await mkdir(path.dirname(command), { recursive: true });
await mkdir(path.dirname(native), { recursive: true });
await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.128.0" }), "utf8");
await writeFile(command, "#!/usr/bin/env node\nconsole.log('codex 0.128.0');\n", "utf8");
await writeFile(native, "#!/bin/sh\nexit 0\n", "utf8");
await chmod(command, 0o755);
await chmod(native, 0o755);
return { root, command };
}
export async function prepareFakeCodexHome(codexHome) {
await mkdir(codexHome, { recursive: true });
await writeFile(path.join(codexHome, "config.toml"), "model = \"gpt-test\"\n", "utf8");
await writeFile(path.join(codexHome, "auth.json"), "{\"auth\":\"redacted-test-placeholder\"}\n", "utf8");
return codexHome;
}
export function createFakeAppServerClient({ text = "真实 Codex stdio 回复", delayMs = 0 } = {}) {
let notificationHandler = null;
return {
async initialize() {
return { initialized: true };
},
setNotificationHandler(handler) {
notificationHandler = typeof handler === "function" ? handler : null;
},
async startThread() {
notificationHandler?.({ method: "thread/started", params: { thread: { id: "thread_server_test_stdio" } } });
return { threadId: "thread_server_test_stdio" };
},
async resumeThread(args) {
notificationHandler?.({ method: "thread/started", params: { thread: { id: args.threadId } } });
return { threadId: args.threadId };
},
async startTurn() {
const turnId = "turn_server_test_stdio";
notificationHandler?.({ method: "turn/started", params: { turn: { id: turnId } } });
if (delayMs > 0) await delay(delayMs);
notificationHandler?.({ method: "item/agentMessage/delta", params: { itemId: "item_server_test_stdio", delta: text } });
notificationHandler?.({ method: "item/completed", params: { item: { id: "item_server_test_stdio", type: "agentMessage", text } } });
notificationHandler?.({ method: "turn/completed", params: { turn: { id: turnId, status: "completed" } } });
return { turnId };
},
close() {}
};
}
export async function postAgentRaw(port, body, headers = {}) {
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
method: "POST",
headers: {
"content-type": "application/json",
...headers
},
body
});
return {
status: response.status,
body: await response.json()
};
}
export function createGreenM3StatusRuntimeStore({ blocker = null } = {}) {
return {
async readiness() {
return {
adapter: "postgres",
durable: true,
durableRequested: true,
durableCapable: true,
ready: true,
status: "ready",
blocker,
reason: "Postgres durable runtime adapter completed live schema, migration, and read readiness queries",
liveRuntimeEvidence: true,
fixtureEvidence: false
};
},
async queryAuditEvents() {
return {
events: [
{
auditId: "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded",
traceId: "trc_hotfix_307_verify_1779527747043_read_false",
actorType: "user",
actorId: "usr_m3_operator",
action: "m3.io.di.read",
targetType: "hardware_operation",
targetId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f",
projectId: "prj_mvp_topology",
gatewaySessionId: "gws_gwsimu_2",
operationId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f",
serviceId: "hwlab-cloud-api",
outcome: "succeeded",
metadata: {
route: "res_boxsimu_1:DO1->hwlab-patch-panel->res_boxsimu_2:DI1"
},
environment: "dev",
occurredAt: "2026-05-23T00:00:00.000Z"
}
],
count: 1
};
},
async queryEvidenceRecords() {
return {
records: [
{
evidenceId: "evd_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f_succeeded",
projectId: "prj_mvp_topology",
operationId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f",
kind: "trace",
uri: "memory://hwlab-cloud-api/op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f/m3-io-control.json",
sha256: "0".repeat(64),
serviceId: "hwlab-cloud-api",
environment: "dev",
metadata: {
traceId: "trc_hotfix_307_verify_1779527747043_read_false",
route: "res_boxsimu_1:DO1->hwlab-patch-panel->res_boxsimu_2:DI1"
},
createdAt: "2026-05-23T00:00:00.000Z"
}
],
count: 1
};
}
};
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+726
View File
@@ -0,0 +1,726 @@
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.ts";
import {
createCodeAgentErrorPayload,
describeCodeAgentAvailability,
handleCodeAgentChat
} from "./code-agent-chat.ts";
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
import { createCodeAgentSessionRegistry } from "./code-agent-session-registry.ts";
import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.ts";
import { createCodexStdioSessionManager } from "./codex-stdio-session.ts";
import { buildGateDiagnosticsRows } from "./gate-diagnostics.ts";
import {
applyRuntimeDbReadinessLayers,
buildDbRuntimeReadiness
} from "./db-contract.ts";
import { buildCloudApiReadiness } from "./health-contract.ts";
import {
M3_IO_CONTROL_ROUTE,
M3_STATUS_ROUTE,
describeM3IoControl,
describeM3IoControlLive,
describeM3StatusLive,
handleM3IoControl
} from "./m3-io-control.ts";
import { createConfiguredCloudRuntimeStore } from "../db/runtime-store.ts";
import {
buildDevicePodRestPayload,
buildDevicePodStatus
} from "../device-pod/fake-data.mjs";
import { createGatewayDemoRegistry } from "./gateway-demo-registry.ts";
import { buildDevicePodCloudApiPayload, buildLiveBuildsPayload } from "./server-rest-payloads.ts";
import {
createCodeAgentChatResultStore,
handleCodeAgentCancelHttp,
handleCodeAgentChatHttp,
handleCodeAgentChatResultHttp,
handleCodeAgentInspectHttp,
handleCodeAgentTraceHttp
} from "./server-code-agent-http.ts";
import { handleM3IoControlHttp } from "./server-m3-http.ts";
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
const LIVE_BUILD_TIMEOUT_MS = 900;
const DEFAULT_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT = 120;
const DEFAULT_CODE_AGENT_PROVIDER_PROFILE = "deepseek";
const CODE_AGENT_PROVIDER_PROFILE_IDS = Object.freeze(["deepseek", "codex-api", "runtime-default"]);
const CODE_AGENT_PROVIDER_PROFILE_LABELS = Object.freeze({
"deepseek": "DeepSeek",
"codex-api": "Codex API",
"runtime-default": "运行默认"
});
const DEFAULT_CODE_AGENT_CODEX_API_MODEL = "gpt-5.5";
const DEFAULT_CODE_AGENT_CODEX_API_BASE_URL = "http://127.0.0.1:49280/responses";
const DEFAULT_CODE_AGENT_DEEPSEEK_MODEL = "deepseek-chat";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const LIVE_BUILD_METADATA_PATHS = Object.freeze({
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-device-pod": Object.freeze({ urlEnv: "HWLAB_DEVICE_POD_URL", defaultUrl: "http://hwlab-device-pod.hwlab-dev.svc.cluster.local:7601" }),
"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 构建产物"
})
]);
function runtimeEnvironment(env = process.env) {
const value = env.HWLAB_GITOPS_PROFILE || env.HWLAB_ENVIRONMENT || ENVIRONMENT_DEV;
return typeof value === "string" && value.trim() ? value.trim() : ENVIRONMENT_DEV;
}
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, 120000)
});
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";
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: runtimeEnvironment(env),
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,
devicePod: {
route: "/v1/device-pods",
statusRoute: "/v1/device-pods/{devicePodId}/status",
eventsRoute: "/v1/device-pods/{devicePodId}/events",
contractVersion: "device-pod-fake-v1",
frontendCallsOnly: "/v1/device-pods",
legacyHardwareUi: "disabled"
},
m3IoControl: describeM3IoControl(options)
});
return;
}
if (request.method === "GET" && (url.pathname === "/v1/device-pods" || url.pathname.startsWith("/v1/device-pods/"))) {
sendJson(response, 200, await buildDevicePodCloudApiPayload(url, 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, { buildHealthPayload, runtimeEnvironment }));
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 === "/v1/agent/chat/inspect") {
await handleCodeAgentInspectHttp(request, response, url, 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: runtimeEnvironment(options.env ?? process.env)
}
},
{
adapter: "rest",
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe,
m3IoRequestJson: options.m3IoRequestJson,
gatewayRegistry: options.gatewayRegistry
}
);
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
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: runtimeEnvironment(env)
};
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.
}
}
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: runtimeEnvironment(options.env ?? process.env),
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 safeOpaqueId(value) {
const text = String(value ?? "").trim();
return /^[A-Za-z0-9_.:-]{3,180}$/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();
}
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { test } from "bun:test";
import { handleJsonRpcRequest } from "../cloud/json-rpc.mjs";
import { handleJsonRpcRequest } from "../cloud/json-rpc.ts";
import { ERROR_CODES, validateResponse } from "../protocol/index.mjs";
import {
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
@@ -14,12 +14,12 @@ import {
buildPostgresPoolConfig,
createCloudRuntimeStore,
createConfiguredCloudRuntimeStore
} from "./runtime-store.mjs";
} from "./runtime-store.ts";
import {
CLOUD_CORE_MIGRATION_ID,
CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "./schema.mjs";
} from "./schema.ts";
test("memory runtime store is never marked durable", () => {
const store = createCloudRuntimeStore();
@@ -16,7 +16,7 @@ import {
CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "./schema.mjs";
} from "./schema.ts";
export const RUNTIME_STORE_KIND = "memory";
export const RUNTIME_STORE_KIND_POSTGRES = "postgres";
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import test from "node:test";
import { test } from "bun:test";
import { fileURLToPath } from "node:url";
import { TABLES, assertProtocolRecord, assertProtocolRecords } from "../protocol/index.mjs";
@@ -15,7 +15,7 @@ import {
FROZEN_CLOUD_CORE_TABLES,
assertFrozenCloudCoreTables,
requiredRuntimeDurableColumns
} from "./schema.mjs";
} from "./schema.ts";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
+15 -15
View File
File diff suppressed because one or more lines are too long
+33 -6
View File
@@ -825,18 +825,43 @@ function shortRevision(value) {
return source.length >= 7 ? source.slice(0, 7) : source || "unknown";
}
function runNodeEntrypoint(file) {
const child = spawn(process.execPath, [file], {
function runEntrypoint(command, args) {
const child = spawn(command, args, {
cwd: "/app",
env: process.env,
stdio: "inherit"
});
child.on("error", (error) => {
process.stderr.write(JSON.stringify({
serviceId,
status: "failed",
error: "entrypoint_spawn_failed",
command,
reason: error.message
}) + "\n");
process.exit(127);
});
child.on("exit", (code, signal) => {
if (signal) process.kill(process.pid, signal);
else process.exit(code ?? 0);
});
}
function runNodeEntrypoint(file) {
runEntrypoint(process.execPath, [file]);
}
function runBunEntrypoint(file) {
const candidates = [
process.env.HWLAB_BUN_COMMAND,
"/app/node_modules/.bin/bun",
"/usr/local/bin/bun",
"bun"
].filter(Boolean);
const command = candidates.find((candidate) => !candidate.includes("/") || existsSync(candidate)) || "bun";
runEntrypoint(command, [file]);
}
function cookieValue(request, name) {
const raw = request.headers.cookie || "";
for (const part of raw.split(";")) {
@@ -1128,6 +1153,8 @@ function serveHealthOnly() {
if (runtimeKind === "node-command" && entrypoint) {
runNodeEntrypoint(entrypoint);
} else if (runtimeKind === "bun-command" && entrypoint) {
runBunEntrypoint(entrypoint);
} else if (runtimeKind === "cloud-web") {
await serveCloudWeb();
} else {
@@ -1138,11 +1165,12 @@ if (runtimeKind === "node-command" && entrypoint) {
}
function dockerfile(baseImage, port, labels = []) {
const dependencyTimingScript = `started_at=$(node -e "console.log(Date.now())"); status=succeeded; if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && codex_version="$(node -p "require('./node_modules/@openai/codex/package.json').version")" && case "$(uname -m)" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-x64@npm:@openai/codex@\${codex_version}-linux-x64" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-arm64@npm:@openai/codex@\${codex_version}-linux-arm64" ;; esac || status=failed; finished_at=$(node -e "console.log(Date.now())"); node -e "console.log(JSON.stringify({event:'g14-cicd-build-step-timing',stage:'dependency-install',status:process.argv[1],durationMs:Number(process.argv[3])-Number(process.argv[2]),source:'dockerfile-run'}))" "$status" "$started_at" "$finished_at"; test "$status" = succeeded`;
const dependencyTimingScript = `started_at=$(node -e "console.log(Date.now())"); status=succeeded; if [ -d /opt/hwlab-node-runtime-base/node_modules ]; then cp -a /opt/hwlab-node-runtime-base/node_modules ./node_modules; fi && if [ -f package-lock.json ]; then npm install --omit=dev --include=optional --ignore-scripts; else npm install --omit=dev --include=optional --ignore-scripts; fi && if [ "\${HWLAB_ARTIFACT_KIND:-}" = "bun-command" ]; then npm install --no-save --omit=dev --include=optional --ignore-scripts bun@1.3.13; fi && codex_version="$(node -p "require('./node_modules/@openai/codex/package.json').version")" && case "$(uname -m)" in x86_64|amd64) test -e node_modules/@openai/codex-linux-x64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-x64@npm:@openai/codex@\${codex_version}-linux-x64" ;; aarch64|arm64) test -e node_modules/@openai/codex-linux-arm64 || npm install --omit=dev --include=optional --ignore-scripts "@openai/codex-linux-arm64@npm:@openai/codex@\${codex_version}-linux-arm64" ;; esac || status=failed; finished_at=$(node -e "console.log(Date.now())"); node -e "console.log(JSON.stringify({event:'g14-cicd-build-step-timing',stage:'dependency-install',status:process.argv[1],durationMs:Number(process.argv[3])-Number(process.argv[2]),source:'dockerfile-run'}))" "$status" "$started_at" "$finished_at"; test "$status" = succeeded`;
return [
`FROM ${baseImage}`,
"WORKDIR /app",
...labels.map(([name, value]) => `LABEL ${name}=${dockerfileQuote(value)}`),
"ARG HWLAB_ARTIFACT_KIND",
"ENV CODEX_HOME=/codex-home",
"ENV HWLAB_CODE_AGENT_CODEX_HOME=/codex-home",
"ENV HWLAB_CODE_AGENT_WORKSPACE=/workspace/hwlab",
@@ -1167,7 +1195,6 @@ function dockerfile(baseImage, port, labels = []) {
`RUN node -e "const fs=require('node:fs'); fs.writeFileSync('/usr/local/bin/hwlab-dev-artifact-runtime.mjs', Buffer.from('${runtimeScriptBase64()}', 'base64')); fs.chmodSync('/usr/local/bin/hwlab-dev-artifact-runtime.mjs', 0o755);"`,
"ARG HWLAB_ENVIRONMENT",
"ARG HWLAB_SERVICE_ID",
"ARG HWLAB_ARTIFACT_KIND",
"ARG HWLAB_SERVICE_ENTRYPOINT",
"ARG HWLAB_COMMIT_ID",
"ARG HWLAB_REVISION",
@@ -1304,8 +1331,8 @@ async function preflight({ args, services, catalog, deployManifest, baseImagePre
blocker({
type: "runtime_blocker",
scope: service.serviceId,
summary: `${service.serviceId} has no cmd/${service.serviceId}/main.mjs runtime entrypoint; a health-only placeholder image was not built as a real implementation.`,
next: `Add cmd/${service.serviceId}/main.mjs or a dedicated Dockerfile for ${service.serviceId}.`
summary: `${service.serviceId} has no runtime entrypoint; a health-only placeholder image was not built as a real implementation.`,
next: `Add cmd/${service.serviceId}/main.mjs, cmd/${service.serviceId}/main.ts, or a dedicated Dockerfile for ${service.serviceId}.`
})
);
}
+2 -2
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import assert from "node:assert/strict";
import { createServer } from "node:net";
import { createCloudApiServer } from "../internal/cloud/server.mjs";
import { createCloudApiServer } from "../internal/cloud/server.ts";
import { ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
const previousEnv = {
+3 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import assert from "node:assert/strict";
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import http from "node:http";
@@ -9,12 +9,12 @@ import path from "node:path";
import {
handleCodeAgentChat,
validateCodeAgentChatSchema
} from "../internal/cloud/code-agent-chat.mjs";
} from "../internal/cloud/code-agent-chat.ts";
import {
codexAppServerArgs,
codexAppServerProviderBaseUrl,
createCodexStdioSessionManager
} from "../internal/cloud/codex-stdio-session.mjs";
} from "../internal/cloud/codex-stdio-session.ts";
import {
classifyCodexRunnerCapability,
classifyCodeAgentChatReadiness,
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import {
runDeployContractPlanCli,
writeDeployContractPlanError
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import {
runDeployDesiredStatePlanCli,
writeDeployDesiredStatePlanError
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import {
formatDevRuntimeMigrationFailure,
runDevRuntimeMigrationCli
+1 -1
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import {
formatDevRuntimeProvisioningFailure,
runDevRuntimeProvisioningCli
+2 -2
View File
@@ -67,7 +67,7 @@ test("component model uses built-in service paths", () => {
"skills/",
"tools/"
]);
assert.deepEqual(matchingPaths(["cmd/hwlab-cloud-api/main.mjs"], models[0].componentPaths), ["cmd/hwlab-cloud-api/main.mjs"]);
assert.deepEqual(matchingPaths(["cmd/hwlab-cloud-api/main.ts"], models[0].componentPaths), ["cmd/hwlab-cloud-api/main.ts"]);
});
test("planner remains compatible with deploy.k3s.serviceMappings shape", async () => {
@@ -103,7 +103,7 @@ async function createFixtureRepo(options = {}) {
}
await writeFile(path.join(repo, "package.json"), JSON.stringify({ type: "module" }, null, 2));
await writeFile(path.join(repo, "package-lock.json"), JSON.stringify({ lockfileVersion: 3 }, null, 2));
await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.mjs"), "console.log('api');\n");
await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.ts"), "console.log('api');\n");
await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.mjs"), "console.log('bridge');\n");
await writeFile(path.join(repo, "cmd/hwlab-deepseek-responses-bridge/main.test.mjs"), "console.log('test');\n");
await writeFile(path.join(repo, "web/hwlab-cloud-web/index.html"), "<html></html>\n");
+16 -3
View File
@@ -1,8 +1,10 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import { spawn } from "node:child_process";
import { createServer } from "node:net";
import { existsSync } from "node:fs";
import http from "node:http";
import https from "node:https";
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import assert from "node:assert/strict";
@@ -24,9 +26,10 @@ const commonEnv = {
HWLAB_CLOUD_RUNTIME_DURABLE: "false"
};
const children = [];
const bunCommand = resolveBunCommand();
try {
const cloud = startNode("cmd/hwlab-cloud-api/main.mjs", {
const cloud = startNode("cmd/hwlab-cloud-api/main.ts", {
...commonEnv,
HWLAB_CLOUD_API_HOST: "127.0.0.1",
HWLAB_CLOUD_API_PORT: String(cloudPort),
@@ -134,7 +137,8 @@ try {
}
function startNode(entrypoint, env) {
const child = spawn(process.execPath, [entrypoint], {
const command = entrypoint.endsWith(".ts") ? bunCommand : process.execPath;
const child = spawn(command, [entrypoint], {
cwd,
env,
stdio: ["ignore", "pipe", "pipe"]
@@ -153,6 +157,15 @@ function startNode(entrypoint, env) {
return child;
}
function resolveBunCommand() {
const candidates = [
process.env.HWLAB_BUN_COMMAND,
process.env.HOME ? path.join(process.env.HOME, ".bun/bin/bun") : null,
"bun"
].filter(Boolean);
return candidates.find((candidate) => !candidate.includes("/") || existsSync(candidate)) || "bun";
}
async function waitForGatewaySession(apiBaseUrl, gatewaySessionId) {
const deadline = Date.now() + 8000;
let last;
+16 -4
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import net from "node:net";
import os from "node:os";
@@ -8,11 +9,12 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../internal/protocol/index.mjs";
import { validateCodeAgentChatSchema } from "../internal/cloud/code-agent-chat.mjs";
import { validateCodeAgentChatSchema } from "../internal/cloud/code-agent-chat.ts";
import { runCli } from "../tools/hwlab-cli/lib/cli.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const children = [];
const bunCommand = resolveBunCommand();
const CODEX_STDIO_FEASIBILITY_BLOCKERS = new Set([
"codex_cli_binary_missing",
"codex_stdio_supervisor_disabled",
@@ -46,7 +48,8 @@ function freePort() {
}
function startNode(relativeScript, env = {}) {
const child = spawn(process.execPath, [relativeScript], {
const command = relativeScript.endsWith(".ts") ? bunCommand : process.execPath;
const child = spawn(command, [relativeScript], {
cwd: repoRoot,
env: {
...process.env,
@@ -73,6 +76,15 @@ function startNode(relativeScript, env = {}) {
return record;
}
function resolveBunCommand() {
const candidates = [
process.env.HWLAB_BUN_COMMAND,
process.env.HOME ? path.join(process.env.HOME, ".bun/bin/bun") : null,
"bun"
].filter(Boolean);
return candidates.find((candidate) => !candidate.includes("/") || existsSync(candidate)) || "bun";
}
function runNodeJson(relativeScript, args = [], env = {}) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [relativeScript, ...args], {
@@ -184,7 +196,7 @@ function rpcEnvelope({ id, method, params = {} }) {
async function smokeCloudApi() {
const port = await freePort();
const service = startNode("cmd/hwlab-cloud-api/main.mjs", {
const service = startNode("cmd/hwlab-cloud-api/main.ts", {
HWLAB_CLOUD_API_HOST: "127.0.0.1",
HWLAB_CLOUD_API_PORT: String(port),
HWLAB_CLOUD_DB_URL: "",
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import os from "node:os";
import path from "node:path";
const args = process.argv.slice(2);
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
process.stdout.write(JSON.stringify({
script: "run-bun",
usage: "node scripts/run-bun.mjs <bun-args...>",
env: "HWLAB_BUN_COMMAND",
resolvedCommand: resolveBunCommand()
}, null, 2) + "\n");
process.exit(0);
}
const command = resolveBunCommand();
if (!command) {
process.stderr.write(JSON.stringify({
script: "run-bun",
status: "failed",
error: "bun_command_not_found",
checked: bunCandidates()
}, null, 2) + "\n");
process.exit(127);
}
const child = spawn(command, args, {
cwd: process.cwd(),
env: process.env,
stdio: "inherit"
});
child.on("error", (error) => {
process.stderr.write(JSON.stringify({
script: "run-bun",
status: "failed",
error: "bun_spawn_failed",
command,
reason: error.message
}, null, 2) + "\n");
process.exit(127);
});
child.on("exit", (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 0);
});
function resolveBunCommand() {
for (const candidate of bunCandidates()) {
if (!candidate) continue;
if (!candidate.includes(path.sep)) return candidate;
if (existsSync(candidate)) return candidate;
}
return null;
}
function bunCandidates() {
return [
process.env.HWLAB_BUN_COMMAND,
path.join(process.cwd(), "node_modules", ".bin", process.platform === "win32" ? "bun.exe" : "bun"),
path.join(os.homedir(), ".bun", "bin", process.platform === "win32" ? "bun.exe" : "bun"),
"/root/.bun/bin/bun",
"/usr/local/bin/bun",
"bun"
].filter(Boolean);
}
@@ -917,7 +917,7 @@ function summarizeCloudWebBuildFreshness(distFreshness, artifactCloudWeb) {
async function inspectCodeAgentTimeoutReadiness(repoRoot) {
const [appSource, cloudApiEntrySource] = await Promise.all([
readFile(path.join(repoRoot, "web/hwlab-cloud-web/app.mjs"), "utf8"),
readFile(path.join(repoRoot, "cmd/hwlab-cloud-api/main.mjs"), "utf8")
readFile(path.join(repoRoot, "cmd/hwlab-cloud-api/main.ts"), "utf8")
]);
const frontendTimeoutMs = numericSourceConstant(appSource, "DEFAULT_CODE_AGENT_TIMEOUT_MS");
const backendTimeoutMs = parseTimeoutFallbackMs(cloudApiEntrySource, "HWLAB_CODE_AGENT_TIMEOUT_MS");
@@ -953,7 +953,7 @@ function parseTimeoutFallbackMs(source, envName) {
async function inspectM3IoSourceReadiness(repoRoot) {
const files = await listTrackedFiles(repoRoot);
const candidateFiles = files.filter((file) =>
/\.(?:mjs|js|json|md|html|css|yaml|yml)$/u.test(file) &&
/\.(?:ts|mjs|js|json|md|html|css|yaml|yml)$/u.test(file) &&
file !== "scripts/src/artifact-runtime-readiness-guard.mjs" &&
file !== "scripts/artifact-runtime-readiness-guard.test.mjs"
);
@@ -971,7 +971,7 @@ async function inspectM3IoSourceReadiness(repoRoot) {
const present = matches.length > 0;
const sourceReady = contractFiles.length > 0 &&
matches.some((file) => file === "web/hwlab-cloud-web/app.mjs") &&
matches.some((file) => file === "internal/cloud/m3-io-control.mjs" || file === "scripts/src/m3-io-control-e2e.mjs");
matches.some((file) => file === "internal/cloud/m3-io-control.ts" || file === "scripts/src/m3-io-control-e2e.mjs");
return {
status: present ? (sourceReady ? "pass" : "blocked") : "absent",
present,
+6 -6
View File
@@ -1,11 +1,11 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.ts";
import {
DEV_CODE_AGENT_PROVIDER_CONTRACT,
codeAgentSecretRefPlaceholder
} from "../../internal/cloud/code-agent-contract.mjs";
} from "../../internal/cloud/code-agent-contract.ts";
import {
HWLAB_M3_IO_API_BASE_URL_ENV,
HWLAB_M3_IO_DEV_SERVICE_BASE_URL
@@ -361,10 +361,10 @@ function validateCloudApiDbSource(ctx, env) {
"cloud API DEV DB SSL mode"
);
expectEqual(ctx, env.HWLAB_CLOUD_DB_CONTRACT, "dev-redacted-presence-only", "$.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_CONTRACT", "cloud API DB redacted contract marker");
expectEqual(ctx, DEV_DB_ENV_CONTRACT.endpointAuthority.source, "secret-url-host", "internal/cloud/db-contract.mjs.endpointAuthority.source", "cloud API DB runtime authority source");
expectEqual(ctx, DEV_DB_ENV_CONTRACT.endpointAuthority.env, "HWLAB_CLOUD_DB_URL", "internal/cloud/db-contract.mjs.endpointAuthority.env", "cloud API DB runtime authority env");
expectEqual(ctx, alias.requiredForReadiness, false, "internal/cloud/db-contract.mjs.dns.requiredForReadiness", "cloud API DB DNS alias readiness requirement");
expectEqual(ctx, alias.usedForProbe, false, "internal/cloud/db-contract.mjs.dns.usedForProbe", "cloud API DB DNS alias probe source");
expectEqual(ctx, DEV_DB_ENV_CONTRACT.endpointAuthority.source, "secret-url-host", "internal/cloud/db-contract.ts.endpointAuthority.source", "cloud API DB runtime authority source");
expectEqual(ctx, DEV_DB_ENV_CONTRACT.endpointAuthority.env, "HWLAB_CLOUD_DB_URL", "internal/cloud/db-contract.ts.endpointAuthority.env", "cloud API DB runtime authority env");
expectEqual(ctx, alias.requiredForReadiness, false, "internal/cloud/db-contract.ts.dns.requiredForReadiness", "cloud API DB DNS alias readiness requirement");
expectEqual(ctx, alias.usedForProbe, false, "internal/cloud/db-contract.ts.dns.usedForProbe", "cloud API DB DNS alias probe source");
for (const name of [
"HWLAB_CLOUD_DB_SERVICE_NAME",
"HWLAB_CLOUD_DB_SERVICE_NAMESPACE",
+2 -2
View File
@@ -7,8 +7,8 @@ import { promisify } from "node:util";
import {
DEV_CODE_AGENT_PROVIDER_CONTRACT,
inspectCodeAgentProviderManifestRefs
} from "../../internal/cloud/code-agent-contract.mjs";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
} from "../../internal/cloud/code-agent-contract.ts";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.ts";
import { tempReportPath } from "./report-paths.mjs";
const execFileAsync = promisify(execFile);
+13
View File
@@ -63,6 +63,19 @@ function serviceSourceState(catalog, serviceId) {
export async function resolveDevArtifactService(repoRoot, serviceId, catalog = null) {
const sourceState = serviceSourceState(catalog, serviceId);
if (serviceId === "hwlab-cloud-api") {
const tsCmdPath = `cmd/${serviceId}/main.ts`;
if (await pathExists(repoRoot, tsCmdPath)) {
return withServicePublishPolicy({
serviceId,
runtimeKind: "bun-command",
entrypoint: tsCmdPath,
implementationState: "repo-entrypoint",
sourceState
});
}
}
const cmdPath = `cmd/${serviceId}/main.mjs`;
if (await pathExists(repoRoot, cmdPath)) {
return withServicePublishPolicy({
@@ -1640,7 +1640,7 @@ function readStaticFiles() {
runtime: readText("web/hwlab-cloud-web/runtime.mjs"),
help: readText("web/hwlab-cloud-web/help.md"),
artifactPublisher: readText("scripts/artifact-publish.mjs"),
cloudApiServer: readText("internal/cloud/server.mjs"),
cloudApiServer: readText("internal/cloud/server.ts"),
devicePodData: readText("internal/device-pod/fake-data.mjs"),
devicePodService: readText("cmd/hwlab-device-pod/main.mjs"),
protocol: readText("internal/protocol/index.mjs"),
+3 -3
View File
@@ -8,11 +8,11 @@ import { fileURLToPath } from "node:url";
import {
DEV_DB_ENV_CONTRACT,
summarizeDbContract
} from "../../internal/cloud/db-contract.mjs";
} from "../../internal/cloud/db-contract.ts";
import {
DEV_CODE_AGENT_PROVIDER_CONTRACT,
codeAgentSecretRefPlaceholder
} from "../../internal/cloud/code-agent-contract.mjs";
} from "../../internal/cloud/code-agent-contract.ts";
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
@@ -187,7 +187,7 @@ async function createDevGateReport(edgeHealth) {
"node scripts/m1-contract-smoke.mjs"
],
evidence: [
"npm run check includes internal/cloud/server.test.mjs and validates /health plus /health/live."
"npm run check includes internal/cloud/server-health.test.ts and validates /health plus /health/live."
],
summary: "Local cloud-api health checks pass and include service, commit, and image evidence fields."
},
+2 -2
View File
@@ -9,11 +9,11 @@ import {
DEV_DB_ENV_CONTRACT,
buildDbHealthContract,
summarizeDbContract
} from "../../internal/cloud/db-contract.mjs";
} from "../../internal/cloud/db-contract.ts";
import {
DEV_CODE_AGENT_PROVIDER_CONTRACT,
codeAgentSecretRefPlaceholder
} from "../../internal/cloud/code-agent-contract.mjs";
} from "../../internal/cloud/code-agent-contract.ts";
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../../internal/protocol/index.mjs";
import { probeRegistryCapabilities } from "./registry-capabilities.mjs";
+2 -2
View File
@@ -13,8 +13,8 @@ import {
RUNTIME_DURABLE_ADAPTER_MISSING,
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
RUNTIME_STORE_KIND_POSTGRES
} from "../../internal/db/runtime-store.mjs";
import { CLOUD_CORE_MIGRATION_ID } from "../../internal/db/schema.mjs";
} from "../../internal/db/runtime-store.ts";
import { CLOUD_CORE_MIGRATION_ID } from "../../internal/db/schema.ts";
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
+2 -2
View File
@@ -7,8 +7,8 @@ const defaultKubeconfig = "/etc/rancher/k3s/k3s.yaml";
const defaultNamespace = "hwlab-dev";
const defaultDeployment = "hwlab-cloud-api";
const defaultConfigMap = "hwlab-cloud-api-code-agent-hotfix";
const defaultMountPath = "/app/internal/cloud/code-agent-chat.mjs";
const defaultSubPath = "code-agent-chat.mjs";
const defaultMountPath = "/app/internal/cloud/code-agent-chat.ts";
const defaultSubPath = "code-agent-chat.ts";
const defaultMarker = "runtime-hotfix-pc-gateway-shell";
const defaultAnnotationKey = "hwlab.pikastech.local/pc-gateway-shell-hotfix";
const defaultPodLabelSelector = "app.kubernetes.io/name=hwlab-cloud-api";
@@ -87,7 +87,7 @@ function fixtureResult(args, options = {}) {
if (options.noHotfix) {
return { ok: false, exitCode: 1, stdout: "", stderr: "Error from server (NotFound): configmaps \"hwlab-cloud-api-code-agent-hotfix\" not found" };
}
return okJson({ data: { "code-agent-chat.mjs": "// runtime-hotfix-pc-gateway-shell" } });
return okJson({ data: { "code-agent-chat.ts": "// runtime-hotfix-pc-gateway-shell" } });
}
if (text.includes("get deployment")) {
return okJson(deploymentFixture({ noHotfix: options.noHotfix }));
@@ -127,7 +127,7 @@ function deploymentFixture({ noHotfix = false } = {}) {
name: "code-agent-hotfix",
configMap: {
name: "hwlab-cloud-api-code-agent-hotfix",
items: [{ key: "code-agent-chat.mjs", path: "code-agent-chat.mjs" }]
items: [{ key: "code-agent-chat.ts", path: "code-agent-chat.ts" }]
}
}],
containers: [{
@@ -135,8 +135,8 @@ function deploymentFixture({ noHotfix = false } = {}) {
image: "127.0.0.1:5000/hwlab/hwlab-cloud-api:dev",
volumeMounts: noHotfix ? [] : [{
name: "code-agent-hotfix",
mountPath: "/app/internal/cloud/code-agent-chat.mjs",
subPath: "code-agent-chat.mjs",
mountPath: "/app/internal/cloud/code-agent-chat.ts",
subPath: "code-agent-chat.ts",
readOnly: true
}]
}]
+3 -3
View File
@@ -9,7 +9,7 @@ import {
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE_COLUMNS,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "../../internal/db/schema.mjs";
} from "../../internal/db/schema.ts";
import {
PostgresCloudRuntimeStore,
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
@@ -19,8 +19,8 @@ import {
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
buildPostgresPoolConfig
} from "../../internal/db/runtime-store.mjs";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
} from "../../internal/db/runtime-store.ts";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.ts";
import { ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
+5 -4
View File
@@ -12,10 +12,11 @@ import {
CLOUD_CORE_MIGRATION_ID,
CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "../../internal/db/schema.mjs";
} from "../../internal/db/schema.ts";
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
const bunCommand = process.env.HWLAB_BUN_COMMAND || (process.versions.bun ? process.execPath : "bun");
test("source check is non-secret and does not attempt live DB access", async () => {
const report = await buildDevRuntimeMigrationReport(parseArgs(["--check"]), {
@@ -41,8 +42,8 @@ test("source check is non-secret and does not attempt live DB access", async ()
test("cloud-api image migration entrypoint exposes the same non-secret source check", async () => {
const { stdout } = await execFileAsync(
process.execPath,
["cmd/hwlab-cloud-api/migrate.mjs", "--check"],
bunCommand,
["cmd/hwlab-cloud-api/migrate.ts", "--check"],
{
cwd: repoRoot,
env: {
@@ -67,7 +68,7 @@ test("cloud-api image migration entrypoint exposes the same non-secret source ch
test("cloud-api image migration entrypoint redacts invalid DSN-shaped argv failures", async () => {
const secretArg = "postgresql://hwlab:secret-password@db.example.test:5432/hwlab?sslmode=require";
await assert.rejects(
execFileAsync(process.execPath, ["cmd/hwlab-cloud-api/migrate.mjs", secretArg], { cwd: repoRoot }),
execFileAsync(bunCommand, ["cmd/hwlab-cloud-api/migrate.ts", secretArg], { cwd: repoRoot }),
(error) => {
const report = JSON.parse(error.stdout);
assert.equal(report.conclusion.status, "blocked");
+2 -2
View File
@@ -3,7 +3,7 @@ import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.mjs";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.ts";
import {
PostgresCloudRuntimeStore,
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
@@ -13,7 +13,7 @@ import {
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
buildPostgresPoolConfig
} from "../../internal/db/runtime-store.mjs";
} from "../../internal/db/runtime-store.ts";
import { ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
import { ensureNotRepoReportsPath, tempReportPath } from "./report-paths.mjs";
@@ -11,6 +11,7 @@ import {
const execFileAsync = promisify(execFile);
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
const bunCommand = process.env.HWLAB_BUN_COMMAND || (process.versions.bun ? process.execPath : "bun");
test("source check parses target role/database without leaking DB URL material", async () => {
const report = await buildDevRuntimeProvisioningReport(parseArgs(["--check"]), {
@@ -33,8 +34,8 @@ test("source check parses target role/database without leaking DB URL material",
test("cloud-api image provisioning entrypoint exposes non-secret source check", async () => {
const { stdout } = await execFileAsync(
process.execPath,
["cmd/hwlab-cloud-api/provision.mjs", "--check"],
bunCommand,
["cmd/hwlab-cloud-api/provision.ts", "--check"],
{
cwd: repoRoot,
env: {
+2
View File
@@ -353,6 +353,7 @@ function reuseCandidate(catalogRecord, affected) {
}
function runtimeKindForService(serviceId) {
if (serviceId === "hwlab-cloud-api") return "bun-command";
if (serviceId === "hwlab-cloud-web") return "cloud-web";
if (serviceId === "hwlab-cli") return "cli";
if (serviceId === "hwlab-agent-skills") return "skills-bundle";
@@ -360,6 +361,7 @@ function runtimeKindForService(serviceId) {
}
function entrypointForService(serviceId) {
if (serviceId === "hwlab-cloud-api") return "cmd/hwlab-cloud-api/main.ts";
if (serviceId === "hwlab-cloud-web") return "web/hwlab-cloud-web/index.html";
if (serviceId === "hwlab-cli") return "tools/hwlab-cli/bin/hwlab-cli.mjs";
if (serviceId === "hwlab-agent-skills") return "skills/hwlab-agent-runtime/SKILL.md";
+4 -4
View File
@@ -8,7 +8,7 @@ import {
M3_IO_CONTROL_CONTRACT_VERSION,
M3_IO_CONTROL_ROUTE,
describeM3IoControl
} from "../../internal/cloud/m3-io-control.mjs";
} from "../../internal/cloud/m3-io-control.ts";
export const M3_IO_E2E_CONTRACT_VERSION = "m3-io-control-e2e-v1";
export const defaultFrontendUrl = "http://74.48.78.17:16666/";
@@ -225,9 +225,9 @@ export async function buildM3IoControlSourceReport(options = {}) {
export async function runM3IoControlSourceChecks({ repoRoot: root = repoRoot } = {}) {
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, "internal/cloud/server.ts"),
readRepoText(root, "internal/cloud/json-rpc.ts"),
readRepoText(root, "internal/cloud/m3-io-control.ts"),
readRepoText(root, "web/hwlab-cloud-web/app.mjs"),
readRepoText(root, "web/hwlab-cloud-web/index.html"),
readRepoText(root, "scripts/artifact-publish.mjs"),
+1 -1
View File
@@ -14,7 +14,7 @@ import {
import {
M3_IO_CHAIN,
M3_IO_CONTROL_ROUTE
} from "../../internal/cloud/m3-io-control.mjs";
} from "../../internal/cloud/m3-io-control.ts";
import {
buildM3IoControlSourceReport,
runM3IoControlLiveReport
+3 -3
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import assert from "node:assert/strict";
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
@@ -6,11 +6,11 @@ import { fileURLToPath } from "node:url";
import {
DEV_DB_ENV_CONTRACT,
parseDbUrlContract
} from "../internal/cloud/db-contract.mjs";
} from "../internal/cloud/db-contract.ts";
import {
DEV_CODE_AGENT_PROVIDER_CONTRACT,
codeAgentSecretRefPlaceholder
} from "../internal/cloud/code-agent-contract.mjs";
} from "../internal/cloud/code-agent-contract.ts";
import {
HWLAB_M3_IO_API_BASE_URL_ENV,
HWLAB_M3_IO_DEV_SERVICE_BASE_URL
+12 -12
View File
@@ -1,9 +1,9 @@
#!/usr/bin/env node
#!/usr/bin/env bun
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { DEV_DB_ENV_CONTRACT } from "../internal/cloud/db-contract.mjs";
import { DEV_DB_ENV_CONTRACT } from "../internal/cloud/db-contract.ts";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, SERVICE_IDS } from "../internal/protocol/index.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
@@ -215,12 +215,12 @@ function assertDurableRuntimeRunbook(value) {
"Moved:",
"Still blocked:",
"No full M3, M4, or M5 acceptance is allowed while runtime durability is blocked",
"node cmd/hwlab-cloud-api/provision.mjs --check",
"node cmd/hwlab-cloud-api/provision.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
"node cmd/hwlab-cloud-api/provision.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
"node cmd/hwlab-cloud-api/migrate.mjs --check",
"node cmd/hwlab-cloud-api/migrate.mjs --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
"node cmd/hwlab-cloud-api/migrate.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
"bun cmd/hwlab-cloud-api/provision.ts --check",
"bun cmd/hwlab-cloud-api/provision.ts --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
"bun cmd/hwlab-cloud-api/provision.ts --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
"bun cmd/hwlab-cloud-api/migrate.ts --check",
"bun cmd/hwlab-cloud-api/migrate.ts --dry-run --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
"bun cmd/hwlab-cloud-api/migrate.ts --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-runtime-migration-report.json",
"DEV CD runtime migration Job uses",
"node scripts/dev-runtime-provisioning.mjs --check",
"node scripts/dev-runtime-provisioning.mjs --dry-run --allow-live-db-read --confirm-dev --report /tmp/hwlab-dev-gate/dev-runtime-provisioning-report.json",
@@ -247,11 +247,11 @@ function assertCloudApiImageRuntimeEntrypoints({ packageJson, artifactPublisher,
assertIncludes(artifactPublisher, expected, `${label} artifact publisher`);
}
for (const expected of [
"cmd/hwlab-cloud-api/provision.mjs",
"cmd/hwlab-cloud-api/migrate.mjs"
"cmd/hwlab-cloud-api/provision.ts",
"cmd/hwlab-cloud-api/migrate.ts"
]) {
assertIncludes(packageJson.scripts?.check, `node --check ${expected}`, `${label} package check`);
assertIncludes(packageJson.scripts?.check, `node ${expected} --check`, `${label} package runtime check`);
assertIncludes(packageJson.scripts?.check, `bun build ${expected} --target=bun --packages=external`, `${label} package check`);
assertIncludes(packageJson.scripts?.check, `bun ${expected} --check`, `${label} package runtime check`);
}
for (const expected of ["scripts/g14-artifact-publish.mjs", "runtime-dev", "runtime-prod"]) {
assertIncludes(gitopsRenderSource, expected, `${label} G14 GitOps render source`);
+2 -2
View File
@@ -39,7 +39,7 @@ const helpMarkdown = readWeb("help.md");
const faviconSvg = readWeb("favicon.svg");
const faviconIco = readWeb("favicon.ico");
const distContractScript = readWeb("scripts/dist-contract.mjs");
const cloudApiServer = readRepo("internal/cloud/server.mjs");
const cloudApiServer = readRepo("internal/cloud/server.ts");
const devicePodData = readRepo("internal/device-pod/fake-data.mjs");
const devicePodService = readRepo("cmd/hwlab-device-pod/main.mjs");
const protocol = readRepo("internal/protocol/index.mjs");
@@ -170,7 +170,7 @@ console.log("hwlab-cloud-web check ok: Device Pod summary sidebar, modal detail,
function runJavaScriptSyntaxCheck() {
const files = new Set([
...collectJavaScriptFiles(rootDir),
path.resolve(repoRoot, "internal/cloud/server.mjs"),
path.resolve(repoRoot, "internal/cloud/server.ts"),
path.resolve(repoRoot, "internal/device-pod/fake-data.mjs"),
path.resolve(repoRoot, "cmd/hwlab-device-pod/main.mjs")
]);