Merge pull request #629 from pikasTech/fix/v02-code-agent-device-pod-auth

fix: grant Code Agent device-pod access
This commit is contained in:
Lyon
2026-05-31 17:10:27 +08:00
committed by GitHub
5 changed files with 151 additions and 6 deletions
+56 -2
View File
@@ -376,6 +376,57 @@ test("cloud api protects device-pod routes when access control is required", asy
}
});
test("cloud api issues a redacted Code Agent device-pod session with admin device access", async () => {
const accessController = createAccessController({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
now: () => "2026-05-28T00:00:00.000Z"
});
const server = createCloudApiServer({
env: {
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
HWLAB_BOOTSTRAP_ADMIN_ID: "usr_v02_admin",
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
},
accessController,
now: () => "2026-05-28T00:00:00.000Z"
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
await postJson(port, "/v1/admin/device-pods", {
devicePodId: "device-pod-71-freq",
profile: { schemaVersion: 1, target: { id: "target-71-freq" }, route: { gatewaySessionId: "gws_unused" } }
}, adminLogin.cookie);
const agentAuth = await accessController.createCodeAgentDevicePodSession();
assert.equal(agentAuth.actor.id, "usr_v02_admin");
assert.equal(agentAuth.actor.role, "admin");
assert.equal(agentAuth.tokenSource, "code-agent-device-pod-session");
assert.equal(agentAuth.valuesRedacted, true);
assert.equal(typeof agentAuth.token, "string");
assert.equal(JSON.stringify(agentAuth.session).includes(agentAuth.token), false);
const status = await getJson(port, "/v1/device-pods/device-pod-71-freq/status", null, {
"x-hwlab-session-token": agentAuth.token
});
assert.equal(status.status, 200);
assert.equal(status.body.devicePodId, "device-pod-71-freq");
assert.equal(status.body.actor.id, "usr_v02_admin");
assert.equal(status.body.actor.role, "admin");
assert.equal(JSON.stringify(status.body).includes(agentAuth.token), false);
} finally {
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
}
});
test("cloud api creates device lease holder session before lease insert", async () => {
const accessController = createAccessController({
env: {
@@ -1075,9 +1126,12 @@ async function putJson(port, path, body, cookie = null, extraHeaders = {}) {
};
}
async function getJson(port, path, cookie = null) {
async function getJson(port, path, cookie = null, extraHeaders = {}) {
const response = await fetch(`http://127.0.0.1:${port}${path}`, {
headers: cookie ? { cookie } : {}
headers: {
...(cookie ? { cookie } : {}),
...extraHeaders
}
});
return {
status: response.status,
+40
View File
@@ -5,6 +5,7 @@ import { getHeader, readBody, sendJson, truthyFlag } from "./server-http-utils.t
const SESSION_COOKIE = "hwlab_session";
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;
const CODE_AGENT_DEVICE_POD_SESSION_REFRESH_SKEW_SECONDS = 60 * 5;
const DEVICE_JOB_CONTRACT_VERSION = "device-pod-job-v1";
const DEVICE_JOB_INTENTS = new Set([
"workspace.ls",
@@ -156,6 +157,7 @@ class AccessController {
this.now = now;
this.required = required;
this.bootstrapAttempted = false;
this.codeAgentDevicePodSession = null;
}
async handleAuthRoute(request, response, url) {
@@ -476,6 +478,38 @@ class AccessController {
return sendJson(response, 200, { authenticated: false, loggedOut: true });
}
async createCodeAgentDevicePodSession() {
await this.ensureBootstrap();
const now = this.now();
if (this.codeAgentDevicePodSession?.token && !sessionExpiresSoon(this.codeAgentDevicePodSession.expiresAt, now)) {
return this.codeAgentDevicePodSession;
}
const actor = await this.bootstrapAdminActor();
if (!actor || actor.status !== "active") return null;
const token = randomBytes(32).toString("base64url");
const expiresAt = new Date(Date.parse(now) + SESSION_MAX_AGE_SECONDS * 1000).toISOString();
const session = await this.store.createSession({
userId: actor.id,
tokenHash: sha256(token),
now,
expiresAt
});
this.codeAgentDevicePodSession = {
token,
expiresAt,
actor: publicActor(actor),
session: publicSession({ ...session, user: actor }),
tokenSource: "code-agent-device-pod-session",
valuesRedacted: true
};
return this.codeAgentDevicePodSession;
}
async bootstrapAdminActor() {
return await this.store.findUserByUsername(this.env.HWLAB_BOOTSTRAP_ADMIN_USERNAME || "admin")
?? await this.store.getUserById(this.env.HWLAB_BOOTSTRAP_ADMIN_ID || "usr_bootstrap_admin");
}
async ensureBootstrap() {
if (this.bootstrapAttempted) return;
this.bootstrapAttempted = true;
@@ -1363,6 +1397,12 @@ function boundedDurationMs(value, fallback = 1000) { const parsed = Number.parse
function uartTailArgs(searchParams, uartId) { return { uartId, durationMs: boundedDurationMs(searchParams.get("durationMs") ?? searchParams.get("duration-ms")), maxBytes: boundedOutputMaxBytes(searchParams) }; }
function safeAgentSessionId(value) { const text = textOr(value, ""); return /^ses_[A-Za-z0-9_.:-]+$/u.test(text) ? text : ""; }
function leaseTokenFromRequest(request, body) { return textOr(body?.leaseToken, "") || textOr(getHeader(request, "x-hwlab-device-lease-token"), ""); }
function sessionExpiresSoon(expiresAt, now) {
const expiresMs = Date.parse(String(expiresAt ?? ""));
const nowMs = Date.parse(String(now ?? ""));
if (!Number.isFinite(expiresMs) || !Number.isFinite(nowMs)) return true;
return expiresMs - nowMs <= CODE_AGENT_DEVICE_POD_SESSION_REFRESH_SKEW_SECONDS * 1000;
}
function devicePodInternalHeaders(serviceId, token) { return token ? { "x-hwlab-internal-service": serviceId, "x-hwlab-internal-token": token } : { "x-hwlab-internal-service": serviceId }; }
function hashPassword(password) { const salt = randomBytes(16).toString("hex"); return `sha256:${salt}:${sha256(`${salt}:${password}`)}`; }
function verifyPassword(stored, password) { const [, salt, digest] = String(stored ?? "").split(":"); return Boolean(salt && digest && sha256(`${salt}:${password}`) === digest); }
@@ -12,6 +12,7 @@ import {
codexAppServerProviderBaseUrl,
createCodexStdioSessionManager
} from "./codex-stdio-session.ts";
import { childProcessEnv } from "./codex-stdio-session-helpers.ts";
import {
classifyCodeAgentChatReadiness,
classifyCodexRunnerCapability
@@ -212,6 +213,28 @@ test("code agent session registry stores bounded non-sensitive conversation fact
assert.match(facts.summary, /workspace=\/workspace\/hwlab/u);
});
test("Codex child env carries only device-pod auth needed by Code Agent tools", () => {
const child = childProcessEnv({
PATH: "/usr/bin",
CODEX_HOME: "/tmp/codex-home",
OPENAI_API_KEY: "test-openai-key-material",
HWLAB_DEVICE_POD_API_URL: "http://127.0.0.1:6667",
HWLAB_CLOUD_API_URL: "http://127.0.0.1:6667",
HWLAB_DEVICE_POD_SESSION_TOKEN: "device-pod-session-token",
HWLAB_SESSION_COOKIE: "hwlab_session=browser-cookie",
HWLAB_SESSION_TOKEN: "browser-session-token",
HWLAB_BEARER_TOKEN: "browser-bearer-token"
});
assert.equal(child.HWLAB_DEVICE_POD_API_URL, "http://127.0.0.1:6667");
assert.equal(child.HWLAB_CLOUD_API_URL, "http://127.0.0.1:6667");
assert.equal(child.HWLAB_DEVICE_POD_SESSION_TOKEN, "device-pod-session-token");
assert.equal(child.OPENAI_API_KEY, undefined);
assert.equal(child.HWLAB_SESSION_COOKIE, undefined);
assert.equal(child.HWLAB_SESSION_TOKEN, undefined);
assert.equal(child.HWLAB_BEARER_TOKEN, undefined);
});
test("code agent session registry inspect locates conversation, session, and trace summary", () => {
const registry = createCodeAgentSessionRegistry({
idFactory: () => "ses_inspect_registry"
@@ -1642,6 +1642,9 @@ export function childProcessEnv(env = process.env) {
CODEX_HOME: resolveCodexHome(env),
CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "hwlab_code_agent",
UNIDESK_SKILLS_PATH: "/app/skills",
...(env.HWLAB_DEVICE_POD_API_URL ? { HWLAB_DEVICE_POD_API_URL: env.HWLAB_DEVICE_POD_API_URL } : {}),
...(env.HWLAB_CLOUD_API_URL ? { HWLAB_CLOUD_API_URL: env.HWLAB_CLOUD_API_URL } : {}),
...(env.HWLAB_DEVICE_POD_SESSION_TOKEN ? { HWLAB_DEVICE_POD_SESSION_TOKEN: env.HWLAB_DEVICE_POD_SESSION_TOKEN } : {}),
NO_PROXY: noProxy,
no_proxy: noProxy,
...(env.LANG ? { LANG: env.LANG } : {}),
+29 -4
View File
@@ -109,16 +109,18 @@ export async function handleCodeAgentChatHttp(request, response, options) {
sendJson(response, responsePayload.status === "failed" && responsePayload.error?.code === "invalid_params" ? 400 : 200, responsePayload);
}
function runCodeAgentChat(params, options) {
return handleCodeAgentChat(params, codeAgentChatExecutionOptions(options, params));
async function runCodeAgentChat(params, options) {
return handleCodeAgentChat(params, await codeAgentChatExecutionOptions(options, params));
}
function codeAgentChatExecutionOptions(options = {}, params = {}) {
async function codeAgentChatExecutionOptions(options = {}, params = {}) {
const env = codeAgentProviderProfileEnv(options.env ?? process.env, params);
Object.assign(env, await codeAgentDevicePodAuthEnv(options, env));
return {
callProvider: options.callCodeAgentProvider,
timeoutMs: options.codeAgentTimeoutMs,
hardTimeoutMs: options.codeAgentHardTimeoutMs,
env: codeAgentProviderProfileEnv(options.env ?? process.env, params),
env,
workspace: options.workspace,
skillsDirs: options.skillsDirs,
skillsDirsExact: options.skillsDirsExact,
@@ -131,6 +133,29 @@ function codeAgentChatExecutionOptions(options = {}, params = {}) {
};
}
async function codeAgentDevicePodAuthEnv(options = {}, env = process.env) {
if (typeof options.accessController?.createCodeAgentDevicePodSession !== "function") return {};
const auth = await options.accessController.createCodeAgentDevicePodSession();
if (!auth?.token) return {};
const apiUrl = firstNonEmptyValue(
env.HWLAB_CODE_AGENT_DEVICE_POD_API_URL,
env.HWLAB_DEVICE_POD_API_URL,
env.HWLAB_CLOUD_API_URL,
env.HWLAB_PUBLIC_ENDPOINT,
localCloudApiUrl(env)
);
return {
HWLAB_DEVICE_POD_API_URL: apiUrl,
HWLAB_CLOUD_API_URL: apiUrl,
HWLAB_DEVICE_POD_SESSION_TOKEN: auth.token
};
}
function localCloudApiUrl(env = process.env) {
const port = parsePositiveInteger(env.HWLAB_CLOUD_API_PORT, 6667);
return `http://127.0.0.1:${port}`;
}
function codeAgentProviderProfileEnv(env = process.env, params = {}) {
const profile = normalizeCodeAgentProviderProfile(params.providerProfile ?? params.codeAgentProviderProfile ?? env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE);
if (profile === "runtime-default") return env;