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:
commit
c104b7e071
|
|
@ -0,0 +1,9 @@
|
|||
node_modules
|
||||
.git
|
||||
.data
|
||||
.next
|
||||
.env
|
||||
*.md
|
||||
.github
|
||||
ops
|
||||
scripts
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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:
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
output: 'standalone',
|
||||
turbopack: {},
|
||||
|
||||
// Security headers
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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.',
|
||||
})
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue