Files
MikrotikManager/packages/contracts/src/mcp.ts
T

175 lines
8.6 KiB
TypeScript

import { z } from "zod"
const identifier = z.string().trim().min(1).max(200)
const serverId = z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
const instant = z.string().datetime({ offset: true })
const uniqueStrings = z.array(identifier).max(1000).refine(values => new Set(values).size === values.length, "Повторяющиеся значения")
const serverIds = z.array(serverId).max(1000).refine(values => new Set(values).size === values.length, "Повторяющиеся серверы")
export const mcpModeSchema = z.enum(["observer", "approval", "autonomous"])
export const mcpLimitsSchema = z.object({
callsPerMinute: z.number().int().min(1).max(10000).default(60),
concurrentReads: z.number().int().min(1).max(100).default(4),
mutationsPerHour: z.number().int().min(1).max(10000).default(10),
serversPerPlan: z.number().int().min(1).max(1000).default(5),
objectsPerPlan: z.number().int().min(1).max(10000).default(100),
}).strict()
export const mcpScopeSchema = z.object({
serverIds,
operationIds: uniqueStrings,
allowGlobal: z.boolean().default(false),
}).strict()
export const mcpTemporaryGrantSchema = z.object({
id: identifier,
startsAt: instant,
expiresAt: instant,
scope: mcpScopeSchema,
}).strict().refine(grant => {
const duration = Date.parse(grant.expiresAt) - Date.parse(grant.startsAt)
return duration > 0 && duration <= 24 * 60 * 60 * 1000
}, "Временный доступ должен действовать не более 24 часов")
export const mcpConnectionPolicySchema = z.object({
id: identifier,
version: z.number().int().positive(),
status: z.enum(["active", "paused", "revoked"]),
expiresAt: instant,
mode: mcpModeSchema,
permissions: uniqueStrings,
scope: mcpScopeSchema,
dangerousPolicy: z.enum(["approval", "explicit-allow"]).default("approval"),
autonomousDangerousOperationIds: uniqueStrings.default([]),
limits: mcpLimitsSchema.default(() => mcpLimitsSchema.parse({})),
}).strict()
export const mcpTargetSchema = z.discriminatedUnion("scope", [
z.object({ scope: z.literal("servers"), serverIds: serverIds.refine(ids => ids.length > 0, "Выберите серверы") }).strict(),
z.object({ scope: z.literal("global") }).strict(),
z.object({ scope: z.literal("connection") }).strict(),
])
export const mcpObjectRefSchema = z.object({
kind: identifier,
id: identifier,
serverId: serverId.optional(),
}).strict()
export const mcpProtectedObjectSchema = mcpObjectRefSchema.extend({
protection: z.enum(["deny", "human-approval-required"]),
}).strict()
export type McpObjectRef = z.infer<typeof mcpObjectRefSchema>
export const mcpOperationDescriptorSchema = z.object({
id: identifier,
toolName: z.string().regex(/^mm_[a-z0-9_]+$/),
version: z.number().int().min(1).max(2147483647),
title: identifier,
section: identifier,
effect: z.enum(["read", "write", "diagnostic"]),
risk: z.enum(["ordinary", "dangerous"]),
scope: z.enum(["servers", "global", "connection"]),
requiredPermissions: uniqueStrings,
implementation: z.enum(["planned", "available"]),
annotations: z.object({
readOnlyHint: z.boolean(),
destructiveHint: z.boolean(),
idempotentHint: z.boolean(),
openWorldHint: z.boolean(),
}).strict(),
}).strict()
// This payload is hypothetical evidence for the human-only simulator, never authorization.
// Future executors must construct runtime evidence from trusted repositories and locks.
export const mcpPolicyPreviewRequestSchema = z.object({
operationId: identifier,
policy: mcpConnectionPolicySchema,
ownerPermissions: uniqueStrings,
target: mcpTargetSchema,
temporaryGrant: mcpTemporaryGrantSchema.optional(),
objectCount: z.number().int().nonnegative().max(1000000).default(0),
objects: z.array(mcpObjectRefSchema).max(1000).default([]),
protections: z.array(mcpProtectedObjectSchema).max(1000).default([]),
usage: z.object({
callsInMinute: z.number().int().nonnegative().default(0),
concurrentReads: z.number().int().nonnegative().default(0),
mutationsInHour: z.number().int().nonnegative().default(0),
}).strict().default(() => ({ callsInMinute: 0, concurrentReads: 0, mutationsInHour: 0 })),
auditAvailable: z.boolean().default(true),
}).strict()
export const mcpPolicyReasonCodeSchema = z.enum([
"UNKNOWN_OPERATION", "CONNECTION_INACTIVE", "CONNECTION_EXPIRED", "OPERATION_DENIED",
"OWNER_PERMISSION_MISSING", "CONNECTION_PERMISSION_MISSING", "TARGET_SCOPE_MISMATCH",
"SERVER_DENIED", "GLOBAL_DENIED", "GRANT_INACTIVE", "GRANT_SCOPE_DENIED",
"INVALID_OBJECT_SCOPE", "OBSERVER_WRITE_DENIED", "RATE_LIMITED", "BUDGET_EXCEEDED",
"PLAN_LIMIT_EXCEEDED", "PROTECTED_OBJECT", "AUDIT_UNAVAILABLE", "APPROVAL_REQUIRED",
])
export const mcpPolicyDecisionSchema = z.object({
decision: z.enum(["denied", "approval-required", "allowed"]),
reasons: z.array(z.object({ code: mcpPolicyReasonCodeSchema, message: z.string() }).strict()),
limits: mcpLimitsSchema,
}).strict()
export const mcpPolicyPreviewResponseSchema = z.object({
simulation: z.literal(true),
executionAvailable: z.literal(false),
evaluatedAt: instant,
policyVersion: z.number().int().positive(),
result: mcpPolicyDecisionSchema,
}).strict()
export const mcpCatalogResponseSchema = z.object({
stage: z.enum(["foundation", "runtime"]),
executionAvailable: z.boolean(),
operations: z.array(mcpOperationDescriptorSchema),
}).strict()
export type McpLimits = z.infer<typeof mcpLimitsSchema>
export type McpConnectionPolicy = z.infer<typeof mcpConnectionPolicySchema>
export type McpOperationDescriptor = z.infer<typeof mcpOperationDescriptorSchema>
export type McpPolicyPreviewRequest = z.infer<typeof mcpPolicyPreviewRequestSchema>
export type McpPolicyDecision = z.infer<typeof mcpPolicyDecisionSchema>
export type McpPolicyReasonCode = z.infer<typeof mcpPolicyReasonCodeSchema>
export const mcpConnectionCreateSchema = z.object({
name: z.string().trim().min(1).max(100),
expiresAt: instant,
mode: mcpModeSchema.default("observer"),
permissions: uniqueStrings,
scope: mcpScopeSchema,
dangerousPolicy: z.enum(["approval", "explicit-allow"]).default("approval"),
autonomousDangerousOperationIds: uniqueStrings.default([]),
limits: mcpLimitsSchema.default(() => mcpLimitsSchema.parse({})),
}).strict()
export const mcpConnectionSchema = z.object({
id: z.string().uuid(), name: z.string(), ownerId: z.string(),
policy: mcpConnectionPolicySchema, createdAt: instant, updatedAt: instant,
lastUsedAt: instant.nullable(), generationValid: z.boolean(),
ownerValidUntil: instant.optional(),
}).strict()
export const mcpConnectionsSchema = z.object({ connections: z.array(mcpConnectionSchema) }).strict()
export const mcpSettingsSchema = z.object({
enabled: z.boolean(), requestsRetentionDays: z.number().int().min(1).max(365),
auditRetentionDays: z.number().int().min(30).max(3650),
}).strict()
export const mcpSettingsPatchSchema = mcpSettingsSchema.partial().strict().refine(value => Object.keys(value).length > 0)
export const mcpPageQuerySchema = z.object({
limit: z.coerce.number().int().min(1).max(100).default(50),
operationId: z.string().uuid().optional(),
cursor: z.union([z.string().uuid(), z.string().regex(/^[1-9]\d{0,18}$/)]).optional(),
connectionId: z.string().uuid().optional(),
status: z.string().max(50).optional(),
tool: z.string().max(200).optional(),
userId: z.string().trim().min(1).max(200).optional(),
serverId: z.coerce.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional(), from: instant.optional(), to: instant.optional(),
}).strict()
export type McpConnection = z.infer<typeof mcpConnectionSchema>
export type McpConnectionCreate = z.infer<typeof mcpConnectionCreateSchema>
const pollOwnerCapacitySchema=z.object({used:z.number().int().min(0).max(8),limit:z.literal(8),remaining:z.number().int().min(0).max(8)}).strict().refine(value=>value.used+value.remaining<=value.limit)
const pollGlobalCapacitySchema=z.object({used:z.number().int().min(0).max(128),limit:z.literal(128),remaining:z.number().int().min(0).max(128)}).strict().refine(value=>value.used+value.remaining<=value.limit)
/** Heavy reservations. Retained archive bytes/rows can further reduce the
* remaining capacity; this observation never grants an execution admission. */
export const mcpPollStorageCapacitySchema=z.discriminatedUnion("status",[
z.object({status:z.literal("unavailable")}).strict(),
z.object({status:z.literal("observed"),owner:pollOwnerCapacitySchema,
global:z.object({exhausted:z.boolean(),usage:pollGlobalCapacitySchema.nullable()}).strict()
.refine(value=>value.usage===null||value.exhausted===(value.usage.remaining===0)),
retentionDays:z.number().int().min(1).max(365)}).strict(),
])
export type McpPollStorageCapacity=z.infer<typeof mcpPollStorageCapacitySchema>