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: {
|
||||
shellExecuted: true,
|
||||
dispatchStatus: "succeeded",
|
||||
stdout: "device-host-cli ok",
|
||||
exitCode: 0
|
||||
},
|
||||
shellExecuted: true,
|
||||
dispatchStatus: "succeeded",
|
||||
stdout: "device-host-cli ok",
|
||||
stderr: "",
|
||||
exitCode: 0,
|
||||
stdoutTruncated: false,
|
||||
stderrTruncated: false,
|
||||
auditId: "aud_devicepod_test",
|
||||
evidenceId: "evd_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");
|
||||
assert.equal(output.status, "completed");
|
||||
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(dispatches.length, 1);
|
||||
assert.equal(dispatches[0].internalService, "hwlab-device-pod");
|
||||
|
||||
@@ -406,7 +406,7 @@ function jobFromGatewayDispatch(job, payload, httpStatus) {
|
||||
output: boundedOutput({
|
||||
text: dispatchText,
|
||||
httpStatus,
|
||||
dispatch,
|
||||
dispatch: gatewayDispatchSummary(result, dispatch),
|
||||
gateway: result.gateway ?? null,
|
||||
auditId: result.auditId ?? null,
|
||||
evidenceId: result.evidenceId ?? null,
|
||||
@@ -417,6 +417,8 @@ function jobFromGatewayDispatch(job, payload, httpStatus) {
|
||||
}
|
||||
|
||||
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.stderr === "string" && dispatch.stderr) return dispatch.stderr;
|
||||
if (typeof result.text === "string") return result.text;
|
||||
@@ -424,6 +426,25 @@ function gatewayDispatchText(result, dispatch) {
|
||||
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) {
|
||||
const args = deviceHostArgs(job.intent, job.args);
|
||||
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 } };
|
||||
}
|
||||
|
||||
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) {
|
||||
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");
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const seen: string[] = [];
|
||||
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 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);
|
||||
try {
|
||||
const url = `${baseUrl}${path}`;
|
||||
const authHeaderValues = auth ? await authHeaders(parsed, env, endpoint) : {};
|
||||
const response = await fetchImpl(`${baseUrl}${path}`, {
|
||||
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,
|
||||
signal: controller.signal
|
||||
});
|
||||
@@ -466,7 +470,37 @@ const uartJsonRpcOptionKeys = [
|
||||
"baudRate"
|
||||
];
|
||||
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 = { _: [] }) {
|
||||
const success = response.status >= 200 && response.status < 300 && response.body?.ok !== false;
|
||||
const full = parsed.full === true;
|
||||
|
||||
Reference in New Issue
Block a user