- 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)
1227 lines
32 KiB
TypeScript
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
|
|
};
|