@@ -44,7 +44,15 @@ type EffectivePolicyReport = {
4444 scopes : ExecPolicyScopeSnapshot [ ] ;
4545 note ?: string ;
4646} ;
47+ type PendingExecApprovalRow = {
48+ ID : string ;
49+ Command : string ;
50+ Host : string ;
51+ Agent : string ;
52+ Expires : string ;
53+ } ;
4754const APPROVALS_GET_DEFAULT_TIMEOUT_MS = 60_000 ;
55+ const APPROVALS_PENDING_DEFAULT_TIMEOUT_MS = 60_000 ;
4856
4957type ExecApprovalsCliOpts = NodesRpcOpts & {
5058 node ?: string ;
@@ -171,6 +179,95 @@ function formatCliError(err: unknown): string {
171179 return safe . length > 300 ? `${ safe . slice ( 0 , 300 ) } ...` : safe ;
172180}
173181
182+ function isObjectRecord ( value : unknown ) : value is Record < string , unknown > {
183+ return Boolean ( value && typeof value === "object" && ! Array . isArray ( value ) ) ;
184+ }
185+
186+ function readPendingApprovals ( payload : unknown ) : unknown [ ] {
187+ if ( Array . isArray ( payload ) ) {
188+ return payload ;
189+ }
190+ if ( isObjectRecord ( payload ) && Array . isArray ( payload . approvals ) ) {
191+ return payload . approvals ;
192+ }
193+ return [ ] ;
194+ }
195+
196+ function readPendingApprovalCommand ( request : Record < string , unknown > ) : string {
197+ const systemRunPlan = isObjectRecord ( request . systemRunPlan ) ? request . systemRunPlan : null ;
198+ const command =
199+ normalizeOptionalString ( request . command ) ??
200+ normalizeOptionalString ( request . commandPreview ) ??
201+ normalizeOptionalString ( systemRunPlan ?. commandPreview ) ??
202+ normalizeOptionalString ( systemRunPlan ?. commandText ) ;
203+ return command ?? "(unknown command)" ;
204+ }
205+
206+ function formatTimestamp ( value : unknown ) : string {
207+ if ( typeof value !== "number" || ! Number . isFinite ( value ) || value <= 0 ) {
208+ return "unknown" ;
209+ }
210+ return new Date ( value ) . toISOString ( ) ;
211+ }
212+
213+ function buildPendingApprovalRows ( payload : unknown ) : PendingExecApprovalRow [ ] {
214+ return readPendingApprovals ( payload )
215+ . map ( ( approval ) : PendingExecApprovalRow | null => {
216+ if ( ! isObjectRecord ( approval ) ) {
217+ return null ;
218+ }
219+ const id = normalizeOptionalString ( approval . id ) ;
220+ const request = isObjectRecord ( approval . request ) ? approval . request : { } ;
221+ if ( ! id ) {
222+ return null ;
223+ }
224+ return {
225+ ID : id ,
226+ Command : readPendingApprovalCommand ( request ) ,
227+ Host : normalizeOptionalString ( request . host ) ?? "gateway" ,
228+ Agent : normalizeOptionalString ( request . agentId ) ?? "unknown" ,
229+ Expires : formatTimestamp ( approval . expiresAtMs ) ,
230+ } ;
231+ } )
232+ . filter ( ( row ) : row is PendingExecApprovalRow => row !== null ) ;
233+ }
234+
235+ function renderPendingApprovals ( payload : unknown ) {
236+ const rows = buildPendingApprovalRows ( payload ) ;
237+ const rich = isRich ( ) ;
238+ const heading = ( text : string ) => ( rich ? theme . heading ( text ) : text ) ;
239+ const muted = ( text : string ) => ( rich ? theme . muted ( text ) : text ) ;
240+ defaultRuntime . log ( heading ( "Pending Exec Approvals" ) ) ;
241+ if ( rows . length === 0 ) {
242+ defaultRuntime . log ( muted ( "No pending approvals." ) ) ;
243+ return ;
244+ }
245+ defaultRuntime . log (
246+ renderTable ( {
247+ width : getTerminalTableWidth ( ) ,
248+ columns : [
249+ { key : "ID" , header : "ID" , minWidth : 12 } ,
250+ { key : "Command" , header : "Command" , minWidth : 24 , flex : true } ,
251+ { key : "Host" , header : "Host" , minWidth : 8 } ,
252+ { key : "Agent" , header : "Agent" , minWidth : 10 } ,
253+ { key : "Expires" , header : "Expires" , minWidth : 20 } ,
254+ ] ,
255+ rows,
256+ } ) . trimEnd ( ) ,
257+ ) ;
258+ }
259+
260+ function normalizeApprovalDecisionInput ( decision : string ) : string {
261+ const normalized = decision . trim ( ) . toLowerCase ( ) ;
262+ if ( normalized === "always" ) {
263+ return "allow-always" ;
264+ }
265+ if ( normalized === "allow-once" || normalized === "allow-always" || normalized === "deny" ) {
266+ return normalized ;
267+ }
268+ exitWithError ( "Decision must be allow-once, allow-always, always, or deny." ) ;
269+ }
270+
174271async function loadConfigForApprovalsTarget ( params : {
175272 opts : ExecApprovalsCliOpts ;
176273 source : ApprovalsTargetSource ;
@@ -553,6 +650,63 @@ export function registerExecApprovalsCli(program: Command) {
553650 } ) ;
554651 nodesCallOpts ( setCmd ) ;
555652
653+ const pendingCmd = approvals
654+ . command ( "pending" )
655+ . alias ( "list" )
656+ . description ( "List pending exec approval requests" )
657+ . action ( async ( opts : ExecApprovalsCliOpts ) => {
658+ try {
659+ const pending = await callGatewayFromCli ( "exec.approval.list" , opts , { } ) ;
660+ if ( opts . json ) {
661+ defaultRuntime . writeJson ( { approvals : readPendingApprovals ( pending ) } , 0 ) ;
662+ return ;
663+ }
664+ renderPendingApprovals ( pending ) ;
665+ } catch ( err ) {
666+ defaultRuntime . error ( formatCliError ( err ) ) ;
667+ defaultRuntime . exit ( 1 ) ;
668+ }
669+ } ) ;
670+ nodesCallOpts ( pendingCmd , { timeoutMs : APPROVALS_PENDING_DEFAULT_TIMEOUT_MS } ) ;
671+
672+ const resolveCmd = approvals
673+ . command ( "resolve <id> <decision>" )
674+ . alias ( "respond" )
675+ . description ( "Resolve a pending exec approval request" )
676+ . addHelpText (
677+ "after" ,
678+ ( ) =>
679+ `\n${ theme . heading ( "Examples:" ) } \n${ formatExample (
680+ "openclaw approvals pending" ,
681+ "Show pending approval IDs." ,
682+ ) } \n${ formatExample (
683+ "openclaw approvals resolve <id> allow-once" ,
684+ "Approve one pending command." ,
685+ ) } \n${ formatExample (
686+ "openclaw approvals resolve <id> deny" ,
687+ "Deny one pending command." ,
688+ ) } \n\n${ theme . muted ( "Docs:" ) } ${ formatDocsLink ( "/cli/approvals" , "docs.openclaw.ai/cli/approvals" ) } \n`,
689+ )
690+ . action ( async ( id : string , decision : string , opts : ExecApprovalsCliOpts ) => {
691+ try {
692+ const trimmedId = requireTrimmedNonEmpty ( id , "Approval id required." ) ;
693+ const normalizedDecision = normalizeApprovalDecisionInput ( decision ) ;
694+ const result = await callGatewayFromCli ( "exec.approval.resolve" , opts , {
695+ id : trimmedId ,
696+ decision : normalizedDecision ,
697+ } ) ;
698+ if ( opts . json ) {
699+ defaultRuntime . writeJson ( result , 0 ) ;
700+ return ;
701+ }
702+ defaultRuntime . log ( `Approval ${ normalizedDecision } submitted for ${ trimmedId } .` ) ;
703+ } catch ( err ) {
704+ defaultRuntime . error ( formatCliError ( err ) ) ;
705+ defaultRuntime . exit ( 1 ) ;
706+ }
707+ } ) ;
708+ nodesCallOpts ( resolveCmd , { timeoutMs : APPROVALS_PENDING_DEFAULT_TIMEOUT_MS } ) ;
709+
556710 const allowlist = approvals
557711 . command ( "allowlist" )
558712 . description ( "Edit the per-agent allowlist" )
0 commit comments