| import { NextRequest, NextResponse } from 'next/server' |
| import { createHash } from 'node:crypto' |
| import { requireRole } from '@/lib/auth' |
| import { config } from '@/lib/config' |
| import { logger } from '@/lib/logger' |
| import path from 'node:path' |
|
|
| function gatewayUrl(p: string): string { |
| return `http://${config.gatewayHost}:${config.gatewayPort}${p}` |
| } |
|
|
| function execApprovalsPath(): string { |
| return path.join(config.openclawHome, 'exec-approvals.json') |
| } |
|
|
| function computeHash(raw: string): string { |
| return createHash('sha256').update(raw, 'utf8').digest('hex') |
| } |
|
|
| |
| |
| |
| |
| export async function GET(request: NextRequest) { |
| const auth = requireRole(request, 'operator') |
| if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) |
|
|
| const action = request.nextUrl.searchParams.get('action') |
|
|
| if (action === 'allowlist') { |
| return getAllowlist() |
| } |
|
|
| const controller = new AbortController() |
| const timeout = setTimeout(() => controller.abort(), 5000) |
|
|
| try { |
| const res = await fetch(gatewayUrl('/api/exec-approvals'), { |
| signal: controller.signal, |
| headers: { 'Accept': 'application/json' }, |
| }) |
| clearTimeout(timeout) |
|
|
| if (!res.ok) { |
| logger.warn({ status: res.status }, 'Gateway exec-approvals endpoint returned error') |
| return NextResponse.json({ approvals: [] }) |
| } |
|
|
| const data = await res.json() |
| return NextResponse.json(data) |
| } catch (err: any) { |
| clearTimeout(timeout) |
| if (err.name === 'AbortError') { |
| logger.warn('Gateway exec-approvals request timed out') |
| } else { |
| logger.warn({ err }, 'Gateway exec-approvals unreachable') |
| } |
| return NextResponse.json({ approvals: [] }) |
| } |
| } |
|
|
| async function getAllowlist(): Promise<NextResponse> { |
| const filePath = execApprovalsPath() |
| try { |
| const { readFile } = require('fs/promises') |
| const raw = await readFile(filePath, 'utf-8') |
| const parsed = JSON.parse(raw) |
| const agents: Record<string, { pattern: string }[]> = {} |
| if (parsed?.agents && typeof parsed.agents === 'object') { |
| for (const [agentId, agentConfig] of Object.entries(parsed.agents)) { |
| const cfg = agentConfig as any |
| if (Array.isArray(cfg?.allowlist)) { |
| agents[agentId] = cfg.allowlist.map((e: any) => ({ pattern: String(e?.pattern ?? '') })) |
| } else { |
| agents[agentId] = [] |
| } |
| } |
| } |
| return NextResponse.json({ agents, hash: computeHash(raw) }) |
| } catch (err: any) { |
| if (err.code === 'ENOENT') { |
| return NextResponse.json({ agents: {}, hash: computeHash('') }) |
| } |
| logger.warn({ err }, 'Failed to read exec-approvals config') |
| return NextResponse.json({ error: `Failed to read config: ${err.message}` }, { status: 500 }) |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function PUT(request: NextRequest) { |
| const auth = requireRole(request, 'operator') |
| if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) |
|
|
| let body: { agents: Record<string, { pattern: string }[]>; hash?: string } |
| try { |
| body = await request.json() |
| } catch { |
| return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) |
| } |
|
|
| if (!body.agents || typeof body.agents !== 'object') { |
| return NextResponse.json({ error: 'Missing required field: agents' }, { status: 400 }) |
| } |
|
|
| const filePath = execApprovalsPath() |
| try { |
| const { readFile, writeFile, mkdir } = require('fs/promises') |
| const { existsSync } = require('fs') |
|
|
| let parsed: any = { version: 1, agents: {} } |
| try { |
| const raw = await readFile(filePath, 'utf-8') |
| parsed = JSON.parse(raw) |
|
|
| if (body.hash) { |
| const serverHash = computeHash(raw) |
| if (body.hash !== serverHash) { |
| return NextResponse.json( |
| { error: 'Config has been modified. Please reload and try again.', code: 'CONFLICT' }, |
| { status: 409 }, |
| ) |
| } |
| } |
| } catch (err: any) { |
| if (err.code !== 'ENOENT') throw err |
| } |
|
|
| if (!parsed.agents) parsed.agents = {} |
|
|
| for (const [agentId, patterns] of Object.entries(body.agents)) { |
| if (!parsed.agents[agentId]) parsed.agents[agentId] = {} |
| if (patterns.length === 0) { |
| delete parsed.agents[agentId].allowlist |
| } else { |
| parsed.agents[agentId].allowlist = patterns.map((p: { pattern: string }) => ({ |
| pattern: String(p.pattern ?? ''), |
| })) |
| } |
| } |
|
|
| const dir = path.dirname(filePath) |
| if (!existsSync(dir)) { |
| await mkdir(dir, { recursive: true }) |
| } |
|
|
| const newRaw = JSON.stringify(parsed, null, 2) + '\n' |
| await writeFile(filePath, newRaw, { mode: 0o600 }) |
|
|
| return NextResponse.json({ ok: true, hash: computeHash(newRaw) }) |
| } catch (err: any) { |
| logger.error({ err }, 'Failed to save exec-approvals config') |
| return NextResponse.json({ error: `Failed to save: ${err.message}` }, { status: 500 }) |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function POST(request: NextRequest) { |
| const auth = requireRole(request, 'operator') |
| if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status }) |
|
|
| let body: { id: string; action: string; reason?: string } |
| try { |
| body = await request.json() |
| } catch { |
| return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }) |
| } |
|
|
| if (!body.id || typeof body.id !== 'string') { |
| return NextResponse.json({ error: 'Missing required field: id' }, { status: 400 }) |
| } |
|
|
| const validActions = ['approve', 'deny', 'always_allow'] |
| if (!validActions.includes(body.action)) { |
| return NextResponse.json({ error: `Invalid action. Must be one of: ${validActions.join(', ')}` }, { status: 400 }) |
| } |
|
|
| const controller = new AbortController() |
| const timeout = setTimeout(() => controller.abort(), 5000) |
|
|
| try { |
| const res = await fetch(gatewayUrl('/api/exec-approvals/respond'), { |
| method: 'POST', |
| signal: controller.signal, |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| id: body.id, |
| action: body.action, |
| reason: body.reason, |
| }), |
| }) |
| clearTimeout(timeout) |
|
|
| const data = await res.json() |
| return NextResponse.json(data, { status: res.status }) |
| } catch (err: any) { |
| clearTimeout(timeout) |
| if (err.name === 'AbortError') { |
| logger.error('Gateway exec-approvals respond request timed out') |
| return NextResponse.json({ error: 'Gateway request timed out' }, { status: 504 }) |
| } |
| logger.error({ err }, 'Gateway exec-approvals respond failed') |
| return NextResponse.json({ error: 'Gateway unreachable' }, { status: 502 }) |
| } |
| } |
|
|