fix: restore devicepod cli auth and output evidence
This commit is contained in:
@@ -216,9 +216,15 @@ test("device pod executor dispatches internal jobs through cloud-api gateway ada
|
|||||||
dispatch: {
|
dispatch: {
|
||||||
shellExecuted: true,
|
shellExecuted: true,
|
||||||
dispatchStatus: "succeeded",
|
dispatchStatus: "succeeded",
|
||||||
stdout: "device-host-cli ok",
|
|
||||||
exitCode: 0
|
exitCode: 0
|
||||||
},
|
},
|
||||||
|
shellExecuted: true,
|
||||||
|
dispatchStatus: "succeeded",
|
||||||
|
stdout: "device-host-cli ok",
|
||||||
|
stderr: "",
|
||||||
|
exitCode: 0,
|
||||||
|
stdoutTruncated: false,
|
||||||
|
stderrTruncated: false,
|
||||||
auditId: "aud_devicepod_test",
|
auditId: "aud_devicepod_test",
|
||||||
evidenceId: "evd_devicepod_test",
|
evidenceId: "evd_devicepod_test",
|
||||||
gateway: { gatewayId: "gtw_devicepod_test" }
|
gateway: { gatewayId: "gtw_devicepod_test" }
|
||||||
@@ -268,6 +274,9 @@ test("device pod executor dispatches internal jobs through cloud-api gateway ada
|
|||||||
const output = await waitForJobStatus(service.port, "device-pod-test", "job_devicepod_gateway_test", "completed");
|
const output = await waitForJobStatus(service.port, "device-pod-test", "job_devicepod_gateway_test", "completed");
|
||||||
assert.equal(output.status, "completed");
|
assert.equal(output.status, "completed");
|
||||||
assert.equal(output.output.text, "device-host-cli ok");
|
assert.equal(output.output.text, "device-host-cli ok");
|
||||||
|
assert.equal(output.output.dispatch.stdoutBytes, 18);
|
||||||
|
assert.equal(output.output.dispatch.exitCode, 0);
|
||||||
|
assert.equal(output.output.dispatch.stdoutTruncated, false);
|
||||||
assert.equal(output.output.auditId, "aud_devicepod_test");
|
assert.equal(output.output.auditId, "aud_devicepod_test");
|
||||||
assert.equal(dispatches.length, 1);
|
assert.equal(dispatches.length, 1);
|
||||||
assert.equal(dispatches[0].internalService, "hwlab-device-pod");
|
assert.equal(dispatches[0].internalService, "hwlab-device-pod");
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ function jobFromGatewayDispatch(job, payload, httpStatus) {
|
|||||||
output: boundedOutput({
|
output: boundedOutput({
|
||||||
text: dispatchText,
|
text: dispatchText,
|
||||||
httpStatus,
|
httpStatus,
|
||||||
dispatch,
|
dispatch: gatewayDispatchSummary(result, dispatch),
|
||||||
gateway: result.gateway ?? null,
|
gateway: result.gateway ?? null,
|
||||||
auditId: result.auditId ?? null,
|
auditId: result.auditId ?? null,
|
||||||
evidenceId: result.evidenceId ?? null,
|
evidenceId: result.evidenceId ?? null,
|
||||||
@@ -417,6 +417,8 @@ function jobFromGatewayDispatch(job, payload, httpStatus) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function gatewayDispatchText(result, dispatch) {
|
function gatewayDispatchText(result, dispatch) {
|
||||||
|
if (typeof result.stdout === "string" && result.stdout) return result.stdout;
|
||||||
|
if (typeof result.stderr === "string" && result.stderr) return result.stderr;
|
||||||
if (typeof dispatch.stdout === "string" && dispatch.stdout) return dispatch.stdout;
|
if (typeof dispatch.stdout === "string" && dispatch.stdout) return dispatch.stdout;
|
||||||
if (typeof dispatch.stderr === "string" && dispatch.stderr) return dispatch.stderr;
|
if (typeof dispatch.stderr === "string" && dispatch.stderr) return dispatch.stderr;
|
||||||
if (typeof result.text === "string") return result.text;
|
if (typeof result.text === "string") return result.text;
|
||||||
@@ -424,6 +426,25 @@ function gatewayDispatchText(result, dispatch) {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function gatewayDispatchSummary(result, dispatch) {
|
||||||
|
const value = (key) => dispatch[key] ?? result[key];
|
||||||
|
return pruneUndefined({
|
||||||
|
shellExecuted: value("shellExecuted"),
|
||||||
|
dispatchStatus: value("dispatchStatus"),
|
||||||
|
status: value("status"),
|
||||||
|
exitCode: value("exitCode"),
|
||||||
|
signal: value("signal"),
|
||||||
|
timedOut: value("timedOut"),
|
||||||
|
cwd: value("cwd"),
|
||||||
|
command: value("command"),
|
||||||
|
stdoutBytes: textBytes(value("stdout")),
|
||||||
|
stderrBytes: textBytes(value("stderr")),
|
||||||
|
stdoutTruncated: value("stdoutTruncated"),
|
||||||
|
stderrTruncated: value("stderrTruncated"),
|
||||||
|
durationMs: value("durationMs")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function deviceHostCommand(profile, job) {
|
function deviceHostCommand(profile, job) {
|
||||||
const args = deviceHostArgs(job.intent, job.args);
|
const args = deviceHostArgs(job.intent, job.args);
|
||||||
if (!args) return null;
|
if (!args) return null;
|
||||||
@@ -725,6 +746,14 @@ function boundedOutput(output, maxBytes = DEVICE_JOB_OUTPUT_MAX_BYTES) {
|
|||||||
return { ...bounded, bytes: Math.min(buffer.length, maxBytes), truncation: { maxBytes, truncated: clipped, originalBytes: buffer.length } };
|
return { ...bounded, bytes: Math.min(buffer.length, maxBytes), truncation: { maxBytes, truncated: clipped, originalBytes: buffer.length } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pruneUndefined(value) {
|
||||||
|
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
function textBytes(value) {
|
||||||
|
return typeof value === "string" ? Buffer.byteLength(value, "utf8") : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function freshness(observedAt, blocker) {
|
function freshness(observedAt, blocker) {
|
||||||
return { observedAt, ageMs: 0, stale: Boolean(blocker), source: blocker ? "blocked" : "device-pod-executor" };
|
return { observedAt, ageMs: 0, stale: Boolean(blocker), source: blocker ? "blocked" : "device-pod-executor" };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,6 +121,70 @@ test("device-pod-cli auto-locates from assembled runtime and rejects manual URL
|
|||||||
assert.equal(rejected.payload.error.code, "runtime_endpoint_manual_url_forbidden");
|
assert.equal(rejected.payload.error.code, "runtime_endpoint_manual_url_forbidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("device-pod-cli reuses matching hwlab-cli web session cookie when no explicit auth is assembled", async () => {
|
||||||
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-device-pod-cli-session-"));
|
||||||
|
await mkdir(path.join(root, ".state/hwlab-cli"), { recursive: true });
|
||||||
|
await writeFile(path.join(root, ".state/hwlab-cli/session.json"), JSON.stringify({
|
||||||
|
baseUrl: "http://74.48.78.17:19666",
|
||||||
|
cookie: "hwlab_session=session-from-web",
|
||||||
|
user: { username: "admin" }
|
||||||
|
}), "utf8");
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
process.chdir(root);
|
||||||
|
try {
|
||||||
|
const result = await runDevicePodCli(["profile", "list"], {
|
||||||
|
env: {
|
||||||
|
HWLAB_RUNTIME_NAMESPACE: "hwlab-v02",
|
||||||
|
HWLAB_RUNTIME_LANE: "v02",
|
||||||
|
HWLAB_RUNTIME_ENDPOINT_LOCKED: "1",
|
||||||
|
HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1"
|
||||||
|
},
|
||||||
|
fetchImpl: async (url, init) => {
|
||||||
|
assert.equal(String(url), "http://74.48.78.17:19667/v1/device-pods");
|
||||||
|
assert.equal(init?.headers?.cookie, "hwlab_session=session-from-web");
|
||||||
|
assert.equal(init?.headers?.["x-hwlab-session-token"], undefined);
|
||||||
|
return jsonResponse(200, { ok: true, devicePods: [] });
|
||||||
|
},
|
||||||
|
now: () => "2026-06-02T00:00:00.000Z"
|
||||||
|
});
|
||||||
|
assert.equal(result.exitCode, 0);
|
||||||
|
assert.equal(result.payload.runtimeEndpoint.source, "runtime-namespace");
|
||||||
|
assert.equal(result.payload.runtimeEndpoint.explicitOverride, false);
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("device-pod-cli ignores stale hwlab-cli web session from another runtime scope", async () => {
|
||||||
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-device-pod-cli-stale-session-"));
|
||||||
|
await mkdir(path.join(root, ".state/hwlab-cli"), { recursive: true });
|
||||||
|
await writeFile(path.join(root, ".state/hwlab-cli/session.json"), JSON.stringify({
|
||||||
|
baseUrl: "http://74.48.78.17:17666",
|
||||||
|
cookie: "hwlab_session=dev-session"
|
||||||
|
}), "utf8");
|
||||||
|
const originalCwd = process.cwd();
|
||||||
|
process.chdir(root);
|
||||||
|
try {
|
||||||
|
const result = await runDevicePodCli(["profile", "list"], {
|
||||||
|
env: {
|
||||||
|
HWLAB_RUNTIME_NAMESPACE: "hwlab-v02",
|
||||||
|
HWLAB_RUNTIME_LANE: "v02",
|
||||||
|
HWLAB_RUNTIME_ENDPOINT_LOCKED: "1",
|
||||||
|
HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1"
|
||||||
|
},
|
||||||
|
fetchImpl: async (_url, init) => {
|
||||||
|
assert.equal(init?.headers?.cookie, undefined);
|
||||||
|
return jsonResponse(401, { ok: false, error: { code: "auth_required" } });
|
||||||
|
},
|
||||||
|
now: () => "2026-06-02T00:00:00.000Z"
|
||||||
|
});
|
||||||
|
assert.equal(result.exitCode, 1);
|
||||||
|
assert.equal(result.payload.httpStatus, 401);
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("device-pod-cli keeps manual API URL only as unlocked local debug override", async () => {
|
test("device-pod-cli keeps manual API URL only as unlocked local debug override", async () => {
|
||||||
const seen: string[] = [];
|
const seen: string[] = [];
|
||||||
const result = await runDevicePodCli(["--api-base-url", "http://debug.test", "--session-token", "debug-session", "profile", "list"], {
|
const result = await runDevicePodCli(["--api-base-url", "http://debug.test", "--session-token", "debug-session", "profile", "list"], {
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { resolveRuntimeEndpoint, runtimeEndpointVisibility } from "./runtime-endpoint-resolver.ts";
|
import { readFile } from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import { resolveRuntimeEndpoint, runtimeEndpointVisibility, sameRuntimeEndpointScope } from "./runtime-endpoint-resolver.ts";
|
||||||
|
|
||||||
const VERSION = "0.2.0-rest";
|
const VERSION = "0.2.0-rest";
|
||||||
const CLI_NAME = "device-pod-cli";
|
const CLI_NAME = "device-pod-cli";
|
||||||
@@ -371,9 +374,10 @@ async function requestJson({ parsed, env, fetchImpl, method, path, body, auth =
|
|||||||
const timeout = setTimeout(() => controller.abort(), numberOption(parsed.timeoutMs) ?? DEFAULT_TIMEOUT_MS);
|
const timeout = setTimeout(() => controller.abort(), numberOption(parsed.timeoutMs) ?? DEFAULT_TIMEOUT_MS);
|
||||||
try {
|
try {
|
||||||
const url = `${baseUrl}${path}`;
|
const url = `${baseUrl}${path}`;
|
||||||
|
const authHeaderValues = auth ? await authHeaders(parsed, env, endpoint) : {};
|
||||||
const response = await fetchImpl(`${baseUrl}${path}`, {
|
const response = await fetchImpl(`${baseUrl}${path}`, {
|
||||||
method,
|
method,
|
||||||
headers: clean({ accept: "application/json", ...(body ? { "content-type": "application/json" } : {}), ...(auth ? authHeaders(parsed, env) : {}), ...extraHeaders }),
|
headers: clean({ accept: "application/json", ...(body ? { "content-type": "application/json" } : {}), ...authHeaderValues, ...extraHeaders }),
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
});
|
});
|
||||||
@@ -466,7 +470,37 @@ const uartJsonRpcOptionKeys = [
|
|||||||
"baudRate"
|
"baudRate"
|
||||||
];
|
];
|
||||||
function defaultOperation(surface: string) { return surface === "workspace" ? "ls" : surface === "debug-probe" ? "status" : "read"; }
|
function defaultOperation(surface: string) { return surface === "workspace" ? "ls" : surface === "debug-probe" ? "status" : "read"; }
|
||||||
function authHeaders(parsed: ParsedArgs, env: EnvLike) { const cookie = text(parsed.cookie ?? env.HWLAB_SESSION_COOKIE); const sessionToken = text(parsed.sessionToken ?? env.HWLAB_DEVICE_POD_SESSION_TOKEN ?? env.HWLAB_CLOUD_API_SESSION_TOKEN ?? env.HWLAB_SESSION_TOKEN); const bearer = text(parsed.bearerToken ?? env.HWLAB_BEARER_TOKEN); return clean({ ...(cookie ? { cookie: cookie.includes("=") ? cookie : `hwlab_session=${encodeURIComponent(cookie)}` } : {}), ...(sessionToken ? { "x-hwlab-session-token": sessionToken } : {}), ...(bearer ? { authorization: `Bearer ${bearer}` } : {}) }); }
|
async function authHeaders(parsed: ParsedArgs, env: EnvLike, endpoint: any) {
|
||||||
|
const explicitCookie = text(parsed.cookie ?? env.HWLAB_SESSION_COOKIE);
|
||||||
|
const sessionToken = text(parsed.sessionToken ?? env.HWLAB_DEVICE_POD_SESSION_TOKEN ?? env.HWLAB_CLOUD_API_SESSION_TOKEN ?? env.HWLAB_SESSION_TOKEN);
|
||||||
|
const bearer = text(parsed.bearerToken ?? env.HWLAB_BEARER_TOKEN);
|
||||||
|
const stateCookie = !explicitCookie && !sessionToken && !bearer ? await savedHwlabCliCookie(parsed, env, endpoint) : "";
|
||||||
|
const cookie = explicitCookie || stateCookie;
|
||||||
|
return clean({
|
||||||
|
...(cookie ? { cookie: cookie.includes("=") ? cookie : `hwlab_session=${encodeURIComponent(cookie)}` } : {}),
|
||||||
|
...(sessionToken ? { "x-hwlab-session-token": sessionToken } : {}),
|
||||||
|
...(bearer ? { authorization: `Bearer ${bearer}` } : {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savedHwlabCliCookie(parsed: ParsedArgs, env: EnvLike, endpoint: any) {
|
||||||
|
const file = hwlabCliSessionFile(parsed, env);
|
||||||
|
let stored: any;
|
||||||
|
try {
|
||||||
|
stored = JSON.parse(await readFile(file, "utf8"));
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const cookie = text(stored?.cookie);
|
||||||
|
const storedBaseUrl = text(stored?.baseUrl);
|
||||||
|
if (!cookie || !storedBaseUrl) return "";
|
||||||
|
return storedBaseUrl === endpoint?.baseUrl || sameRuntimeEndpointScope(storedBaseUrl, endpoint?.baseUrl) ? cookie : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function hwlabCliSessionFile(parsed: ParsedArgs, env: EnvLike) {
|
||||||
|
const explicit = text(parsed.stateFile ?? env.HWLAB_CLI_STATE_FILE ?? env.HWLAB_SESSION_STATE_FILE);
|
||||||
|
return path.resolve(process.cwd(), explicit || ".state/hwlab-cli/session.json");
|
||||||
|
}
|
||||||
function responsePayload(action: string, response: any, extra: Record<string, unknown> = {}, parsed: ParsedArgs = { _: [] }) {
|
function responsePayload(action: string, response: any, extra: Record<string, unknown> = {}, parsed: ParsedArgs = { _: [] }) {
|
||||||
const success = response.status >= 200 && response.status < 300 && response.body?.ok !== false;
|
const success = response.status >= 200 && response.status < 300 && response.body?.ok !== false;
|
||||||
const full = parsed.full === true;
|
const full = parsed.full === true;
|
||||||
|
|||||||
Reference in New Issue
Block a user