fix: enforce atomic agent registration contract
This commit is contained in:
1 parent
47d323c17b
commit
f64d00673c
58 files changed
+2692
-1667
No files matched your search
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user