import { randomUUID } from "node:crypto"; import type { Pool } from "pg"; import { allowedAgentScopes, type AgentScope } from "../domain/scopes.js"; import { hashAgentSetupCode, hashAgentToken } from "../security/agent-access.js"; const allowedScopeSet = new Set(allowedAgentScopes); export type AgentStatus = "active" | "disabled" | "revoked"; export type AgentGrantMode = "voluntary" | "reporting"; export type AgentSetupCodeStatus = "active" | "used" | "expired" | "revoked"; export type AgentTokenStatus = "active" | "revoked" | "expired"; export type AgentTokenGrantScope = "agent" | "token"; export type AgentRecord = { id: string; ownerUserId: string; ownerEmail: string | null; displayName: string; avatarUrl: string | null; status: AgentStatus; createdAt: string; updatedAt: string; }; export type AgentGrantRecord = { id: string; agentId: string; workspaceSlug: string; projectId: string | null; scopes: AgentScope[]; mode: AgentGrantMode; createdByUserId: string; createdAt: string; updatedAt: string; }; export type AgentTokenRecord = { id: string; agentId: string; name: string; status: AgentTokenStatus; grantScope: AgentTokenGrantScope; tokenSuffix: string | null; expiresAt: string | null; lastUsedAt: string | null; createdAt: string; }; export type AgentSessionRecord = { agent: AgentRecord; token: AgentTokenRecord; grants: AgentGrantRecord[]; grantSource: AgentTokenGrantScope; }; export type AgentSetupCodeRecord = { id: string; agentId: string; codeSuffix: string | null; status: AgentSetupCodeStatus; tokenName: string; createdByUserId: string | null; expiresAt: string; usedAt: string | null; usedTokenId: string | null; createdAt: string; }; type AgentRow = { id: string; owner_user_id: string; owner_email: string | null; display_name: string; avatar_url: string | null; status: AgentStatus; created_at: Date; updated_at: Date; }; type GrantRow = { id: string; agent_id: string; workspace_slug: string; project_id: string; scopes: AgentScope[]; mode: AgentGrantMode; created_by_user_id: string; created_at: Date; updated_at: Date; }; type TokenRow = { id: string; agent_id: string; name: string; status: AgentTokenStatus; grant_scope: AgentTokenGrantScope; token_suffix: string | null; expires_at: Date | null; last_used_at: Date | null; created_at: Date; }; type SetupCodeRow = { id: string; agent_id: string; code_hash: string; code_suffix: string | null; status: AgentSetupCodeStatus; token_name: string; created_by_user_id: string | null; expires_at: Date; used_at: Date | null; used_token_id: string | null; created_at: Date; }; type SessionRow = AgentRow & { token_id: string; token_name: string; token_status: AgentTokenStatus; token_grant_scope: AgentTokenGrantScope; token_suffix: string | null; token_expires_at: Date | null; token_last_used_at: Date | null; token_created_at: Date; }; type TokenGrantRow = { id: string; token_id: string; agent_id: string; workspace_slug: string; project_id: string; scopes: AgentScope[]; mode: AgentGrantMode; created_by_user_id: string; source_grant_id: string | null; created_at: Date; updated_at: Date; }; type IdempotencyRow = { key: string; agent_id: string; request_hash: string; response_body: unknown | null; status: "processing" | "completed"; created_at: Date; expires_at: Date; locked_until: Date | null; }; export type CreateAgentInput = { ownerUserId: string; ownerEmail?: string | null; displayName: string; avatarUrl?: string | null; }; export type UpdateAgentProfileInput = { displayName?: string; avatarUrl?: string | null; actorUserId?: string; }; export type UpsertGrantInput = { workspaceSlug: string; projectId?: string | null; scopes: AgentScope[]; mode: AgentGrantMode; createdByUserId: string; }; export type ReplaceWorkspaceGrantsInput = { workspaceSlug: string; projectIds: string[]; scopes: AgentScope[]; mode: AgentGrantMode; actorUserId: string; }; export type ReplaceProjectGrantTarget = { workspaceSlug: string; projectId: string; }; export type ReplaceProjectGrantsInput = { grants: ReplaceProjectGrantTarget[]; scopes: AgentScope[]; mode: AgentGrantMode; actorUserId: string; }; export type CreateTokenInput = { token: string; name: string; expiresAt?: string | null; tokenGrants?: CreateTokenGrantInput[]; }; export type CreateSetupCodeInput = { code: string; createdByUserId?: string | null; expiresAt: string; tokenName: string; }; export type CreateTokenGrantInput = { workspaceSlug: string; projectId?: string | null; scopes: AgentScope[]; mode: AgentGrantMode; createdByUserId: string; sourceGrantId?: string | null; }; export type IdempotencyClaim = | { status: "claimed"; } | { status: "replay"; responseBody: unknown; } | { status: "in_progress"; lockedUntil: string | null; } | { status: "conflict"; }; export type RedeemSetupCodeResult = | { status: "redeemed"; agent: AgentRecord; token: AgentTokenRecord; grants: AgentGrantRecord[]; setupCode: AgentSetupCodeRecord; } | { status: "invalid" | "expired" | "used" | "revoked"; }; export class AgentsRepository { constructor(private readonly pool: Pool) {} async createAgent(input: CreateAgentInput): Promise { const result = await this.pool.query( ` INSERT INTO agents(id, owner_user_id, owner_email, display_name, avatar_url) VALUES ($1, $2, $3, $4, $5) RETURNING * `, [randomUUID(), input.ownerUserId, input.ownerEmail ?? null, input.displayName, input.avatarUrl ?? null] ); return mapAgent(result.rows[0]); } async listAgents(ownerUserId?: string): Promise { const result = ownerUserId ? await this.pool.query("SELECT * FROM agents WHERE owner_user_id = $1 ORDER BY created_at DESC", [ownerUserId]) : await this.pool.query("SELECT * FROM agents ORDER BY created_at DESC"); return result.rows.map(mapAgent); } async listAgentsByOwnerIdentity(ownerUserId?: string, ownerEmail?: string): Promise { const normalizedOwnerUserId = normalizeNullableText(ownerUserId); const normalizedOwnerEmail = normalizeNullableText(ownerEmail)?.toLowerCase(); if (!normalizedOwnerUserId && !normalizedOwnerEmail) { return []; } const result = await this.pool.query( ` SELECT * FROM agents WHERE ($1::text IS NOT NULL AND owner_user_id = $1) OR ($2::text IS NOT NULL AND owner_email IS NOT NULL AND lower(owner_email) = $2) ORDER BY created_at DESC `, [normalizedOwnerUserId, normalizedOwnerEmail] ); return result.rows.map(mapAgent); } async getAgent(agentId: string): Promise { const result = await this.pool.query("SELECT * FROM agents WHERE id = $1", [agentId]); return result.rows[0] ? mapAgent(result.rows[0]) : null; } async updateAgentProfile(agentId: string, input: UpdateAgentProfileInput): Promise { const currentAgent = await this.getAgent(agentId); if (!currentAgent) { return null; } const displayName = input.displayName ?? currentAgent.displayName; const avatarUrl = input.avatarUrl === undefined ? currentAgent.avatarUrl : input.avatarUrl; const result = await this.pool.query( ` UPDATE agents SET display_name = $2, avatar_url = $3, updated_at = now() WHERE id = $1 RETURNING * `, [agentId, displayName, avatarUrl] ); await this.createAuditEvent(agentId, "agent.profile.updated", input.actorUserId, { displayName, avatarUrl, }); return mapAgent(result.rows[0]); } async revokeAgent(agentId: string, actorUserId?: string): Promise { const client = await this.pool.connect(); try { await client.query("BEGIN"); const result = await client.query( ` UPDATE agents SET status = 'revoked', updated_at = now() WHERE id = $1 RETURNING * `, [agentId] ); if (!result.rows[0]) { await client.query("ROLLBACK"); return null; } await client.query("UPDATE agent_tokens SET status = 'revoked' WHERE agent_id = $1 AND status = 'active'", [agentId]); await client.query( "INSERT INTO agent_audit_events(agent_id, event_type, actor_user_id) VALUES ($1, $2, $3)", [agentId, "agent.revoked", actorUserId ?? null] ); await client.query("COMMIT"); return mapAgent(result.rows[0]); } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } async upsertGrant(agentId: string, input: UpsertGrantInput): Promise { assertAllowedScopes(input.scopes); const projectId = input.projectId ?? ""; const result = await this.pool.query( ` INSERT INTO agent_grants(agent_id, workspace_slug, project_id, scopes, mode, created_by_user_id) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (agent_id, workspace_slug, project_id) DO UPDATE SET scopes = EXCLUDED.scopes, mode = EXCLUDED.mode, created_by_user_id = EXCLUDED.created_by_user_id, updated_at = now() RETURNING * `, [agentId, input.workspaceSlug, projectId, input.scopes, input.mode, input.createdByUserId] ); await this.createAuditEvent(agentId, "agent.grant.upserted", input.createdByUserId, { workspaceSlug: input.workspaceSlug, projectId: input.projectId ?? null, scopes: input.scopes, mode: input.mode, }); return mapGrant(result.rows[0]); } async listGrants(agentId: string): Promise { const result = await this.pool.query("SELECT * FROM agent_grants WHERE agent_id = $1 ORDER BY created_at DESC", [agentId]); return result.rows.map(mapGrant); } async replaceWorkspaceProjectGrants(agentId: string, input: ReplaceWorkspaceGrantsInput): Promise { assertAllowedScopes(input.scopes); const projectIds = [...new Set(input.projectIds.map((projectId) => projectId.trim()).filter(Boolean))]; const client = await this.pool.connect(); try { await client.query("BEGIN"); await client.query( ` DELETE FROM agent_grants WHERE agent_id = $1 AND workspace_slug = $2 AND NOT (project_id = ANY($3::text[])) `, [agentId, input.workspaceSlug, projectIds] ); const grants: AgentGrantRecord[] = []; for (const projectId of projectIds) { const result = await client.query( ` INSERT INTO agent_grants(agent_id, workspace_slug, project_id, scopes, mode, created_by_user_id) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (agent_id, workspace_slug, project_id) DO UPDATE SET scopes = EXCLUDED.scopes, mode = EXCLUDED.mode, created_by_user_id = EXCLUDED.created_by_user_id, updated_at = now() RETURNING * `, [agentId, input.workspaceSlug, projectId, input.scopes, input.mode, input.actorUserId] ); grants.push(mapGrant(result.rows[0])); } await client.query( ` INSERT INTO agent_audit_events(agent_id, event_type, actor_user_id, metadata) VALUES ($1, $2, $3, $4) `, [ agentId, "agent.grants.replaced", input.actorUserId, { workspaceSlug: input.workspaceSlug, projectIds, scopes: input.scopes, mode: input.mode, }, ] ); await client.query("COMMIT"); return grants; } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } async replaceProjectGrants(agentId: string, input: ReplaceProjectGrantsInput): Promise { assertAllowedScopes(input.scopes); const seenKeys = new Set(); const grantTargets = input.grants .map((grant) => ({ workspaceSlug: grant.workspaceSlug.trim(), projectId: grant.projectId.trim(), })) .filter((grant) => grant.workspaceSlug && grant.projectId) .filter((grant) => { const key = `${grant.workspaceSlug}:${grant.projectId}`; if (seenKeys.has(key)) { return false; } seenKeys.add(key); return true; }); if (grantTargets.length === 0) { throw new Error("At least one project grant is required."); } const client = await this.pool.connect(); try { await client.query("BEGIN"); await client.query("DELETE FROM agent_grants WHERE agent_id = $1 AND project_id <> ''", [agentId]); const grants: AgentGrantRecord[] = []; for (const grantTarget of grantTargets) { const result = await client.query( ` INSERT INTO agent_grants(agent_id, workspace_slug, project_id, scopes, mode, created_by_user_id) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (agent_id, workspace_slug, project_id) DO UPDATE SET scopes = EXCLUDED.scopes, mode = EXCLUDED.mode, created_by_user_id = EXCLUDED.created_by_user_id, updated_at = now() RETURNING * `, [agentId, grantTarget.workspaceSlug, grantTarget.projectId, input.scopes, input.mode, input.actorUserId] ); grants.push(mapGrant(result.rows[0])); } await client.query( ` INSERT INTO agent_audit_events(agent_id, event_type, actor_user_id, metadata) VALUES ($1, $2, $3, $4) `, [ agentId, "agent.project_grants.replaced", input.actorUserId, { grants: grantTargets, scopes: input.scopes, mode: input.mode, }, ] ); await client.query("COMMIT"); return grants; } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } async createToken(agentId: string, input: CreateTokenInput): Promise { const tokenGrants = input.tokenGrants ?? []; const grantScope: AgentTokenGrantScope = tokenGrants.length > 0 ? "token" : "agent"; const client = await this.pool.connect(); let tokenRecord: AgentTokenRecord; for (const grant of tokenGrants) { assertAllowedScopes(grant.scopes); } try { await client.query("BEGIN"); const result = await client.query( ` INSERT INTO agent_tokens(agent_id, token_hash, token_suffix, name, expires_at, grant_scope) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, agent_id, name, status, grant_scope, token_suffix, expires_at, last_used_at, created_at `, [agentId, hashAgentToken(input.token), input.token.slice(-8), input.name, input.expiresAt ?? null, grantScope] ); tokenRecord = mapToken(result.rows[0]); for (const grant of tokenGrants) { await client.query( ` INSERT INTO agent_token_grants(token_id, workspace_slug, project_id, scopes, mode, created_by_user_id, source_grant_id) VALUES ($1, $2, $3, $4, $5, $6, $7) `, [ tokenRecord.id, grant.workspaceSlug, grant.projectId ?? "", grant.scopes, grant.mode, grant.createdByUserId, grant.sourceGrantId ?? null, ] ); } await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } await this.createAuditEvent(agentId, "agent.token.created", undefined, { tokenId: tokenRecord.id, name: input.name, expiresAt: input.expiresAt ?? null, grantScope, tokenGrantCount: tokenGrants.length, }); return tokenRecord; } async createSetupCode(agentId: string, input: CreateSetupCodeInput): Promise { const result = await this.pool.query( ` INSERT INTO agent_setup_codes(agent_id, code_hash, code_suffix, token_name, created_by_user_id, expires_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING * `, [ agentId, hashAgentSetupCode(input.code), input.code.slice(-8), input.tokenName, input.createdByUserId ?? null, input.expiresAt, ] ); const setupCode = mapSetupCode(result.rows[0]); await this.createAuditEvent(agentId, "agent.setup_code.created", input.createdByUserId ?? undefined, { setupCodeId: setupCode.id, codeSuffix: setupCode.codeSuffix, expiresAt: setupCode.expiresAt, tokenName: setupCode.tokenName, }); return setupCode; } async redeemSetupCode(code: string, token: string): Promise { const client = await this.pool.connect(); try { await client.query("BEGIN"); const setupCodeResult = await client.query( ` SELECT * FROM agent_setup_codes WHERE code_hash = $1 FOR UPDATE `, [hashAgentSetupCode(code)] ); const setupCodeRow = setupCodeResult.rows[0]; if (!setupCodeRow) { await client.query("COMMIT"); return { status: "invalid" }; } if (setupCodeRow.status === "used" || setupCodeRow.used_at) { await client.query("COMMIT"); return { status: "used" }; } if (setupCodeRow.status === "revoked") { await client.query("COMMIT"); return { status: "revoked" }; } if (setupCodeRow.status !== "active" || setupCodeRow.expires_at <= new Date()) { await client.query( ` UPDATE agent_setup_codes SET status = 'expired' WHERE id = $1 AND status = 'active' `, [setupCodeRow.id] ); await client.query( ` INSERT INTO agent_audit_events(agent_id, event_type, actor_user_id, metadata) VALUES ($1, $2, $3, $4) `, [ setupCodeRow.agent_id, "agent.setup_code.expired", setupCodeRow.created_by_user_id, { setupCodeId: setupCodeRow.id, codeSuffix: setupCodeRow.code_suffix, }, ] ); await client.query("COMMIT"); return { status: "expired" }; } const agentResult = await client.query( ` SELECT * FROM agents WHERE id = $1 AND status = 'active' FOR UPDATE `, [setupCodeRow.agent_id] ); const agentRow = agentResult.rows[0]; if (!agentRow) { await client.query("COMMIT"); return { status: "revoked" }; } const tokenResult = await client.query( ` INSERT INTO agent_tokens(agent_id, token_hash, token_suffix, name, expires_at, grant_scope) VALUES ($1, $2, $3, $4, NULL, 'agent') RETURNING id, agent_id, name, status, grant_scope, token_suffix, expires_at, last_used_at, created_at `, [setupCodeRow.agent_id, hashAgentToken(token), token.slice(-8), setupCodeRow.token_name] ); const tokenRow = tokenResult.rows[0]; const redeemedSetupCodeResult = await client.query( ` UPDATE agent_setup_codes SET status = 'used', used_at = now(), used_token_id = $2 WHERE id = $1 RETURNING * `, [setupCodeRow.id, tokenRow.id] ); const grantResult = await client.query( "SELECT * FROM agent_grants WHERE agent_id = $1 ORDER BY created_at DESC", [setupCodeRow.agent_id] ); await client.query( ` INSERT INTO agent_audit_events(agent_id, event_type, actor_user_id, metadata) VALUES ($1, $2, $3, $4) `, [ setupCodeRow.agent_id, "agent.setup_code.redeemed", setupCodeRow.created_by_user_id, { setupCodeId: setupCodeRow.id, codeSuffix: setupCodeRow.code_suffix, tokenId: tokenRow.id, tokenName: tokenRow.name, }, ] ); await client.query("COMMIT"); return { status: "redeemed", agent: mapAgent(agentRow), token: mapToken(tokenRow), grants: grantResult.rows.map(mapGrant), setupCode: mapSetupCode(redeemedSetupCodeResult.rows[0]), }; } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } async listTokens(agentId: string): Promise { const result = await this.pool.query( ` SELECT id, agent_id, name, status, grant_scope, token_suffix, expires_at, last_used_at, created_at FROM agent_tokens WHERE agent_id = $1 ORDER BY created_at DESC `, [agentId] ); return result.rows.map(mapToken); } async revokeToken(agentId: string, tokenId: string, actorUserId?: string): Promise { const result = await this.pool.query( ` UPDATE agent_tokens SET status = 'revoked' WHERE agent_id = $1 AND id = $2 RETURNING id, agent_id, name, status, grant_scope, token_suffix, expires_at, last_used_at, created_at `, [agentId, tokenId] ); if (!result.rows[0]) { return null; } await this.createAuditEvent(agentId, "agent.token.revoked", actorUserId, { tokenId }); return mapToken(result.rows[0]); } async findActiveSessionByToken(token: string): Promise { const result = await this.pool.query( ` SELECT agents.id, agents.owner_user_id, agents.owner_email, agents.display_name, agents.avatar_url, agents.status, agents.created_at, agents.updated_at, agent_tokens.id AS token_id, agent_tokens.name AS token_name, agent_tokens.status AS token_status, agent_tokens.grant_scope AS token_grant_scope, agent_tokens.token_suffix AS token_suffix, agent_tokens.expires_at AS token_expires_at, agent_tokens.last_used_at AS token_last_used_at, agent_tokens.created_at AS token_created_at FROM agent_tokens JOIN agents ON agents.id = agent_tokens.agent_id WHERE agent_tokens.token_hash = $1 AND agent_tokens.status = 'active' AND agents.status = 'active' AND (agent_tokens.expires_at IS NULL OR agent_tokens.expires_at > now()) `, [hashAgentToken(token)] ); const row = result.rows[0]; if (!row) { return null; } const updatedToken = await this.pool.query( ` UPDATE agent_tokens SET last_used_at = now() WHERE id = $1 RETURNING id, agent_id, name, status, grant_scope, token_suffix, expires_at, last_used_at, created_at `, [row.token_id] ); return { agent: mapAgent(row), token: mapToken(updatedToken.rows[0]), grants: row.token_grant_scope === "token" ? await this.listTokenGrants(row.token_id) : await this.listGrants(row.id), grantSource: row.token_grant_scope, }; } async listTokenGrants(tokenId: string): Promise { const result = await this.pool.query( ` SELECT agent_token_grants.id, agent_token_grants.token_id, agent_tokens.agent_id, agent_token_grants.workspace_slug, agent_token_grants.project_id, ARRAY( SELECT token_scope FROM unnest(agent_token_grants.scopes) AS token_scope WHERE token_scope = ANY(COALESCE(agent_grants.scopes, agent_token_grants.scopes)) ORDER BY token_scope ) AS scopes, COALESCE(agent_grants.mode, agent_token_grants.mode) AS mode, agent_token_grants.created_by_user_id, agent_token_grants.source_grant_id, agent_token_grants.created_at, agent_token_grants.updated_at FROM agent_token_grants JOIN agent_tokens ON agent_tokens.id = agent_token_grants.token_id LEFT JOIN agent_grants ON agent_grants.id = agent_token_grants.source_grant_id WHERE agent_token_grants.token_id = $1 AND (agent_token_grants.source_grant_id IS NULL OR agent_grants.id IS NOT NULL) ORDER BY agent_token_grants.created_at DESC `, [tokenId] ); return result.rows.map(mapTokenGrant); } async createAuditEvent(agentId: string | null, eventType: string, actorUserId?: string, metadata: Record = {}): Promise { await this.pool.query( ` INSERT INTO agent_audit_events(agent_id, event_type, actor_user_id, metadata) VALUES ($1, $2, $3, $4) `, [agentId, eventType, actorUserId ?? null, metadata] ); } async claimIdempotencyKey(agentId: string, idempotencyKey: string, requestHash: string): Promise { const storageKey = buildIdempotencyStorageKey(agentId, idempotencyKey); const client = await this.pool.connect(); try { await client.query("BEGIN"); await client.query("DELETE FROM idempotency_keys WHERE key = $1 AND expires_at <= now()", [storageKey]); const inserted = await client.query( ` INSERT INTO idempotency_keys(key, agent_id, request_hash, response_body, status, expires_at, locked_until) VALUES ($1, $2, $3, NULL, 'processing', now() + interval '24 hours', now() + interval '5 minutes') ON CONFLICT (key) DO NOTHING RETURNING * `, [storageKey, agentId, requestHash] ); if (inserted.rows[0]) { await client.query("COMMIT"); return { status: "claimed" }; } const existing = await client.query("SELECT * FROM idempotency_keys WHERE key = $1 AND agent_id = $2 FOR UPDATE", [ storageKey, agentId, ]); const row = existing.rows[0]; if (!row) { await client.query("COMMIT"); return { status: "conflict" }; } if (row.request_hash !== requestHash) { await client.query("COMMIT"); return { status: "conflict" }; } if (row.status === "completed") { await client.query("COMMIT"); return { status: "replay", responseBody: row.response_body, }; } if (row.locked_until && row.locked_until > new Date()) { await client.query("COMMIT"); return { status: "in_progress", lockedUntil: row.locked_until.toISOString(), }; } await client.query( ` UPDATE idempotency_keys SET locked_until = now() + interval '5 minutes' WHERE key = $1 AND agent_id = $2 `, [storageKey, agentId] ); await client.query("COMMIT"); return { status: "claimed" }; } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } async completeIdempotencyKey(agentId: string, idempotencyKey: string, responseBody: unknown): Promise { await this.pool.query( ` UPDATE idempotency_keys SET response_body = $3, status = 'completed', locked_until = NULL WHERE key = $1 AND agent_id = $2 `, [buildIdempotencyStorageKey(agentId, idempotencyKey), agentId, responseBody] ); } async releaseIdempotencyKey(agentId: string, idempotencyKey: string): Promise { await this.pool.query("DELETE FROM idempotency_keys WHERE key = $1 AND agent_id = $2 AND status = 'processing'", [ buildIdempotencyStorageKey(agentId, idempotencyKey), agentId, ]); } } function buildIdempotencyStorageKey(agentId: string, idempotencyKey: string): string { return `${agentId}:${idempotencyKey}`; } function normalizeNullableText(value: string | undefined): string | null { const normalized = value?.trim(); return normalized ? normalized : null; } function assertAllowedScopes(scopes: AgentScope[]): void { const deniedScope = scopes.find((scope) => !allowedScopeSet.has(scope)); if (deniedScope) { throw new Error(`Unsupported agent scope: ${deniedScope}`); } } function mapAgent(row: AgentRow): AgentRecord { return { id: row.id, ownerUserId: row.owner_user_id, ownerEmail: row.owner_email, displayName: row.display_name, avatarUrl: row.avatar_url, status: row.status, createdAt: row.created_at.toISOString(), updatedAt: row.updated_at.toISOString(), }; } function mapGrant(row: GrantRow): AgentGrantRecord { return { id: row.id, agentId: row.agent_id, workspaceSlug: row.workspace_slug, projectId: row.project_id === "" ? null : row.project_id, scopes: row.scopes, mode: row.mode, createdByUserId: row.created_by_user_id, createdAt: row.created_at.toISOString(), updatedAt: row.updated_at.toISOString(), }; } function mapToken(row: TokenRow): AgentTokenRecord { return { id: row.id, agentId: row.agent_id, name: row.name, status: row.status, grantScope: row.grant_scope, tokenSuffix: row.token_suffix, expiresAt: row.expires_at?.toISOString() ?? null, lastUsedAt: row.last_used_at?.toISOString() ?? null, createdAt: row.created_at.toISOString(), }; } function mapSetupCode(row: SetupCodeRow): AgentSetupCodeRecord { return { id: row.id, agentId: row.agent_id, codeSuffix: row.code_suffix, status: row.status, tokenName: row.token_name, createdByUserId: row.created_by_user_id, expiresAt: row.expires_at.toISOString(), usedAt: row.used_at?.toISOString() ?? null, usedTokenId: row.used_token_id, createdAt: row.created_at.toISOString(), }; } function mapTokenGrant(row: TokenGrantRow): AgentGrantRecord { return { id: row.id, agentId: row.agent_id, workspaceSlug: row.workspace_slug, projectId: row.project_id === "" ? null : row.project_id, scopes: row.scopes, mode: row.mode, createdByUserId: row.created_by_user_id, createdAt: row.created_at.toISOString(), updatedAt: row.updated_at.toISOString(), }; }