feat: enforce v02 runtime authority
This commit is contained in:
@@ -5,28 +5,50 @@ import test from "node:test";
|
||||
|
||||
const bunCommand = process.env.HWLAB_TEST_BUN_COMMAND || process.env.HWLAB_BUN_COMMAND || (process.versions.bun ? process.execPath : "bun");
|
||||
|
||||
test("device pod fake service exposes health, list, status, events, and method guards", async () => {
|
||||
test("device pod executor exposes cloud-api authority boundary and method guards", async () => {
|
||||
const service = await startDevicePod("device-pod-test");
|
||||
try {
|
||||
const health = await fetchJson(`http://127.0.0.1:${service.port}/health/live`);
|
||||
assert.equal(health.serviceId, "hwlab-device-pod");
|
||||
assert.equal(health.status, "live");
|
||||
assert.equal(health.devicePodId, "device-pod-test");
|
||||
assert.equal(health.role, "fake-device-pod-server");
|
||||
assert.equal(health.contractVersion, "device-pod-executor-v1");
|
||||
assert.equal(health.role, "device-pod-internal-executor");
|
||||
assert.equal(health.authority, "hwlab-cloud-api");
|
||||
assert.equal(health.fake, false);
|
||||
|
||||
const list = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods`);
|
||||
assert.equal(list.serviceId, "hwlab-device-pod");
|
||||
assert.equal(list.selectedDevicePodId, "device-pod-test");
|
||||
assert.equal(list.devicePods.length, 1);
|
||||
assert.equal(list.source.fake, false);
|
||||
|
||||
const status = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/status`);
|
||||
assert.equal(status.devicePod.devicePodId, "device-pod-test");
|
||||
assert.equal(status.summary.targetId, "stm32f103-minsys-01");
|
||||
assert.equal(status.devicePodId, "device-pod-test");
|
||||
assert.equal(status.status, "blocked");
|
||||
assert.equal(status.summary.blocker.code, "gateway_dispatch_unavailable");
|
||||
|
||||
const events = await fetchJson(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/events?limit=1`);
|
||||
assert.equal(events.events.length, 1);
|
||||
assert.equal(events.truncation.limit, 1);
|
||||
assert.equal(events.truncation.truncated, true);
|
||||
assert.equal(events.truncation.truncated, false);
|
||||
assert.equal(events.events[0].blocker.code, "gateway_dispatch_unavailable");
|
||||
|
||||
const externalJob = await fetch(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/jobs`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ intent: "workspace.ls" })
|
||||
});
|
||||
assert.equal(externalJob.status, 403);
|
||||
assert.equal((await externalJob.json()).error.code, "cloud_api_authority_required");
|
||||
|
||||
const internalJob = await fetch(`http://127.0.0.1:${service.port}/v1/device-pods/device-pod-test/jobs`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json", "x-hwlab-internal-service": "hwlab-cloud-api" },
|
||||
body: JSON.stringify({ intent: "workspace.ls", traceId: "trc_device_pod_test" })
|
||||
});
|
||||
assert.equal(internalJob.status, 409);
|
||||
assert.equal((await internalJob.json()).blocker.code, "gateway_dispatch_unavailable");
|
||||
|
||||
const missing = await fetch(`http://127.0.0.1:${service.port}/missing`);
|
||||
assert.equal(missing.status, 404);
|
||||
@@ -34,7 +56,7 @@ test("device pod fake service exposes health, list, status, events, and method g
|
||||
|
||||
const methodGuard = await fetch(`http://127.0.0.1:${service.port}/health`, { method: "POST" });
|
||||
assert.equal(methodGuard.status, 405);
|
||||
assert.deepEqual(await methodGuard.json(), { error: "method_not_allowed", method: "POST" });
|
||||
assert.deepEqual(await methodGuard.json(), { error: "method_not_allowed", method: "POST", path: "/health" });
|
||||
} finally {
|
||||
await service.stop();
|
||||
}
|
||||
|
||||
+156
-29
@@ -1,47 +1,174 @@
|
||||
#!/usr/bin/env node
|
||||
import { createServer } from "node:http";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { jsonResponse, listen, parsePort } from "../../internal/sim/http.mjs";
|
||||
import {
|
||||
DEFAULT_DEVICE_POD_ID,
|
||||
DEVICE_POD_SERVICE_ID,
|
||||
buildDevicePodRestPayload
|
||||
} from "../../internal/device-pod/fake-data.mjs";
|
||||
import { jsonResponse, listen, parsePort, readJson } from "../../internal/sim/http.mjs";
|
||||
|
||||
const SERVICE_ID = "hwlab-device-pod";
|
||||
const CONTRACT_VERSION = "device-pod-executor-v1";
|
||||
const DEFAULT_DEVICE_POD_ID = "device-pod-71-freq";
|
||||
const port = parsePort(process.env.HWLAB_DEVICE_POD_PORT, parsePort(process.env.PORT, 7601));
|
||||
const devicePodId = process.env.HWLAB_DEVICE_POD_ID || DEFAULT_DEVICE_POD_ID;
|
||||
const environment = process.env.HWLAB_ENVIRONMENT || process.env.HWLAB_GITOPS_PROFILE || "dev";
|
||||
|
||||
listen(createServer((request, response) => {
|
||||
const url = new URL(request.url ?? "/", "http://localhost");
|
||||
if (request.method !== "GET") {
|
||||
jsonResponse(response, 405, { error: "method_not_allowed", method: request.method ?? "UNKNOWN" });
|
||||
return;
|
||||
listen(createServer(async (request, response) => {
|
||||
try {
|
||||
const url = new URL(request.url ?? "/", "http://localhost");
|
||||
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) {
|
||||
jsonResponse(response, 200, healthPayload());
|
||||
return;
|
||||
}
|
||||
if (url.pathname === "/health" || url.pathname === "/health/live") {
|
||||
jsonResponse(response, 405, { error: "method_not_allowed", method: request.method ?? "UNKNOWN", path: url.pathname });
|
||||
return;
|
||||
}
|
||||
if (request.method === "GET" && url.pathname === "/v1/device-pods") {
|
||||
jsonResponse(response, 200, listPayload());
|
||||
return;
|
||||
}
|
||||
const route = parseDevicePodPath(url.pathname);
|
||||
if (!route) {
|
||||
jsonResponse(response, 404, { error: "not_found", path: url.pathname });
|
||||
return;
|
||||
}
|
||||
if (request.method === "GET" && route.subpath === "status") {
|
||||
jsonResponse(response, 200, statusPayload(route.devicePodId));
|
||||
return;
|
||||
}
|
||||
if (request.method === "GET" && route.subpath === "events") {
|
||||
jsonResponse(response, 200, eventsPayload(route.devicePodId, url.searchParams));
|
||||
return;
|
||||
}
|
||||
if (request.method === "POST" && route.subpath === "jobs") {
|
||||
await handleInternalJob(request, response, route.devicePodId);
|
||||
return;
|
||||
}
|
||||
jsonResponse(response, 405, { error: "method_not_allowed", method: request.method ?? "UNKNOWN", path: url.pathname });
|
||||
} catch (error) {
|
||||
jsonResponse(response, 500, { error: "internal_error", message: error?.message ?? String(error), valuesRedacted: true });
|
||||
}
|
||||
if (url.pathname === "/health" || url.pathname === "/health/live") {
|
||||
jsonResponse(response, 200, healthPayload());
|
||||
return;
|
||||
}
|
||||
const payload = buildDevicePodRestPayload(url.pathname, url.searchParams, {
|
||||
devicePodId,
|
||||
sourceKind: "FAKE-SERVICE"
|
||||
});
|
||||
if (!payload) {
|
||||
jsonResponse(response, 404, { error: "not_found", path: url.pathname });
|
||||
return;
|
||||
}
|
||||
jsonResponse(response, 200, payload);
|
||||
}), port);
|
||||
|
||||
function healthPayload() {
|
||||
return {
|
||||
serviceId: DEVICE_POD_SERVICE_ID,
|
||||
environment: process.env.HWLAB_ENVIRONMENT || "dev",
|
||||
serviceId: SERVICE_ID,
|
||||
environment,
|
||||
status: "live",
|
||||
ready: true,
|
||||
contractVersion: "device-pod-fake-v1",
|
||||
contractVersion: CONTRACT_VERSION,
|
||||
devicePodId,
|
||||
observedAt: new Date().toISOString(),
|
||||
role: "fake-device-pod-server",
|
||||
note: "Only fake frontend data is served; no hardware connection is opened."
|
||||
role: "device-pod-internal-executor",
|
||||
authority: "hwlab-cloud-api",
|
||||
acceptsUserAuthority: false,
|
||||
fake: false,
|
||||
source: sourcePayload(),
|
||||
note: "Profile, grant, lease, and user-facing job authority live in hwlab-cloud-api; this service only exposes the internal executor boundary."
|
||||
};
|
||||
}
|
||||
|
||||
function listPayload() {
|
||||
return {
|
||||
serviceId: SERVICE_ID,
|
||||
contractVersion: CONTRACT_VERSION,
|
||||
status: "ok",
|
||||
source: sourcePayload(),
|
||||
selectedDevicePodId: devicePodId,
|
||||
devicePods: [{
|
||||
devicePodId,
|
||||
status: "executor-ready",
|
||||
authority: "hwlab-cloud-api",
|
||||
fake: false,
|
||||
routes: {
|
||||
cloudAuthority: "/v1/device-pods",
|
||||
internalJob: `/v1/device-pods/${encodeURIComponent(devicePodId)}/jobs`
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
function statusPayload(id) {
|
||||
return {
|
||||
serviceId: SERVICE_ID,
|
||||
contractVersion: CONTRACT_VERSION,
|
||||
status: "blocked",
|
||||
devicePodId: id,
|
||||
observedAt: new Date().toISOString(),
|
||||
source: sourcePayload(),
|
||||
summary: {
|
||||
devicePodId: id,
|
||||
status: "blocked",
|
||||
blocker: gatewayAdapterBlocker("device-host-cli dispatch is owned by hwlab-cloud-api gateway routing")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function eventsPayload(id, searchParams) {
|
||||
const limit = Math.min(Math.max(Number.parseInt(searchParams.get("limit") ?? "80", 10) || 80, 1), 1000);
|
||||
const event = {
|
||||
eventId: `evt_${id}_executor_boundary`,
|
||||
devicePodId: id,
|
||||
ts: new Date().toISOString(),
|
||||
level: "warn",
|
||||
scope: "executor",
|
||||
status: "blocked",
|
||||
summary: "No standalone fake device events are emitted by hwlab-device-pod; use hwlab-cloud-api job authority.",
|
||||
blocker: gatewayAdapterBlocker("standalone device-pod executor has no gateway session")
|
||||
};
|
||||
return {
|
||||
serviceId: SERVICE_ID,
|
||||
contractVersion: CONTRACT_VERSION,
|
||||
status: "ok",
|
||||
devicePodId: id,
|
||||
source: sourcePayload(),
|
||||
events: limit > 0 ? [event] : [],
|
||||
truncation: { limit, returned: limit > 0 ? 1 : 0, truncated: false }
|
||||
};
|
||||
}
|
||||
|
||||
async function handleInternalJob(request, response, id) {
|
||||
if (!isInternalCaller(request)) {
|
||||
jsonResponse(response, 403, {
|
||||
accepted: false,
|
||||
status: "blocked",
|
||||
error: {
|
||||
code: "cloud_api_authority_required",
|
||||
message: "Device Pod jobs must be authorized by hwlab-cloud-api before reaching the internal executor."
|
||||
},
|
||||
source: sourcePayload()
|
||||
});
|
||||
return;
|
||||
}
|
||||
const body = await readJson(request);
|
||||
const traceId = typeof body.traceId === "string" && body.traceId.startsWith("trc_") ? body.traceId : `trc_devicepod_${randomUUID()}`;
|
||||
jsonResponse(response, 409, {
|
||||
accepted: false,
|
||||
status: "blocked",
|
||||
contractVersion: CONTRACT_VERSION,
|
||||
devicePodId: id,
|
||||
traceId,
|
||||
operationId: typeof body.operationId === "string" ? body.operationId : `op_devicepod_${randomUUID()}`,
|
||||
blocker: gatewayAdapterBlocker("gateway/device-host-cli adapter is not connected to this standalone executor service"),
|
||||
source: sourcePayload()
|
||||
});
|
||||
}
|
||||
|
||||
function parseDevicePodPath(pathname) {
|
||||
const prefix = "/v1/device-pods/";
|
||||
if (!pathname.startsWith(prefix)) return null;
|
||||
const [id, ...rest] = pathname.slice(prefix.length).split("/").filter(Boolean).map((part) => decodeURIComponent(part));
|
||||
return id ? { devicePodId: id, subpath: rest.join("/") } : null;
|
||||
}
|
||||
|
||||
function isInternalCaller(request) {
|
||||
const header = request.headers["x-hwlab-internal-service"];
|
||||
return String(Array.isArray(header) ? header[0] : header ?? "").trim() === "hwlab-cloud-api";
|
||||
}
|
||||
|
||||
function sourcePayload() {
|
||||
return { kind: "INTERNAL_EXECUTOR", serviceId: SERVICE_ID, authority: "hwlab-cloud-api", fake: false, devLiveEvidence: false };
|
||||
}
|
||||
|
||||
function gatewayAdapterBlocker(summary) {
|
||||
return { code: "gateway_dispatch_unavailable", layer: "device-pod", retryable: true, summary, userMessage: "Device Pod executor boundary is present, but gateway/device-host-cli dispatch is not connected here." };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user