Files
pikasTech-HWLAB/internal/cloud/access-control.ts
T
Codex eb5bada0d1 fix(v0.2): device-pod output.text evidence read-only selectors for #773
Tracks pikasTech/HWLAB#773. PR #765 fixed selector confusion but did
not touch cloud-api evidence propagation. This change closes the
real root cause and adds the read-only evidence selectors that #760
follow-up called for.

cloud-api (internal/cloud/access-control.ts):
- DEVICE_JOB_INTENTS adds workspace.evidence and debug.evidence.
- DEVICE_JOB_READ_ONLY_SUB_ACTIONS = { status, output, wait, cancel,
  evidence } and DEVICE_JOB_ACTIONABLE_INTENTS = { workspace.build,
  debug.download } make the mutating-intent / sub-action matrix
  explicit.
- _deviceJobRequiresReason(intent, args, reason) returns false when
  the caller already provided reason OR when the actionable mutating
  intent is paired with a read-only sub-action. debug.reset and other
  non-actionable mutating intents still always require reason.
- executorOutputPayload now also surfaces output.summary,
  nestedOutput.summary, evidence.text, evidence.logTail and
  evidence.summary as body.output.text, so a dispatcher that
  includes the host logTail / buildSummary in its result becomes
  visible to the Code Agent without a separate bootsharp dance.

device-pod executor (cmd/hwlab-device-pod/main.ts):
- gatewayDispatchText also walks result.evidence.{text,logTail,
  summary}, dispatch.message (only when dispatchStatus=completed),
  dispatch.summary, result.summary and dispatch.buildSummary before
  falling back to JSON.stringify.
- deviceHostArgs maps workspace.evidence / debug.evidence to the
  host device-host-cli evidence subcommands.

device-host-cli (skills/device-pod-cli/assets/device-host-cli.mjs):
- new readJobEvidence(kindPrefix, requestedId, { tail, full,
  target }) reads the most recent (or specified) keil-build /
  keil-download job log file and returns it as keil-build.evidence /
  keil-download.evidence. tail defaults to 200, --full for entire
  log. Missing job returns ok=false but does not throw.
- workspace build evidence and debug-probe download evidence now
  wired in main() and help text.

device-pod-cli (tools/src/device-pod-cli-lib.ts):
- selectorCheatSheet adds the evidence row and clarifies that
  build/download status/output/wait/cancel/evidence are read-only
  and do NOT need --reason. build/download start still require
  --reason. usage examples now include the two evidence selectors.
- failed-fast selectors are unchanged; existing 26 tests still pass.

SKILL.md (skills/device-pod-cli/SKILL.md):
- Selector Cheat Sheet adds workspace.evidence and
  debug.evidence rows.
- #773 follow-up note: --reason is now required only for
  mutating sub-actions, not for the read-only ones.

cloud-api tests (internal/cloud/access-control.test.ts):
- workspace.evidence and debug.evidence must be in DEVICE_JOB_INTENTS.
- _deviceJobRequiresReason signature and the read-only / actionable
  branch table.
- executorOutputPayload must look at evidence and summary fields
  in addition to text. 3 new tests; pre-existing 4 workbench
  failures are unrelated to this change and reproduce on the
  unchanged v0.2 base.

Verification (host-side, G14 /root/hwlab-v02, source=0cf9a8c6):
- bun test tools/device-pod-cli.test.ts → 26/26 pass.
- bun test internal/cloud/access-control.test.ts → 23 pass,
  4 pre-existing workbench failures (unchanged on v0.2 base).
- bun test cmd/hwlab-device-pod/main.test.ts → 7/7 pass.
- node --check skills/device-pod-cli/assets/device-host-cli.mjs
  → syntax OK.

Hot probe: live v0.2 cloud-api at 74.48.78.17:19667 confirmed
job_devicepod_804c5db4... (workspace.build) returned
text="", bytes=0, output={} before this change. After the
executor text-extraction update and the new evidence selector,
a follow-up workspace.build start + build evidence will surface
the host logTail / buildSummary in body.output.text without
requiring bootsharp + host file fallback.

Tracked-by: pikasTech/HWLAB#773
2026-06-03 21:57:39 +08:00

2202 lines
120 KiB
TypeScript

import { createHash, randomBytes, randomUUID } from "node:crypto";
import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs";
import {
codeAgentAgentRunAdapterEnabled,
loadPersistedAgentRunResult,
syncAgentRunChatResult
} from "./code-agent-agentrun-adapter.ts";
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
import { getHeader, readBody, sendJson, truthyFlag } from "./server-http-utils.ts";
const SESSION_COOKIE = "hwlab_session";
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7;
const DEVICE_POD_API_KEY_HEADER = "x-hwlab-device-pod-api-key";
const DEVICE_JOB_CONTRACT_VERSION = "device-pod-job-v1";
const DEVICE_JOB_INTENTS = new Set([
"workspace.bootsharp",
"workspace.ls",
"workspace.cat",
"workspace.rg",
"workspace.apply-patch",
"workspace.put",
"workspace.rm",
"workspace.rmdir",
"workspace.keil",
"workspace.build",
"debug.status",
"debug.chip-id",
"debug.download",
"debug.reset",
"workspace.evidence",
"debug.evidence",
"io.ports",
"io.uart.read",
"io.uart.read-after-launch-flash",
"io.uart.write",
"io.uart.jsonrpc"
]);
const ACCESS_SCHEMA_STATEMENTS = Object.freeze([
`CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL DEFAULT '',
role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
password_hash TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS user_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
session_token_hash TEXT NOT NULL UNIQUE,
created_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
revoked_at TEXT
)`,
`CREATE TABLE IF NOT EXISTS agent_sessions (
id TEXT PRIMARY KEY,
project_id TEXT,
agent_id TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
started_at TEXT,
ended_at TEXT,
owner_user_id TEXT REFERENCES users(id),
conversation_id TEXT,
thread_id TEXT,
last_trace_id TEXT,
session_json TEXT NOT NULL DEFAULT '{}',
updated_at TEXT
)`,
`ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS owner_user_id TEXT REFERENCES users(id)`,
`ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS conversation_id TEXT`,
`ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS thread_id TEXT`,
`ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS last_trace_id TEXT`,
`ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS session_json TEXT NOT NULL DEFAULT '{}'`,
`ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS updated_at TEXT`,
`CREATE INDEX IF NOT EXISTS idx_agent_sessions_owner ON agent_sessions(owner_user_id)`,
`CREATE INDEX IF NOT EXISTS idx_agent_sessions_conversation ON agent_sessions(conversation_id)`,
`CREATE INDEX IF NOT EXISTS idx_agent_sessions_last_trace ON agent_sessions(last_trace_id)`,
`CREATE TABLE IF NOT EXISTS account_workspaces (
id TEXT PRIMARY KEY,
owner_user_id TEXT NOT NULL REFERENCES users(id),
project_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT 'Default Workbench',
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')),
is_default BOOLEAN NOT NULL DEFAULT true,
selected_conversation_id TEXT,
selected_agent_session_id TEXT,
selected_device_pod_id TEXT,
active_trace_id TEXT,
provider_profile TEXT,
workspace_json TEXT NOT NULL DEFAULT '{}',
revision INTEGER NOT NULL DEFAULT 1,
updated_by_session_id TEXT,
updated_by_client TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`,
`CREATE UNIQUE INDEX IF NOT EXISTS idx_account_workspaces_default ON account_workspaces(owner_user_id, project_id) WHERE is_default = TRUE AND status = 'active'`,
`CREATE INDEX IF NOT EXISTS idx_account_workspaces_owner ON account_workspaces(owner_user_id, project_id, updated_at DESC)`,
`CREATE TABLE IF NOT EXISTS device_pods (
id TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
profile_json TEXT NOT NULL DEFAULT '{}',
profile_hash TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS device_pod_grants (
device_pod_id TEXT NOT NULL REFERENCES device_pods(id) ON DELETE CASCADE,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_by_admin_id TEXT NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL,
PRIMARY KEY (device_pod_id, user_id)
)`,
`CREATE TABLE IF NOT EXISTS device_pod_jobs (
id TEXT PRIMARY KEY,
device_pod_id TEXT NOT NULL REFERENCES device_pods(id) ON DELETE CASCADE,
owner_user_id TEXT NOT NULL REFERENCES users(id),
status TEXT NOT NULL,
intent TEXT NOT NULL,
args_json TEXT NOT NULL DEFAULT '{}',
reason TEXT NOT NULL DEFAULT '',
trace_id TEXT NOT NULL,
operation_id TEXT NOT NULL,
output_json TEXT NOT NULL DEFAULT '{}',
blocker_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
completed_at TEXT
)`
]);
const MUTATING_INTENTS = new Set([
"workspace.apply-patch",
"workspace.build",
"workspace.put",
"workspace.rm",
"workspace.rmdir",
"workspace.keil",
"debug.download",
"debug.reset",
"io.uart.read-after-launch-flash",
"io.uart.write",
"io.uart.jsonrpc"
]);
const DEVICE_JOB_READ_ONLY_SUB_ACTIONS = new Set([
"status",
"output",
"wait",
"cancel",
"evidence"
]);
const DEVICE_JOB_ACTIONABLE_INTENTS = new Set([
"workspace.build",
"debug.download"
]);
const DEVICE_JOB_OUTPUT_MAX_BYTES = 12000;
const DEVICE_POD_PROFILE_SECRET_KEY_PATTERN = /(?:token|secret|password|passphrase|credential|api[_-]?key|access[_-]?key|private[_-]?key|git[_-]?key|cloud[_-]?token|kubeconfig|database[_-]?url|db[_-]?url|connection[_-]?string)/iu;
const DEVICE_POD_PROFILE_SECRET_VALUE_PATTERN = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|postgres(?:ql)?:\/\/|mysql:\/\/|mongodb(?:\+srv)?:\/\/|redis:\/\/|gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})/u;
export function createAccessController(options = {}) {
return new AccessController({
...options,
store: options.store ?? accessStoreForRuntime(options.runtimeStore, options)
});
}
function accessStoreForRuntime(runtimeStore, options = {}) {
if (runtimeStore && typeof runtimeStore.query === "function") {
return new PostgresAccessStore({ query: runtimeStore.query.bind(runtimeStore), now: options.now });
}
return new MemoryAccessStore({ now: options.now });
}
class AccessController {
constructor({ store, env = process.env, gatewayRegistry = null, fetchImpl = fetch, traceStore = null, codeAgentChatResults = null, devicePodExecutorUrl = env.HWLAB_DEVICE_POD_URL, devicePodExecutorTimeoutMs = 1200, now = () => new Date().toISOString(), required = truthyFlag(env.HWLAB_ACCESS_CONTROL_REQUIRED) } = {}) {
this.store = store;
this.env = env;
this.gatewayRegistry = gatewayRegistry;
this.fetchImpl = fetchImpl;
this.traceStore = traceStore;
this.codeAgentChatResults = codeAgentChatResults;
this.codeAgentEnv = env;
this.devicePodExecutorUrl = normalizeBaseUrl(devicePodExecutorUrl);
this.devicePodInternalToken = textOr(env.HWLAB_DEVICE_POD_INTERNAL_TOKEN, "");
this.devicePodApiKey = textOr(env.HWLAB_DEVICE_POD_API_KEY, "");
this.devicePodExecutorTimeoutMs = Number.parseInt(String(devicePodExecutorTimeoutMs), 10) || 1200;
this.now = now;
this.required = required;
this.bootstrapAttempted = false;
}
configureCodeAgentWorkspaceContext({ env = null, fetchImpl = null, traceStore = null, codeAgentChatResults = null } = {}) {
if (env) this.codeAgentEnv = env;
if (fetchImpl) this.fetchImpl = fetchImpl;
if (traceStore) this.traceStore = traceStore;
if (codeAgentChatResults) this.codeAgentChatResults = codeAgentChatResults;
}
async handleAuthRoute(request, response, url) {
try {
await this.ensureBootstrap();
if (request.method === "GET" && url.pathname === "/auth/session") {
sendJson(response, 200, await this.sessionPayload(request));
return;
}
if (request.method === "POST" && url.pathname === "/auth/login") {
await this.handleLogin(request, response);
return;
}
if (request.method === "POST" && url.pathname === "/auth/logout") {
await this.handleLogout(request, response);
return;
}
sendJson(response, 404, errorPayload("not_found", "Auth route is not implemented", 404));
} catch (error) {
sendAccessError(response, error);
}
}
async handleUserRoute(request, response, url) {
try {
await this.ensureBootstrap();
if (request.method === "GET" && url.pathname === "/v1/users/me") {
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
return sendJson(response, 200, {
authenticated: true,
actor: publicActor(auth.actor),
session: auth.session,
contractVersion: "user-access-v1"
});
}
sendJson(response, 404, errorPayload("not_found", "User route is not implemented", 404));
} catch (error) {
sendAccessError(response, error);
}
}
async handleAccessStatusRoute(request, response, url) {
try {
await this.ensureBootstrap();
if (request.method === "GET" && url.pathname === "/v1/access/status") {
return sendJson(response, 200, await this.accessStatusPayload(request));
}
if (request.method === "GET" && url.pathname === "/v1/setup/status") {
return sendJson(response, 200, await this.setupStatusPayload());
}
sendJson(response, 404, errorPayload("not_found", "Access status route is not implemented", 404));
} catch (error) {
sendAccessError(response, error);
}
}
async handleSetupRoute(request, response, url) {
try {
await this.ensureBootstrap();
if (request.method !== "POST" || url.pathname !== "/v1/setup/first-admin") {
return sendJson(response, 404, errorPayload("not_found", "Setup route is not implemented", 404));
}
if (!(await this.setupRequired())) {
return sendJson(response, 409, errorPayload("setup_already_completed", "First admin setup is closed because at least one user already exists", 409));
}
const body = await jsonBody(request);
const password = requiredText(body.password, "password");
const devicePodSeeds = firstAdminDevicePodSeeds(body);
const now = this.now();
const user = await this.store.createFirstAdmin?.({
id: this.env.HWLAB_BOOTSTRAP_ADMIN_ID || "usr_bootstrap_admin",
username: textOr(body.username, this.env.HWLAB_BOOTSTRAP_ADMIN_USERNAME || "admin"),
displayName: textOr(body.displayName, this.env.HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME || "HWLAB Admin"),
passwordHash: hashPassword(password),
now
}) ?? null;
if (!user) {
return sendJson(response, 409, errorPayload("setup_already_completed", "First admin setup is closed because at least one user already exists", 409));
}
const devicePodsInitialized = [];
for (const seed of devicePodSeeds) {
const pod = await this.store.upsertDevicePod({ ...seed, now });
const grant = await this.store.grantDevicePod({
devicePodId: pod.id,
userId: user.id,
createdByAdminId: user.id,
now
});
devicePodsInitialized.push({
devicePod: publicDevicePod(pod, { includeAdmin: true }),
grant
});
}
const token = randomBytes(32).toString("base64url");
const session = await this.store.createSession({
userId: user.id,
tokenHash: sha256(token),
now,
expiresAt: new Date(Date.parse(now) + SESSION_MAX_AGE_SECONDS * 1000).toISOString()
});
setSessionCookie(response, token, SESSION_MAX_AGE_SECONDS);
return sendJson(response, 201, {
created: true,
authenticated: true,
actor: publicActor(user),
session: publicSession({ ...session, user }),
setupRequired: false,
devicePodBootstrap: {
requested: devicePodSeeds.length,
initialized: devicePodsInitialized.length
},
devicePodsInitialized,
contractVersion: "user-access-v1"
});
} catch (error) {
sendAccessError(response, error);
}
}
async sessionPayload(request) {
const auth = await this.authenticate(request, { required: false });
return {
authenticated: Boolean(auth.actor),
actor: auth.actor ? publicActor(auth.actor) : null,
session: auth.session ?? null,
setupRequired: await this.setupRequired(),
contractVersion: "user-access-v1"
};
}
async accessStatusPayload(request) {
const session = await this.sessionPayload(request);
return {
serviceId: CLOUD_API_SERVICE_ID,
...session,
accessControlRequired: this.required,
roles: ["admin", "user"],
routes: accessRoutes()
};
}
async setupStatusPayload() {
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "user-access-v1",
accessControlRequired: this.required,
setupRequired: await this.setupRequired(),
bootstrapAdminConfigured: Boolean(textOr(this.env.HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH, "") || textOr(this.env.HWLAB_BOOTSTRAP_ADMIN_PASSWORD, "")),
routes: accessRoutes()
};
}
async handleAdminRoute(request, response, url) {
try {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
if (auth.devicePodApiKey) {
return sendJson(response, 403, errorPayload("admin_session_required", "Admin APIs require a user session", 403));
}
if (auth.actor.role !== "admin") {
return sendJson(response, 403, errorPayload("admin_required", "Only admin users can call v0.2 admin APIs", 403));
}
if (request.method === "POST" && url.pathname === "/v1/admin/users") {
const body = await jsonBody(request);
const user = await this.store.createUser({
username: requiredText(body.username, "username"),
displayName: textOr(body.displayName, body.username),
role: body.role === "admin" ? "admin" : "user",
status: body.status === "disabled" ? "disabled" : "active",
passwordHash: hashPassword(requiredText(body.password, "password")),
now: this.now()
});
return sendJson(response, 201, { created: true, user: redactedUser(user) });
}
if ((request.method === "POST" && url.pathname === "/v1/admin/device-pods") || (request.method === "PUT" && url.pathname.startsWith("/v1/admin/device-pods/"))) {
const body = await jsonBody(request);
const devicePodId = request.method === "PUT"
? decodeURIComponent(url.pathname.split("/").filter(Boolean)[3] ?? "")
: textOr(body.devicePodId ?? body.id, "");
const profile = assertDevicePodProfileSafe(normalizeObject(body.profile ?? body.profileJson), "profile");
const pod = await this.store.upsertDevicePod({
id: requiredText(devicePodId, "devicePodId"),
name: textOr(body.name, devicePodId),
status: body.status === "disabled" ? "disabled" : "active",
profile,
now: this.now()
});
return sendJson(response, request.method === "POST" ? 201 : 200, {
upserted: true,
devicePod: publicDevicePod(pod, { includeAdmin: true })
});
}
if (request.method === "POST" && url.pathname === "/v1/admin/device-pod-grants") {
const body = await jsonBody(request);
const devicePodId = requiredText(body.devicePodId, "devicePodId");
const userId = requiredText(body.userId, "userId");
await this.requireGrantTargets({ devicePodId, userId });
const grant = await this.store.grantDevicePod({
devicePodId,
userId,
createdByAdminId: auth.actor.id,
now: this.now()
});
return sendJson(response, 201, { granted: true, grant });
}
if (request.method === "DELETE" && url.pathname.startsWith("/v1/admin/device-pod-grants/")) {
const [, , , devicePodId, userId] = url.pathname.split("/").filter(Boolean).map((part) => decodeURIComponent(part));
await this.store.revokeDevicePodGrant({ devicePodId: requiredText(devicePodId, "devicePodId"), userId: requiredText(userId, "userId") });
return sendJson(response, 200, { revoked: true, devicePodId, userId });
}
sendJson(response, 404, errorPayload("not_found", "Admin route is not implemented", 404));
} catch (error) {
sendAccessError(response, error);
}
}
async handleDevicePodRoute(request, response, url) {
try {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
if (request.method === "GET" && url.pathname === "/v1/device-pods") {
const pods = await this.store.listVisibleDevicePods(auth.actor);
return sendJson(response, 200, {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "device-pod-authority-v1",
status: "ok",
source: authoritySource(),
actor: publicActor(auth.actor),
devicePods: pods.map((pod) => publicDevicePod(pod)),
selectedDevicePodId: pods[0]?.id ?? null,
observedAt: this.now()
});
}
const parsed = parseDevicePodPath(url.pathname);
if (!parsed) return sendJson(response, 404, errorPayload("not_found", "Device Pod route is not implemented", 404));
const pod = await this.store.getVisibleDevicePod(auth.actor, parsed.devicePodId);
if (!pod) {
const existing = await this.store.getDevicePod(parsed.devicePodId);
if (existing?.status === "active") {
return sendJson(response, 403, errorPayload("device_pod_forbidden", `Device Pod ${parsed.devicePodId} is not authorized for the current actor`, 403));
}
return sendJson(response, 404, errorPayload("device_pod_not_found", `Device Pod ${parsed.devicePodId} was not found`, 404));
}
if (request.method === "GET" && parsed.route === "status") return sendJson(response, 200, this.devicePodStatus(pod, auth.actor));
if (request.method === "GET" && parsed.route === "events") return sendJson(response, 200, await this.devicePodEvents(pod, url.searchParams));
if (request.method === "GET" && parsed.route === "debug-probe/chip-id") return this.createDevicePodProbeJob(response, pod, auth.actor, { interfaceName: "debug-probe", intent: "debug.chip-id" });
if (request.method === "GET" && parsed.route === "io-probe/uart/1") return this.createDevicePodProbeJob(response, pod, auth.actor, { interfaceName: "io-probe", intent: "io.ports", args: { uartId: "uart/1" } });
if (request.method === "GET" && parsed.route === "io-probe/uart/1/tail") return this.createDevicePodProbeJob(response, pod, auth.actor, { interfaceName: "io-probe", intent: "io.uart.read", args: uartTailArgs(url.searchParams, "uart/1"), outputMaxBytes: boundedOutputMaxBytes(url.searchParams) });
if (request.method === "POST" && parsed.route === "jobs") return this.createDevicePodJob(request, response, pod, auth.actor);
if (request.method === "GET" && parsed.route.startsWith("jobs/")) return this.getDevicePodJob(response, pod, parsed.route, auth.actor);
if (request.method === "POST" && parsed.route.startsWith("jobs/") && parsed.route.endsWith("/cancel")) return this.cancelDevicePodJob(response, pod, parsed.route, auth.actor);
sendJson(response, 404, errorPayload("not_found", "Device Pod route is not implemented", 404));
} catch (error) {
sendAccessError(response, error);
}
}
async authenticate(request, { required = this.required } = {}) {
await this.ensureBootstrap();
const devicePodApiKey = devicePodApiKeyFromRequest(request);
if (devicePodApiKey) return this.authenticateDevicePodApiKey(devicePodApiKey);
const token = sessionTokenFromRequest(request);
if (!token) {
return required ? errorPayload("auth_required", "Authentication is required", 401) : { ok: true, actor: null };
}
const session = await this.store.findSessionByTokenHash(sha256(token), this.now());
if (!session?.user || session.user.status !== "active") {
return errorPayload("auth_session_invalid", "Session is missing, expired, revoked, or disabled", 401);
}
await this.store.touchSession(session.id, this.now());
return { ok: true, actor: session.user, session: publicSession(session) };
}
async authenticateDevicePodApiKey(apiKey) {
if (!this.devicePodApiKey || !constantSecretEquals(apiKey, this.devicePodApiKey)) {
return errorPayload("device_pod_api_key_invalid", "Device Pod API key is missing or invalid", 401);
}
const actor = await this.bootstrapAdminActor();
if (!actor || actor.status !== "active") return errorPayload("device_pod_api_key_actor_unavailable", "Device Pod API key actor is unavailable", 401);
return {
ok: true,
actor,
session: { id: "device-pod-api-key", tokenSource: "device-pod-api-key", valuesRedacted: true },
devicePodApiKey: true
};
}
async handleLogin(request, response) {
const body = await jsonBody(request);
const user = await this.store.findUserByUsername(requiredText(body.username, "username"));
if (!user || user.status !== "active" || !verifyPassword(user.passwordHash, requiredText(body.password, "password"))) {
return sendJson(response, 401, errorPayload("invalid_credentials", "Username or password is invalid", 401));
}
const token = randomBytes(32).toString("base64url");
const now = this.now();
const session = await this.store.createSession({
userId: user.id,
tokenHash: sha256(token),
now,
expiresAt: new Date(Date.parse(now) + SESSION_MAX_AGE_SECONDS * 1000).toISOString()
});
setSessionCookie(response, token, SESSION_MAX_AGE_SECONDS);
return sendJson(response, 200, {
authenticated: true,
actor: publicActor(user),
session: publicSession({ ...session, user })
});
}
async handleLogout(request, response) {
const token = sessionTokenFromRequest(request);
if (token) await this.store.revokeSessionByTokenHash(sha256(token), this.now());
clearSessionCookie(response);
return sendJson(response, 200, { authenticated: false, loggedOut: true });
}
async createCodeAgentDevicePodApiKey() {
await this.ensureBootstrap();
if (!this.devicePodApiKey) return null;
const actor = await this.bootstrapAdminActor();
if (!actor || actor.status !== "active") return null;
return {
apiKey: this.devicePodApiKey,
actor: publicActor(actor),
tokenSource: "code-agent-device-pod-api-key",
valuesRedacted: true
};
}
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;
await this.store.ensureSchema?.();
const count = await this.store.countUsers();
const passwordHash = textOr(this.env.HWLAB_BOOTSTRAP_ADMIN_PASSWORD_HASH, "") || (this.env.HWLAB_BOOTSTRAP_ADMIN_PASSWORD ? hashPassword(this.env.HWLAB_BOOTSTRAP_ADMIN_PASSWORD) : "");
if (count > 0) {
if (passwordHash) await this.store.syncBootstrapAdminPassword?.({
id: this.env.HWLAB_BOOTSTRAP_ADMIN_ID || "usr_bootstrap_admin",
username: this.env.HWLAB_BOOTSTRAP_ADMIN_USERNAME || "admin",
passwordHash,
now: this.now()
});
return;
}
if (!passwordHash) return;
await this.store.createUser({
id: this.env.HWLAB_BOOTSTRAP_ADMIN_ID || "usr_bootstrap_admin",
username: this.env.HWLAB_BOOTSTRAP_ADMIN_USERNAME || "admin",
displayName: this.env.HWLAB_BOOTSTRAP_ADMIN_DISPLAY_NAME || "HWLAB Admin",
role: "admin",
status: "active",
passwordHash,
now: this.now()
});
}
async setupRequired() {
return (await this.store.countUsers()) === 0;
}
async recordAgentSessionOwner(input = {}) {
await this.ensureBootstrap();
const ownerUserId = textOr(input.ownerUserId, "");
const sessionId = agentSessionIdForRecord(input.sessionId, input.traceId);
if (!ownerUserId || !sessionId) return null;
return this.store.recordAgentSessionOwner?.({
...input,
sessionId,
ownerUserId,
projectId: textOr(input.projectId, "prj_v02_code_agent"),
agentId: textOr(input.agentId, "hwlab-code-agent"),
status: textOr(input.status, "active"),
now: this.now()
}) ?? null;
}
async getAgentSessionByTraceId(traceId) {
await this.ensureBootstrap();
const id = textOr(traceId, "");
if (!/^trc_[A-Za-z0-9_.:-]+$/u.test(id)) return null;
return this.store.getAgentSessionByTraceId?.(id) ?? null;
}
async getAgentSession(sessionId) {
await this.ensureBootstrap();
const id = safeAgentSessionId(sessionId);
if (!id) return null;
return this.store.getAgentSession?.(id) ?? null;
}
async listAgentConversations(request, response, url) {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
const projectId = textOr(url.searchParams.get("projectId"), "");
const limit = boundedListLimit(url.searchParams.get("limit"));
const sessions = await this.store.listAgentSessionsForUser?.({
ownerUserId: auth.actor.id,
actorRole: auth.actor.role,
ownerScoped: true,
projectId,
limit,
includeArchived: false,
now: this.now()
}) ?? [];
const conversations = conversationsFromAgentSessions(sessions);
return sendJson(response, 200, {
ok: true,
status: "succeeded",
contractVersion: "agent-conversations-v1",
actor: publicActor(auth.actor),
projectId: projectId || null,
conversations,
defaultConversation: conversations[0] ?? null,
count: conversations.length
});
}
async getAgentConversation(request, response, conversationId) {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
if (!safeConversationIdLocal(conversationId)) {
return sendJson(response, 400, errorPayload("invalid_conversation_id", "conversationId must start with cnv_", 400));
}
const sessions = await this.store.listAgentSessionsForUser?.({
ownerUserId: auth.actor.id,
actorRole: auth.actor.role,
ownerScoped: true,
conversationId,
limit: 20,
includeArchived: true,
now: this.now()
}) ?? [];
const conversation = conversationsFromAgentSessions(sessions).find((item) => item.conversationId === conversationId) ?? null;
if (!conversation) return sendJson(response, 404, errorPayload("agent_conversation_not_found", "Agent conversation is not visible to the current actor", 404));
return sendJson(response, 200, { ok: true, status: "found", contractVersion: "agent-conversations-v1", actor: publicActor(auth.actor), conversation });
}
async updateAgentConversation(request, response, conversationId) {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
if (!safeConversationIdLocal(conversationId)) {
return sendJson(response, 400, errorPayload("invalid_conversation_id", "conversationId must start with cnv_", 400));
}
const body = await jsonBody(request);
const sessionId = safeAgentSessionId(body.sessionId) || safeAgentSessionId(body.session?.sessionId) || `ses_${conversationId.slice(4)}`;
const session = await this.recordAgentSessionOwner({
ownerUserId: auth.actor.id,
sessionId,
projectId: textOr(body.projectId, "prj_device_pod_workbench"),
agentId: textOr(body.agentId, "hwlab-code-agent"),
status: textOr(body.status ?? body.sessionStatus, "active"),
conversationId,
threadId: textOr(body.threadId, null),
traceId: textOr(body.lastTraceId ?? body.traceId, null),
session: normalizeConversationSnapshot(body, auth.actor)
});
const conversation = conversationsFromAgentSessions(session ? [session] : []).find((item) => item.conversationId === conversationId) ?? null;
return sendJson(response, 200, { ok: true, status: "stored", contractVersion: "agent-conversations-v1", conversation });
}
async deleteAgentConversation(request, response, conversationId) {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
if (!safeConversationIdLocal(conversationId)) {
return sendJson(response, 400, errorPayload("invalid_conversation_id", "conversationId must start with cnv_", 400));
}
const body = await jsonBody(request);
const projectId = textOr(body.projectId, "prj_device_pod_workbench");
const visible = await this.visibleConversationForActor(auth.actor, conversationId, projectId, { includeArchived: true });
if (!visible) return sendJson(response, 404, errorPayload("agent_conversation_not_found", "Agent conversation is not visible to the current actor", 404));
const archived = await this.store.archiveAgentConversation?.({
ownerUserId: auth.actor.id,
actorRole: auth.actor.role,
ownerScoped: true,
conversationId,
projectId,
now: this.now()
});
let workspace = null;
const workspaceId = textOr(body.workspaceId, "");
if (safeWorkspaceId(workspaceId)) {
const current = await this.store.getWorkspaceForUser?.({ workspaceId, ownerUserId: auth.actor.id, actorRole: auth.actor.role });
if (current?.selectedConversationId === conversationId) {
const next = await this.nextVisibleConversationForActor(auth.actor, projectId, conversationId);
workspace = await this.store.updateWorkspace?.({
workspaceId,
ownerUserId: auth.actor.id,
actorRole: auth.actor.role,
selectedConversationId: next?.conversationId ?? null,
selectedAgentSessionId: next?.sessionId ?? null,
activeTraceId: null,
patch: {
selectedConversationId: next?.conversationId ?? null,
selectedAgentSessionId: next?.sessionId ?? null,
activeTraceId: null,
messages: next?.messages ?? [],
deletedConversationId: conversationId,
source: "workbench-delete-conversation"
},
updatedBySessionId: currentSessionIdFromAuth(auth),
updatedByClient: textOr(body.updatedByClient ?? body.client, "unknown"),
now: this.now()
});
}
}
return sendJson(response, 200, {
ok: true,
status: "deleted",
contractVersion: "agent-conversations-v1",
conversationId,
archivedCount: archived?.count ?? 0,
workspace: workspace ? await this.publicWorkbenchWorkspace(workspace, auth.actor) : null
});
}
async getWorkbenchWorkspace(request, response, url) {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
const projectId = textOr(url.searchParams.get("projectId"), "prj_device_pod_workbench");
let workspace = await this.store.getOrCreateDefaultWorkspace?.({
ownerUserId: auth.actor.id,
projectId,
now: this.now()
});
workspace = await this.syncTerminalWorkbenchWorkspace(workspace, auth.actor);
return sendJson(response, 200, await this.workbenchWorkspacePayload(workspace, auth.actor));
}
async updateWorkbenchWorkspace(request, response, workspaceId) {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
if (!safeWorkspaceId(workspaceId)) {
return sendJson(response, 400, errorPayload("invalid_workspace_id", "workspaceId must start with wsp_", 400));
}
const current = await this.store.getWorkspaceForUser?.({ workspaceId, ownerUserId: auth.actor.id, actorRole: auth.actor.role });
if (!current) return sendJson(response, 404, errorPayload("workbench_workspace_not_found", "Workbench workspace is not visible to the current actor", 404));
const body = await jsonBody(request);
const expectedRevision = Number.parseInt(String(body.expectedRevision ?? body.revision ?? ""), 10);
if (Number.isInteger(expectedRevision) && expectedRevision > 0 && expectedRevision !== current.revision) {
return sendJson(response, 409, {
ok: false,
status: "revision_conflict",
error: { code: "workspace_revision_conflict", message: "Workbench workspace revision changed; reload and retry." },
expectedRevision,
workspace: await this.publicWorkbenchWorkspace(current, auth.actor)
});
}
const selectedConversationId = textOr(body.selectedConversationId ?? body.conversationId, current.selectedConversationId ?? "");
if (selectedConversationId) {
const visible = await this.visibleConversationForActor(auth.actor, selectedConversationId, body.projectId ?? current.projectId);
if (!visible) return sendJson(response, 403, errorPayload("workspace_conversation_forbidden", "Selected conversation is not visible to the current actor", 403));
}
const selectedDevicePodId = textOr(body.selectedDevicePodId ?? body.devicePodId, current.selectedDevicePodId ?? "");
if (selectedDevicePodId && !(await this.store.getVisibleDevicePod(auth.actor, selectedDevicePodId))) {
return sendJson(response, 403, errorPayload("workspace_device_pod_forbidden", "Selected device pod is not visible to the current actor", 403));
}
const workspace = await this.store.updateWorkspace?.({
workspaceId,
ownerUserId: auth.actor.id,
actorRole: auth.actor.role,
projectId: textOr(body.projectId, current.projectId),
name: textOr(body.name, current.name),
status: body.status === "archived" ? "archived" : "active",
selectedConversationId,
selectedAgentSessionId: safeAgentSessionId(body.selectedAgentSessionId ?? body.sessionId) || current.selectedAgentSessionId,
selectedDevicePodId,
activeTraceId: safeTraceIdLocal(body.activeTraceId ?? body.traceId) || null,
providerProfile: textOr(body.providerProfile, current.providerProfile ?? ""),
patch: normalizeWorkspacePatch(body, auth.actor),
updatedBySessionId: currentSessionIdFromAuth(auth),
updatedByClient: textOr(body.updatedByClient ?? body.client, "unknown"),
now: this.now()
});
return sendJson(response, 200, await this.workbenchWorkspacePayload(workspace, auth.actor));
}
async selectWorkbenchConversation(request, response, workspaceId) {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
if (!safeWorkspaceId(workspaceId)) return sendJson(response, 400, errorPayload("invalid_workspace_id", "workspaceId must start with wsp_", 400));
const current = await this.store.getWorkspaceForUser?.({ workspaceId, ownerUserId: auth.actor.id, actorRole: auth.actor.role });
if (!current) return sendJson(response, 404, errorPayload("workbench_workspace_not_found", "Workbench workspace is not visible to the current actor", 404));
const body = await jsonBody(request);
const conversationId = textOr(body.conversationId, "") || `cnv_${randomUUID()}`;
if (!safeConversationIdLocal(conversationId)) return sendJson(response, 400, errorPayload("invalid_conversation_id", "conversationId must start with cnv_", 400));
const existing = await this.visibleConversationForActor(auth.actor, conversationId, body.projectId ?? current.projectId);
if (!existing && body.create !== true) return sendJson(response, 404, errorPayload("agent_conversation_not_found", "Agent conversation is not visible to the current actor", 404));
const sessionId = safeAgentSessionId(body.sessionId ?? existing?.sessionId) || `ses_${conversationId.slice(4)}`;
if (!existing) {
await this.recordAgentSessionOwner({
ownerUserId: auth.actor.id,
sessionId,
projectId: current.projectId,
agentId: "hwlab-code-agent",
status: "active",
conversationId,
threadId: textOr(body.threadId, null),
traceId: textOr(body.lastTraceId ?? body.traceId, null),
session: normalizeConversationSnapshot({ ...body, snapshot: { source: "workbench-select-conversation" } }, auth.actor)
});
}
const workspace = await this.store.updateWorkspace?.({
workspaceId,
ownerUserId: auth.actor.id,
actorRole: auth.actor.role,
selectedConversationId: conversationId,
selectedAgentSessionId: sessionId,
activeTraceId: safeTraceIdLocal(body.activeTraceId ?? body.traceId) || null,
patch: normalizeWorkspacePatch({ ...body, selectedConversationId: conversationId, sessionId }, auth.actor),
updatedBySessionId: currentSessionIdFromAuth(auth),
updatedByClient: textOr(body.updatedByClient ?? body.client, "unknown"),
now: this.now()
});
return sendJson(response, 200, await this.workbenchWorkspacePayload(workspace, auth.actor));
}
async resetWorkbenchWorkspace(request, response, workspaceId) {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
if (!safeWorkspaceId(workspaceId)) return sendJson(response, 400, errorPayload("invalid_workspace_id", "workspaceId must start with wsp_", 400));
const current = await this.store.getWorkspaceForUser?.({ workspaceId, ownerUserId: auth.actor.id, actorRole: auth.actor.role });
if (!current) return sendJson(response, 404, errorPayload("workbench_workspace_not_found", "Workbench workspace is not visible to the current actor", 404));
const body = await jsonBody(request);
if (body.confirm !== true && body.confirmReset !== true) {
return sendJson(response, 400, errorPayload("workspace_reset_confirmation_required", "Reset requires confirm=true", 400));
}
const workspace = await this.store.updateWorkspace?.({
workspaceId,
ownerUserId: auth.actor.id,
actorRole: auth.actor.role,
selectedConversationId: null,
selectedAgentSessionId: null,
selectedDevicePodId: null,
activeTraceId: null,
providerProfile: null,
patch: { resetAt: this.now(), resetBy: publicActor(auth.actor), messages: [] },
replaceJson: true,
updatedBySessionId: currentSessionIdFromAuth(auth),
updatedByClient: textOr(body.updatedByClient ?? body.client, "unknown"),
now: this.now()
});
return sendJson(response, 200, { ...(await this.workbenchWorkspacePayload(workspace, auth.actor)), reset: true });
}
async workbenchWorkspaceEvents(request, response, url, workspaceId) {
await this.ensureBootstrap();
const auth = await this.authenticate(request, { required: true });
if (!auth.ok) return sendJson(response, auth.status, auth);
let workspace = await this.store.getWorkspaceForUser?.({ workspaceId, ownerUserId: auth.actor.id, actorRole: auth.actor.role });
if (!workspace) return sendJson(response, 404, errorPayload("workbench_workspace_not_found", "Workbench workspace is not visible to the current actor", 404));
workspace = await this.syncTerminalWorkbenchWorkspace(workspace, auth.actor);
const afterRevision = Number.parseInt(String(url.searchParams.get("afterRevision") ?? ""), 10) || 0;
const changed = workspace.revision > afterRevision;
return sendJson(response, 200, {
ok: true,
status: changed ? "changed" : "unchanged",
contractVersion: "workbench-workspace-v1",
afterRevision,
latestRevision: workspace.revision,
events: changed ? [{ type: "workspace", revision: workspace.revision, updatedAt: workspace.updatedAt }] : [],
workspace: changed ? await this.publicWorkbenchWorkspace(workspace, auth.actor) : null
});
}
async workbenchWorkspacePayload(workspace, actor) {
return {
ok: true,
status: "succeeded",
contractVersion: "workbench-workspace-v1",
actor: publicActor(actor),
workspace: await this.publicWorkbenchWorkspace(workspace, actor)
};
}
async syncTerminalWorkbenchWorkspace(workspace, actor) {
const activeTraceId = safeTraceIdLocal(workspace?.activeTraceId);
const codeAgentEnv = this.codeAgentEnv ?? this.env;
if (!workspace || !codeAgentAgentRunAdapterEnabled(codeAgentEnv)) return workspace;
const workspaceJson = normalizeObject(workspace.workspace);
const repairTraceId = activeTraceId ? "" : await this.terminalWorkbenchRepairTraceId(workspace, actor, workspaceJson);
const traceId = activeTraceId || repairTraceId;
if (!traceId) return workspace;
const options = {
env: codeAgentEnv,
fetchImpl: this.fetchImpl,
accessController: this,
codeAgentChatResults: this.codeAgentChatResults,
traceStore: this.traceStore ?? defaultCodeAgentTraceStore
};
try {
const cached = this.codeAgentChatResults?.get?.(traceId) ?? null;
const persisted = cached ? null : await loadPersistedAgentRunResult(traceId, options);
const current = cached ?? persisted ?? null;
if (!current || !canActorReadWorkspaceTrace(current, actor)) return workspace;
const synced = current?.agentRun?.runId && current.status === "running"
? await syncAgentRunChatResult({ traceId, currentResult: current, options, traceStore: options.traceStore })
: { result: current };
const result = synced.result ?? current;
if (!isTerminalCodeAgentResult(result)) return workspace;
const now = this.now();
const staleContinuation = isThreadResumeFailedResult(result);
const resultConversationId = safeConversationIdLocal(result.conversationId) ? result.conversationId : workspace.selectedConversationId;
const resultAgentSessionId = safeAgentSessionId(result.sessionId ?? result.session?.sessionId ?? result.sessionReuse?.sessionId) || workspace.selectedAgentSessionId;
const selectedConversationId = staleContinuation ? null : resultConversationId;
const selectedAgentSessionId = staleContinuation ? null : resultAgentSessionId;
await this.syncTerminalWorkbenchConversation({
workspace,
actor,
result,
activeTraceId: traceId,
selectedConversationId: resultConversationId,
selectedAgentSessionId: resultAgentSessionId,
now
});
const staleThreadId = threadResumeFailureThreadId(result);
const updated = await this.store.updateWorkspace?.({
workspaceId: workspace.id,
ownerUserId: workspace.ownerUserId,
actorRole: actor?.role ?? "user",
selectedConversationId,
selectedAgentSessionId,
selectedDevicePodId: workspace.selectedDevicePodId,
activeTraceId: null,
providerProfile: workspace.providerProfile,
patch: {
...workspaceJson,
selectedConversationId,
selectedAgentSessionId,
activeTraceId: null,
sessionStatus: terminalWorkbenchSessionStatus(result),
lastTraceId: traceId,
syncedTraceStatus: result.status ?? result.agentRun?.terminalStatus ?? null,
...(staleContinuation ? {
staleContinuationCleared: true,
staleContinuationReason: "thread-resume-failed",
staleConversationId: resultConversationId,
staleAgentSessionId: resultAgentSessionId,
staleThreadId,
recoveryAction: "new-session-on-next-turn"
} : {}),
updatedAt: now,
source: "workbench-status-sync",
valuesRedacted: true,
secretMaterialStored: false
},
updatedBySessionId: workspace.updatedBySessionId,
updatedByClient: "workbench-status-sync",
now
});
return updated ?? workspace;
} catch {
return workspace;
}
}
async terminalWorkbenchRepairTraceId(workspace, actor, workspaceJson) {
try {
const traceId = safeTraceIdLocal(workspaceJson?.lastTraceId);
if (!traceId || !safeConversationIdLocal(workspace?.selectedConversationId)) return "";
const conversation = await this.visibleConversationForActor(actor, workspace.selectedConversationId, workspace.projectId);
return conversationNeedsTerminalRepair(conversation, traceId) ? traceId : "";
} catch {
return "";
}
}
async publicWorkbenchWorkspace(workspace, actor) {
const conversation = workspace?.selectedConversationId
? await this.visibleConversationForActor(actor, workspace.selectedConversationId, workspace.projectId)
: null;
const selectedDevicePod = workspace?.selectedDevicePodId
? await this.store.getVisibleDevicePod(actor, workspace.selectedDevicePodId)
: null;
return publicWorkbenchWorkspace(workspace, { conversation, selectedDevicePod });
}
async syncTerminalWorkbenchConversation({ workspace, actor, result, activeTraceId, selectedConversationId, selectedAgentSessionId, now }) {
if (!safeConversationIdLocal(selectedConversationId) || !safeAgentSessionId(selectedAgentSessionId)) return null;
const existing = await this.visibleConversationForActor(actor, selectedConversationId, workspace.projectId);
const sessionStatus = terminalWorkbenchSessionStatus(result);
const threadId = textOr(result.threadId ?? result.session?.threadId ?? result.sessionReuse?.threadId ?? result.providerTrace?.threadId, existing?.threadId ?? null);
const messages = mergeTerminalConversationMessages(existing?.messages, result, {
traceId: activeTraceId,
conversationId: selectedConversationId,
sessionId: selectedAgentSessionId,
threadId,
now,
existingMessageCount: existing?.messageCount ?? existing?.snapshot?.messageCount ?? null,
firstUserMessagePreview: existing?.firstUserMessagePreview ?? existing?.snapshot?.firstUserMessagePreview ?? null
});
const mergedMessages = Array.isArray(messages) ? messages : messages?.messages ?? [];
const mergedCount = Array.isArray(messages) ? null : messages?.messageCount ?? null;
const mergedPreview = Array.isArray(messages) ? null : messages?.firstUserMessagePreview ?? null;
return await this.recordAgentSessionOwner({
ownerUserId: actor.id,
sessionId: selectedAgentSessionId,
projectId: workspace.projectId,
agentId: existing?.agentId ?? "hwlab-code-agent",
status: sessionStatus,
conversationId: selectedConversationId,
threadId,
traceId: activeTraceId,
session: normalizeConversationSnapshot({
...(existing?.snapshot ?? {}),
source: "workbench-status-sync",
sessionStatus,
updatedAt: now,
lastTraceId: activeTraceId,
messages: mergedMessages,
messageCount: mergedCount,
firstUserMessagePreview: mergedPreview
}, actor),
now
});
}
async visibleConversationForActor(actor, conversationId, projectId = "", options = {}) {
if (!safeConversationIdLocal(conversationId)) return null;
const sessions = await this.store.listAgentSessionsForUser?.({
ownerUserId: actor.id,
actorRole: actor.role,
ownerScoped: true,
conversationId,
projectId: textOr(projectId, ""),
limit: 20,
includeArchived: options.includeArchived === true,
now: this.now()
}) ?? [];
return conversationsFromAgentSessions(sessions).find((item) => item.conversationId === conversationId) ?? null;
}
async nextVisibleConversationForActor(actor, projectId = "", excludedConversationId = "") {
const sessions = await this.store.listAgentSessionsForUser?.({
ownerUserId: actor.id,
actorRole: actor.role,
ownerScoped: true,
projectId: textOr(projectId, ""),
limit: 20,
includeArchived: false,
now: this.now()
}) ?? [];
return conversationsFromAgentSessions(sessions).find((item) => item.conversationId !== excludedConversationId) ?? null;
}
async requireGrantTargets({ devicePodId, userId }) {
const pod = await this.store.getDevicePod(devicePodId);
if (!pod || pod.status !== "active") {
throw Object.assign(new Error(`Device Pod ${devicePodId} does not exist or is disabled`), { statusCode: 404, code: "device_pod_not_found" });
}
const user = await this.store.getUserById(userId);
if (!user || user.status !== "active") {
throw Object.assign(new Error(`User ${userId} does not exist or is disabled`), { statusCode: 404, code: "user_not_found" });
}
}
devicePodStatus(pod, actor) {
const route = pod.profile.route ?? {};
const gatewaySessionId = textOr(route.gatewaySessionId, "");
const gatewayOnline = gatewaySessionId && this.gatewayRegistry?.isOnline?.(gatewaySessionId) === true;
const blocker = gatewayOnline ? null : gatewayDispatchBlocker(gatewaySessionId ? "gateway session is not connected" : "profile route.gatewaySessionId is missing");
const observedAt = this.now();
const status = blocker ? "blocked" : "ok";
const output = boundedOutput({ text: blocker?.summary ?? "device-pod status ok", summary: blocker?.summary ?? "device-pod status ok" });
const freshnessPayload = freshness(observedAt, blocker);
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "device-pod-authority-v1",
status,
devicePodId: pod.id,
targetId: targetIdFromProfile(pod.profile),
profileHash: pod.profileHash,
traceId: `trc_devicepod_${randomUUID()}`,
operationId: `op_devicepod_${randomUUID()}`,
observedAt,
source: authoritySource(),
actor: publicActor(actor),
devicePod: publicDevicePod(pod),
blocker,
freshness: freshnessPayload,
output: output.output,
text: output.text,
bytes: output.bytes,
truncation: output.truncation,
summary: {
devicePodId: pod.id,
targetId: targetIdFromProfile(pod.profile),
status,
freshness: freshnessPayload,
profileHash: pod.profileHash,
blocker
}
};
}
async devicePodEvents(pod, searchParams) {
const limit = Math.min(Math.max(Number.parseInt(searchParams.get("limit") ?? "80", 10) || 80, 1), 1000);
const jobs = await this.store.listDevicePodJobs(pod.id, limit);
const events = jobs.map((job) => ({
eventId: `evt_${job.id}`,
devicePodId: pod.id,
targetId: targetIdFromProfile(pod.profile),
ts: job.updatedAt,
level: job.status === "blocked" || job.status === "failed" ? "warn" : "info",
scope: "job",
intent: job.intent,
status: job.status,
summary: normalizeBlocker(job.blocker)?.summary ?? `job ${job.id} ${job.status}`,
blocker: normalizeBlocker(job.blocker),
refs: jobRefs(job)
}));
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "device-pod-authority-v1",
status: "ok",
observedAt: this.now(),
devicePodId: pod.id,
targetId: targetIdFromProfile(pod.profile),
source: authoritySource(),
events,
lines: events.map(formatEventLine),
truncation: { limit, returned: events.length, truncated: jobs.length >= limit }
};
}
async createDevicePodProbeJob(response, pod, actor, { interfaceName, intent, args = {}, outputMaxBytes = DEVICE_JOB_OUTPUT_MAX_BYTES }) {
const result = await this.createReadOnlyDevicePodJob({ pod, actor, intent, args });
const payload = this.jobOutputPayload(result.job, pod, { maxBytes: outputMaxBytes });
return sendJson(response, result.httpStatus, {
...payload,
interface: interfaceName,
intent,
source: authoritySource()
});
}
async createReadOnlyDevicePodJob({ pod, actor, intent, args = {} }) {
if (!DEVICE_JOB_INTENTS.has(intent) || MUTATING_INTENTS.has(intent)) {
throw Object.assign(new Error(`Device probe intent ${intent} is not supported as a read-only probe`), { statusCode: 400, code: "unsupported_device_probe_intent" });
}
const traceId = `trc_devicepod_${randomUUID()}`;
const operationId = `op_devicepod_${randomUUID()}`;
const now = this.now();
const job = await this.store.createDevicePodJob({
id: `job_devicepod_${randomUUID()}`,
devicePodId: pod.id,
ownerUserId: actor.id,
status: "running",
intent,
args: normalizeObject(args),
reason: "",
traceId,
operationId,
output: { text: "" },
blocker: null,
now,
completedAt: null
});
if (this.devicePodExecutorUrl) return this.dispatchDevicePodExecutorJob({ job, pod, actor });
return this.blockDevicePodJobWithoutExecutor({ job, pod });
}
async createDevicePodJob(request, response, pod, actor) {
const body = await jsonBody(request);
const intent = requiredText(body.intent, "intent");
if (!DEVICE_JOB_INTENTS.has(intent)) {
return sendJson(response, 400, errorPayload("unsupported_device_job_intent", `Device job intent ${intent} is not supported`, 400));
}
const reason = textOr(body.reason, "");
if (_deviceJobRequiresReason(intent, normalizeObject(body.args), reason)) {
return this.rejectDevicePodJob(response, pod, actor, {
httpStatus: 400,
intent,
args: normalizeObject(body.args),
blocker: deviceJobBlocked("device_job_reason_required", `Device job intent ${intent} requires reason`, false)
});
}
const traceId = `trc_devicepod_${randomUUID()}`;
const operationId = `op_devicepod_${randomUUID()}`;
const now = this.now();
const args = normalizeObject(body.args);
const job = await this.store.createDevicePodJob({
id: `job_devicepod_${randomUUID()}`,
devicePodId: pod.id,
ownerUserId: actor.id,
status: "running",
intent,
args,
reason,
traceId,
operationId,
output: {},
blocker: null,
now,
completedAt: null
});
const dispatched = this.devicePodExecutorUrl
? await this.dispatchDevicePodExecutorJob({ job, pod, actor })
: await this.blockDevicePodJobWithoutExecutor({ job, pod });
sendJson(response, dispatched.httpStatus, this.jobPayload(dispatched.job, pod));
}
async rejectDevicePodJob(response, pod, actor, { httpStatus, intent, reason = "", args = {}, blocker }) {
const now = this.now();
const job = await this.store.createDevicePodJob({
id: `job_devicepod_${randomUUID()}`,
devicePodId: pod.id,
ownerUserId: actor.id,
status: "blocked",
intent,
args,
reason,
traceId: `trc_devicepod_${randomUUID()}`,
operationId: `op_devicepod_${randomUUID()}`,
output: { error: blocker.summary },
blocker,
now,
completedAt: now
});
return sendJson(response, httpStatus, {
...this.jobPayload(job, pod),
error: { code: blocker.code, message: blocker.summary }
});
}
async dispatchDevicePodExecutorJob({ job, pod, actor }) {
const target = `${this.devicePodExecutorUrl}/v1/device-pods/${encodeURIComponent(pod.id)}/jobs`;
try {
const response = await fetchJsonWithTimeout(this.fetchImpl, target, {
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
...devicePodInternalHeaders(CLOUD_API_SERVICE_ID, this.devicePodInternalToken)
},
body: stableJson({
jobId: job.id,
devicePodId: pod.id,
targetId: targetIdFromProfile(pod.profile),
profileHash: pod.profileHash,
profile: pod.profile,
ownerUserId: actor.id,
intent: job.intent,
args: job.args,
reason: job.reason,
traceId: job.traceId,
operationId: job.operationId
})
}, this.devicePodExecutorTimeoutMs);
const updated = await this.updateDevicePodJobFromExecutorResponse({ job, pod, response });
const status = updated.status;
return { job: updated ?? job, httpStatus: response.status >= 400 ? response.status : status === "running" ? 202 : 200 };
} catch (error) {
const blocker = devicePodExecutorBlocker(error?.message ?? "device-pod executor request failed");
const updated = await this.store.updateDevicePodJob(pod.id, job.id, {
status: "blocked",
output: { error: blocker.summary },
blocker,
completedAt: this.now(),
updatedAt: this.now()
});
return { job: updated ?? job, httpStatus: 409 };
}
}
async getDevicePodJob(response, pod, route, actor) {
const parts = route.split("/");
const jobId = decodeURIComponent(parts[1] ?? "");
let job = await this.store.getDevicePodJob(pod.id, jobId);
if (!job) return sendJson(response, 404, errorPayload("device_job_not_found", `Device job ${jobId} was not found`, 404));
if (job.ownerUserId !== actor.id && actor.role !== "admin") return sendJson(response, 403, errorPayload("device_job_owner_required", "Only the owner or admin can inspect the job", 403));
if (this.devicePodExecutorUrl && !terminalJobStatus(job.status)) {
job = await this.refreshDevicePodExecutorJob({ job, pod, output: parts[2] === "output" });
}
if (parts[2] === "output") return sendJson(response, 200, this.jobOutputPayload(job, pod));
return sendJson(response, 200, this.jobPayload(job, pod));
}
async cancelDevicePodJob(response, pod, route, actor) {
const jobId = decodeURIComponent(route.split("/")[1] ?? "");
const job = await this.store.getDevicePodJob(pod.id, jobId);
if (!job) return sendJson(response, 404, errorPayload("device_job_not_found", `Device job ${jobId} was not found`, 404));
if (job.ownerUserId !== actor.id && actor.role !== "admin") return sendJson(response, 403, errorPayload("device_job_owner_required", "Only the owner or admin can cancel the job", 403));
if (this.devicePodExecutorUrl && !terminalJobStatus(job.status)) {
const canceledByExecutor = await this.cancelDevicePodExecutorJob({ job, pod });
return sendJson(response, 200, this.jobPayload(canceledByExecutor, pod));
}
const canceled = await this.store.updateDevicePodJob(pod.id, jobId, {
status: terminalJobStatus(job.status) ? job.status : "canceled",
blocker: terminalJobStatus(job.status) ? job.blocker : { code: "device_job_canceled", summary: "Device job was canceled by user" },
completedAt: terminalJobStatus(job.status) ? job.completedAt : this.now(),
updatedAt: this.now()
});
return sendJson(response, 200, this.jobPayload(canceled, pod));
}
async refreshDevicePodExecutorJob({ job, pod, output = false }) {
const suffix = output ? "/output" : "";
const target = `${this.devicePodExecutorUrl}/v1/device-pods/${encodeURIComponent(pod.id)}/jobs/${encodeURIComponent(job.id)}${suffix}`;
try {
const response = await fetchJsonWithTimeout(this.fetchImpl, target, {
method: "GET",
headers: { accept: "application/json", ...devicePodInternalHeaders(CLOUD_API_SERVICE_ID, this.devicePodInternalToken) }
}, this.devicePodExecutorTimeoutMs);
return await this.updateDevicePodJobFromExecutorResponse({ job, pod, response });
} catch (error) {
return await this.blockDevicePodJobOnExecutorError({ job, pod, error });
}
}
async cancelDevicePodExecutorJob({ job, pod }) {
const target = `${this.devicePodExecutorUrl}/v1/device-pods/${encodeURIComponent(pod.id)}/jobs/${encodeURIComponent(job.id)}/cancel`;
try {
const response = await fetchJsonWithTimeout(this.fetchImpl, target, {
method: "POST",
headers: { accept: "application/json", ...devicePodInternalHeaders(CLOUD_API_SERVICE_ID, this.devicePodInternalToken) }
}, this.devicePodExecutorTimeoutMs);
return await this.updateDevicePodJobFromExecutorResponse({ job, pod, response });
} catch (error) {
return await this.blockDevicePodJobOnExecutorError({ job, pod, error });
}
}
async updateDevicePodJobFromExecutorResponse({ job, pod, response }) {
const status = normalizeDeviceJobStatus(response.body?.status ?? response.body?.job?.status, response.status);
const blocker = response.body?.blocker ?? (status === "completed" || status === "running" || status === "queued" ? null : devicePodExecutorBlocker(`device-pod executor returned HTTP ${response.status}`));
const updated = await this.store.updateDevicePodJob(pod.id, job.id, {
status,
output: executorOutputPayload(response.body, response.status),
blocker,
completedAt: terminalJobStatus(status) ? job.completedAt ?? this.now() : null,
updatedAt: this.now()
});
return updated ?? job;
}
async blockDevicePodJobOnExecutorError({ job, pod, error }) {
const blocker = devicePodExecutorBlocker(error?.message ?? "device-pod executor request failed");
const updated = await this.store.updateDevicePodJob(pod.id, job.id, {
status: "blocked",
output: { error: blocker.summary },
blocker,
completedAt: this.now(),
updatedAt: this.now()
});
return updated ?? job;
}
async blockDevicePodJobWithoutExecutor({ job, pod }) {
const blocker = devicePodExecutorBlocker("HWLAB_DEVICE_POD_URL is not configured; cloud-api will not bypass hwlab-device-pod executor");
const updated = await this.store.updateDevicePodJob(pod.id, job.id, {
status: "blocked",
output: { error: blocker.summary },
blocker,
completedAt: this.now(),
updatedAt: this.now()
});
return { job: updated ?? job, httpStatus: 409 };
}
jobPayload(job, pod) {
const blocker = normalizeBlocker(job.blocker);
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: DEVICE_JOB_CONTRACT_VERSION,
accepted: !["blocked", "failed"].includes(job.status),
status: job.status,
devicePodId: pod.id,
targetId: targetIdFromProfile(pod.profile),
profileHash: pod.profileHash,
traceId: job.traceId,
operationId: job.operationId,
job: publicJob(job),
blocker,
freshness: freshness(job.updatedAt, blocker),
outputUrl: `/v1/device-pods/${encodeURIComponent(pod.id)}/jobs/${encodeURIComponent(job.id)}/output`,
cancelUrl: `/v1/device-pods/${encodeURIComponent(pod.id)}/jobs/${encodeURIComponent(job.id)}/cancel`
};
}
jobOutputPayload(job, pod, { maxBytes = DEVICE_JOB_OUTPUT_MAX_BYTES } = {}) {
const bounded = boundedOutput(job.output, maxBytes);
return {
...this.jobPayload(job, pod),
output: bounded.output,
text: bounded.text,
bytes: bounded.bytes,
truncation: bounded.truncation
};
}
}
class MemoryAccessStore {
constructor({ now = () => new Date().toISOString() } = {}) {
this.now = now;
this.users = new Map();
this.sessions = new Map();
this.devicePods = new Map();
this.grants = new Set();
this.jobs = new Map();
this.agentSessions = new Map();
this.workspaces = new Map();
}
async countUsers() { return this.users.size; }
async getUserById(id) { return this.users.get(id) ?? null; }
async findUserByUsername(username) { return [...this.users.values()].find((user) => user.username === username) ?? null; }
async syncBootstrapAdminPassword(input) {
const existing = await this.findUserByUsername(input.username) ?? await this.getUserById(input.id);
if (!existing || existing.role !== "admin") return null;
const user = { ...existing, passwordHash: input.passwordHash, updatedAt: input.now ?? this.now() };
this.users.set(user.id, user);
return user;
}
async createUser(input) {
const now = input.now ?? this.now();
const existing = await this.findUserByUsername(input.username);
const user = { id: input.id ?? existing?.id ?? `usr_${randomUUID()}`, username: input.username, displayName: input.displayName ?? existing?.displayName ?? "", role: input.role, status: input.status ?? "active", passwordHash: input.passwordHash ?? existing?.passwordHash ?? null, createdAt: existing?.createdAt ?? now, updatedAt: now };
this.users.set(user.id, user);
return user;
}
async createFirstAdmin(input) {
if (this.users.size > 0) return null;
return this.createUser({ ...input, role: "admin", status: "active" });
}
async createSession(input) {
const session = { id: `uss_${randomUUID()}`, userId: input.userId, tokenHash: input.tokenHash, createdAt: input.now, lastSeenAt: input.now, expiresAt: input.expiresAt, revokedAt: null };
this.sessions.set(session.id, session);
return session;
}
async findSessionByTokenHash(tokenHash, now) {
const session = [...this.sessions.values()].find((item) => item.tokenHash === tokenHash && !item.revokedAt && item.expiresAt > now) ?? null;
return session ? { ...session, user: this.users.get(session.userId) ?? null } : null;
}
async touchSession(id, now) { const session = this.sessions.get(id); if (session) session.lastSeenAt = now; }
async revokeSessionByTokenHash(tokenHash, now) { for (const session of this.sessions.values()) if (session.tokenHash === tokenHash) session.revokedAt = now; }
async upsertDevicePod(input) {
const existing = this.devicePods.get(input.id);
const profile = devicePodProfileForAuthority(input.id, normalizeObject(input.profile));
const now = input.now ?? this.now();
const pod = { id: input.id, name: input.name ?? existing?.name ?? input.id, status: input.status ?? existing?.status ?? "active", profile, profileHash: profileHash(profile), createdAt: existing?.createdAt ?? now, updatedAt: now };
this.devicePods.set(pod.id, pod);
return pod;
}
async grantDevicePod(input) { const grant = { devicePodId: input.devicePodId, userId: input.userId, createdByAdminId: input.createdByAdminId, createdAt: input.now ?? this.now() }; this.grants.add(grantKey(grant.devicePodId, grant.userId)); return grant; }
async revokeDevicePodGrant(input) { this.grants.delete(grantKey(input.devicePodId, input.userId)); }
async getDevicePod(id) { return this.devicePods.get(id) ?? null; }
async listVisibleDevicePods(actor) { return [...this.devicePods.values()].filter((pod) => pod.status === "active" && (actor.role === "admin" || this.grants.has(grantKey(pod.id, actor.id)))); }
async getVisibleDevicePod(actor, id) { return (await this.listVisibleDevicePods(actor)).find((pod) => pod.id === id) ?? null; }
async createDevicePodJob(input) { const job = normalizeJob(input); this.jobs.set(job.id, job); return job; }
async updateDevicePodJob(devicePodId, jobId, patch) { const job = this.jobs.get(jobId); if (!job || job.devicePodId !== devicePodId) return null; const next = { ...job, ...patch, updatedAt: patch.updatedAt ?? this.now() }; this.jobs.set(jobId, next); return next; }
async getDevicePodJob(devicePodId, jobId) { const job = this.jobs.get(jobId); return job?.devicePodId === devicePodId ? job : null; }
async listDevicePodJobs(devicePodId, limit) { return [...this.jobs.values()].filter((job) => job.devicePodId === devicePodId).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, limit); }
async recordAgentSessionOwner(input) {
const now = input.now ?? this.now();
const existing = this.agentSessions.get(input.sessionId) ?? null;
const session = normalizeAgentSessionOwnerRecord(input, existing, now);
this.agentSessions.set(session.id, session);
return session;
}
async getAgentSession(id) { return this.agentSessions.get(id) ?? null; }
async getAgentSessionByTraceId(traceId) {
return [...this.agentSessions.values()]
.filter((session) => session.lastTraceId === traceId)
.sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")))[0] ?? null;
}
async listAgentSessionsForUser(input = {}) {
const ownerUserId = textOr(input.ownerUserId, "");
const role = textOr(input.actorRole, "user");
const projectId = textOr(input.projectId, "");
const conversationId = textOr(input.conversationId, "");
const limit = boundedListLimit(input.limit);
return [...this.agentSessions.values()]
.filter((session) => input.ownerScoped === true || role !== "admin" ? session.ownerUserId === ownerUserId : true)
.filter((session) => !projectId || session.projectId === projectId)
.filter((session) => !conversationId || session.conversationId === conversationId)
.filter((session) => input.includeArchived === true || session.status !== "archived")
.sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")))
.slice(0, limit);
}
async archiveAgentConversation(input = {}) {
const ownerUserId = textOr(input.ownerUserId, "");
const role = textOr(input.actorRole, "user");
const conversationId = textOr(input.conversationId, "");
const projectId = textOr(input.projectId, "");
const now = input.now ?? this.now();
let count = 0;
for (const [id, session] of this.agentSessions.entries()) {
if ((input.ownerScoped === true || role !== "admin") && session.ownerUserId !== ownerUserId) continue;
if (conversationId && session.conversationId !== conversationId) continue;
if (projectId && session.projectId !== projectId) continue;
this.agentSessions.set(id, { ...session, status: "archived", endedAt: session.endedAt ?? now, updatedAt: now });
count += 1;
}
return { count };
}
async getOrCreateDefaultWorkspace(input = {}) {
const ownerUserId = textOr(input.ownerUserId, "");
const projectId = textOr(input.projectId, "prj_device_pod_workbench");
const existing = [...this.workspaces.values()].find((workspace) => workspace.ownerUserId === ownerUserId && workspace.projectId === projectId && workspace.isDefault === true && workspace.status === "active") ?? null;
if (existing) return existing;
const now = input.now ?? this.now();
const workspace = normalizeWorkspaceRecord({
id: defaultWorkspaceId(ownerUserId, projectId),
ownerUserId,
projectId,
now
}, null, now, { create: true });
this.workspaces.set(workspace.id, workspace);
return workspace;
}
async getWorkspaceForUser(input = {}) {
const workspace = this.workspaces.get(textOr(input.workspaceId, "")) ?? null;
if (!workspace) return null;
if (input.actorRole === "admin" || workspace.ownerUserId === input.ownerUserId) return workspace;
return null;
}
async updateWorkspace(input = {}) {
const current = await this.getWorkspaceForUser(input);
if (!current) return null;
const now = input.now ?? this.now();
const workspace = normalizeWorkspaceRecord(input, current, now, { replaceJson: input.replaceJson === true });
this.workspaces.set(workspace.id, workspace);
return workspace;
}
}
class PostgresAccessStore extends MemoryAccessStore {
constructor({ query, now } = {}) {
super({ now });
this.query = query;
this.schemaReady = false;
}
async ensureSchema() {
if (this.schemaReady) return;
for (const sql of ACCESS_SCHEMA_STATEMENTS) await this.query(sql, []);
this.schemaReady = true;
}
async countUsers() { await this.ensureSchema(); const result = await this.query("SELECT COUNT(*)::int AS count FROM users", []); return Number(result.rows?.[0]?.count ?? 0); }
async getUserById(id) { await this.ensureSchema(); const result = await this.query("SELECT id, username, display_name, role, status, password_hash, created_at, updated_at FROM users WHERE id = $1 LIMIT 1", [id]); return pgUser(result.rows?.[0]); }
async findUserByUsername(username) { await this.ensureSchema(); const result = await this.query("SELECT id, username, display_name, role, status, password_hash, created_at, updated_at FROM users WHERE username = $1 LIMIT 1", [username]); return pgUser(result.rows?.[0]); }
async syncBootstrapAdminPassword(input) {
await this.ensureSchema();
const result = await this.query("UPDATE users SET password_hash = $3, updated_at = $4 WHERE role = 'admin' AND (username = $1 OR id = $2) RETURNING id, username, display_name, role, status, password_hash, created_at, updated_at", [input.username, input.id, input.passwordHash, input.now ?? this.now()]);
return pgUser(result.rows?.[0]);
}
async createUser(input) {
await this.ensureSchema();
const now = input.now ?? this.now();
const user = { id: input.id ?? `usr_${randomUUID()}`, username: input.username, displayName: input.displayName ?? "", role: input.role, status: input.status ?? "active", passwordHash: input.passwordHash ?? null, createdAt: now, updatedAt: now };
const result = await this.query("INSERT INTO users (id, username, display_name, role, status, password_hash, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT (username) DO UPDATE SET display_name = EXCLUDED.display_name, role = EXCLUDED.role, status = EXCLUDED.status, password_hash = EXCLUDED.password_hash, updated_at = EXCLUDED.updated_at RETURNING id, username, display_name, role, status, password_hash, created_at, updated_at", [user.id, user.username, user.displayName, user.role, user.status, user.passwordHash, user.createdAt, user.updatedAt]);
return pgUser(result.rows?.[0]);
}
async createFirstAdmin(input) {
await this.ensureSchema();
const now = input.now ?? this.now();
const user = { id: input.id, username: input.username, displayName: input.displayName ?? "", role: "admin", status: "active", passwordHash: input.passwordHash ?? null, createdAt: now, updatedAt: now };
const result = await this.query("INSERT INTO users (id, username, display_name, role, status, password_hash, created_at, updated_at) SELECT $1,$2,$3,$4,$5,$6,$7,$8 WHERE NOT EXISTS (SELECT 1 FROM users) ON CONFLICT DO NOTHING RETURNING id, username, display_name, role, status, password_hash, created_at, updated_at", [user.id, user.username, user.displayName, user.role, user.status, user.passwordHash, user.createdAt, user.updatedAt]);
return pgUser(result.rows?.[0]);
}
async createSession(input) { await this.ensureSchema(); const session = await super.createSession(input); await this.query("INSERT INTO user_sessions (id, user_id, session_token_hash, created_at, last_seen_at, expires_at, revoked_at) VALUES ($1,$2,$3,$4,$5,$6,$7)", [session.id, session.userId, session.tokenHash, session.createdAt, session.lastSeenAt, session.expiresAt, session.revokedAt]); return session; }
async findSessionByTokenHash(tokenHash, now) { await this.ensureSchema(); const result = await this.query("SELECT s.id, s.user_id, s.session_token_hash, s.created_at, s.last_seen_at, s.expires_at, s.revoked_at, u.username, u.display_name, u.role, u.status, u.password_hash, u.created_at AS user_created_at, u.updated_at AS user_updated_at FROM user_sessions s JOIN users u ON u.id = s.user_id WHERE s.session_token_hash = $1 AND s.revoked_at IS NULL AND s.expires_at > $2 LIMIT 1", [tokenHash, now]); const row = result.rows?.[0]; return row ? pgSession(row) : null; }
async touchSession(id, now) { await this.ensureSchema(); await this.query("UPDATE user_sessions SET last_seen_at = $2 WHERE id = $1", [id, now]); }
async revokeSessionByTokenHash(tokenHash, now) { await this.ensureSchema(); await this.query("UPDATE user_sessions SET revoked_at = $2 WHERE session_token_hash = $1", [tokenHash, now]); }
async upsertDevicePod(input) { await this.ensureSchema(); const pod = await super.upsertDevicePod(input); const result = await this.query("INSERT INTO device_pods (id, name, status, profile_json, profile_hash, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, status = EXCLUDED.status, profile_json = EXCLUDED.profile_json, profile_hash = EXCLUDED.profile_hash, updated_at = EXCLUDED.updated_at RETURNING *", [pod.id, pod.name, pod.status, stableJson(pod.profile), pod.profileHash, pod.createdAt, pod.updatedAt]); return pgDevicePod(result.rows?.[0]); }
async grantDevicePod(input) { await this.ensureSchema(); const grant = await super.grantDevicePod(input); await this.query("INSERT INTO device_pod_grants (device_pod_id, user_id, created_by_admin_id, created_at) VALUES ($1,$2,$3,$4) ON CONFLICT (device_pod_id, user_id) DO UPDATE SET created_by_admin_id = EXCLUDED.created_by_admin_id, created_at = EXCLUDED.created_at", [grant.devicePodId, grant.userId, grant.createdByAdminId, grant.createdAt]); return grant; }
async revokeDevicePodGrant(input) { await this.ensureSchema(); await this.query("DELETE FROM device_pod_grants WHERE device_pod_id = $1 AND user_id = $2", [input.devicePodId, input.userId]); }
async getDevicePod(id) { await this.ensureSchema(); const result = await this.query("SELECT * FROM device_pods WHERE id = $1 LIMIT 1", [id]); return pgDevicePod(result.rows?.[0]); }
async listVisibleDevicePods(actor) { await this.ensureSchema(); const sql = actor.role === "admin" ? "SELECT * FROM device_pods WHERE status = 'active' ORDER BY id" : "SELECT p.* FROM device_pods p JOIN device_pod_grants g ON g.device_pod_id = p.id WHERE p.status = 'active' AND g.user_id = $1 ORDER BY p.id"; const result = await this.query(sql, actor.role === "admin" ? [] : [actor.id]); return result.rows.map(pgDevicePod); }
async createDevicePodJob(input) { await this.ensureSchema(); const job = normalizeJob(input); await this.query("INSERT INTO device_pod_jobs (id, device_pod_id, owner_user_id, status, intent, args_json, reason, trace_id, operation_id, output_json, blocker_json, created_at, updated_at, completed_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)", jobParams(job)); return job; }
async updateDevicePodJob(devicePodId, jobId, patch) { await this.ensureSchema(); const current = await this.getDevicePodJob(devicePodId, jobId); if (!current) return null; const next = { ...current, ...patch, updatedAt: patch.updatedAt ?? this.now() }; await this.query("UPDATE device_pod_jobs SET status=$3, output_json=$4, blocker_json=$5, updated_at=$6, completed_at=$7 WHERE device_pod_id=$1 AND id=$2", [devicePodId, jobId, next.status, stableJson(next.output ?? {}), stableJson(next.blocker ?? {}), next.updatedAt, next.completedAt]); return next; }
async getDevicePodJob(devicePodId, jobId) { await this.ensureSchema(); const result = await this.query("SELECT * FROM device_pod_jobs WHERE device_pod_id = $1 AND id = $2 LIMIT 1", [devicePodId, jobId]); return pgJob(result.rows?.[0]); }
async listDevicePodJobs(devicePodId, limit) { await this.ensureSchema(); const result = await this.query("SELECT * FROM device_pod_jobs WHERE device_pod_id = $1 ORDER BY updated_at DESC LIMIT $2", [devicePodId, limit]); return result.rows.map(pgJob); }
async recordAgentSessionOwner(input) {
await this.ensureSchema();
const now = input.now ?? this.now();
const existing = input.sessionId ? await this.getAgentSession(input.sessionId) : null;
const session = normalizeAgentSessionOwnerRecord(input, existing, now);
const result = await this.query("INSERT INTO agent_sessions (id, project_id, agent_id, status, started_at, ended_at, owner_user_id, conversation_id, thread_id, last_trace_id, session_json, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT (id) DO UPDATE SET project_id = COALESCE(EXCLUDED.project_id, agent_sessions.project_id), agent_id = EXCLUDED.agent_id, status = EXCLUDED.status, ended_at = EXCLUDED.ended_at, owner_user_id = EXCLUDED.owner_user_id, conversation_id = COALESCE(EXCLUDED.conversation_id, agent_sessions.conversation_id), thread_id = COALESCE(EXCLUDED.thread_id, agent_sessions.thread_id), last_trace_id = COALESCE(EXCLUDED.last_trace_id, agent_sessions.last_trace_id), session_json = EXCLUDED.session_json, updated_at = EXCLUDED.updated_at RETURNING id, project_id, agent_id, status, started_at, ended_at, owner_user_id, conversation_id, thread_id, last_trace_id, session_json, updated_at", [session.id, session.projectId, session.agentId, session.status, session.startedAt, session.endedAt, session.ownerUserId, session.conversationId, session.threadId, session.lastTraceId, stableJson(session.session ?? {}), session.updatedAt]);
return pgAgentSession(result.rows?.[0]);
}
async getAgentSession(id) { await this.ensureSchema(); const result = await this.query("SELECT id, project_id, agent_id, status, started_at, ended_at, owner_user_id, conversation_id, thread_id, last_trace_id, session_json, updated_at FROM agent_sessions WHERE id = $1 LIMIT 1", [id]); return pgAgentSession(result.rows?.[0]); }
async getAgentSessionByTraceId(traceId) { await this.ensureSchema(); const result = await this.query("SELECT id, project_id, agent_id, status, started_at, ended_at, owner_user_id, conversation_id, thread_id, last_trace_id, session_json, updated_at FROM agent_sessions WHERE last_trace_id = $1 ORDER BY updated_at DESC NULLS LAST LIMIT 1", [traceId]); return pgAgentSession(result.rows?.[0]); }
async listAgentSessionsForUser(input = {}) {
await this.ensureSchema();
const role = textOr(input.actorRole, "user");
const ownerUserId = textOr(input.ownerUserId, "");
const projectId = textOr(input.projectId, "");
const conversationId = textOr(input.conversationId, "");
const limit = boundedListLimit(input.limit);
const clauses = [];
const params = [];
if (input.ownerScoped === true || role !== "admin") {
params.push(ownerUserId);
clauses.push(`owner_user_id = $${params.length}`);
}
if (projectId) {
params.push(projectId);
clauses.push(`project_id = $${params.length}`);
}
if (conversationId) {
params.push(conversationId);
clauses.push(`conversation_id = $${params.length}`);
}
if (input.includeArchived !== true) clauses.push("status <> 'archived'");
params.push(limit);
const sql = `SELECT id, project_id, agent_id, status, started_at, ended_at, owner_user_id, conversation_id, thread_id, last_trace_id, session_json, updated_at FROM agent_sessions ${clauses.length ? `WHERE ${clauses.join(" AND ")}` : ""} ORDER BY updated_at DESC NULLS LAST LIMIT $${params.length}`;
const result = await this.query(sql, params);
return result.rows.map(pgAgentSession);
}
async archiveAgentConversation(input = {}) {
await this.ensureSchema();
const role = textOr(input.actorRole, "user");
const ownerUserId = textOr(input.ownerUserId, "");
const conversationId = textOr(input.conversationId, "");
const projectId = textOr(input.projectId, "");
const now = input.now ?? this.now();
const clauses = ["conversation_id = $1"];
const params = [conversationId];
if (input.ownerScoped === true || role !== "admin") {
params.push(ownerUserId);
clauses.push(`owner_user_id = $${params.length}`);
}
if (projectId) {
params.push(projectId);
clauses.push(`project_id = $${params.length}`);
}
params.push(now);
const sql = `UPDATE agent_sessions SET status = 'archived', ended_at = COALESCE(ended_at, $${params.length}), updated_at = $${params.length} WHERE ${clauses.join(" AND ")} RETURNING id`;
const result = await this.query(sql, params);
return { count: result.rows?.length ?? 0 };
}
async getOrCreateDefaultWorkspace(input = {}) {
await this.ensureSchema();
const ownerUserId = textOr(input.ownerUserId, "");
const projectId = textOr(input.projectId, "prj_device_pod_workbench");
const found = await this.query("SELECT * FROM account_workspaces WHERE owner_user_id = $1 AND project_id = $2 AND is_default = TRUE AND status = 'active' ORDER BY updated_at DESC LIMIT 1", [ownerUserId, projectId]);
const existing = pgWorkspace(found.rows?.[0]);
if (existing) return existing;
const now = input.now ?? this.now();
const workspace = normalizeWorkspaceRecord({ id: defaultWorkspaceId(ownerUserId, projectId), ownerUserId, projectId, now }, null, now, { create: true });
const result = await this.query("INSERT INTO account_workspaces (id, owner_user_id, project_id, name, status, is_default, selected_conversation_id, selected_agent_session_id, selected_device_pod_id, active_trace_id, provider_profile, workspace_json, revision, updated_by_session_id, updated_by_client, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17) ON CONFLICT (id) DO UPDATE SET updated_at = account_workspaces.updated_at RETURNING *", workspaceParams(workspace));
return pgWorkspace(result.rows?.[0]) ?? workspace;
}
async getWorkspaceForUser(input = {}) {
await this.ensureSchema();
const workspaceId = textOr(input.workspaceId, "");
const params = [workspaceId];
let sql = "SELECT * FROM account_workspaces WHERE id = $1";
if (input.actorRole !== "admin") {
params.push(textOr(input.ownerUserId, ""));
sql += " AND owner_user_id = $2";
}
sql += " LIMIT 1";
const result = await this.query(sql, params);
return pgWorkspace(result.rows?.[0]);
}
async updateWorkspace(input = {}) {
await this.ensureSchema();
const current = await this.getWorkspaceForUser(input);
if (!current) return null;
const now = input.now ?? this.now();
const workspace = normalizeWorkspaceRecord(input, current, now, { replaceJson: input.replaceJson === true });
const result = await this.query("UPDATE account_workspaces SET project_id=$3, name=$4, status=$5, is_default=$6, selected_conversation_id=$7, selected_agent_session_id=$8, selected_device_pod_id=$9, active_trace_id=$10, provider_profile=$11, workspace_json=$12, revision=$13, updated_by_session_id=$14, updated_by_client=$15, created_at=$16, updated_at=$17 WHERE id=$1 AND ($18::text = 'admin' OR owner_user_id=$2) RETURNING *", [...workspaceParams(workspace), textOr(input.actorRole, "user")]);
return pgWorkspace(result.rows?.[0]);
}
}
function parseDevicePodPath(pathname) {
const prefix = "/v1/device-pods/";
if (!pathname.startsWith(prefix)) return null;
const [devicePodId, ...rest] = pathname.slice(prefix.length).split("/").map((part) => decodeURIComponent(part)).filter(Boolean);
return devicePodId ? { devicePodId, route: rest.join("/") } : null;
}
async function jsonBody(request) {
const text = await readBody(request);
try { return text ? JSON.parse(text) : {}; } catch (error) { throw Object.assign(new Error("Invalid JSON body"), { statusCode: 400, code: "parse_error", reason: error.message }); }
}
function requiredText(value, field) { const text = textOr(value, ""); if (!text) throw Object.assign(new Error(`${field} is required`), { statusCode: 400, code: "invalid_params" }); return text; }
function textOr(value, fallback) { const text = String(value ?? "").trim(); return text || fallback; }
function firstString(...values) { return values.find((value) => typeof value === "string") ?? ""; }
function normalizeObject(value) { return value && typeof value === "object" && !Array.isArray(value) ? { ...value } : {}; }
function assertDevicePodProfileSafe(profile, field = "profile") {
const findings = [];
collectDevicePodProfileSecretFindings(profile, field, findings);
if (findings.length > 0) {
const paths = findings.slice(0, 3).join(", ");
throw Object.assign(new Error(`${field} contains forbidden secret material at ${paths}`), { statusCode: 400, code: "device_pod_profile_secret_forbidden" });
}
return profile;
}
function devicePodProfileForAuthority(devicePodId, profile) { return { ...assertDevicePodProfileSafe(profile, "profile"), devicePodId }; }
function collectDevicePodProfileSecretFindings(value, path, findings) {
if (findings.length >= 5) return;
if (Array.isArray(value)) {
value.forEach((item, index) => collectDevicePodProfileSecretFindings(item, `${path}[${index}]`, findings));
return;
}
if (value && typeof value === "object") {
for (const [key, child] of Object.entries(value)) {
const childPath = `${path}.${key}`;
if (DEVICE_POD_PROFILE_SECRET_KEY_PATTERN.test(key)) findings.push(childPath);
collectDevicePodProfileSecretFindings(child, childPath, findings);
if (findings.length >= 5) return;
}
return;
}
if (typeof value === "string" && DEVICE_POD_PROFILE_SECRET_VALUE_PATTERN.test(value)) findings.push(path);
}
function firstAdminDevicePodSeeds(body = {}) {
const hasSingle = Object.hasOwn(body, "devicePod");
const hasMany = Object.hasOwn(body, "devicePods");
if (hasSingle && hasMany) throw Object.assign(new Error("Use devicePod or devicePods, not both"), { statusCode: 400, code: "invalid_params" });
if (!hasSingle && !hasMany) return [];
const values = hasMany ? body.devicePods : [body.devicePod];
if (!Array.isArray(values)) throw Object.assign(new Error("devicePods must be an array"), { statusCode: 400, code: "invalid_params" });
return values.map((item, index) => firstAdminDevicePodSeed(item, hasSingle ? "devicePod" : `devicePods[${index}]`));
}
function firstAdminDevicePodSeed(value, field) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw Object.assign(new Error(`${field} must be an object`), { statusCode: 400, code: "invalid_params" });
const id = requiredText(value.devicePodId ?? value.id, `${field}.devicePodId`);
const profile = normalizeObject(value.profile ?? value.profileJson);
if (Object.keys(profile).length === 0) throw Object.assign(new Error(`${field}.profile is required`), { statusCode: 400, code: "invalid_params" });
assertDevicePodProfileSafe(profile, `${field}.profile`);
return {
id,
name: textOr(value.name, id),
status: value.status === "disabled" ? "disabled" : "active",
profile
};
}
function normalizeBaseUrl(value) { const text = textOr(value, "").replace(/\/+$/u, ""); return /^https?:\/\//u.test(text) ? text : ""; }
function boundedOutputMaxBytes(searchParams) { return Math.min(Math.max(Number.parseInt(searchParams.get("maxBytes") ?? "12000", 10) || 12000, 1), DEVICE_JOB_OUTPUT_MAX_BYTES); }
function boundedDurationMs(value, fallback = 1000) { const parsed = Number.parseInt(String(value ?? ""), 10); return Math.min(Math.max(Number.isInteger(parsed) && parsed > 0 ? parsed : fallback, 1), 60000); }
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 safeTraceIdLocal(value) { const text = textOr(value, ""); return /^trc_[A-Za-z0-9_.:-]+$/u.test(text) ? text : ""; }
function safeWorkspaceId(value) { return /^wsp_[A-Za-z0-9_.:-]+$/u.test(textOr(value, "")); }
function devicePodApiKeyFromRequest(request) { return textOr(getHeader(request, DEVICE_POD_API_KEY_HEADER), ""); }
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); }
function sha256(value) { return createHash("sha256").update(String(value)).digest("hex"); }
function constantSecretEquals(a, b) { const left = sha256(a); const right = sha256(b); return left === right; }
function stableJson(value) { return JSON.stringify(sortJson(value)); }
function sortJson(value) { if (Array.isArray(value)) return value.map(sortJson); if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])])); return value; }
function profileHash(profile) { return `sha256:${sha256(stableJson(profile))}`; }
function grantKey(devicePodId, userId) { return `${devicePodId}\u0000${userId}`; }
function authoritySource() { return { kind: "CLOUD_API_PROFILE_AUTHORITY", serviceId: CLOUD_API_SERVICE_ID, fake: false, devLiveEvidence: false, note: "Profile, grant, and job authority are owned by hwlab-cloud-api; this is not fake data." }; }
function accessRoutes() { return { session: "/auth/session", login: "/auth/login", logout: "/auth/logout", v1Session: "/v1/auth/session", currentUser: "/v1/users/me", accessStatus: "/v1/access/status", setupStatus: "/v1/setup/status", firstAdminSetup: "/v1/setup/first-admin", devicePods: "/v1/device-pods" }; }
function _deviceJobRequiresReason(intent, args, reason) {
if (!MUTATING_INTENTS.has(intent)) return false;
if (reason) return false;
if (!DEVICE_JOB_ACTIONABLE_INTENTS.has(intent)) return true;
const action = typeof args?.action === "string" ? args.action.trim().toLowerCase() : "";
if (action && DEVICE_JOB_READ_ONLY_SUB_ACTIONS.has(action)) return false;
return true;
}
function deviceJobBlocked(code, summary, retryable) { return { code, layer: "device-pod", retryable, summary, userMessage: summary }; }
function devicePodExecutorBlocker(summary) { return { code: "device_pod_executor_unavailable", layer: "device-pod", retryable: true, summary, userMessage: "Device Pod 已授权,但内部执行服务当前不可用。" }; }
function gatewayDispatchBlocker(summary) { return { code: "gateway_dispatch_unavailable", layer: "device-pod", retryable: true, summary, userMessage: "Device Pod 已授权,但当前没有可用 gateway/device-host-cli 执行通道。" }; }
function normalizeDeviceJobStatus(status, httpStatus) { const text = textOr(status, ""); return ["queued", "running", "completed", "failed", "blocked", "canceled"].includes(text) ? text : httpStatus >= 400 ? "blocked" : "running"; }
function executorOutputPayload(body, httpStatus) {
const output = normalizeObject(body?.output);
const nestedOutput = normalizeObject(output.output);
const text = firstString(
body?.text,
output.text,
nestedOutput.text,
output.summary,
nestedOutput.summary,
normalizeObject(nestedOutput.evidence).text,
normalizeObject(output.evidence).text,
normalizeObject(nestedOutput.evidence).logTail,
normalizeObject(output.evidence).logTail,
normalizeObject(nestedOutput.evidence).summary,
normalizeObject(output.evidence).summary
);
return {
executor: body ?? {},
output: Object.keys(nestedOutput).length > 0 ? nestedOutput : output,
text,
httpStatus
};
}
function boundedOutput(output, maxBytes = DEVICE_JOB_OUTPUT_MAX_BYTES) {
const source = normalizeObject(output);
const text = typeof source.text === "string" ? source.text : stableJson(source);
const buffer = Buffer.from(text, "utf8");
const clipped = buffer.length > maxBytes;
const boundedText = clipped ? buffer.subarray(0, maxBytes).toString("utf8") : text;
const bounded = { ...source, text: boundedText };
if (clipped) {
delete bounded.executor;
delete bounded.output;
bounded.omitted = { reason: "device_job_output_truncated", originalBytes: buffer.length };
}
return { output: bounded, text: boundedText, bytes: Math.min(buffer.length, maxBytes), truncation: { maxBytes, truncated: clipped, originalBytes: buffer.length } };
}
function freshness(observedAt, blocker) { return { observedAt, ageMs: 0, stale: Boolean(blocker), source: blocker ? "blocked" : "cloud-api" }; }
function publicActor(user) { return user ? { id: user.id, username: user.username, displayName: user.displayName, role: user.role, status: user.status } : null; }
function redactedUser(user) { return publicActor(user); }
function publicSession(session) { return { id: session.id, userId: session.userId, createdAt: session.createdAt, lastSeenAt: session.lastSeenAt, expiresAt: session.expiresAt, revoked: Boolean(session.revokedAt) }; }
function publicDevicePod(pod, { includeAdmin = false } = {}) { return { devicePodId: pod.id, name: pod.name, status: pod.status, targetId: targetIdFromProfile(pod.profile), profileHash: pod.profileHash, profile: redactedProfile(pod.profile), createdAt: pod.createdAt, updatedAt: pod.updatedAt, blocker: null, ...(includeAdmin ? { admin: { profileStored: true, routeStored: Boolean(pod.profile.route) } } : {}) }; }
function redactedProfile(profile = {}) { return { schemaVersion: profile.schemaVersion ?? null, target: { id: profile.target?.id ?? null }, projectWorkspace: { projectPath: profile.projectWorkspace?.projectPath ?? null, targetName: profile.projectWorkspace?.targetName ?? null, hexPath: profile.projectWorkspace?.hexPath ?? null }, debugInterface: { type: profile.debugInterface?.type ?? null }, ioInterface: { uartCount: Array.isArray(profile.ioInterface?.uart) ? profile.ioInterface.uart.length : 0 }, route: { configured: Boolean(profile.route?.gatewaySessionId), gatewaySessionId: "redacted", resourceId: profile.route?.resourceId ? "redacted" : null, capabilityId: profile.route?.capabilityId ? "redacted" : null } }; }
function targetIdFromProfile(profile = {}) { return profile.target?.id ?? profile.targetId ?? null; }
function terminalJobStatus(status) { return ["completed", "failed", "blocked", "canceled"].includes(status); }
function publicJob(job) { return { id: job.id, devicePodId: job.devicePodId, ownerUserId: job.ownerUserId, status: job.status, intent: job.intent, reason: job.reason, traceId: job.traceId, operationId: job.operationId, createdAt: job.createdAt, updatedAt: job.updatedAt, completedAt: job.completedAt }; }
function jobRefs(job) { return { jobId: job.id, traceId: job.traceId, operationId: job.operationId }; }
function formatEventLine(event) { return [event.ts?.slice(11, 19) ?? "00:00:00", event.scope?.toUpperCase() ?? "JOB", event.status, event.intent, event.summary, event.refs?.traceId ? `trace=${event.refs.traceId}` : null, event.blocker?.code ? `blocker=${event.blocker.code}` : null].filter(Boolean).join(" "); }
function normalizeJob(input) { return { id: input.id, devicePodId: input.devicePodId, ownerUserId: input.ownerUserId, status: input.status, intent: input.intent, args: normalizeObject(input.args), reason: input.reason ?? "", traceId: input.traceId, operationId: input.operationId, output: normalizeObject(input.output), blocker: input.blocker ?? null, createdAt: input.now, updatedAt: input.now, completedAt: input.completedAt ?? null }; }
function normalizeAgentSessionOwnerRecord(input, existing, now) { return { id: input.sessionId, projectId: input.projectId ?? existing?.projectId ?? "prj_v02_code_agent", agentId: input.agentId ?? existing?.agentId ?? "hwlab-code-agent", status: input.status ?? existing?.status ?? "active", startedAt: existing?.startedAt ?? input.startedAt ?? now, endedAt: input.endedAt ?? existing?.endedAt ?? null, ownerUserId: input.ownerUserId, conversationId: input.conversationId ?? existing?.conversationId ?? null, threadId: input.threadId ?? existing?.threadId ?? null, lastTraceId: input.traceId ?? input.lastTraceId ?? existing?.lastTraceId ?? null, session: mergeAgentSessionOwnerEvidence(input.session, existing?.session), updatedAt: now }; }
function mergeAgentSessionOwnerEvidence(nextValue, existingValue) {
const existing = normalizeObject(existingValue);
const next = normalizeObject(nextValue);
const merged = { ...existing, ...next };
if (!next.agentRun && existing.agentRun) merged.agentRun = existing.agentRun;
const nextCount = numberOrNull(next.messageCount);
const existingCount = numberOrNull(existing.messageCount);
if (nextCount !== null || existingCount !== null) {
merged.messageCount = Math.max(nextCount ?? 0, existingCount ?? 0);
}
if (next.firstUserMessagePreview && !existing.firstUserMessagePreview) {
merged.firstUserMessagePreview = next.firstUserMessagePreview;
}
return merged;
}
function agentSessionIdForRecord(sessionId, traceId) { const sessionText = textOr(sessionId, ""); if (/^ses_[A-Za-z0-9_.:-]+$/u.test(sessionText)) return sessionText; const traceText = textOr(traceId, ""); return /^trc_[A-Za-z0-9_.:-]+$/u.test(traceText) ? `ses_pending_${traceText.slice(4)}` : null; }
function safeConversationIdLocal(value) { return /^cnv_[A-Za-z0-9_.:-]+$/u.test(textOr(value, "")); }
function boundedListLimit(value) { const parsed = Number.parseInt(String(value ?? ""), 10); return Math.min(Math.max(Number.isInteger(parsed) && parsed > 0 ? parsed : 20, 1), 100); }
function defaultWorkspaceId(ownerUserId, projectId) { return `wsp_${sha256(`${ownerUserId}:${projectId}`).slice(0, 24)}`; }
function currentSessionIdFromAuth(auth) { return textOr(auth?.session?.id, null); }
function normalizeWorkspacePatch(body = {}, actor = null) {
const workspace = normalizeObject(body.workspace ?? body.workspaceJson ?? body.snapshot ?? body);
return {
...workspace,
selectedConversationId: textOr(body.selectedConversationId ?? body.conversationId ?? workspace.selectedConversationId, workspace.selectedConversationId ?? null),
selectedAgentSessionId: safeAgentSessionId(body.selectedAgentSessionId ?? body.sessionId ?? workspace.selectedAgentSessionId) || workspace.selectedAgentSessionId,
selectedDevicePodId: textOr(body.selectedDevicePodId ?? body.devicePodId ?? workspace.selectedDevicePodId, workspace.selectedDevicePodId ?? null),
activeTraceId: safeTraceIdLocal(body.activeTraceId ?? body.traceId ?? workspace.activeTraceId) || null,
providerProfile: textOr(body.providerProfile ?? workspace.providerProfile, workspace.providerProfile ?? null),
messages: Array.isArray(body.messages) ? body.messages.slice(-50).map(redactConversationMessage).filter(Boolean) : Array.isArray(workspace.messages) ? workspace.messages : undefined,
actor: actor ? publicActor(actor) : undefined,
secretMaterialStored: false,
valuesRedacted: true
};
}
function normalizeWorkspaceRecord(input = {}, existing = null, now = new Date().toISOString(), { create = false, replaceJson = false } = {}) {
const patch = normalizeObject(input.patch);
const workspaceJson = replaceJson ? patch : { ...normalizeObject(existing?.workspace), ...patch };
return {
id: textOr(input.workspaceId ?? input.id, existing?.id ?? (create ? defaultWorkspaceId(input.ownerUserId, input.projectId) : "")),
ownerUserId: textOr(input.ownerUserId, existing?.ownerUserId ?? ""),
projectId: textOr(input.projectId, existing?.projectId ?? "prj_device_pod_workbench"),
name: textOr(input.name, existing?.name ?? "Default Workbench"),
status: input.status === "archived" ? "archived" : existing?.status ?? "active",
isDefault: input.isDefault === false ? false : existing?.isDefault ?? true,
selectedConversationId: nullableText(input.selectedConversationId, existing?.selectedConversationId),
selectedAgentSessionId: nullableText(input.selectedAgentSessionId, existing?.selectedAgentSessionId),
selectedDevicePodId: nullableText(input.selectedDevicePodId, existing?.selectedDevicePodId),
activeTraceId: nullableText(input.activeTraceId, existing?.activeTraceId),
providerProfile: nullableText(input.providerProfile, existing?.providerProfile),
workspace: workspaceJson,
revision: (Number(existing?.revision) || 0) + (existing ? 1 : 1),
updatedBySessionId: nullableText(input.updatedBySessionId, existing?.updatedBySessionId),
updatedByClient: nullableText(input.updatedByClient, existing?.updatedByClient),
createdAt: existing?.createdAt ?? now,
updatedAt: now
};
}
function nullableText(value, fallback = null) {
if (value === null) return null;
const text = textOr(value, "");
return text || fallback || null;
}
function publicWorkbenchWorkspace(workspace, { conversation = null, selectedDevicePod = null } = {}) {
if (!workspace) return null;
return {
workspaceId: workspace.id,
ownerUserId: workspace.ownerUserId,
projectId: workspace.projectId,
name: workspace.name,
status: workspace.status,
isDefault: workspace.isDefault,
revision: workspace.revision,
selectedConversationId: workspace.selectedConversationId,
selectedAgentSessionId: workspace.selectedAgentSessionId,
selectedDevicePodId: workspace.selectedDevicePodId,
activeTraceId: workspace.activeTraceId,
providerProfile: workspace.providerProfile,
selectedConversation: conversation,
selectedDevicePod: selectedDevicePod ? publicDevicePod(selectedDevicePod) : null,
workspace: redactedWorkspaceJson(workspace.workspace),
updatedBySessionId: workspace.updatedBySessionId,
updatedByClient: workspace.updatedByClient,
createdAt: workspace.createdAt,
updatedAt: workspace.updatedAt,
valuesRedacted: true,
secretMaterialStored: false
};
}
function redactedWorkspaceJson(value = {}) {
const workspace = normalizeObject(value);
return pruneEmpty({
selectedConversationId: textOr(workspace.selectedConversationId, ""),
selectedAgentSessionId: textOr(workspace.selectedAgentSessionId, ""),
selectedDevicePodId: textOr(workspace.selectedDevicePodId, ""),
activeTraceId: textOr(workspace.activeTraceId, ""),
previousActiveTraceId: textOr(workspace.previousActiveTraceId, ""),
providerProfile: textOr(workspace.providerProfile, ""),
sessionStatus: textOr(workspace.sessionStatus, ""),
lastTraceId: textOr(workspace.lastTraceId, ""),
workspaceRevisionConflict: workspace.workspaceRevisionConflict === true,
expectedWorkspaceRevision: Number.isFinite(Number(workspace.expectedWorkspaceRevision)) ? Number(workspace.expectedWorkspaceRevision) : undefined,
staleContinuationCleared: workspace.staleContinuationCleared === true,
staleContinuationReason: textOr(workspace.staleContinuationReason, ""),
staleConversationId: textOr(workspace.staleConversationId, ""),
staleAgentSessionId: textOr(workspace.staleAgentSessionId, ""),
staleThreadId: textOr(workspace.staleThreadId, ""),
recoveryAction: textOr(workspace.recoveryAction, ""),
messages: Array.isArray(workspace.messages) ? workspace.messages.slice(-50).map(redactConversationMessage).filter(Boolean) : undefined,
actor: workspace.actor && typeof workspace.actor === "object" ? publicActor(workspace.actor) : undefined,
updatedAt: textOr(workspace.updatedAt, ""),
resetAt: textOr(workspace.resetAt, ""),
secretMaterialStored: false,
valuesRedacted: true
});
}
function workspaceParams(workspace) {
return [workspace.id, workspace.ownerUserId, workspace.projectId, workspace.name, workspace.status, workspace.isDefault, workspace.selectedConversationId, workspace.selectedAgentSessionId, workspace.selectedDevicePodId, workspace.activeTraceId, workspace.providerProfile, stableJson(workspace.workspace ?? {}), workspace.revision, workspace.updatedBySessionId, workspace.updatedByClient, workspace.createdAt, workspace.updatedAt];
}
function normalizeConversationSnapshot(body = {}, actor = null) {
const snapshot = normalizeObject(body.snapshot ?? body);
const messagesSource = Array.isArray(body.messages) ? body.messages : snapshot.messages;
const chatMessagesSource = Array.isArray(body.chatMessages) ? body.chatMessages : snapshot.chatMessages;
return {
...snapshot,
messages: Array.isArray(messagesSource) ? messagesSource.slice(-50).map(redactConversationMessage).filter(Boolean) : [],
chatMessages: Array.isArray(chatMessagesSource) ? chatMessagesSource.slice(-50).map(redactConversationMessage).filter(Boolean) : undefined,
messageCount: resolveSnapshotMessageCount(messagesSource, snapshot.messageCount),
firstUserMessagePreview: firstUserPreviewFromMessages(messagesSource) ?? snapshot.firstUserMessagePreview ?? null,
actor: actor ? publicActor(actor) : undefined,
secretMaterialStored: false,
valuesRedacted: true
};
}
function resolveSnapshotMessageCount(messagesSource, existingCount) {
const explicit = numberOrNull(existingCount);
const fromSource = Array.isArray(messagesSource) ? messagesSource.length : null;
if (explicit !== null && fromSource !== null) return Math.max(explicit, fromSource);
return explicit ?? fromSource ?? 0;
}
function firstUserPreviewFromMessages(messagesSource) {
if (!Array.isArray(messagesSource)) return null;
for (const message of messagesSource) {
if (!message || typeof message !== "object") continue;
if (String(message.role ?? "").toLowerCase() !== "user") continue;
const text = textOr(message.text ?? message.title ?? message.content, "");
if (text) return boundedText(text, 240);
}
return null;
}
function numberOrNull(value) {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
function isTerminalCodeAgentResult(result = {}) {
const status = textOr(result.status, "").toLowerCase();
const terminalStatus = textOr(result.agentRun?.terminalStatus, "").toLowerCase();
return [status, terminalStatus].some((value) => ["completed", "failed", "blocked", "canceled", "cancelled", "timeout", "error"].includes(value));
}
function isThreadResumeFailedResult(result = {}) {
const values = [
result.error?.code,
result.error?.category,
result.blocker?.code,
result.blocker?.category,
result.providerTrace?.failureKind,
result.agentRun?.providerTrace?.failureKind,
result.agentRun?.failureKind
].map((value) => textOr(value, "").toLowerCase().replace(/_/gu, "-"));
return values.some((value) => value === "thread-resume-failed")
|| /no rollout found for thread id|thread\/resume failed/iu.test(String(result.error?.message ?? result.blocker?.message ?? result.providerTrace?.failureMessage ?? result.agentRun?.providerTrace?.failureMessage ?? ""));
}
function threadResumeFailureThreadId(result = {}) {
return boundedText(textOr(
result.providerTrace?.threadId
?? result.agentRun?.providerTrace?.threadId
?? result.session?.threadId
?? result.sessionReuse?.threadId
?? result.threadId,
""
), 240) || null;
}
function terminalWorkbenchSessionStatus(result = {}) {
if (isThreadResumeFailedResult(result)) return "failed";
const sessionStatus = textOr(result.session?.status ?? result.sessionSummary?.status ?? result.sessionLifecycleStatus ?? result.runnerTrace?.sessionStatus, "").toLowerCase();
if (sessionStatus && sessionStatus !== "running" && sessionStatus !== "busy" && sessionStatus !== "pending") return sessionStatus === "cancelled" ? "canceled" : sessionStatus;
const status = textOr(result.status ?? result.agentRun?.terminalStatus, "").toLowerCase();
if (status === "completed") return "idle";
if (status === "cancelled") return "canceled";
return status || "idle";
}
function canActorReadWorkspaceTrace(result = {}, actor = null) {
const ownerUserId = textOr(result.ownerUserId, "");
if (!ownerUserId || actor?.role === "admin") return true;
return ownerUserId === actor?.id;
}
function conversationNeedsTerminalRepair(conversation, traceId) {
if (!conversation || !safeTraceIdLocal(traceId)) return false;
const status = textOr(conversation.status, "").toLowerCase();
if (["running", "busy", "pending", "active"].includes(status)) return true;
const messages = Array.isArray(conversation.messages) ? conversation.messages : [];
const traceMessage = messages.find((message) => message?.traceId === traceId && message?.role === "agent") ?? null;
if (!traceMessage) return true;
const messageStatus = textOr(traceMessage.status, "").toLowerCase();
return ["", "running", "busy", "pending", "active"].includes(messageStatus);
}
function mergeTerminalConversationMessages(existingMessages = [], result = {}, context = {}) {
const messages = Array.isArray(existingMessages) ? existingMessages.map(redactConversationMessage).filter(Boolean) : [];
const traceId = safeTraceIdLocal(result.traceId ?? context.traceId);
const terminalStatus = terminalWorkbenchSessionStatus(result);
const assistantText = textOr(result.reply?.content ?? result.assistantText ?? result.message?.content ?? result.error?.message ?? result.userMessage, "");
const now = textOr(context.now, new Date().toISOString());
const existingCount = numberOrNull(context.existingMessageCount) ?? messages.length;
const firstUserPreview = firstUserPreviewFromMessages(messages) ?? textOr(context.firstUserMessagePreview, null);
const next = messages.map((message) => {
if (traceId && message.traceId === traceId && message.role === "agent") {
return redactConversationMessage({
...message,
text: assistantText || message.text,
status: terminalStatus,
conversationId: context.conversationId ?? message.conversationId,
sessionId: context.sessionId ?? message.sessionId,
threadId: context.threadId ?? message.threadId,
updatedAt: now
});
}
return message;
}).filter(Boolean);
let addedAgentMessage = false;
if (traceId && !next.some((message) => message.traceId === traceId && message.role === "agent")) {
next.push(redactConversationMessage({
id: result.reply?.messageId ?? result.messageId ?? `msg_${traceId.slice(4)}`,
role: "agent",
title: "Code Agent result",
text: assistantText,
status: terminalStatus,
traceId,
conversationId: context.conversationId,
sessionId: context.sessionId,
threadId: context.threadId,
createdAt: result.reply?.createdAt ?? result.createdAt ?? now,
updatedAt: now
}));
addedAgentMessage = true;
}
return {
messages: next.slice(-50),
messageCount: existingCount + (addedAgentMessage ? 1 : 0),
firstUserMessagePreview: firstUserPreviewFromMessages(next) ?? firstUserPreview
};
}
function redactConversationMessage(message) {
if (!message || typeof message !== "object") return null;
const runnerTrace = message.runnerTrace && typeof message.runnerTrace === "object" ? message.runnerTrace : null;
return pruneEmpty({
id: textOr(message.id, ""),
role: textOr(message.role, ""),
title: textOr(message.title, ""),
text: boundedText(message.text, 12000),
status: textOr(message.status, ""),
traceId: textOr(message.traceId, ""),
conversationId: textOr(message.conversationId, ""),
sessionId: textOr(message.sessionId, ""),
threadId: textOr(message.threadId, ""),
retryOf: textOr(message.retryOf, ""),
createdAt: textOr(message.createdAt, ""),
updatedAt: textOr(message.updatedAt, ""),
runnerTrace: runnerTrace ? pruneEmpty({
traceId: runnerTrace.traceId,
status: runnerTrace.status,
sessionId: runnerTrace.sessionId,
threadId: runnerTrace.threadId,
eventCount: runnerTrace.eventCount,
eventsCompacted: runnerTrace.eventsCompacted === true,
fullTraceLoaded: runnerTrace.fullTraceLoaded === true,
lastEvent: runnerTrace.lastEvent
}) : undefined
});
}
function conversationsFromAgentSessions(sessions = []) {
const byConversation = new Map();
for (const session of sessions) {
const conversationId = textOr(session.conversationId, "");
if (!conversationId) continue;
const previous = byConversation.get(conversationId);
if (previous && String(previous.updatedAt ?? "") >= String(session.updatedAt ?? "")) continue;
byConversation.set(conversationId, publicAgentConversation(session));
}
return [...byConversation.values()].sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")));
}
function publicAgentConversation(session) {
const snapshot = normalizeObject(session.session);
const messages = Array.isArray(snapshot.messages) ? snapshot.messages : Array.isArray(snapshot.chatMessages) ? snapshot.chatMessages : [];
return {
conversationId: session.conversationId,
sessionId: session.id,
threadId: session.threadId,
status: session.status,
projectId: session.projectId,
agentId: session.agentId,
ownerUserId: session.ownerUserId,
lastTraceId: session.lastTraceId,
updatedAt: session.updatedAt,
startedAt: session.startedAt,
endedAt: session.endedAt,
session: pruneEmpty({ sessionId: session.id, threadId: session.threadId, status: session.status }),
messages,
messageCount: numberOrNull(snapshot.messageCount) ?? messages.length,
firstUserMessagePreview: textOr(snapshot.firstUserMessagePreview, null),
snapshot: pruneEmpty({
sessionStatus: snapshot.sessionStatus,
source: snapshot.source,
updatedAt: snapshot.updatedAt,
valuesRedacted: snapshot.valuesRedacted !== false
}),
valuesRedacted: true
};
}
function boundedText(value, maxBytes) {
const text = String(value ?? "");
const buffer = Buffer.from(text, "utf8");
return buffer.length > maxBytes ? buffer.subarray(0, maxBytes).toString("utf8") : text;
}
function pruneEmpty(value) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)); }
function sessionTokenFromRequest(request) { const auth = getHeader(request, "authorization"); if (/^Bearer\s+/iu.test(String(auth ?? ""))) return String(auth).replace(/^Bearer\s+/iu, "").trim(); const header = textOr(getHeader(request, "x-hwlab-session-token"), ""); if (header) return header; const cookie = parseCookie(getHeader(request, "cookie")); return cookie[SESSION_COOKIE] ?? ""; }
function parseCookie(value) { return Object.fromEntries(String(value ?? "").split(";").map((part) => part.trim()).filter(Boolean).map((part) => { const index = part.indexOf("="); return index > 0 ? [part.slice(0, index), decodeURIComponent(part.slice(index + 1))] : [part, ""]; })); }
function setSessionCookie(response, token, maxAge) { response.setHeader("set-cookie", `${SESSION_COOKIE}=${encodeURIComponent(token)}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${maxAge}`); }
function clearSessionCookie(response) { response.setHeader("set-cookie", `${SESSION_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`); }
function errorPayload(code, message, status) { return { ok: false, status, error: { code, message } }; }
function sendAccessError(response, error) { const status = error?.statusCode ?? 500; sendJson(response, status, errorPayload(error?.code ?? "access_control_error", error?.message ?? "Access control request failed", status)); }
function pgUser(row) { return row ? { id: row.id, username: row.username, displayName: row.display_name, role: row.role, status: row.status, passwordHash: row.password_hash, createdAt: row.created_at, updatedAt: row.updated_at } : null; }
function pgSession(row) { return { id: row.id, userId: row.user_id, tokenHash: row.session_token_hash, createdAt: row.created_at, lastSeenAt: row.last_seen_at, expiresAt: row.expires_at, revokedAt: row.revoked_at, user: { id: row.user_id, username: row.username, displayName: row.display_name, role: row.role, status: row.status, passwordHash: row.password_hash, createdAt: row.user_created_at, updatedAt: row.user_updated_at } }; }
function pgDevicePod(row) { return { id: row.id, name: row.name, status: row.status, profile: parseJson(row.profile_json, {}), profileHash: row.profile_hash, createdAt: row.created_at, updatedAt: row.updated_at }; }
function pgJob(row) { return row ? { id: row.id, devicePodId: row.device_pod_id, ownerUserId: row.owner_user_id, status: row.status, intent: row.intent, args: parseJson(row.args_json, {}), reason: row.reason, traceId: row.trace_id, operationId: row.operation_id, output: parseJson(row.output_json, {}), blocker: normalizeBlocker(parseJson(row.blocker_json, null)), createdAt: row.created_at, updatedAt: row.updated_at, completedAt: row.completed_at } : null; }
function pgAgentSession(row) { return row ? { id: row.id, projectId: row.project_id, agentId: row.agent_id, status: row.status, startedAt: row.started_at, endedAt: row.ended_at, ownerUserId: row.owner_user_id, conversationId: row.conversation_id, threadId: row.thread_id, lastTraceId: row.last_trace_id, session: parseJson(row.session_json, {}), updatedAt: row.updated_at } : null; }
function pgWorkspace(row) { return row ? { id: row.id, ownerUserId: row.owner_user_id, projectId: row.project_id, name: row.name, status: row.status, isDefault: row.is_default !== false, selectedConversationId: row.selected_conversation_id, selectedAgentSessionId: row.selected_agent_session_id, selectedDevicePodId: row.selected_device_pod_id, activeTraceId: row.active_trace_id, providerProfile: row.provider_profile, workspace: parseJson(row.workspace_json, {}), revision: Number(row.revision ?? 1), updatedBySessionId: row.updated_by_session_id, updatedByClient: row.updated_by_client, createdAt: row.created_at, updatedAt: row.updated_at } : null; }
function jobParams(job) { return [job.id, job.devicePodId, job.ownerUserId, job.status, job.intent, stableJson(job.args), job.reason, job.traceId, job.operationId, stableJson(job.output), stableJson(job.blocker ?? {}), job.createdAt, job.updatedAt, job.completedAt]; }
function parseJson(value, fallback) { if (!value) return fallback; if (typeof value === "object") return value; try { return JSON.parse(String(value)); } catch { return fallback; } }
function normalizeBlocker(value) { return value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0 ? value : null; }
async function fetchJsonWithTimeout(fetchImpl, url, options, timeoutMs) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetchImpl(url, { ...options, signal: controller.signal });
const text = await response.text();
return { status: response.status, body: parseJson(text, {}) };
} finally {
clearTimeout(timeout);
}
}