diff --git a/internal/cloud/access-control.test.ts b/internal/cloud/access-control.test.ts index a82dff5b..6ac3db4a 100644 --- a/internal/cloud/access-control.test.ts +++ b/internal/cloud/access-control.test.ts @@ -472,7 +472,7 @@ test("cloud api internal device-pod gateway dispatch is service-only and fail-cl test("cloud api internal device-pod gateway dispatch uses gateway poll result", async () => { const server = createCloudApiServer({ - env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1" }, + env: { HWLAB_ACCESS_CONTROL_REQUIRED: "1", HWLAB_ENVIRONMENT: "v02" }, now: () => "2026-05-28T00:00:00.000Z" }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); @@ -503,6 +503,7 @@ test("cloud api internal device-pod gateway dispatch uses gateway poll result", const queued = await postJson(port, "/v1/gateway/poll", { gatewayId: "gtw_devicepod_test", gatewaySessionId: "gws_devicepod_test" }); assert.equal(queued.status, 200); assert.equal(queued.body.type, "request"); + assert.equal(queued.body.request.meta.environment, "v02"); assert.equal(queued.body.request.method, "hardware.invoke.shell"); assert.equal(queued.body.request.params.input.command, "node tools/device-host-cli.mjs health"); @@ -527,6 +528,7 @@ test("cloud api internal device-pod gateway dispatch uses gateway poll result", const dispatch = await dispatchPromise; assert.equal(dispatch.status, 200); + assert.equal(dispatch.body.meta.environment, "v02"); assert.equal(dispatch.body.result.status, "completed"); assert.equal(dispatch.body.result.dispatch.stdout, "host cli ok"); } finally { diff --git a/internal/cloud/gateway-demo-registry.ts b/internal/cloud/gateway-demo-registry.ts index 28e485ed..5a5a44cb 100644 --- a/internal/cloud/gateway-demo-registry.ts +++ b/internal/cloud/gateway-demo-registry.ts @@ -1,6 +1,6 @@ import { randomUUID } from "node:crypto"; -import { ENVIRONMENT_DEV, JSON_RPC_VERSION } from "../protocol/index.mjs"; +import { ENVIRONMENT_DEV, JSON_RPC_VERSION, isProtocolEnvironment } from "../protocol/index.mjs"; import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs"; const DEFAULT_STALE_MS = 30000; @@ -188,7 +188,7 @@ export function createGatewayShellRequest({ id, params = {}, meta = {} }) { traceId: meta.traceId ?? params.traceId ?? `trc_gateway_shell_${randomUUID()}`, actorId: meta.actorId, serviceId: CLOUD_API_SERVICE_ID, - environment: ENVIRONMENT_DEV + environment: isProtocolEnvironment(meta.environment) ? meta.environment : ENVIRONMENT_DEV } }; } diff --git a/internal/cloud/json-rpc.test.ts b/internal/cloud/json-rpc.test.ts index 0ccfc8b5..a67e038d 100644 --- a/internal/cloud/json-rpc.test.ts +++ b/internal/cloud/json-rpc.test.ts @@ -207,7 +207,7 @@ test("cloud-api runtime accepts register/report/invoke and exposes audit/evidenc projectId: "prj_01J00000000000000000000010", gatewaySessionId: "gws_01J00000000000000000000010", gatewayId: "gtw_01J00000000000000000000010", - serviceId: "hwlab-gateway-simu", + serviceId: "hwlab-gateway", endpoint: "http://127.0.0.1:7101" }); assert.equal(gateway.registered, true); diff --git a/internal/cloud/json-rpc.ts b/internal/cloud/json-rpc.ts index f182fbb9..08b0c75a 100644 --- a/internal/cloud/json-rpc.ts +++ b/internal/cloud/json-rpc.ts @@ -5,6 +5,7 @@ import { ERROR_CODES, HwlabProtocolError, JSON_RPC_VERSION, + isProtocolEnvironment, validateRequest, validateResponse } from "../protocol/index.mjs"; @@ -215,7 +216,7 @@ export function createResponseMeta(requestMeta = {}) { const meta = { traceId: requestMeta.traceId || `trc_${randomUUID()}`, serviceId: CLOUD_API_SERVICE_ID, - environment: ENVIRONMENT_DEV + environment: isProtocolEnvironment(requestMeta.environment) ? requestMeta.environment : ENVIRONMENT_DEV }; if (requestMeta.actorId) { diff --git a/internal/cloud/server.ts b/internal/cloud/server.ts index 2bc9518f..4aadb272 100644 --- a/internal/cloud/server.ts +++ b/internal/cloud/server.ts @@ -551,7 +551,7 @@ async function handleInternalDevicePodGatewayDispatch(request, response, options summary: dispatch.message ?? dispatch.stderr ?? dispatch.status ?? result.status ?? "gateway dispatch failed" } }, - requestMeta: { traceId, serviceId: "hwlab-device-pod" } + requestMeta: { traceId, serviceId: "hwlab-device-pod", environment: runtimeEnvironment(options.env ?? process.env) } })); } diff --git a/internal/protocol/index.mjs b/internal/protocol/index.mjs index b25b324d..16fc3c28 100644 --- a/internal/protocol/index.mjs +++ b/internal/protocol/index.mjs @@ -1,4 +1,6 @@ export const ENVIRONMENT_DEV = "dev"; +export const ENVIRONMENT_V02 = "v02"; +export const PROTOCOL_ENVIRONMENTS = Object.freeze([ENVIRONMENT_DEV, ENVIRONMENT_V02]); export const DEV_ENDPOINT = "http://74.48.78.17:16667"; export const DEV_FRONTEND_ENDPOINT = "http://74.48.78.17:16666"; export const JSON_RPC_VERSION = "2.0"; @@ -307,6 +309,10 @@ export function isFrozenServiceId(serviceId) { return SERVICE_IDS.includes(serviceId); } +export function isProtocolEnvironment(environment) { + return PROTOCOL_ENVIRONMENTS.includes(environment); +} + export function isProtocolId(value) { return typeof value === "string" && protocolIdPattern.test(value); } @@ -321,7 +327,7 @@ export function validateMeta(meta) { if (!isFrozenServiceId(meta.serviceId)) { throw new Error(`unknown serviceId ${JSON.stringify(meta.serviceId)}`); } - if (meta.environment !== ENVIRONMENT_DEV) { + if (!isProtocolEnvironment(meta.environment)) { throw new Error(`unsupported environment ${JSON.stringify(meta.environment)}`); } } @@ -445,8 +451,8 @@ function assertProtocolField(kind, field, value) { if (timestampFieldNames.has(field) && Number.isNaN(Date.parse(value))) { throw new Error(`${kind}.${field} must be an RFC 3339 timestamp`); } - if (field === "environment" && value !== ENVIRONMENT_DEV) { - throw new Error(`${kind}.environment must be ${ENVIRONMENT_DEV}`); + if (field === "environment" && !isProtocolEnvironment(value)) { + throw new Error(`${kind}.environment must be one of ${PROTOCOL_ENVIRONMENTS.join(", ")}`); } if (field === "serviceId" && !isFrozenServiceId(value)) { throw new Error(`${kind}.serviceId is not a frozen service id`);