/** * Community ADE Approval System - API Specification * Express routes with Zod validation schemas * * @module approval-system/api * @version 1.0.0 */ import { Router, Request, Response, NextFunction } from 'express'; import { z } from 'zod'; // ============================================================================ // BASE SCHEMAS // ============================================================================ /** * Common identifiers */ const IdSchema = z.string().uuid(); const TimestampSchema = z.string().datetime(); const ResourceTypeSchema = z.enum([ 'database', 'service', 'infrastructure', 'configuration', 'secret', 'network', 'storage' ]); /** * Task state enumeration */ const TaskStateSchema = z.enum([ 'DRAFT', 'SUBMITTED', 'REVIEWING', 'APPROVED', 'APPLYING', 'COMPLETED', 'REJECTED', 'CANCELLED' ]); /** * Lock mode enumeration */ const LockModeSchema = z.enum(['exclusive', 'shared']); /** * Approval action enumeration */ const ApprovalActionSchema = z.enum(['approve', 'reject', 'request_changes', 'delegate']); /** * Pagination parameters */ const PaginationSchema = z.object({ page: z.coerce.number().int().min(1).default(1), limit: z.coerce.number().int().min(1).max(100).default(20), cursor: z.string().optional() }); /** * Sort parameters */ const SortSchema = z.object({ sort_by: z.enum(['created_at', 'updated_at', 'risk_score', 'state']).default('created_at'), sort_order: z.enum(['asc', 'desc']).default('desc') }); // ============================================================================ // TASK SCHEMAS // ============================================================================ /** * Resource reference schema */ const ResourceRefSchema = z.object({ type: ResourceTypeSchema, id: z.string(), name: z.string().optional(), scope: z.enum(['global', 'namespace', 'cluster', 'instance']).default('namespace'), namespace: z.string().optional(), actions: z.array(z.enum(['read', 'write', 'delete', 'execute'])).default(['read']) }); /** * Task configuration schema */ const TaskConfigSchema = z.object({ type: z.string().min(1).max(100), version: z.string().default('1.0.0'), description: z.string().min(1).max(5000), resources: z.array(ResourceRefSchema).min(1), parameters: z.record(z.unknown()).default({}), secrets: z.array(z.string()).default([]), // Secret references, not values rollback_strategy: z.enum(['automatic', 'manual', 'none']).default('automatic'), timeout_seconds: z.number().int().min(1).max(3600).default(300), priority: z.number().int().min(0).max(100).default(50) }); /** * Risk assessment schema */ const RiskAssessmentSchema = z.object({ score: z.number().int().min(0).max(100), level: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']), factors: z.array(z.object({ name: z.string(), weight: z.number(), contribution: z.number() })), auto_approvable: z.boolean() }); /** * Preview result schema */ const PreviewResultSchema = z.object({ valid: z.boolean(), changes: z.array(z.object({ resource: ResourceRefSchema, action: z.string(), before: z.unknown().optional(), after: z.unknown().optional(), diff: z.string().optional() })), warnings: z.array(z.string()).default([]), errors: z.array(z.string()).default([]), estimated_duration_seconds: z.number().int().optional(), affected_services: z.array(z.string()).default([]) }); /** * Create task request schema */ const CreateTaskRequestSchema = z.object({ config: TaskConfigSchema, metadata: z.object({ author_id: z.string(), author_name: z.string(), team: z.string().optional(), ticket_ref: z.string().optional(), tags: z.array(z.string()).default([]) }), dry_run: z.boolean().default(false) }); /** * Submit task request schema */ const SubmitTaskRequestSchema = z.object({ force: z.boolean().default(false), skip_preview: z.boolean().default(false), requested_reviewers: z.array(z.string()).optional() }); /** * Task response schema */ const TaskResponseSchema = z.object({ id: IdSchema, state: TaskStateSchema, config: TaskConfigSchema, metadata: z.object({ author_id: z.string(), author_name: z.string(), team: z.string().optional(), ticket_ref: z.string().optional(), tags: z.array(z.string()), created_at: TimestampSchema, updated_at: TimestampSchema, submitted_at: TimestampSchema.optional(), approved_at: TimestampSchema.optional(), completed_at: TimestampSchema.optional() }), risk: RiskAssessmentSchema.optional(), preview: PreviewResultSchema.optional(), approvals: z.array(z.object({ id: IdSchema, reviewer_id: z.string(), reviewer_name: z.string(), action: ApprovalActionSchema, reason: z.string().optional(), created_at: TimestampSchema })).default([]), required_approvals: z.number().int().min(0).default(1), current_approvals: z.number().int().min(0).default(0), lock_info: z.object({ acquired_at: TimestampSchema, expires_at: TimestampSchema, agent_id: z.string() }).optional(), execution: z.object({ started_at: TimestampSchema.optional(), completed_at: TimestampSchema.optional(), result: z.enum(['success', 'failure', 'timeout', 'cancelled']).optional(), output: z.string().optional(), error: z.string().optional() }).optional() }); /** * List tasks query schema */ const ListTasksQuerySchema = PaginationSchema.merge(SortSchema).merge(z.object({ state: z.array(TaskStateSchema).optional(), author_id: z.string().optional(), resource_type: ResourceTypeSchema.optional(), resource_id: z.string().optional(), risk_level: z.enum(['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']).optional(), created_after: TimestampSchema.optional(), created_before: TimestampSchema.optional(), tags: z.array(z.string()).optional(), needs_my_approval: z.coerce.boolean().optional() })); // ============================================================================ // APPROVAL SCHEMAS // ============================================================================ /** * Approval request schema (internal) */ const ApprovalRequestSchema = z.object({ id: IdSchema, task_id: IdSchema, reviewer_id: z.string(), reviewer_name: z.string(), status: z.enum(['PENDING', 'APPROVED', 'REJECTED', 'DELEGATED']), priority: z.enum(['LOW', 'NORMAL', 'HIGH', 'URGENT']).default('NORMAL'), delegated_to: z.string().optional(), due_at: TimestampSchema.optional(), created_at: TimestampSchema, responded_at: TimestampSchema.optional() }); /** * Approve/reject request schema */ const RespondApprovalRequestSchema = z.object({ action: ApprovalActionSchema, reason: z.string().max(2000).optional(), delegate_to: z.string().optional(), options: z.object({ apply_immediately: z.boolean().default(false), require_additional_approvals: z.array(z.string()).optional() }).default({}) }); /** * Batch approval request schema */ const BatchApprovalRequestSchema = z.object({ approval_ids: z.array(IdSchema).min(1).max(100), action: z.enum(['approve', 'reject']), reason: z.string().max(2000).optional(), options: z.object({ skip_validation: z.boolean().default(false), apply_immediately: z.boolean().default(false), continue_on_error: z.boolean().default(false) }).default({}) }); /** * Batch approval response schema */ const BatchApprovalResponseSchema = z.object({ success: z.boolean(), processed: z.number().int(), succeeded: z.number().int(), failed: z.number().int(), results: z.array(z.object({ approval_id: IdSchema, success: z.boolean(), error: z.string().optional() })), task_updates: z.array(z.object({ task_id: IdSchema, new_state: TaskStateSchema.optional() })) }); /** * Delegation policy schema */ const DelegationPolicySchema = z.object({ id: IdSchema.optional(), owner_id: z.string(), conditions: z.object({ task_types: z.array(z.string()).optional(), resource_patterns: z.array(z.string()).optional(), risk_above: z.number().int().min(0).max(100).optional(), namespaces: z.array(z.string()).optional(), tags: z.array(z.string()).optional() }), delegate_to: z.string(), cascade: z.boolean().default(true), expires_at: TimestampSchema.optional(), active: z.boolean().default(true) }); // ============================================================================ // LOCK SCHEMAS // ============================================================================ /** * Lock acquisition request schema */ const AcquireLockRequestSchema = z.object({ resource_type: z.enum(['task', 'resource', 'agent']), resource_id: z.string(), mode: LockModeSchema.default('exclusive'), ttl_seconds: z.number().int().min(5).max(300).default(30), purpose: z.string().max(200).optional(), wait_for_available: z.boolean().default(true), max_wait_seconds: z.number().int().min(0).max(300).default(60) }); /** * Lock acquisition response schema */ const LockResponseSchema = z.object({ id: IdSchema, acquired: z.boolean(), resource_type: z.enum(['task', 'resource', 'agent']), resource_id: z.string(), mode: LockModeSchema, holder: z.object({ agent_id: z.string(), acquired_at: TimestampSchema, expires_at: TimestampSchema, purpose: z.string().optional() }), queue_position: z.number().int().optional(), estimated_wait_seconds: z.number().int().optional() }); /** * Lock heartbeat request schema */ const LockHeartbeatRequestSchema = z.object({ lock_id: IdSchema, ttl_extension_seconds: z.number().int().min(5).max(300).default(30) }); /** * Lock release request schema */ const ReleaseLockRequestSchema = z.object({ lock_id: IdSchema, force: z.boolean().default(false), reason: z.string().optional() }); /** * Lock info schema */ const LockInfoSchema = z.object({ id: IdSchema, resource_type: z.enum(['task', 'resource', 'agent']), resource_id: z.string(), mode: LockModeSchema, holder: z.object({ agent_id: z.string(), acquired_at: TimestampSchema, expires_at: TimestampSchema, purpose: z.string().optional() }), queue: z.array(z.object({ agent_id: z.string(), mode: LockModeSchema, requested_at: TimestampSchema, priority: z.number().int() })) }); /** * Deadlock info schema */ const DeadlockInfoSchema = z.object({ detected_at: TimestampSchema, cycle: z.array(z.object({ agent_id: z.string(), holds_lock: IdSchema, waits_for: IdSchema })), resolution: z.object({ victim_agent_id: z.string(), strategy: z.enum(['abort_youngest', 'abort_shortest', 'abort_lowest_priority']), released_locks: z.array(IdSchema) }) }); // ============================================================================ // WEBSOCKET EVENT SCHEMAS // ============================================================================ /** * Base WebSocket message schema */ const WebSocketMessageSchema = z.object({ event: z.string(), timestamp: TimestampSchema, payload: z.unknown() }); /** * Lock acquired event */ const LockAcquiredEventSchema = z.object({ event: z.literal('lock:acquired'), timestamp: TimestampSchema, payload: z.object({ lock_id: IdSchema, resource_type: z.string(), resource_id: z.string(), agent_id: z.string(), acquired_at: TimestampSchema, expires_at: TimestampSchema }) }); /** * Lock released event */ const LockReleasedEventSchema = z.object({ event: z.literal('lock:released'), timestamp: TimestampSchema, payload: z.object({ lock_id: IdSchema, resource_type: z.string(), resource_id: z.string(), agent_id: z.string(), released_at: TimestampSchema, reason: z.string().optional() }) }); /** * Lock expired event */ const LockExpiredEventSchema = z.object({ event: z.literal('lock:expired'), timestamp: TimestampSchema, payload: z.object({ lock_id: IdSchema, resource_type: z.string(), resource_id: z.string(), expired_at: TimestampSchema }) }); /** * Deadlock detected event */ const DeadlockDetectedEventSchema = z.object({ event: z.literal('lock:deadlock_detected'), timestamp: TimestampSchema, payload: DeadlockInfoSchema }); /** * Approval requested event */ const ApprovalRequestedEventSchema = z.object({ event: z.literal('approval:requested'), timestamp: TimestampSchema, payload: z.object({ approval_id: IdSchema, task_id: IdSchema, task_type: z.string(), reviewer_id: z.string(), requested_by: z.string(), priority: z.enum(['LOW', 'NORMAL', 'HIGH', 'URGENT']), due_at: TimestampSchema.optional(), risk_score: z.number().int() }) }); /** * Approval responded event */ const ApprovalRespondedEventSchema = z.object({ event: z.literal('approval:responded'), timestamp: TimestampSchema, payload: z.object({ approval_id: IdSchema, task_id: IdSchema, reviewer_id: z.string(), action: ApprovalActionSchema, reason: z.string().optional() }) }); /** * Task state changed event */ const TaskStateChangedEventSchema = z.object({ event: z.literal('task:state_changed'), timestamp: TimestampSchema, payload: z.object({ task_id: IdSchema, previous_state: TaskStateSchema, new_state: TaskStateSchema, triggered_by: z.string(), reason: z.string().optional() }) }); /** * Task execution completed event */ const TaskCompletedEventSchema = z.object({ event: z.literal('task:completed'), timestamp: TimestampSchema, payload: z.object({ task_id: IdSchema, result: z.enum(['success', 'failure', 'timeout', 'cancelled']), duration_seconds: z.number(), output: z.string().optional(), error: z.string().optional() }) }); // ============================================================================ // ERROR SCHEMAS // ============================================================================ /** * API error response schema */ const ApiErrorSchema = z.object({ error: z.object({ code: z.string(), message: z.string(), details: z.unknown().optional(), request_id: z.string().uuid(), timestamp: TimestampSchema }) }); /** * Validation error schema */ const ValidationErrorSchema = ApiErrorSchema.extend({ error: z.object({ code: z.literal('VALIDATION_ERROR'), message: z.string(), details: z.object({ field: z.string(), issue: z.string(), value: z.unknown().optional() }), request_id: z.string().uuid(), timestamp: TimestampSchema }) }); // ============================================================================ // ROUTE HANDLERS (Type definitions) // ============================================================================ /** * Typed request/response helpers */ type CreateTaskRequest = z.infer; type CreateTaskResponse = z.infer; type ListTasksQuery = z.infer; type SubmitTaskRequest = z.infer; type RespondApprovalRequest = z.infer; type BatchApprovalRequest = z.infer; type BatchApprovalResponse = z.infer; type AcquireLockRequest = z.infer; type LockResponse = z.infer; type LockHeartbeatRequest = z.infer; type ReleaseLockRequest = z.infer; // ============================================================================ // EXPRESS ROUTES // ============================================================================ const router = Router(); // Middleware: Validate request body against schema const validateBody = (schema: z.ZodSchema) => { return (req: Request, res: Response, next: NextFunction) => { const result = schema.safeParse(req.body); if (!result.success) { return res.status(400).json({ error: { code: 'VALIDATION_ERROR', message: 'Request body validation failed', details: result.error.format(), request_id: req.headers['x-request-id'] || crypto.randomUUID(), timestamp: new Date().toISOString() } }); } req.body = result.data; next(); }; }; // Middleware: Validate query parameters const validateQuery = (schema: z.ZodSchema) => { return (req: Request, res: Response, next: NextFunction) => { const result = schema.safeParse(req.query); if (!result.success) { return res.status(400).json({ error: { code: 'VALIDATION_ERROR', message: 'Query parameter validation failed', details: result.error.format(), request_id: req.headers['x-request-id'] || crypto.randomUUID(), timestamp: new Date().toISOString() } }); } req.query = result.data as unknown as Request['query']; next(); }; }; // Middleware: Validate URL parameters const validateParams = (schema: z.ZodSchema) => { return (req: Request, res: Response, next: NextFunction) => { const result = schema.safeParse(req.params); if (!result.success) { return res.status(400).json({ error: { code: 'VALIDATION_ERROR', message: 'URL parameter validation failed', details: result.error.format(), request_id: req.headers['x-request-id'] || crypto.randomUUID(), timestamp: new Date().toISOString() } }); } req.params = result.data as unknown as Request['params']; next(); }; }; // ============================================================================ // TASK ROUTES // ============================================================================ /** * @route POST /api/v1/tasks * @desc Create a new task * @access Authenticated * * Request body: CreateTaskRequestSchema * Response: 201 Created with TaskResponseSchema */ router.post( '/tasks', validateBody(CreateTaskRequestSchema), async (req: Request, res: Response) => { // Implementation: Create task in DRAFT state res.status(201).json({ id: crypto.randomUUID(), state: 'DRAFT', config: req.body.config, metadata: { ...req.body.metadata, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), tags: req.body.metadata.tags || [] }, approvals: [], required_approvals: 1, current_approvals: 0 }); } ); /** * @route GET /api/v1/tasks * @desc List tasks with filtering and pagination * @access Authenticated * * Query params: ListTasksQuerySchema * Response: 200 OK with { tasks: TaskResponseSchema[], pagination: {...} } */ router.get( '/tasks', validateQuery(ListTasksQuerySchema), async (req: Request, res: Response) => { // Implementation: Query tasks from database res.json({ tasks: [], pagination: { page: req.query.page, limit: req.query.limit, total: 0, has_more: false } }); } ); /** * @route GET /api/v1/tasks/:id * @desc Get task by ID * @access Authenticated * * URL params: { id: uuid } * Response: 200 OK with TaskResponseSchema */ router.get( '/tasks/:id', validateParams(z.object({ id: IdSchema })), async (req: Request, res: Response) => { // Implementation: Fetch task from database res.json({ id: req.params.id, state: 'DRAFT', config: {} as any, metadata: {} as any, approvals: [], required_approvals: 1, current_approvals: 0 }); } ); /** * @route POST /api/v1/tasks/:id/submit * @desc Submit task for approval * @access Authenticated (task author or admin) * * URL params: { id: uuid } * Request body: SubmitTaskRequestSchema * Response: 202 Accepted with TaskResponseSchema */ router.post( '/tasks/:id/submit', validateParams(z.object({ id: IdSchema })), validateBody(SubmitTaskRequestSchema), async (req: Request, res: Response) => { // Implementation: // 1. Validate task is in DRAFT state // 2. Run preview generation // 3. Calculate risk score // 4. Determine required approvals // 5. Create approval requests // 6. Transition to SUBMITTED/REVIEWING // 7. Emit approval:requested events res.status(202).json({ id: req.params.id, state: 'REVIEWING', config: {} as any, metadata: { author_id: '', author_name: '', created_at: new Date().toISOString(), updated_at: new Date().toISOString(), submitted_at: new Date().toISOString(), tags: [] }, risk: { score: 45, level: 'MEDIUM', factors: [], auto_approvable: false }, approvals: [], required_approvals: 2, current_approvals: 0 }); } ); /** * @route POST /api/v1/tasks/:id/cancel * @desc Cancel a task * @access Authenticated (task author or admin) * * URL params: { id: uuid } * Response: 200 OK with TaskResponseSchema */ router.post( '/tasks/:id/cancel', validateParams(z.object({ id: IdSchema })), async (req: Request, res: Response) => { // Implementation: // 1. Validate task can be cancelled (not APPLYING or COMPLETED) // 2. Release any held locks // 3. Transition to CANCELLED state // 4. Notify waiters res.json({ id: req.params.id, state: 'CANCELLED', config: {} as any, metadata: {} as any, approvals: [], required_approvals: 0, current_approvals: 0 }); } ); /** * @route GET /api/v1/tasks/:id/preview * @desc Get task preview/changes * @access Authenticated * * URL params: { id: uuid } * Response: 200 OK with PreviewResultSchema */ router.get( '/tasks/:id/preview', validateParams(z.object({ id: IdSchema })), async (req: Request, res: Response) => { res.json({ valid: true, changes: [], warnings: [], errors: [], affected_services: [] }); } ); // ============================================================================ // APPROVAL ROUTES // ============================================================================ /** * @route GET /api/v1/approvals * @desc List pending approvals for current user * @access Authenticated * * Query params: PaginationSchema + { status: string, task_id: uuid } * Response: 200 OK with { approvals: ApprovalRequestSchema[], pagination: {...} } */ router.get( '/approvals', validateQuery(PaginationSchema.merge(z.object({ status: z.enum(['PENDING', 'APPROVED', 'REJECTED', 'DELEGATED']).optional(), task_id: IdSchema.optional() }))), async (req: Request, res: Response) => { res.json({ approvals: [], pagination: { page: req.query.page, limit: req.query.limit, total: 0, has_more: false } }); } ); /** * @route POST /api/v1/approvals/:id/respond * @desc Respond to an approval request * @access Authenticated (assigned reviewer) * * URL params: { id: uuid } * Request body: RespondApprovalRequestSchema * Response: 200 OK with { success: boolean, task_state: string } */ router.post( '/approvals/:id/respond', validateParams(z.object({ id: IdSchema })), validateBody(RespondApprovalRequestSchema), async (req: Request, res: Response) => { // Implementation: // 1. Validate approval is PENDING // 2. Record response // 3. Check if quorum reached // 4. Transition task state if needed // 5. Emit approval:responded and task:state_changed events res.json({ success: true, approval_id: req.params.id, task_id: crypto.randomUUID(), task_state: req.body.action === 'approve' ? 'APPROVED' : 'REJECTED' }); } ); /** * @route POST /api/v1/approvals/batch * @desc Batch approve/reject multiple approvals * @access Authenticated * * Request body: BatchApprovalRequestSchema * Response: 200 OK with BatchApprovalResponseSchema */ router.post( '/approvals/batch', validateBody(BatchApprovalRequestSchema), async (req: Request, res: Response) => { // Implementation: // 1. Validate all approvals exist and are pending // 2. Process each approval atomically // 3. Rollback on error unless continue_on_error // 4. Check task state transitions res.json({ success: true, processed: req.body.approval_ids.length, succeeded: req.body.approval_ids.length, failed: 0, results: req.body.approval_ids.map(id => ({ approval_id: id, success: true })), task_updates: [] }); } ); /** * @route GET /api/v1/approvals/policies * @desc List delegation policies for current user * @access Authenticated */ router.get('/approvals/policies', async (req: Request, res: Response) => { res.json({ policies: [] }); }); /** * @route POST /api/v1/approvals/policies * @desc Create a delegation policy * @access Authenticated * * Request body: DelegationPolicySchema * Response: 201 Created with DelegationPolicySchema */ router.post( '/approvals/policies', validateBody(DelegationPolicySchema), async (req: Request, res: Response) => { res.status(201).json({ id: crypto.randomUUID(), ...req.body }); } ); // ============================================================================ // LOCK ROUTES // ============================================================================ /** * @route POST /api/v1/locks/acquire * @desc Acquire a distributed lock * @access Service (agents/workers) * * Request body: AcquireLockRequestSchema * Response: * 201 Created with LockResponseSchema (acquired) * 202 Accepted with queue info (waiting) * 423 Locked (max wait exceeded, not waiting) */ router.post( '/locks/acquire', validateBody(AcquireLockRequestSchema), async (req: Request, res: Response) => { // Implementation: // 1. Check if lock available // 2. If available: acquire, set TTL, return 201 // 3. If not available and wait_for_available: queue, return 202 // 4. If not available and not waiting: return 423 const acquired = Math.random() > 0.5; // Placeholder if (acquired) { res.status(201).json({ id: crypto.randomUUID(), acquired: true, resource_type: req.body.resource_type, resource_id: req.body.resource_id, mode: req.body.mode, holder: { agent_id: 'agent-001', acquired_at: new Date().toISOString(), expires_at: new Date(Date.now() + req.body.ttl_seconds * 1000).toISOString(), purpose: req.body.purpose } }); } else if (req.body.wait_for_available) { res.status(202).json({ id: crypto.randomUUID(), acquired: false, resource_type: req.body.resource_type, resource_id: req.body.resource_id, mode: req.body.mode, holder: {} as any, queue_position: 1, estimated_wait_seconds: 30 }); } else { res.status(423).json({ error: { code: 'RESOURCE_LOCKED', message: 'Resource is locked by another agent', request_id: crypto.randomUUID(), timestamp: new Date().toISOString() } }); } } ); /** * @route POST /api/v1/locks/heartbeat * @desc Extend lock TTL via heartbeat * @access Service (lock holder) * * Request body: LockHeartbeatRequestSchema * Response: 200 OK with updated LockResponseSchema * 404 Not Found (lock expired) * 403 Forbidden (not lock holder) */ router.post( '/locks/heartbeat', validateBody(LockHeartbeatRequestSchema), async (req: Request, res: Response) => { // Implementation: Extend lock TTL res.json({ id: req.body.lock_id, acquired: true, resource_type: 'task', resource_id: 'task-001', mode: 'exclusive', holder: { agent_id: 'agent-001', acquired_at: new Date().toISOString(), expires_at: new Date(Date.now() + req.body.ttl_extension_seconds * 1000).toISOString(), purpose: 'Task execution' } }); } ); /** * @route POST /api/v1/locks/release * @desc Release a held lock * @access Service (lock holder or admin) * * Request body: ReleaseLockRequestSchema * Response: 204 No Content */ router.post( '/locks/release', validateBody(ReleaseLockRequestSchema), async (req: Request, res: Response) => { // Implementation: // 1. Verify lock exists // 2. Verify holder matches (or force=true with admin) // 3. Release lock // 4. Notify next waiter in queue // 5. Emit lock:released event res.status(204).send(); } ); /** * @route GET /api/v1/locks * @desc List active locks * @access Admin * * Query params: { resource_type, resource_id, agent_id } * Response: 200 OK with { locks: LockInfoSchema[] } */ router.get( '/locks', validateQuery(z.object({ resource_type: z.enum(['task', 'resource', 'agent']).optional(), resource_id: z.string().optional(), agent_id: z.string().optional() })), async (req: Request, res: Response) => { res.json({ locks: [] }); } ); /** * @route GET /api/v1/locks/:id * @desc Get lock info by ID * @access Admin */ router.get( '/locks/:id', validateParams(z.object({ id: IdSchema })), async (req: Request, res: Response) => { res.json({ id: req.params.id, resource_type: 'task', resource_id: 'task-001', mode: 'exclusive', holder: { agent_id: 'agent-001', acquired_at: new Date().toISOString(), expires_at: new Date(Date.now() + 30000).toISOString() }, queue: [] }); } ); /** * @route GET /api/v1/locks/deadlocks * @desc Get current deadlock information * @access Admin * * Response: 200 OK with { deadlocks: DeadlockInfoSchema[] } */ router.get('/locks/deadlocks', async (req: Request, res: Response) => { res.json({ deadlocks: [] }); }); // ============================================================================ // WEBSOCKET HANDLER (Type definitions for socket.io or ws) // ============================================================================ interface WebSocketHandler { /** * Subscribe client to events for specific resources */ subscribe(clientId: string, channels: string[]): void; /** * Unsubscribe client from channels */ unsubscribe(clientId: string, channels: string[]): void; /** * Broadcast event to all subscribers of a channel */ broadcast(channel: string, event: z.infer): void; /** * Send event to specific client */ emit(clientId: string, event: z.infer): void; } /** * WebSocket event channels */ const WebSocketChannels = { // Task-specific events task: (taskId: string) => `task:${taskId}`, // User-specific events user: (userId: string) => `user:${userId}`, // Agent-specific events agent: (agentId: string) => `agent:${agentId}`, // Resource-specific events resource: (type: string, id: string) => `resource:${type}:${id}`, // System-wide events (admin only) system: 'system', // Lock events locks: 'locks' } as const; // ============================================================================ // EXPORTS // ============================================================================ export { // Router router as approvalRouter, // Schemas (for use in other modules) IdSchema, TaskStateSchema, LockModeSchema, ApprovalActionSchema, ResourceRefSchema, TaskConfigSchema, RiskAssessmentSchema, PreviewResultSchema, CreateTaskRequestSchema, SubmitTaskRequestSchema, TaskResponseSchema, ListTasksQuerySchema, ApprovalRequestSchema, RespondApprovalRequestSchema, BatchApprovalRequestSchema, BatchApprovalResponseSchema, DelegationPolicySchema, AcquireLockRequestSchema, LockResponseSchema, LockHeartbeatRequestSchema, ReleaseLockRequestSchema, LockInfoSchema, DeadlockInfoSchema, WebSocketMessageSchema, LockAcquiredEventSchema, LockReleasedEventSchema, LockExpiredEventSchema, DeadlockDetectedEventSchema, ApprovalRequestedEventSchema, ApprovalRespondedEventSchema, TaskStateChangedEventSchema, TaskCompletedEventSchema, ApiErrorSchema, ValidationErrorSchema, // Types type CreateTaskRequest, type CreateTaskResponse, type ListTasksQuery, type SubmitTaskRequest, type RespondApprovalRequest, type BatchApprovalRequest, type BatchApprovalResponse, type AcquireLockRequest, type LockResponse, type LockHeartbeatRequest, type ReleaseLockRequest, type WebSocketHandler, WebSocketChannels };