@@ -37,7 +37,13 @@ export type CanonicalGesture =
3737 | { mode : 'element' ; from : CanonicalSelector ; direction ?: string ; duration ?: number | string } ;
3838
3939export type CanonicalCommand =
40- | { kind : 'launchApp' ; appId ?: string ; clearState ?: boolean ; stopApp ?: boolean }
40+ | {
41+ kind : 'launchApp' ;
42+ appId ?: string ;
43+ clearState ?: boolean ;
44+ stopApp ?: boolean ;
45+ permissions ?: Record < string , string > ;
46+ }
4147 // Upstream models `doubleTapOn` as a tap with repeat.repeat == 2, so the repeat
4248 // COUNT is the canonical field on both sides rather than a `double` variant on
4349 // one — that keeps our distinct tapOn/doubleTapOn kinds comparable to upstream
@@ -77,6 +83,7 @@ export type CanonicalCommand =
7783 | { kind : 'takeScreenshot' }
7884 | { kind : 'waitForAnimationToEnd' ; timeout ?: number | string }
7985 | { kind : 'stopApp' }
86+ | { kind : 'setPermissions' ; appId ?: string ; permissions ?: Record < string , string > }
8087 | { kind : 'repeat' ; times : string | number }
8188 | { kind : 'retry' ; maxRetries ?: string | number }
8289 | { kind : 'runFlow' ; label ?: string ; source : 'file' | 'commands' }
@@ -98,7 +105,19 @@ export function canonicalizeUpstreamFlow(commands: UpstreamCommand[]): Canonical
98105 . map ( canonicalizeUpstreamCommand ) ;
99106}
100107
108+ /** Upstream commands that canonicalize to a bare kind with no fields. */
109+ const BARE_UPSTREAM_CANONICAL : Record < string , CanonicalCommand > = {
110+ ScrollCommand : { kind : 'scroll' } ,
111+ BackPressCommand : { kind : 'back' } ,
112+ HideKeyboardCommand : { kind : 'hideKeyboard' } ,
113+ TakeScreenshotCommand : { kind : 'takeScreenshot' } ,
114+ StopAppCommand : { kind : 'stopApp' } ,
115+ RunScriptCommand : { kind : 'runScript' } ,
116+ } ;
117+
101118function canonicalizeUpstreamCommand ( command : UpstreamCommand ) : CanonicalCommand {
119+ const bare = BARE_UPSTREAM_CANONICAL [ command . type ] ;
120+ if ( bare ) return bare ;
102121 const f = command . fields ;
103122 switch ( command . type ) {
104123 case 'LaunchAppCommand' :
@@ -107,6 +126,7 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand
107126 appId : str ( f . appId ) ,
108127 clearState : bool ( f . clearState ) ,
109128 stopApp : bool ( f . stopApp ) ,
129+ permissions : permissionsRecord ( f . permissions ) ,
110130 } ) ;
111131 case 'TapOnElementCommand' : {
112132 const repeat = asRecord ( f . repeat ) ;
@@ -164,8 +184,6 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand
164184 }
165185 case 'SwipeCommand' :
166186 return dropUndefined ( { kind : 'swipe' , label : str ( f . label ) , gesture : upstreamGesture ( f ) } ) ;
167- case 'ScrollCommand' :
168- return { kind : 'scroll' } ;
169187 case 'ScrollUntilVisibleCommand' :
170188 return dropUndefined ( {
171189 kind : 'scrollUntilVisible' ,
@@ -185,19 +203,17 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand
185203 return dropUndefined ( { kind : 'openLink' , link : str ( f . link ) } ) ;
186204 case 'PressKeyCommand' :
187205 return { kind : 'pressKey' , key : lower ( str ( f . code ) ) ?? '' } ;
188- case 'BackPressCommand' :
189- return { kind : 'back' } ;
190- case 'HideKeyboardCommand' :
191- return { kind : 'hideKeyboard' } ;
192- case 'TakeScreenshotCommand' :
193- return { kind : 'takeScreenshot' } ;
194206 case 'WaitForAnimationToEndCommand' :
195207 return dropUndefined ( {
196208 kind : 'waitForAnimationToEnd' ,
197209 timeout : numLike ( f . timeout ) ?? str ( f . timeout ) ,
198210 } ) ;
199- case 'StopAppCommand' :
200- return { kind : 'stopApp' } ;
211+ case 'SetPermissionsCommand' :
212+ return dropUndefined ( {
213+ kind : 'setPermissions' ,
214+ appId : str ( f . appId ) ,
215+ permissions : permissionsRecord ( f . permissions ) ,
216+ } ) ;
201217 case 'RepeatCommand' :
202218 return { kind : 'repeat' , times : numLike ( f . times ) ?? str ( f . times ) ?? '' } ;
203219 case 'RetryCommand' :
@@ -211,8 +227,6 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand
211227 label : str ( f . label ) ,
212228 source : f . sourceDescription != null ? 'file' : 'commands' ,
213229 } ) ;
214- case 'RunScriptCommand' :
215- return { kind : 'runScript' } ;
216230 default :
217231 return { kind : 'unsupported' , command : unsupportedName ( command . type ) } ;
218232 }
@@ -286,6 +300,18 @@ function lower(value: string | undefined): string | undefined {
286300 return value ?. toLowerCase ( ) ;
287301}
288302
303+ function permissionsRecord ( value : unknown ) : Record < string , string > | undefined {
304+ const record = asRecord ( value ) ;
305+ if ( ! record ) return undefined ;
306+ const permissions : Record < string , string > = { } ;
307+ for ( const [ key , entry ] of Object . entries ( record ) ) {
308+ const coerced = str ( entry ) ?. toLowerCase ( ) ;
309+ if ( coerced === undefined ) return undefined ;
310+ permissions [ key ] = coerced ;
311+ }
312+ return permissions ;
313+ }
314+
289315// ---------------------------------------------------------------------------
290316// agent-device engine IR → canonical
291317// ---------------------------------------------------------------------------
@@ -302,18 +328,32 @@ export function canonicalizeAgentCommands(
302328 return program . commands . map ( ( command ) => canonicalizeAgentCommand ( command , program . config ) ) ;
303329}
304330
305- function canonicalizeAgentCommand (
306- command : MaestroCommand ,
307- config : MaestroProgram [ 'config' ] ,
308- ) : CanonicalCommand {
331+ /** Agent commands that canonicalize to a bare kind with no fields. */
332+ const BARE_AGENT_CANONICAL = {
333+ scroll : { kind : 'scroll' } ,
334+ back : { kind : 'back' } ,
335+ hideKeyboard : { kind : 'hideKeyboard' } ,
336+ takeScreenshot : { kind : 'takeScreenshot' } ,
337+ stopApp : { kind : 'stopApp' } ,
338+ runScript : { kind : 'runScript' } ,
339+ } satisfies Record < string , CanonicalCommand > ;
340+
341+ type BareAgentCommand = Extract < MaestroCommand , { kind : keyof typeof BARE_AGENT_CANONICAL } > ;
342+
343+ function isBareAgentCommand ( command : MaestroCommand ) : command is BareAgentCommand {
344+ return command . kind in BARE_AGENT_CANONICAL ;
345+ }
346+
347+ type AgentTapCommand = Extract < MaestroCommand , { kind : 'tapOn' | 'doubleTapOn' | 'longPressOn' } > ;
348+
349+ function isAgentTapCommand ( command : MaestroCommand ) : command is AgentTapCommand {
350+ return (
351+ command . kind === 'tapOn' || command . kind === 'doubleTapOn' || command . kind === 'longPressOn'
352+ ) ;
353+ }
354+
355+ function canonicalizeAgentTapCommand ( command : AgentTapCommand ) : CanonicalCommand {
309356 switch ( command . kind ) {
310- case 'launchApp' :
311- return dropUndefined ( {
312- kind : 'launchApp' ,
313- appId : command . appId ?? config . appId ,
314- clearState : command . clearState ,
315- stopApp : command . stopApp ,
316- } ) ;
317357 case 'tapOn' : {
318358 const repeat = numLike ( command . repeat ) ?? 1 ;
319359 const repeatIsNumber = typeof repeat === 'number' ;
@@ -343,6 +383,25 @@ function canonicalizeAgentCommand(
343383 label : command . label ,
344384 target : canonicalizeAgentTarget ( command . target ) ,
345385 } ) ;
386+ }
387+ }
388+
389+ type AgentAssertCommand = Extract <
390+ MaestroCommand ,
391+ { kind : 'assertVisible' | 'assertNotVisible' | 'assertTrue' | 'extendedWaitUntil' }
392+ > ;
393+
394+ function isAgentAssertCommand ( command : MaestroCommand ) : command is AgentAssertCommand {
395+ return (
396+ command . kind === 'assertVisible' ||
397+ command . kind === 'assertNotVisible' ||
398+ command . kind === 'assertTrue' ||
399+ command . kind === 'extendedWaitUntil'
400+ ) ;
401+ }
402+
403+ function canonicalizeAgentAssertCommand ( command : AgentAssertCommand ) : CanonicalCommand {
404+ switch ( command . kind ) {
346405 case 'assertVisible' :
347406 return dropUndefined ( {
348407 kind : 'assert' ,
@@ -376,6 +435,25 @@ function canonicalizeAgentCommand(
376435 label : command . label ,
377436 selector : canonicalizeAgentSelector ( command . notVisible ?? command . visible ) ,
378437 } ) ;
438+ }
439+ }
440+
441+ function canonicalizeAgentCommand (
442+ command : MaestroCommand ,
443+ config : MaestroProgram [ 'config' ] ,
444+ ) : CanonicalCommand {
445+ if ( isBareAgentCommand ( command ) ) return BARE_AGENT_CANONICAL [ command . kind ] ;
446+ if ( isAgentTapCommand ( command ) ) return canonicalizeAgentTapCommand ( command ) ;
447+ if ( isAgentAssertCommand ( command ) ) return canonicalizeAgentAssertCommand ( command ) ;
448+ switch ( command . kind ) {
449+ case 'launchApp' :
450+ return dropUndefined ( {
451+ kind : 'launchApp' ,
452+ appId : command . appId ?? config . appId ,
453+ clearState : command . clearState ,
454+ stopApp : command . stopApp ,
455+ permissions : command . permissions ,
456+ } ) ;
379457 case 'swipe' :
380458 return { kind : 'swipe' , label : command . label , gesture : agentGesture ( command . gesture ) } ;
381459 case 'inputText' :
@@ -384,8 +462,6 @@ function canonicalizeAgentCommand(
384462 return dropUndefined ( { kind : 'eraseText' , count : numLike ( command . charactersToErase ) } ) ;
385463 case 'openLink' :
386464 return dropUndefined ( { kind : 'openLink' , link : command . link } ) ;
387- case 'scroll' :
388- return { kind : 'scroll' } ;
389465 case 'scrollUntilVisible' :
390466 // Upstream materializes the DOWN default onto the command at parse time;
391467 // our engine defers it to execution (runtime-port-commands.ts). Materialize
@@ -400,16 +476,14 @@ function canonicalizeAgentCommand(
400476 } ) ;
401477 case 'pressKey' :
402478 return { kind : 'pressKey' , key : command . key . toLowerCase ( ) } ;
403- case 'back' :
404- return { kind : 'back' } ;
405- case 'hideKeyboard' :
406- return { kind : 'hideKeyboard' } ;
407- case 'takeScreenshot' :
408- return { kind : 'takeScreenshot' } ;
409479 case 'waitForAnimationToEnd' :
410480 return dropUndefined ( { kind : 'waitForAnimationToEnd' , timeout : numLike ( command . timeout ) } ) ;
411- case 'stopApp' :
412- return { kind : 'stopApp' } ;
481+ case 'setPermissions' :
482+ return dropUndefined ( {
483+ kind : 'setPermissions' ,
484+ appId : command . appId ?? config . appId ,
485+ permissions : command . permissions ,
486+ } ) ;
413487 case 'repeat' :
414488 return { kind : 'repeat' , times : numLike ( command . times ) ?? str ( command . times ) ?? '' } ;
415489 case 'retry' :
@@ -423,8 +497,6 @@ function canonicalizeAgentCommand(
423497 label : command . label ,
424498 source : command . include . kind === 'file' ? 'file' : 'commands' ,
425499 } ) ;
426- case 'runScript' :
427- return { kind : 'runScript' } ;
428500 default : {
429501 const exhaustive : never = command ;
430502 throw new Error ( `Unhandled agent command: ${ JSON . stringify ( exhaustive ) } ` ) ;
0 commit comments