repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
The Living OS cockpit
stars
latest
clone command
git clone gitlawb://did:key:z6Mku78K...XywC/living-os-cockp...git clone gitlawb://did:key:z6Mku78K.../living-os-cockp...59751530feat: surface worker supervisor health in live work8h ago| #1 | import { NextRequest, NextResponse } from 'next/server'; |
| #2 | import { spawn } from 'child_process'; |
| #3 | import { promises as fs } from 'fs'; |
| #4 | import { basename, join, resolve } from 'path'; |
| #5 | import { getUserContext, AuthMismatchError, AuthRequiredError } from '@/lib/user-context'; |
| #6 | |
| #7 | export const runtime = 'nodejs'; |
| #8 | |
| #9 | const QUEUE_DIR = '/home/kingbau/Documents/Aethon-Core/data/approval_queue/decisions'; |
| #10 | const AETHON_ROOT = '/home/kingbau/Documents/Aethon-Core'; |
| #11 | const RUNNER = join(AETHON_ROOT, 'scripts', 'agentic_tool_runner.py'); |
| #12 | const NEWS_DRAFTS_DIR = join(AETHON_ROOT, 'data', 'aethon_drafts', 'news'); |
| #13 | const CONTENT_DRAFTS_DIR = join(AETHON_ROOT, 'data', 'content', 'drafts'); |
| #14 | const USER_MAP: Record<string, string> = { |
| #15 | master: 'kingbau', |
| #16 | queen_maaxx: 'maaxx', |
| #17 | }; |
| #18 | const FIXTURE_MARKERS = ['/tmp/', 'pytest', 'test_']; |
| #19 | |
| #20 | type Decision = Record<string, any>; |
| #21 | |
| #22 | function nowIso() { |
| #23 | return new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00'); |
| #24 | } |
| #25 | |
| #26 | function safeId(value: string) { |
| #27 | return value.replace(/[^a-zA-Z0-9_.:-]/g, ''); |
| #28 | } |
| #29 | |
| #30 | function safeSlug(value: string) { |
| #31 | return value.replace(/[^a-zA-Z0-9_-]/g, ''); |
| #32 | } |
| #33 | |
| #34 | function isRecord(value: unknown): value is Record<string, any> { |
| #35 | return Boolean(value && typeof value === 'object' && !Array.isArray(value)); |
| #36 | } |
| #37 | |
| #38 | function isFixtureDecision(decision: Decision) { |
| #39 | if (decision.action === 'delegation_harness_execute_step' || decision.source === 'b2_delegation_harness') { |
| #40 | return false; |
| #41 | } |
| #42 | const blob = JSON.stringify({ |
| #43 | id: decision.id, |
| #44 | title: decision.title, |
| #45 | action: decision.action, |
| #46 | source_ref: decision.source_ref, |
| #47 | context: decision.context, |
| #48 | }).toLowerCase(); |
| #49 | return FIXTURE_MARKERS.some(marker => blob.includes(marker)); |
| #50 | } |
| #51 | |
| #52 | function describeTarget(decision: Decision) { |
| #53 | const context = isRecord(decision.context) ? decision.context : {}; |
| #54 | const task = isRecord(context.task) ? context.task : {}; |
| #55 | const payload = isRecord(context.payload) ? context.payload : {}; |
| #56 | const email = String(payload.to ?? payload.recipient ?? task.to ?? task.recipient ?? context.to ?? '').trim(); |
| #57 | const subject = String(payload.subject ?? task.subject ?? context.subject ?? '').trim(); |
| #58 | if (decision.action === 'email_send') { |
| #59 | const parts = [email ? `Recipient: ${email}` : '', subject ? `Subject: ${subject}` : ''].filter(Boolean); |
| #60 | return parts.join('. ') || ''; |
| #61 | } |
| #62 | if (context.source_path) return `Draft file: ${context.source_path}`; |
| #63 | if (context.task_path) return `Task file: ${context.task_path}`; |
| #64 | return String(task.id ?? context.id ?? decision.source_ref ?? '').trim(); |
| #65 | } |
| #66 | |
| #67 | function cleanBriefText(value: unknown) { |
| #68 | return String(value ?? '') |
| #69 | .replace(/^---[\s\S]*?---/m, ' ') |
| #70 | .replace(/```[\s\S]*?```/g, ' ') |
| #71 | .replace(/^#{1,6}\s+/gm, '') |
| #72 | .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') |
| #73 | .replace(/[*_`>~-]/g, ' ') |
| #74 | .replace(/\s+/g, ' ') |
| #75 | .trim(); |
| #76 | } |
| #77 | |
| #78 | function firstSentences(text: string, count = 2) { |
| #79 | const sentences = text.match(/[^.!?]+[.!?]+(?:\s|$)/g)?.map(item => item.trim()) ?? []; |
| #80 | const selected = (sentences.length ? sentences : [text]).slice(0, count).join(' ').trim(); |
| #81 | return selected.length > 260 ? `${selected.slice(0, 257).trim()}...` : selected; |
| #82 | } |
| #83 | |
| #84 | function newsDraftPlainView(decision: Decision, draftView: Record<string, any>) { |
| #85 | const context = isRecord(decision.context) ? decision.context : {}; |
| #86 | const title = String(draftView.title || context.title || decision.title || draftView.slug || 'news draft').trim(); |
| #87 | const rawBody = cleanBriefText(draftView.article_body || context.problem_preview || decision.title || ''); |
| #88 | const body = rawBody.toLowerCase().startsWith(title.toLowerCase()) |
| #89 | ? rawBody.slice(title.length).replace(/^[:\s-]+/, '').trim() |
| #90 | : rawBody; |
| #91 | const summary = firstSentences(body, 2) || `This draft is titled "${title}" and needs King's review before it moves forward.`; |
| #92 | const whyMatch = body.match(/(?:This matters|It matters|Why it matters|The point is|The risk is|The remedy is)[^.!?]*[.!?]/i)?.[0]?.trim(); |
| #93 | const fallbackWhy = body |
| #94 | .split(/(?<=[.!?])\s+/) |
| #95 | .find(sentence => sentence.length > 55 && !sentence.toLowerCase().startsWith(title.toLowerCase()) && /\b(King|court|remedy|filing|jurisdiction|fraud|packet|notice|public|private|rights?)\b/i.test(sentence)); |
| #96 | const why = cleanBriefText(whyMatch || fallbackWhy || `It matters because this draft may become a public-facing article or operator-reviewed news item, so King should verify the framing before approval.`); |
| #97 | const urgency = String(decision.urgency ?? 'medium').toLowerCase(); |
| #98 | const target = describeTarget(decision) || `Draft: ${title}`; |
| #99 | return { |
| #100 | what: `Aethon wants to review "${title}". ${summary}`, |
| #101 | why, |
| #102 | target, |
| #103 | blast_radius: `${urgency === 'high' ? 'High' : urgency === 'low' ? 'Low' : 'Medium'}: approving moves this specific draft forward; it does not execute outside the article queue.`, |
| #104 | if_approve: `Aethon will accept this draft item and keep "${title}" moving through the news approval flow.`, |
| #105 | if_deny: 'The draft will stay declined from this approval and will not be retried from this card.', |
| #106 | warning: draftView.source_exists === false ? 'The draft file was not readable; review the raw context before approving.' : '', |
| #107 | }; |
| #108 | } |
| #109 | |
| #110 | function plainViewForDecision(decision: Decision, draftView?: Record<string, any>) { |
| #111 | const context = isRecord(decision.context) ? decision.context : {}; |
| #112 | const explicit = isRecord(context.plain_english) ? context.plain_english : null; |
| #113 | if (explicit) { |
| #114 | return { |
| #115 | what: String(explicit.what ?? 'Aethon wants approval.'), |
| #116 | why: String(explicit.why ?? 'Aethon needs King to approve before acting.'), |
| #117 | target: String(explicit.target ?? 'No clear target was attached to this approval.'), |
| #118 | blast_radius: String(explicit.blast_radius ?? 'Medium: Aethon waits for King before acting.'), |
| #119 | if_approve: String(explicit.if_approve ?? 'Aethon will perform the approved action.'), |
| #120 | if_deny: String(explicit.if_deny ?? 'The task will be marked declined and will not be retried from this approval.'), |
| #121 | warning: String(explicit.warning ?? ''), |
| #122 | }; |
| #123 | } |
| #124 | if (draftView && isNewsDraftDecision(decision)) { |
| #125 | return newsDraftPlainView(decision, draftView); |
| #126 | } |
| #127 | const action = String(decision.action ?? 'approval').replace(/_/g, ' '); |
| #128 | const urgency = String(decision.urgency ?? 'medium').toLowerCase(); |
| #129 | const target = describeTarget(decision); |
| #130 | const malformed = !target || isFixtureDecision(decision); |
| #131 | const source = String(decision.source ?? 'Aethon').replace(/_/g, ' '); |
| #132 | return { |
| #133 | what: `Aethon wants to ${action}.`, |
| #134 | why: `Because ${source} found a task that needs King's approval before Aethon acts.`, |
| #135 | target: target || 'No clear target was attached to this approval.', |
| #136 | blast_radius: `${urgency === 'high' ? 'High' : urgency === 'low' ? 'Low' : 'Medium'}: this affects ${target || 'an unclear target'}, so Aethon waits for King before acting.`, |
| #137 | if_approve: `Aethon will ${action}${target ? ` for ${target}` : ''}.`, |
| #138 | if_deny: 'The task will be marked declined and will not be retried from this approval.', |
| #139 | warning: malformed ? 'This approval is missing a clear live target or looks like a test fixture. Treat it as malformed.' : '', |
| #140 | }; |
| #141 | } |
| #142 | |
| #143 | async function appendApprovalAudit(event: Record<string, any>) { |
| #144 | const logPath = join(AETHON_ROOT, 'data', 'approval_queue', 'dispatch_log.jsonl'); |
| #145 | await fs.mkdir(join(AETHON_ROOT, 'data', 'approval_queue'), { recursive: true }); |
| #146 | await fs.appendFile(logPath, `${JSON.stringify({ timestamp: nowIso(), ...event })}\n`, 'utf-8'); |
| #147 | } |
| #148 | |
| #149 | async function runRunnerTool(tool: string, parameters: Record<string, any>) { |
| #150 | const payload = JSON.stringify({ tool, parameters }); |
| #151 | return await new Promise<Record<string, any>>((resolvePromise, reject) => { |
| #152 | const child = spawn('/home/kingbau/Documents/Aethon-Core/.venv/bin/python3', [RUNNER], { |
| #153 | cwd: AETHON_ROOT, |
| #154 | stdio: ['pipe', 'pipe', 'pipe'], |
| #155 | env: { ...process.env, PYTHONPATH: AETHON_ROOT }, |
| #156 | }); |
| #157 | let stdout = ''; |
| #158 | let stderr = ''; |
| #159 | child.stdout.on('data', chunk => { stdout += chunk.toString(); }); |
| #160 | child.stderr.on('data', chunk => { stderr += chunk.toString(); }); |
| #161 | child.on('error', reject); |
| #162 | child.on('close', code => { |
| #163 | if (code !== 0) { |
| #164 | reject(new Error(stderr || stdout || `runner exited ${code}`)); |
| #165 | return; |
| #166 | } |
| #167 | try { |
| #168 | resolvePromise(JSON.parse(stdout)); |
| #169 | } catch (error) { |
| #170 | reject(new Error(`runner returned invalid JSON: ${error instanceof Error ? error.message : String(error)}`)); |
| #171 | } |
| #172 | }); |
| #173 | child.stdin.end(payload); |
| #174 | }); |
| #175 | } |
| #176 | |
| #177 | function approvedDelegationToolCall(decision: Decision) { |
| #178 | if (decision.action !== 'delegation_harness_execute_step') return null; |
| #179 | const context = isRecord(decision.context) ? decision.context : {}; |
| #180 | const step = isRecord(context.step) ? context.step : {}; |
| #181 | const call = isRecord(step.tool_call) ? step.tool_call : null; |
| #182 | if (!call) throw new Error('delegation_step_missing_tool_call'); |
| #183 | const tool = String(call.tool ?? ''); |
| #184 | const parameters = isRecord(call.parameters) ? call.parameters : {}; |
| #185 | if (!['shell_exec', 'control_computer', 'generate_3d'].includes(tool)) { |
| #186 | throw new Error(`delegation_step_tool_not_allowed:${tool || 'missing'}`); |
| #187 | } |
| #188 | if (tool === 'control_computer' && String(parameters.scope ?? 'bow') !== 'bow') { |
| #189 | throw new Error('delegation_step_mac_control_is_dormant'); |
| #190 | } |
| #191 | if (!String(parameters.delegation_harness_id ?? '').trim()) { |
| #192 | throw new Error('delegation_step_missing_harness_id'); |
| #193 | } |
| #194 | return { tool, parameters }; |
| #195 | } |
| #196 | |
| #197 | function authErrorResponse(error: unknown) { |
| #198 | if (error instanceof AuthRequiredError || error instanceof AuthMismatchError) { |
| #199 | return NextResponse.json({ error: error.message }, { status: error.status }); |
| #200 | } |
| #201 | throw error; |
| #202 | } |
| #203 | |
| #204 | function isNewsDraftDecision(decision: Decision) { |
| #205 | const context = isRecord(decision.context) ? decision.context : {}; |
| #206 | return decision.source === 'aethon_news' || decision.action === 'review_news_article' || Boolean(context.source_path && context.solution_action); |
| #207 | } |
| #208 | |
| #209 | function resolveAllowedDraftPath(value: unknown) { |
| #210 | const raw = String(value ?? '').trim(); |
| #211 | if (!raw) return null; |
| #212 | const draftPath = resolve(raw); |
| #213 | const allowedRoots = [resolve(NEWS_DRAFTS_DIR), resolve(CONTENT_DRAFTS_DIR)]; |
| #214 | return allowedRoots.some(root => draftPath === root || draftPath.startsWith(`${root}/`)) ? draftPath : null; |
| #215 | } |
| #216 | |
| #217 | function splitMarkdownDraft(raw: string) { |
| #218 | const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); |
| #219 | if (!match) return { frontmatter: '', body: raw.trim() }; |
| #220 | return { frontmatter: match[1].trim(), body: match[2].trim() }; |
| #221 | } |
| #222 | |
| #223 | function composeMarkdownDraft(original: string, articleBody: string) { |
| #224 | const { frontmatter } = splitMarkdownDraft(original); |
| #225 | const body = articleBody.trim(); |
| #226 | if (!frontmatter) return `${body}\n`; |
| #227 | return `---\n${frontmatter}\n---\n\n${body}\n`; |
| #228 | } |
| #229 | |
| #230 | function titleFromMarkdown(markdown: string) { |
| #231 | const match = markdown.match(/^#\s+(.+)$/m); |
| #232 | return match ? match[1].trim() : ''; |
| #233 | } |
| #234 | |
| #235 | function stemFromPath(path: string) { |
| #236 | return basename(path).replace(/\.(md|json)$/i, ''); |
| #237 | } |
| #238 | |
| #239 | async function ensureDir() { |
| #240 | await fs.mkdir(QUEUE_DIR, { recursive: true }); |
| #241 | } |
| #242 | |
| #243 | async function readDecision(file: string): Promise<Decision | null> { |
| #244 | try { |
| #245 | const text = await fs.readFile(join(QUEUE_DIR, file), 'utf-8'); |
| #246 | return JSON.parse(text); |
| #247 | } catch { |
| #248 | return null; |
| #249 | } |
| #250 | } |
| #251 | |
| #252 | async function writeDecision(decision: Decision) { |
| #253 | await ensureDir(); |
| #254 | decision.updated_at = nowIso(); |
| #255 | await fs.writeFile(join(QUEUE_DIR, `${safeId(String(decision.id))}.json`), `${JSON.stringify(decision, null, 2)}\n`); |
| #256 | } |
| #257 | |
| #258 | async function readDraftView(decision: Decision) { |
| #259 | const context = isRecord(decision.context) ? decision.context : {}; |
| #260 | const sourcePath = resolveAllowedDraftPath(context.source_path); |
| #261 | const slug = String(context.slug ?? (sourcePath ? stemFromPath(sourcePath) : '')); |
| #262 | const baseView = { |
| #263 | kind: 'news_article', |
| #264 | title: String(context.title ?? decision.title ?? ''), |
| #265 | slug, |
| #266 | category: String(context.category ?? ''), |
| #267 | remedy_category: String(context.solution_action?.remedy_category ?? context.category ?? ''), |
| #268 | solution_action: context.solution_action ?? null, |
| #269 | source_path: sourcePath, |
| #270 | source_exists: false, |
| #271 | source_error: sourcePath ? '' : 'source_path_not_allowed', |
| #272 | article_body: String(context.problem_preview ?? ''), |
| #273 | }; |
| #274 | |
| #275 | if (!sourcePath) return baseView; |
| #276 | |
| #277 | try { |
| #278 | const raw = await fs.readFile(sourcePath, 'utf-8'); |
| #279 | if (sourcePath.endsWith('.json')) { |
| #280 | const draft = JSON.parse(raw); |
| #281 | const articleBody = String(draft.content ?? draft.article ?? ''); |
| #282 | return { |
| #283 | ...baseView, |
| #284 | title: String(draft.title ?? baseView.title), |
| #285 | slug: String(draft.slug ?? baseView.slug), |
| #286 | category: String(draft.category ?? baseView.category), |
| #287 | remedy_category: String(draft.solution_action?.remedy_category ?? draft.category ?? baseView.remedy_category), |
| #288 | solution_action: draft.solution_action ?? baseView.solution_action, |
| #289 | source_exists: true, |
| #290 | source_error: '', |
| #291 | article_body: articleBody, |
| #292 | }; |
| #293 | } |
| #294 | const { body } = splitMarkdownDraft(raw); |
| #295 | return { |
| #296 | ...baseView, |
| #297 | title: titleFromMarkdown(body) || baseView.title, |
| #298 | source_exists: true, |
| #299 | source_error: '', |
| #300 | article_body: body, |
| #301 | }; |
| #302 | } catch (error) { |
| #303 | return { |
| #304 | ...baseView, |
| #305 | source_error: error instanceof Error ? error.message : 'draft_read_failed', |
| #306 | }; |
| #307 | } |
| #308 | } |
| #309 | |
| #310 | async function hydrateDecision(decision: Decision) { |
| #311 | if (!isNewsDraftDecision(decision)) { |
| #312 | return { |
| #313 | ...decision, |
| #314 | plain_view: plainViewForDecision(decision), |
| #315 | }; |
| #316 | } |
| #317 | const draftView = await readDraftView(decision); |
| #318 | return { |
| #319 | ...decision, |
| #320 | plain_view: plainViewForDecision(decision, draftView), |
| #321 | draft_view: draftView, |
| #322 | }; |
| #323 | } |
| #324 | |
| #325 | async function updateContentDraftJson(decision: Decision, articleBody: string) { |
| #326 | const context = isRecord(decision.context) ? decision.context : {}; |
| #327 | const slug = safeSlug(String(context.slug ?? '')); |
| #328 | if (!slug) return null; |
| #329 | const draftPath = join(CONTENT_DRAFTS_DIR, `${slug}.json`); |
| #330 | try { |
| #331 | const raw = await fs.readFile(draftPath, 'utf-8'); |
| #332 | const draft = JSON.parse(raw); |
| #333 | const title = titleFromMarkdown(articleBody); |
| #334 | draft.content = articleBody; |
| #335 | draft.article = articleBody; |
| #336 | if (title) draft.title = title; |
| #337 | await fs.writeFile(draftPath, `${JSON.stringify(draft, null, 2)}\n`); |
| #338 | return draftPath; |
| #339 | } catch { |
| #340 | return null; |
| #341 | } |
| #342 | } |
| #343 | |
| #344 | async function persistDraftEdit(decision: Decision, articleBody: string) { |
| #345 | const trimmed = String(articleBody ?? '').trim(); |
| #346 | if (!trimmed) throw new Error('draft_body_required'); |
| #347 | const context = isRecord(decision.context) ? decision.context : {}; |
| #348 | const sourcePath = resolveAllowedDraftPath(context.source_path); |
| #349 | let editedSourcePath: string | null = null; |
| #350 | |
| #351 | if (sourcePath) { |
| #352 | const raw = await fs.readFile(sourcePath, 'utf-8'); |
| #353 | if (sourcePath.endsWith('.json')) { |
| #354 | const draft = JSON.parse(raw); |
| #355 | const title = titleFromMarkdown(trimmed); |
| #356 | draft.content = trimmed; |
| #357 | draft.article = trimmed; |
| #358 | if (title) draft.title = title; |
| #359 | await fs.writeFile(sourcePath, `${JSON.stringify(draft, null, 2)}\n`); |
| #360 | } else { |
| #361 | await fs.writeFile(sourcePath, composeMarkdownDraft(raw, trimmed)); |
| #362 | } |
| #363 | editedSourcePath = sourcePath; |
| #364 | } |
| #365 | |
| #366 | const editedContentDraftPath = await updateContentDraftJson(decision, trimmed); |
| #367 | return { editedSourcePath, editedContentDraftPath }; |
| #368 | } |
| #369 | |
| #370 | export async function GET(req: NextRequest) { |
| #371 | try { |
| #372 | const ctx = await getUserContext(); |
| #373 | const userKey = ctx.vaultUserKey || USER_MAP[ctx.userId] || USER_MAP[ctx.legacyUserId] || 'kingbau'; |
| #374 | const status = req.nextUrl.searchParams.get('status') ?? 'pending'; |
| #375 | await ensureDir(); |
| #376 | const files = await fs.readdir(QUEUE_DIR).catch(() => []); |
| #377 | const decisions = (await Promise.all(files.filter(f => f.endsWith('.json')).map(readDecision))) |
| #378 | .filter((item): item is Decision => Boolean(item)) |
| #379 | .filter(item => !isFixtureDecision(item)) |
| #380 | .filter(item => !status || item.status === status) |
| #381 | .filter(item => item.user_key === userKey || userKey === 'kingbau') |
| #382 | .sort((a, b) => String(b.created_at ?? '').localeCompare(String(a.created_at ?? ''))) |
| #383 | .slice(0, 80); |
| #384 | const hydrated = await Promise.all(decisions.map(hydrateDecision)); |
| #385 | return NextResponse.json({ user: userKey, status, decisions: hydrated, count: hydrated.length }); |
| #386 | } catch (error) { |
| #387 | return authErrorResponse(error); |
| #388 | } |
| #389 | } |
| #390 | |
| #391 | export async function POST(req: NextRequest) { |
| #392 | try { |
| #393 | const ctx = await getUserContext(); |
| #394 | const actor = ctx.displayName || 'operator'; |
| #395 | const body = await req.json(); |
| #396 | const id = safeId(String(body.id ?? '')); |
| #397 | if (!id) return NextResponse.json({ ok: false, error: 'id_required' }, { status: 400 }); |
| #398 | const decision = await readDecision(`${id}.json`); |
| #399 | if (!decision) return NextResponse.json({ ok: false, error: 'not_found' }, { status: 404 }); |
| #400 | if (isFixtureDecision(decision)) { |
| #401 | decision.status = 'canceled'; |
| #402 | decision.resolved_at = nowIso(); |
| #403 | decision.resolved_by = actor; |
| #404 | decision.feedback = 'Fixture approval ignored. It came from a pytest /tmp task path, not the live vault.'; |
| #405 | decision.fixture_noop = true; |
| #406 | decision.no_real_action_taken = true; |
| #407 | await writeDecision(decision); |
| #408 | await appendApprovalAudit({ |
| #409 | event: 'fixture_noop', |
| #410 | decision_id: decision.id, |
| #411 | actor, |
| #412 | reason: 'pytest_tmp_fixture_path', |
| #413 | no_real_email_sent: true, |
| #414 | }); |
| #415 | return NextResponse.json({ ok: true, decision: await hydrateDecision(decision), no_op: true }); |
| #416 | } |
| #417 | |
| #418 | const action = String(body.action ?? '').toLowerCase(); |
| #419 | const feedback = String(body.feedback ?? ''); |
| #420 | const draftBody = typeof body.draft_body === 'string' ? body.draft_body : ''; |
| #421 | decision.feedback = feedback; |
| #422 | decision.last_action_by = actor; |
| #423 | |
| #424 | if (draftBody && isNewsDraftDecision(decision)) { |
| #425 | try { |
| #426 | const editResult = await persistDraftEdit(decision, draftBody); |
| #427 | decision.context = { |
| #428 | ...(decision.context ?? {}), |
| #429 | last_article_edit_at: nowIso(), |
| #430 | last_article_edit_by: actor, |
| #431 | edited_source_path: editResult.editedSourcePath, |
| #432 | edited_content_draft_path: editResult.editedContentDraftPath, |
| #433 | }; |
| #434 | } catch (error) { |
| #435 | return NextResponse.json({ ok: false, error: error instanceof Error ? error.message : 'draft_edit_failed' }, { status: 400 }); |
| #436 | } |
| #437 | } |
| #438 | |
| #439 | if (action === 'approve') { |
| #440 | const wasAlreadyApproved = decision.status === 'approved'; |
| #441 | decision.status = 'approved'; |
| #442 | decision.resolved_at = wasAlreadyApproved && decision.resolved_at ? decision.resolved_at : nowIso(); |
| #443 | decision.resolved_by = wasAlreadyApproved && decision.resolved_by ? decision.resolved_by : actor; |
| #444 | const approvedStep = approvedDelegationToolCall(decision); |
| #445 | if (approvedStep) { |
| #446 | const startedAt = nowIso(); |
| #447 | delete decision.execution_result; |
| #448 | delete decision.execution_failed_at; |
| #449 | delete decision.executed_at; |
| #450 | decision.context = { |
| #451 | ...(decision.context ?? {}), |
| #452 | execution_started_at: startedAt, |
| #453 | }; |
| #454 | await writeDecision(decision); |
| #455 | try { |
| #456 | const execution = await runRunnerTool(approvedStep.tool, approvedStep.parameters); |
| #457 | decision.execution_result = execution; |
| #458 | decision.executed_at = nowIso(); |
| #459 | decision.context = { |
| #460 | ...(decision.context ?? {}), |
| #461 | execution_started_at: startedAt, |
| #462 | execution_result: execution, |
| #463 | }; |
| #464 | await appendApprovalAudit({ |
| #465 | event: 'delegation_step_executed', |
| #466 | decision_id: decision.id, |
| #467 | actor, |
| #468 | tool: approvedStep.tool, |
| #469 | ok: Boolean(execution?.ok), |
| #470 | }); |
| #471 | } catch (error) { |
| #472 | const execution = { |
| #473 | ok: false, |
| #474 | error: error instanceof Error ? error.message : String(error), |
| #475 | }; |
| #476 | decision.execution_result = execution; |
| #477 | decision.execution_failed_at = nowIso(); |
| #478 | decision.context = { |
| #479 | ...(decision.context ?? {}), |
| #480 | execution_started_at: startedAt, |
| #481 | execution_result: execution, |
| #482 | }; |
| #483 | await appendApprovalAudit({ |
| #484 | event: 'delegation_step_execution_failed', |
| #485 | decision_id: decision.id, |
| #486 | actor, |
| #487 | tool: approvedStep.tool, |
| #488 | error: execution.error, |
| #489 | }); |
| #490 | } |
| #491 | } |
| #492 | } else if (action === 'deny' || action === 'reject') { |
| #493 | decision.status = 'denied'; |
| #494 | decision.resolved_at = nowIso(); |
| #495 | decision.resolved_by = actor; |
| #496 | } else if (action === 'edit') { |
| #497 | decision.status = 'pending'; |
| #498 | decision.edits = Array.isArray(decision.edits) ? decision.edits : []; |
| #499 | decision.edits.push({ at: nowIso(), by: actor, feedback }); |
| #500 | decision.context = { |
| #501 | ...(decision.context ?? {}), |
| #502 | operator_edit: feedback, |
| #503 | }; |
| #504 | } else { |
| #505 | return NextResponse.json({ ok: false, error: 'unknown_action' }, { status: 400 }); |
| #506 | } |
| #507 | |
| #508 | await writeDecision(decision); |
| #509 | return NextResponse.json({ ok: true, decision }); |
| #510 | } catch (error) { |
| #511 | return authErrorResponse(error); |
| #512 | } |
| #513 | } |
| #514 |