feat: wire device pod gateway dispatch

This commit is contained in:
Codex
2026-05-29 10:44:59 +08:00
parent dfc1b42fe3
commit 4cdc80e672
12 changed files with 543 additions and 18 deletions
+108
View File
@@ -16,6 +16,7 @@ import {
import {
SUPPORTED_RPC_METHODS,
createErrorEnvelope,
createResultEnvelope,
handleJsonRpcRequest
} from "./json-rpc.ts";
import {
@@ -341,6 +342,11 @@ async function handleRestAdapter(request, response, url, options) {
return;
}
if (url.pathname === "/v1/internal/device-pod/gateway-dispatch") {
await handleInternalDevicePodGatewayDispatch(request, response, options);
return;
}
if (request.method === "POST" && url.pathname === "/v1/gateway/poll") {
await handleGatewayPollHttp(request, response, options);
return;
@@ -459,6 +465,91 @@ async function handleRestAdapter(request, response, url, options) {
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
async function handleInternalDevicePodGatewayDispatch(request, response, options) {
if (request.method !== "POST") {
sendJson(response, 405, { error: { code: "method_not_allowed", message: "Internal device-pod gateway dispatch only supports POST" } });
return;
}
if (internalServiceHeader(request) !== "hwlab-device-pod") {
sendJson(response, 403, { error: { code: "device_pod_internal_authority_required", message: "Only hwlab-device-pod may use the internal gateway dispatch route" } });
return;
}
const body = await readJsonObject(request, options.bodyLimitBytes);
if (!body.ok) {
sendJson(response, 400, body.error);
return;
}
const params = body.value?.params && typeof body.value.params === "object" && !Array.isArray(body.value.params) ? body.value.params : {};
const gatewaySessionId = typeof params.gatewaySessionId === "string" ? params.gatewaySessionId.trim() : "";
if (!gatewaySessionId || options.gatewayRegistry?.isOnline?.(gatewaySessionId) !== true) {
sendJson(response, 409, {
accepted: false,
status: "blocked",
blocker: {
code: "gateway_dispatch_unavailable",
layer: "device-pod",
retryable: true,
summary: gatewaySessionId ? "gateway session is not connected" : "profile route.gatewaySessionId is missing"
}
});
return;
}
const operationId = typeof params.operationId === "string" && params.operationId ? params.operationId : `op_devicepod_gateway_${randomUUID()}`;
const traceId = typeof params.traceId === "string" && params.traceId ? params.traceId : `trc_devicepod_gateway_${randomUUID()}`;
const rpcResponse = await handleJsonRpcRequest({
jsonrpc: "2.0",
id: typeof body.value.id === "string" && body.value.id ? body.value.id : getHeader(request, "x-request-id") || `req_devicepod_gateway_${randomUUID()}`,
method: "hardware.invoke.shell",
params: { ...params, gatewaySessionId, operationId, traceId },
meta: {
traceId,
actorId: typeof body.value.actorId === "string" ? body.value.actorId : "svc_hwlab-device-pod",
serviceId: "hwlab-device-pod",
environment: runtimeEnvironment(options.env ?? process.env)
}
}, {
adapter: "device-pod-internal",
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe,
m3IoRequestJson: options.m3IoRequestJson,
gatewayRegistry: options.gatewayRegistry
});
if (rpcResponse.error) {
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
return;
}
const result = rpcResponse.result ?? {};
const dispatch = result.dispatch ?? {};
const completed = result.status === "succeeded" || result.status === "completed" || dispatch.dispatchStatus === "succeeded" || dispatch.dispatchStatus === "completed";
sendJson(response, completed ? 200 : 409, createResultEnvelope({
id: rpcResponse.id,
result: {
accepted: completed,
status: completed ? "completed" : "failed",
operationId: result.operationId ?? operationId,
traceId,
gatewaySessionId,
resourceId: result.resourceId ?? params.resourceId ?? null,
capabilityId: result.capabilityId ?? params.capabilityId ?? null,
dispatch,
auditId: result.auditId ?? null,
evidenceId: result.evidenceId ?? null,
gateway: result.gateway ?? null,
blocker: completed ? null : {
code: "gateway_dispatch_failed",
layer: "device-pod",
retryable: true,
summary: dispatch.message ?? dispatch.stderr ?? dispatch.status ?? result.status ?? "gateway dispatch failed"
}
},
requestMeta: { traceId, serviceId: "hwlab-device-pod" }
}));
}
async function codeAgentOptions(request, response, options) {
const auth = await options.accessController.authenticate(request, { required: options.accessController.required });
if (!auth.ok) {
@@ -468,6 +559,23 @@ async function codeAgentOptions(request, response, options) {
return { ...options, actor: auth.actor ?? null, authSession: auth.session ?? null };
}
async function readJsonObject(request, limitBytes) {
const body = await readBody(request, limitBytes);
try {
const value = body ? JSON.parse(body) : {};
if (!value || typeof value !== "object" || Array.isArray(value)) {
return { ok: false, error: { accepted: false, error: { code: "invalid_params", message: "body must be a JSON object" } } };
}
return { ok: true, value };
} catch (error) {
return { ok: false, error: { accepted: false, error: { code: "parse_error", message: "Invalid JSON body", reason: error.message } } };
}
}
function internalServiceHeader(request) {
return String(getHeader(request, "x-hwlab-internal-service") ?? "").trim();
}
async function handleGatewayPollHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};