diff --git a/README.md b/README.md index f6e0e53..7f0fccc 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,7 @@ bash scripts/security-audit.sh ### Known Limitations -- **CSP still includes `unsafe-inline`** — `unsafe-eval` has been removed, but inline styles remain for framework compatibility +- No major security limitations currently tracked here for CSP; policy now uses per-request nonces (no `unsafe-inline` / `unsafe-eval`). ### Security Considerations diff --git a/docs/SECURITY-HARDENING.md b/docs/SECURITY-HARDENING.md index a6c941f..f908bef 100644 --- a/docs/SECURITY-HARDENING.md +++ b/docs/SECURITY-HARDENING.md @@ -105,7 +105,7 @@ Mission Control sets these headers automatically: | Header | Value | |--------|-------| -| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'unsafe-inline' 'nonce-...'` | +| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'nonce-' 'strict-dynamic'; style-src 'self' 'nonce-'` | | `X-Frame-Options` | `DENY` | | `X-Content-Type-Options` | `nosniff` | | `Referrer-Policy` | `strict-origin-when-cross-origin` | diff --git a/next.config.js b/next.config.js index 9522c9c..9b4728e 100644 --- a/next.config.js +++ b/next.config.js @@ -9,20 +9,8 @@ const nextConfig = { transpilePackages: ['react-markdown', 'remark-gfm'], // Security headers + // Content-Security-Policy is set in src/proxy.ts with a per-request nonce. async headers() { - const googleEnabled = !!(process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || process.env.GOOGLE_CLIENT_ID) - - const csp = [ - `default-src 'self'`, - `script-src 'self' 'unsafe-inline' blob:${googleEnabled ? ' https://accounts.google.com' : ''}`, - `style-src 'self' 'unsafe-inline'`, - `connect-src 'self' ws: wss: http://127.0.0.1:* http://localhost:* https://cdn.jsdelivr.net`, - `img-src 'self' data: blob:${googleEnabled ? ' https://*.googleusercontent.com https://lh3.googleusercontent.com' : ''}`, - `font-src 'self' data:`, - `frame-src 'self'${googleEnabled ? ' https://accounts.google.com' : ''}`, - `worker-src 'self' blob:`, - ].join('; ') - return [ { source: '/:path*', @@ -30,7 +18,6 @@ const nextConfig = { { key: 'X-Frame-Options', value: 'DENY' }, { key: 'X-Content-Type-Options', value: 'nosniff' }, { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' }, - { key: 'Content-Security-Policy', value: csp }, { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' }, ...(process.env.MC_ENABLE_HSTS === '1' ? [ { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' } diff --git a/src/app/api/agents/route.ts b/src/app/api/agents/route.ts index dc061af..8e26b48 100644 --- a/src/app/api/agents/route.ts +++ b/src/app/api/agents/route.ts @@ -58,30 +58,57 @@ export async function GET(request: NextRequest) { config: enrichAgentConfigFromWorkspace(agent.config ? JSON.parse(agent.config) : {}) })); - // Get task counts for each agent (prepare once, reuse per agent) - const taskCountStmt = db.prepare(` - SELECT - COUNT(*) as total, - SUM(CASE WHEN status = 'assigned' THEN 1 ELSE 0 END) as assigned, - SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress, - SUM(CASE WHEN status = 'quality_review' THEN 1 ELSE 0 END) as quality_review, - SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done - FROM tasks - WHERE assigned_to = ? AND workspace_id = ? - `); + // Get task counts for all listed agents in one query (avoids N+1 queries) + const agentNames = agentsWithParsedData.map(agent => agent.name).filter(Boolean) + const taskStatsByAgent = new Map() + + if (agentNames.length > 0) { + const placeholders = agentNames.map(() => '?').join(', ') + const groupedTaskStats = db.prepare(` + SELECT + assigned_to, + COUNT(*) as total, + SUM(CASE WHEN status = 'assigned' THEN 1 ELSE 0 END) as assigned, + SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress, + SUM(CASE WHEN status = 'quality_review' THEN 1 ELSE 0 END) as quality_review, + SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as done + FROM tasks + WHERE workspace_id = ? AND assigned_to IN (${placeholders}) + GROUP BY assigned_to + `).all(workspaceId, ...agentNames) as Array<{ + assigned_to: string + total: number | null + assigned: number | null + in_progress: number | null + quality_review: number | null + done: number | null + }> + + for (const row of groupedTaskStats) { + taskStatsByAgent.set(row.assigned_to, { + total: row.total || 0, + assigned: row.assigned || 0, + in_progress: row.in_progress || 0, + quality_review: row.quality_review || 0, + done: row.done || 0, + }) + } + } const agentsWithStats = agentsWithParsedData.map(agent => { - const taskStats = taskCountStmt.get(agent.name, workspaceId) as any; + const taskStats = taskStatsByAgent.get(agent.name) || { + total: 0, + assigned: 0, + in_progress: 0, + quality_review: 0, + done: 0, + } return { ...agent, taskStats: { - total: taskStats.total || 0, - assigned: taskStats.assigned || 0, - in_progress: taskStats.in_progress || 0, - quality_review: taskStats.quality_review || 0, - done: taskStats.done || 0, - completed: taskStats.done || 0 + ...taskStats, + completed: taskStats.done, } }; }); diff --git a/src/components/layout/nav-rail.tsx b/src/components/layout/nav-rail.tsx index 185e4df..04ca850 100644 --- a/src/components/layout/nav-rail.tsx +++ b/src/components/layout/nav-rail.tsx @@ -1004,10 +1004,11 @@ function ContextSwitcher({ currentUser, isAdmin, isLocal, isConnected, tenants, {!createMode ? (