Merge remote-tracking branch 'origin/main' into feat/medium-priority-v1.1

# Conflicts:
#	src/app/api/agents/route.ts
#	src/app/api/alerts/route.ts
#	src/app/api/auth/login/route.ts
#	src/app/api/spawn/route.ts
#	src/app/api/tasks/[id]/route.ts
#	src/app/api/tasks/route.ts
#	src/app/api/webhooks/route.ts
#	src/lib/validation.ts
This commit is contained in:
Nyk 2026-02-27 21:47:56 +07:00
commit c104b7e071
21 changed files with 351 additions and 71 deletions

9
.dockerignore Normal file
View File

@ -0,0 +1,9 @@
node_modules
.git
.data
.next
.env
*.md
.github
ops
scripts

23
Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM node:20-slim AS base
RUN corepack enable && corepack prepare pnpm@latest --activate
WORKDIR /app
FROM base AS deps
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
FROM base AS build
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm build
FROM node:20-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
COPY --from=build /app/.next/standalone ./
COPY --from=build /app/.next/static ./.next/static
COPY --from=build /app/public ./public
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

12
docker-compose.yml Normal file
View File

@ -0,0 +1,12 @@
services:
mission-control:
build: .
ports:
- "3000:3000"
env_file: .env
volumes:
- mc-data:/app/.data
restart: unless-stopped
volumes:
mc-data:

View File

@ -1,5 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
turbopack: {},
// Security headers

View File

@ -5,6 +5,7 @@ import { getTemplate, buildAgentConfig } from '@/lib/agent-templates';
import { writeAgentToConfig } from '@/lib/agent-sync';
import { logAuditEvent } from '@/lib/db';
import { getUserFromRequest, requireRole } from '@/lib/auth';
import { mutationLimiter } from '@/lib/rate-limit';
import { logger } from '@/lib/logger';
import { validateBody, createAgentSchema } from '@/lib/validation';
@ -109,6 +110,9 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'operator');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const rateCheck = mutationLimiter(request);
if (rateCheck) return rateCheck;
try {
const db = getDatabase();
const validated = await validateBody(request, createAgentSchema);
@ -134,11 +138,11 @@ export async function POST(request: NextRequest) {
const tpl = getTemplate(template);
if (tpl) {
const builtConfig = buildAgentConfig(tpl, (gateway_config || {}) as any);
finalConfig = { ...builtConfig, ...config };
finalConfig = { ...builtConfig, ...finalConfig };
if (!finalRole) finalRole = tpl.config.identity?.theme || tpl.type;
}
} else if (gateway_config) {
finalConfig = { ...config, ...gateway_config };
finalConfig = { ...finalConfig, ...(gateway_config as Record<string, any>) };
}
if (!name || !finalRole) {
@ -247,10 +251,13 @@ export async function PUT(request: NextRequest) {
const auth = requireRole(request, 'operator');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const rateCheck = mutationLimiter(request);
if (rateCheck) return rateCheck;
try {
const db = getDatabase();
const body = await request.json();
// Handle single agent update or bulk updates
if (body.name) {
// Single agent update

View File

@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
import { getDatabase } from '@/lib/db'
import { mutationLimiter } from '@/lib/rate-limit'
import { createAlertSchema } from '@/lib/validation'
interface AlertRule {
@ -45,6 +46,9 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'operator')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = mutationLimiter(request)
if (rateCheck) return rateCheck
const db = getDatabase()
const body = await request.json()
@ -103,6 +107,9 @@ export async function PUT(request: NextRequest) {
const auth = requireRole(request, 'operator')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = mutationLimiter(request)
if (rateCheck) return rateCheck
const db = getDatabase()
const body = await request.json()
const { id, ...updates } = body
@ -141,6 +148,9 @@ export async function DELETE(request: NextRequest) {
const auth = requireRole(request, 'admin')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = mutationLimiter(request)
if (rateCheck) return rateCheck
const db = getDatabase()
const body = await request.json()
const { id } = body

View File

@ -2,28 +2,13 @@ import { NextResponse } from 'next/server'
import { authenticateUser, createSession } from '@/lib/auth'
import { logAuditEvent } from '@/lib/db'
import { getMcSessionCookieOptions } from '@/lib/session-cookie'
import { loginLimiter } from '@/lib/rate-limit'
import { logger } from '@/lib/logger'
// Rate limiting: 5 attempts per minute per IP
const loginAttempts = new Map<string, { count: number; resetAt: number }>()
function checkRateLimit(ip: string): boolean {
const now = Date.now()
const entry = loginAttempts.get(ip)
if (!entry || now > entry.resetAt) {
loginAttempts.set(ip, { count: 1, resetAt: now + 60_000 })
return true
}
entry.count++
return entry.count <= 5
}
export async function POST(request: Request) {
try {
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
if (!checkRateLimit(ip)) {
return NextResponse.json({ error: 'Too many login attempts. Try again in a minute.' }, { status: 429 })
}
const rateCheck = loginLimiter(request)
if (rateCheck) return rateCheck
const { username, password } = await request.json()

View File

@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
import { getDatabase, logAuditEvent } from '@/lib/db'
import { heavyLimiter } from '@/lib/rate-limit'
/**
* GET /api/export?type=audit|tasks|activities|pipelines&format=csv|json&since=UNIX&until=UNIX
@ -10,6 +11,9 @@ export async function GET(request: NextRequest) {
const auth = requireRole(request, 'admin')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = heavyLimiter(request)
if (rateCheck) return rateCheck
const { searchParams } = new URL(request.url)
const type = searchParams.get('type')
const format = searchParams.get('format') || 'csv'

View File

@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { getDatabase, Notification } from '@/lib/db';
import { requireRole } from '@/lib/auth';
import { mutationLimiter } from '@/lib/rate-limit';
/**
* GET /api/notifications - Get notifications for a specific recipient
@ -138,6 +139,9 @@ export async function PUT(request: NextRequest) {
const auth = requireRole(request, 'operator');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const rateCheck = mutationLimiter(request);
if (rateCheck) return rateCheck;
try {
const db = getDatabase();
const body = await request.json();
@ -193,6 +197,9 @@ export async function DELETE(request: NextRequest) {
const auth = requireRole(request, 'admin');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const rateCheck = mutationLimiter(request);
if (rateCheck) return rateCheck;
try {
const db = getDatabase();
const body = await request.json();
@ -244,6 +251,9 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'operator');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const rateCheck = mutationLimiter(request);
if (rateCheck) return rateCheck;
try {
const db = getDatabase();
const body = await request.json();

View File

@ -0,0 +1,61 @@
import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
import { runClawdbot } from '@/lib/command'
import { db_helpers } from '@/lib/db'
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const auth = requireRole(request, 'operator')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
try {
const { id } = await params
const { action } = await request.json()
if (!['monitor', 'pause', 'terminate'].includes(action)) {
return NextResponse.json(
{ error: 'Invalid action. Must be: monitor, pause, terminate' },
{ status: 400 }
)
}
let result
if (action === 'terminate') {
result = await runClawdbot(
['-c', `sessions_kill("${id}")`],
{ timeoutMs: 10000 }
)
} else {
const message = action === 'monitor'
? JSON.stringify({ type: 'control', action: 'monitor' })
: JSON.stringify({ type: 'control', action: 'pause' })
result = await runClawdbot(
['-c', `sessions_send("${id}", ${JSON.stringify(message)})`],
{ timeoutMs: 10000 }
)
}
db_helpers.logActivity(
'session_control',
'session',
0,
auth.user.username,
`Session ${action}: ${id}`,
{ session_key: id, action }
)
return NextResponse.json({
success: true,
action,
session: id,
stdout: result.stdout.trim(),
})
} catch (error: any) {
return NextResponse.json(
{ error: error.message || 'Session control failed' },
{ status: 500 }
)
}
}

View File

@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
import { requireRole } from '@/lib/auth'
import { getDatabase, logAuditEvent } from '@/lib/db'
import { config } from '@/lib/config'
import { mutationLimiter } from '@/lib/rate-limit'
interface SettingRow {
key: string
@ -101,6 +102,9 @@ export async function PUT(request: NextRequest) {
const auth = requireRole(request, 'admin')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = mutationLimiter(request)
if (rateCheck) return rateCheck
const body = await request.json().catch(() => null)
if (!body?.settings || typeof body.settings !== 'object') {
return NextResponse.json({ error: 'settings object required' }, { status: 400 })
@ -157,6 +161,9 @@ export async function DELETE(request: NextRequest) {
const auth = requireRole(request, 'admin')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = mutationLimiter(request)
if (rateCheck) return rateCheck
let body: any
try { body = await request.json() } catch { return NextResponse.json({ error: 'Request body required' }, { status: 400 }) }
const key = body.key

View File

@ -4,12 +4,16 @@ import { requireRole } from '@/lib/auth'
import { config } from '@/lib/config'
import { readdir, readFile, stat } from 'fs/promises'
import { join } from 'path'
import { heavyLimiter } from '@/lib/rate-limit'
import { logger } from '@/lib/logger'
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'operator')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = heavyLimiter(request)
if (rateCheck) return rateCheck
try {
const { task, model, label, timeoutSeconds } = await request.json()

View File

@ -6,6 +6,7 @@ import { config } from '@/lib/config'
import { getDatabase } from '@/lib/db'
import { getAllGatewaySessions, getAgentLiveStatuses } from '@/lib/sessions'
import { requireRole } from '@/lib/auth'
import { MODEL_CATALOG } from '@/lib/models'
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'viewer')
@ -340,17 +341,8 @@ async function getGatewayStatus() {
async function getAvailableModels() {
// This would typically query the gateway or config files
// For now, return the models from AGENTS.md
const models = [
{ alias: 'haiku', name: 'anthropic/claude-3-5-haiku-latest', provider: 'anthropic', description: 'Ultra-cheap, simple tasks', costPer1k: 0.25 },
{ alias: 'sonnet', name: 'anthropic/claude-sonnet-4-20250514', provider: 'anthropic', description: 'Standard workhorse', costPer1k: 3.0 },
{ alias: 'opus', name: 'anthropic/claude-opus-4-5', provider: 'anthropic', description: 'Premium quality', costPer1k: 15.0 },
{ alias: 'deepseek', name: 'ollama/deepseek-r1:14b', provider: 'ollama', description: 'Local reasoning (free)', costPer1k: 0.0 },
{ alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPer1k: 0.05 },
{ alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPer1k: 0.59 },
{ alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPer1k: 1.0 },
{ alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective (1/10th price), strong coding', costPer1k: 0.3 },
]
// Model catalog is the single source of truth
const models = [...MODEL_CATALOG]
try {
// Check which Ollama models are available locally

View File

@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { getDatabase, Task, db_helpers } from '@/lib/db';
import { eventBus } from '@/lib/event-bus';
import { getUserFromRequest, requireRole } from '@/lib/auth';
import { mutationLimiter } from '@/lib/rate-limit';
import { logger } from '@/lib/logger';
import { validateBody, updateTaskSchema } from '@/lib/validation';
@ -65,6 +66,9 @@ export async function PUT(
const auth = requireRole(request, 'operator');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const rateCheck = mutationLimiter(request);
if (rateCheck) return rateCheck;
try {
const db = getDatabase();
const resolvedParams = await params;
@ -259,6 +263,9 @@ export async function DELETE(
const auth = requireRole(request, 'operator');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const rateCheck = mutationLimiter(request);
if (rateCheck) return rateCheck;
try {
const db = getDatabase();
const resolvedParams = await params;

View File

@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
import { getDatabase, Task, db_helpers } from '@/lib/db';
import { eventBus } from '@/lib/event-bus';
import { requireRole } from '@/lib/auth';
import { mutationLimiter } from '@/lib/rate-limit';
import { logger } from '@/lib/logger';
import { validateBody, createTaskSchema } from '@/lib/validation';
@ -97,6 +98,9 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'operator');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const rateCheck = mutationLimiter(request);
if (rateCheck) return rateCheck;
try {
const db = getDatabase();
const validated = await validateBody(request, createTaskSchema);
@ -199,6 +203,9 @@ export async function PUT(request: NextRequest) {
const auth = requireRole(request, 'operator');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
const rateCheck = mutationLimiter(request);
if (rateCheck) return rateCheck;
try {
const db = getDatabase();
const { tasks } = await request.json();

View File

@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'
import { getDatabase } from '@/lib/db'
import { requireRole } from '@/lib/auth'
import { randomBytes, createHmac } from 'crypto'
import { mutationLimiter } from '@/lib/rate-limit'
import { logger } from '@/lib/logger'
import { validateBody, createWebhookSchema } from '@/lib/validation'
@ -45,6 +46,9 @@ export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = mutationLimiter(request)
if (rateCheck) return rateCheck
try {
const db = getDatabase()
const validated = await validateBody(request, createWebhookSchema)
@ -82,6 +86,9 @@ export async function PUT(request: NextRequest) {
const auth = requireRole(request, 'admin')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = mutationLimiter(request)
if (rateCheck) return rateCheck
try {
const db = getDatabase()
const body = await request.json()
@ -137,6 +144,9 @@ export async function DELETE(request: NextRequest) {
const auth = requireRole(request, 'admin')
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
const rateCheck = mutationLimiter(request)
if (rateCheck) return rateCheck
try {
const db = getDatabase()
let body: any

View File

@ -26,6 +26,7 @@ export function SessionDetailsPanel() {
useSmartPoll(loadSessions, 60000, { pauseWhenConnected: true })
const [controllingSession, setControllingSession] = useState<string | null>(null)
const [sessionFilter, setSessionFilter] = useState<'all' | 'active' | 'idle'>('all')
const [sortBy, setSortBy] = useState<'age' | 'tokens' | 'model'>('age')
const [expandedSession, setExpandedSession] = useState<string | null>(null)
@ -323,36 +324,80 @@ export function SessionDetailsPanel() {
{/* Actions */}
<div className="flex space-x-2">
<button
className="px-3 py-1 text-xs bg-blue-500/20 text-blue-400 border border-blue-500/30 rounded hover:bg-blue-500/30 transition-colors"
onClick={(e) => {
className="px-3 py-1 text-xs bg-blue-500/20 text-blue-400 border border-blue-500/30 rounded hover:bg-blue-500/30 transition-colors disabled:opacity-50"
disabled={controllingSession !== null}
onClick={async (e) => {
e.stopPropagation()
// TODO: Implement session monitoring
console.log('Monitor session:', session.id)
}}
>
Monitor
</button>
<button
className="px-3 py-1 text-xs bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 rounded hover:bg-yellow-500/30 transition-colors"
onClick={(e) => {
e.stopPropagation()
// TODO: Implement session pause
console.log('Pause session:', session.id)
}}
>
Pause
</button>
<button
className="px-3 py-1 text-xs bg-red-500/20 text-red-400 border border-red-500/30 rounded hover:bg-red-500/30 transition-colors"
onClick={(e) => {
e.stopPropagation()
if (confirm('Are you sure you want to terminate this session?')) {
// TODO: Implement session termination
console.log('Terminate session:', session.id)
setControllingSession(`monitor-${session.id}`)
try {
const res = await fetch(`/api/sessions/${session.id}/control`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'monitor' }),
})
if (!res.ok) {
const data = await res.json()
alert(data.error || 'Failed to monitor session')
}
} catch {
alert('Failed to monitor session')
} finally {
setControllingSession(null)
}
}}
>
Terminate
{controllingSession === `monitor-${session.id}` ? 'Working...' : 'Monitor'}
</button>
<button
className="px-3 py-1 text-xs bg-yellow-500/20 text-yellow-400 border border-yellow-500/30 rounded hover:bg-yellow-500/30 transition-colors disabled:opacity-50"
disabled={controllingSession !== null}
onClick={async (e) => {
e.stopPropagation()
setControllingSession(`pause-${session.id}`)
try {
const res = await fetch(`/api/sessions/${session.id}/control`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'pause' }),
})
if (!res.ok) {
const data = await res.json()
alert(data.error || 'Failed to pause session')
}
} catch {
alert('Failed to pause session')
} finally {
setControllingSession(null)
}
}}
>
{controllingSession === `pause-${session.id}` ? 'Working...' : 'Pause'}
</button>
<button
className="px-3 py-1 text-xs bg-red-500/20 text-red-400 border border-red-500/30 rounded hover:bg-red-500/30 transition-colors disabled:opacity-50"
disabled={controllingSession !== null}
onClick={async (e) => {
e.stopPropagation()
if (!window.confirm('Are you sure you want to terminate this session?')) return
setControllingSession(`terminate-${session.id}`)
try {
const res = await fetch(`/api/sessions/${session.id}/control`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'terminate' }),
})
if (!res.ok) {
const data = await res.json()
alert(data.error || 'Failed to terminate session')
}
} catch {
alert('Failed to terminate session')
} finally {
setControllingSession(null)
}
}}
>
{controllingSession === `terminate-${session.id}` ? 'Working...' : 'Terminate'}
</button>
</div>
</div>

30
src/lib/models.ts Normal file
View File

@ -0,0 +1,30 @@
export interface ModelConfig {
alias: string
name: string
provider: string
description: string
costPer1k: number
}
export const MODEL_CATALOG: ModelConfig[] = [
{ alias: 'haiku', name: 'anthropic/claude-3-5-haiku-latest', provider: 'anthropic', description: 'Ultra-cheap, simple tasks', costPer1k: 0.25 },
{ alias: 'sonnet', name: 'anthropic/claude-sonnet-4-20250514', provider: 'anthropic', description: 'Standard workhorse', costPer1k: 3.0 },
{ alias: 'opus', name: 'anthropic/claude-opus-4-5', provider: 'anthropic', description: 'Premium quality', costPer1k: 15.0 },
{ alias: 'deepseek', name: 'ollama/deepseek-r1:14b', provider: 'ollama', description: 'Local reasoning (free)', costPer1k: 0.0 },
{ alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPer1k: 0.05 },
{ alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPer1k: 0.59 },
{ alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPer1k: 1.0 },
{ alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective (1/10th price), strong coding', costPer1k: 0.3 },
]
export function getModelByAlias(alias: string): ModelConfig | undefined {
return MODEL_CATALOG.find(m => m.alias === alias)
}
export function getModelByName(name: string): ModelConfig | undefined {
return MODEL_CATALOG.find(m => m.name === name)
}
export function getAllModels(): ModelConfig[] {
return [...MODEL_CATALOG]
}

64
src/lib/rate-limit.ts Normal file
View File

@ -0,0 +1,64 @@
import { NextResponse } from 'next/server'
interface RateLimitEntry {
count: number
resetAt: number
}
interface RateLimiterOptions {
windowMs: number
maxRequests: number
message?: string
}
export function createRateLimiter(options: RateLimiterOptions) {
const store = new Map<string, RateLimitEntry>()
// Periodic cleanup every 60s
const cleanupInterval = setInterval(() => {
const now = Date.now()
for (const [key, entry] of store) {
if (now > entry.resetAt) store.delete(key)
}
}, 60_000)
// Don't prevent process exit
if (cleanupInterval.unref) cleanupInterval.unref()
return function checkRateLimit(request: Request): NextResponse | null {
const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown'
const now = Date.now()
const entry = store.get(ip)
if (!entry || now > entry.resetAt) {
store.set(ip, { count: 1, resetAt: now + options.windowMs })
return null
}
entry.count++
if (entry.count > options.maxRequests) {
return NextResponse.json(
{ error: options.message || 'Too many requests. Please try again later.' },
{ status: 429 }
)
}
return null
}
}
export const loginLimiter = createRateLimiter({
windowMs: 60_000,
maxRequests: 5,
message: 'Too many login attempts. Try again in a minute.',
})
export const mutationLimiter = createRateLimiter({
windowMs: 60_000,
maxRequests: 60,
})
export const heavyLimiter = createRateLimiter({
windowMs: 60_000,
maxRequests: 10,
message: 'Too many requests for this resource. Please try again later.',
})

View File

@ -36,8 +36,8 @@ export const createTaskSchema = z.object({
due_date: z.number().optional(),
estimated_hours: z.number().min(0).optional(),
actual_hours: z.number().min(0).optional(),
tags: z.array(z.string()).default([]),
metadata: z.record(z.string(), z.unknown()).default({}),
tags: z.array(z.string()).default([] as string[]),
metadata: z.record(z.string(), z.unknown()).default({} as Record<string, unknown>),
})
export const updateTaskSchema = createTaskSchema.partial()
@ -48,7 +48,7 @@ export const createAgentSchema = z.object({
session_key: z.string().max(200).optional(),
soul_content: z.string().max(50000).optional(),
status: z.enum(['online', 'offline', 'busy', 'idle', 'error']).default('offline'),
config: z.record(z.string(), z.unknown()).default({}),
config: z.record(z.string(), z.unknown()).default({} as Record<string, unknown>),
template: z.string().max(100).optional(),
gateway_config: z.record(z.string(), z.unknown()).optional(),
write_to_gateway: z.boolean().optional(),

View File

@ -2,6 +2,7 @@
import { create } from 'zustand'
import { subscribeWithSelector } from 'zustand/middleware'
import { MODEL_CATALOG } from '@/lib/models'
// Enhanced types for Mission Control
export interface Session {
@ -523,16 +524,7 @@ export const useMissionControl = create<MissionControlStore>()(
},
// Model Configuration
availableModels: [
{ alias: 'haiku', name: 'anthropic/claude-3-5-haiku-latest', provider: 'anthropic', description: 'Ultra-cheap, simple tasks', costPer1k: 0.25 },
{ alias: 'sonnet', name: 'anthropic/claude-sonnet-4-20250514', provider: 'anthropic', description: 'Standard workhorse', costPer1k: 3.0 },
{ alias: 'opus', name: 'anthropic/claude-opus-4-5', provider: 'anthropic', description: 'Premium quality', costPer1k: 15.0 },
{ alias: 'deepseek', name: 'ollama/deepseek-r1:14b', provider: 'ollama', description: 'Local reasoning (free)', costPer1k: 0.0 },
{ alias: 'groq-fast', name: 'groq/llama-3.1-8b-instant', provider: 'groq', description: '840 tok/s, ultra fast', costPer1k: 0.05 },
{ alias: 'groq', name: 'groq/llama-3.3-70b-versatile', provider: 'groq', description: 'Fast + quality balance', costPer1k: 0.59 },
{ alias: 'kimi', name: 'moonshot/kimi-k2.5', provider: 'moonshot', description: 'Alternative provider', costPer1k: 1.0 },
{ alias: 'minimax', name: 'minimax/minimax-m2.1', provider: 'minimax', description: 'Cost-effective (1/10th price), strong coding', costPer1k: 0.3 },
],
availableModels: [...MODEL_CATALOG],
setAvailableModels: (models) => set({ availableModels: models }),
// Auth