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:
@@ -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,
|
||||
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user