NODEDC_TASKMANAGER_CODEXAPI/src/repositories/agents.ts

1063 lines
31 KiB
TypeScript

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<string>(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<AgentRecord> {
const result = await this.pool.query<AgentRow>(
`
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<AgentRecord[]> {
const result = ownerUserId
? await this.pool.query<AgentRow>("SELECT * FROM agents WHERE owner_user_id = $1 ORDER BY created_at DESC", [ownerUserId])
: await this.pool.query<AgentRow>("SELECT * FROM agents ORDER BY created_at DESC");
return result.rows.map(mapAgent);
}
async listAgentsByOwnerIdentity(ownerUserId?: string, ownerEmail?: string): Promise<AgentRecord[]> {
const normalizedOwnerUserId = normalizeNullableText(ownerUserId);
const normalizedOwnerEmail = normalizeNullableText(ownerEmail)?.toLowerCase();
if (!normalizedOwnerUserId && !normalizedOwnerEmail) {
return [];
}
const result = await this.pool.query<AgentRow>(
`
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<AgentRecord | null> {
const result = await this.pool.query<AgentRow>("SELECT * FROM agents WHERE id = $1", [agentId]);
return result.rows[0] ? mapAgent(result.rows[0]) : null;
}
async updateAgentProfile(agentId: string, input: UpdateAgentProfileInput): Promise<AgentRecord | null> {
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<AgentRow>(
`
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<AgentRecord | null> {
const client = await this.pool.connect();
try {
await client.query("BEGIN");
const result = await client.query<AgentRow>(
`
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<AgentGrantRecord> {
assertAllowedScopes(input.scopes);
const projectId = input.projectId ?? "";
const result = await this.pool.query<GrantRow>(
`
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<AgentGrantRecord[]> {
const result = await this.pool.query<GrantRow>("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<AgentGrantRecord[]> {
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<GrantRow>(
`
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<AgentGrantRecord[]> {
assertAllowedScopes(input.scopes);
const seenKeys = new Set<string>();
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<GrantRow>(
`
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<AgentTokenRecord> {
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<TokenRow>(
`
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<AgentSetupCodeRecord> {
const result = await this.pool.query<SetupCodeRow>(
`
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<RedeemSetupCodeResult> {
const client = await this.pool.connect();
try {
await client.query("BEGIN");
const setupCodeResult = await client.query<SetupCodeRow>(
`
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<AgentRow>(
`
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<TokenRow>(
`
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<SetupCodeRow>(
`
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<GrantRow>(
"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<AgentTokenRecord[]> {
const result = await this.pool.query<TokenRow>(
`
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<AgentTokenRecord | null> {
const result = await this.pool.query<TokenRow>(
`
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<AgentSessionRecord | null> {
const result = await this.pool.query<SessionRow>(
`
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<TokenRow>(
`
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<AgentGrantRecord[]> {
const result = await this.pool.query<TokenGrantRow>(
`
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<string, unknown> = {}): Promise<void> {
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<IdempotencyClaim> {
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<IdempotencyRow>(
`
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<IdempotencyRow>("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<void> {
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<void> {
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(),
};
}