fix: restore devicepod cli auth and output evidence

This commit is contained in:
Codex
2026-06-02 14:53:41 +08:00
parent 363fbc28de
commit 10af8aaf76
4 changed files with 141 additions and 5 deletions
+10 -1
View File
@@ -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");
+30 -1
View File
@@ -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" };
}
+64
View File
@@ -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"], {
+37 -3
View File
@@ -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;