fix: enforce atomic agent registration contract

This commit is contained in:
ZacharyZhang-NY committed 2026-07-19 15:38:40 -04:00
1 parent 47d323c17b
commit f64d00673c
58 files changed
+2692 -1667

No files matched your search

+1 -5
View File
@@ -98,17 +98,13 @@ agents.patch(
const agentId = c.req.param('agent_id');
const body = await c.req.json<UpdateAgentRequest>();
if (!body || !body.integration_type || typeof body.integration_type !== 'string') {
throw new AppError(400, 'VALIDATION_ERROR', { key: 'agent.missingIntegrationType' });
}
const orgId = c.get('org_id');
const actor = c.get('actor');
const result = await agentService.updateAgentIntegrationType(
c.env.ELYDORA_DB,
agentId,
body.integration_type,
body?.integration_type,
orgId,
actor,
);
@@ -0,0 +1,86 @@
import {
INTEGRATION_TYPES,
type AdminAction,
type Agent,
type IntegrationType,
} from '../shared/index.js';
import type { Database, PreparedStatement } from '../adapters/interfaces.js';
import { AppError } from '../middleware/error-handler.js';
import { generateUUIDv7 } from '../utils/uuid.js';
export function requireIntegrationType(value: unknown): IntegrationType {
if (typeof value !== 'string' || value.length === 0) {
throw new AppError(400, 'VALIDATION_ERROR', { key: 'agent.missingIntegrationType' });
}
if (!(INTEGRATION_TYPES as readonly string[]).includes(value)) {
throw new AppError(
400,
'VALIDATION_ERROR',
{
key: 'agent.invalidIntegrationType',
params: { value, valid: INTEGRATION_TYPES.join(', ') },
},
);
}
return value as IntegrationType;
}
export async function updateAgentIntegrationType(
db: Database,
agentId: string,
integrationValue: unknown,
orgId: string,
actor: string,
): Promise<{ agent: Agent }> {
const integrationType = requireIntegrationType(integrationValue);
const now = Date.now();
const agentRow = await db
.prepare('SELECT * FROM agents WHERE agent_id = ? AND org_id = ?')
.bind(agentId, orgId)
.first<Agent>();
if (!agentRow) {
throw new AppError(404, 'NOT_FOUND', { key: 'agent.notFound', params: { id: agentId } });
}
const statements: PreparedStatement[] = [];
statements.push(
db
.prepare('UPDATE agents SET integration_type = ?, updated_at = ? WHERE agent_id = ? AND org_id = ?')
.bind(integrationType, now, agentId, orgId),
);
const eventId = generateUUIDv7();
const action: AdminAction = 'agent.update';
statements.push(
db
.prepare(
`INSERT INTO admin_events (event_id, org_id, actor, action, target_type, target_id, details, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
)
.bind(
eventId,
orgId,
actor,
action,
'agent',
agentId,
JSON.stringify({
integration_type: integrationType,
previous_integration_type: agentRow.integration_type,
}),
now,
),
);
await db.batch(statements);
return {
agent: {
...agentRow,
integration_type: integrationType,
updated_at: now,
},
};
}
+5 -76
View File
@@ -18,6 +18,9 @@ import { generateUUIDv7 } from '../utils/uuid.js';
import { base64urlDecode } from '../utils/crypto.js';
import { AppError } from '../middleware/error-handler.js';
import type { Database, PreparedStatement } from '../adapters/interfaces.js';
import { requireIntegrationType } from './agent-integration.js';
export { updateAgentIntegrationType } from './agent-integration.js';
// ---------------------------------------------------------------------------
// Register agent
@@ -29,6 +32,7 @@ export async function registerAgent(
orgId: string,
actor: string,
): Promise<RegisterAgentResponse> {
const integrationType = requireIntegrationType(body.integration_type);
const now = Date.now();
// Check if agent already exists
@@ -68,7 +72,7 @@ export async function registerAgent(
org_id: orgId,
display_name: body.display_name ?? body.agent_id,
responsible_entity: body.responsible_entity ?? '',
integration_type: body.integration_type ?? 'sdk',
integration_type: integrationType,
status: 'active',
created_at: now,
updated_at: now,
@@ -197,81 +201,6 @@ export async function getAgent(
return { agent: agentRow, keys };
}
// ---------------------------------------------------------------------------
// Update agent integration type
// ---------------------------------------------------------------------------
const VALID_INTEGRATION_TYPES = [
'sdk', 'claudecode', 'cursor', 'gemini', 'kirocli', 'kiroide',
'opencode', 'copilot', 'letta', 'codex', 'kimi',
'enterprise', 'gui', 'other',
] as const;
export async function updateAgentIntegrationType(
db: Database,
agentId: string,
integrationValue: string,
orgId: string,
actor: string,
): Promise<{ agent: Agent }> {
const now = Date.now();
const agentRow = await db
.prepare('SELECT * FROM agents WHERE agent_id = ? AND org_id = ?')
.bind(agentId, orgId)
.first<Agent>();
if (!agentRow) {
throw new AppError(404, 'NOT_FOUND', { key: 'agent.notFound', params: { id: agentId } });
}
if (!(VALID_INTEGRATION_TYPES as readonly string[]).includes(integrationValue)) {
throw new AppError(
400,
'VALIDATION_ERROR',
{ key: 'agent.invalidIntegrationType', params: { value: integrationValue, valid: VALID_INTEGRATION_TYPES.join(', ') } },
);
}
const statements: PreparedStatement[] = [];
statements.push(
db
.prepare('UPDATE agents SET integration_type = ?, updated_at = ? WHERE agent_id = ?')
.bind(integrationValue, now, agentId),
);
const eventId = generateUUIDv7();
const action: AdminAction = 'agent.update';
statements.push(
db
.prepare(
`INSERT INTO admin_events (event_id, org_id, actor, action, target_type, target_id, details, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
)
.bind(
eventId,
orgId,
actor,
action,
'agent',
agentId,
JSON.stringify({ integration_type: integrationValue, previous_integration_type: agentRow.integration_type }),
now,
),
);
await db.batch(statements);
const updatedAgent: Agent = {
...agentRow,
integration_type: integrationValue,
updated_at: now,
};
return { agent: updatedAgent };
}
// ---------------------------------------------------------------------------
// Freeze agent
// ---------------------------------------------------------------------------
+3
View File
@@ -3,6 +3,7 @@ export type {
KeyStatus,
ExportStatus,
AdminAction,
IntegrationType,
RbacRole,
ErrorCode,
EOR,
@@ -51,6 +52,8 @@ export type {
ErrorResponse,
} from './types/index.js';
export { INTEGRATION_TYPES } from './types/index.js';
export {
ERROR_CODES,
MAX_PAYLOAD_SIZE,
+3 -3
View File
@@ -1,5 +1,5 @@
import type { Agent, AgentKey, Epoch, Export, Operation, Organization, Receipt, User } from './entities.js';
import type { ErrorCode } from './enums.js';
import type { ErrorCode, IntegrationType } from './enums.js';
import type { EAR, EOR } from './protocol.js';
// ---------------------------------------------------------------------------
@@ -10,7 +10,7 @@ export interface RegisterAgentRequest {
readonly agent_id: string;
readonly display_name?: string;
readonly responsible_entity?: string;
readonly integration_type?: string;
readonly integration_type: IntegrationType;
readonly keys: ReadonlyArray<{
readonly kid: string;
readonly public_key: string;
@@ -46,7 +46,7 @@ export interface RevokeAgentRequest {
}
export interface UpdateAgentRequest {
readonly integration_type: string;
readonly integration_type: IntegrationType;
}
// ---------------------------------------------------------------------------
+2 -2
View File
@@ -1,4 +1,4 @@
import type { AgentStatus, ExportStatus, KeyStatus, RbacRole } from './enums.js';
import type { AgentStatus, ExportStatus, IntegrationType, KeyStatus, RbacRole } from './enums.js';
/** Registered AI agent within an organization */
export interface Agent {
@@ -6,7 +6,7 @@ export interface Agent {
readonly org_id: string;
readonly display_name: string;
readonly responsible_entity: string;
readonly integration_type: string;
readonly integration_type: IntegrationType;
readonly status: AgentStatus;
readonly created_at: number;
readonly updated_at: number;
+23 -4
View File
@@ -19,10 +19,29 @@ export type AdminAction =
| 'export.create';
/** Supported agent integration types */
export type IntegrationType =
| 'claudecode' | 'cursor' | 'gemini' | 'kirocli' | 'kiroide'
| 'opencode' | 'copilot' | 'letta' | 'codex' | 'kimi'
| 'enterprise' | 'gui' | 'sdk' | 'other';
export const INTEGRATION_TYPES = [
'augment',
'claudecode',
'cline',
'codex',
'copilot',
'cursor',
'droid',
'gemini',
'grok',
'kimi',
'kirocli',
'kiroide',
'letta',
'opencode',
'qwen',
'enterprise',
'gui',
'sdk',
'other',
] as const;
export type IntegrationType = (typeof INTEGRATION_TYPES)[number];
/** RBAC roles for console access */
export type RbacRole =
@@ -3,10 +3,13 @@ export type {
KeyStatus,
ExportStatus,
AdminAction,
IntegrationType,
RbacRole,
ErrorCode,
} from './enums.js';
export { INTEGRATION_TYPES } from './enums.js';
export type {
EOR,
ECH,