From 046b99c7457bf47e03eba02fe9333c3fe609ed8f Mon Sep 17 00:00:00 2001 From: gavrielc Date: Fri, 8 May 2026 16:31:30 +0300 Subject: [PATCH] feat(cli): wire approval flow for agent CLI commands When a container agent calls an approval-gated ncl command, dispatch now sends an approval card to an admin instead of returning a stub error. On approve, the handler re-dispatches the original command and notifies the agent with the result. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/cli/dispatch.ts | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts index f4c898744..7b247eb42 100644 --- a/src/cli/dispatch.ts +++ b/src/cli/dispatch.ts @@ -6,6 +6,9 @@ * Approval gating for risky calls from the container is the only branch * that differs by caller. Host callers and `open` commands run inline. */ +import { getAgentGroup } from '../db/agent-groups.js'; +import { getSession } from '../db/sessions.js'; +import { registerApprovalHandler, requestApproval } from '../modules/approvals/index.js'; import type { CallerContext, ErrorCode, RequestFrame, ResponseFrame } from './frame.js'; import { lookup } from './registry.js'; @@ -15,10 +18,28 @@ export async function dispatch(req: RequestFrame, ctx: CallerContext): Promise `--${k} ${v}`) + .join(' '); + + await requestApproval({ + session, + agentName, + action: 'cli_command', + payload: { frame: { id: req.id, command: req.command, args: req.args } }, + title: `CLI: ${req.command}`, + question: `Agent "${agentName}" wants to run:\n\`ncl ${req.command}${argSummary ? ' ' + argSummary : ''}\``, + }); + + return err(req.id, 'approval-pending', 'Approval request sent to admin. You will be notified of the result.'); } let parsed: unknown; @@ -36,6 +57,18 @@ export async function dispatch(req: RequestFrame, ctx: CallerContext): Promise { + const frame = payload.frame as RequestFrame; + const response = await dispatch(frame, { caller: 'host' }); + + if (response.ok) { + const data = typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2); + notify(`Your \`ncl ${frame.command}\` request was approved and executed.\n\n${data}`); + } else { + notify(`Your \`ncl ${frame.command}\` request was approved but failed: ${response.error.message}`); + } +}); + function err(id: string, code: ErrorCode, message: string): ResponseFrame { return { id, ok: false, error: { code, message } }; }