Files
community-ade/docs/api-spec.ts
Ani (Annie Tunturi) ce8dd84840 feat: Add approval system and agent config UI
- Omega (Kimi-K2.5): Approval system architecture
  - design.md: Full system architecture with state machines
  - api-spec.ts: Express routes + Zod schemas (33KB)
  - redis-schema.md: Redis key patterns (19KB)
  - ui-components.md: Dashboard UI specs (31KB)

- Epsilon (Nemotron-3-super): Agent configuration UI
  - AgentWizard: 5-step creation flow
  - AgentConfigPanel: Parameter tuning
  - AgentCard: Health monitoring
  - AgentList: List/grid views
  - hooks/useAgents.ts: WebSocket integration
  - types/agent.ts: TypeScript definitions

Total: 150KB new code, 22 components

👾 Generated with [Letta Code](https://letta.com)
2026-03-18 12:23:59 -04:00

1227 lines
32 KiB
TypeScript

/**
* 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<typeof CreateTaskRequestSchema>;
type CreateTaskResponse = z.infer<typeof TaskResponseSchema>;
type ListTasksQuery = z.infer<typeof ListTasksQuerySchema>;
type SubmitTaskRequest = z.infer<typeof SubmitTaskRequestSchema>;
type RespondApprovalRequest = z.infer<typeof RespondApprovalRequestSchema>;
type BatchApprovalRequest = z.infer<typeof BatchApprovalRequestSchema>;
type BatchApprovalResponse = z.infer<typeof BatchApprovalResponseSchema>;
type AcquireLockRequest = z.infer<typeof AcquireLockRequestSchema>;
type LockResponse = z.infer<typeof LockResponseSchema>;
type LockHeartbeatRequest = z.infer<typeof LockHeartbeatRequestSchema>;
type ReleaseLockRequest = z.infer<typeof ReleaseLockRequestSchema>;
// ============================================================================
// EXPRESS ROUTES
// ============================================================================
const router = Router();
// Middleware: Validate request body against schema
const validateBody = <T>(schema: z.ZodSchema<T>) => {
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 = <T>(schema: z.ZodSchema<T>) => {
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 = <T>(schema: z.ZodSchema<T>) => {
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<typeof WebSocketMessageSchema>): void;
/**
* Send event to specific client
*/
emit(clientId: string, event: z.infer<typeof WebSocketMessageSchema>): 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
};