@@ -77,6 +77,7 @@ export type CanonicalCommand =
7777 | { kind : 'takeScreenshot' }
7878 | { kind : 'waitForAnimationToEnd' ; timeout ?: number | string }
7979 | { kind : 'stopApp' }
80+ | { kind : 'setPermissions' ; appId ?: string ; permissions ?: Record < string , string > }
8081 | { kind : 'repeat' ; times : string | number }
8182 | { kind : 'retry' ; maxRetries ?: string | number }
8283 | { kind : 'runFlow' ; label ?: string ; source : 'file' | 'commands' }
@@ -98,7 +99,19 @@ export function canonicalizeUpstreamFlow(commands: UpstreamCommand[]): Canonical
9899 . map ( canonicalizeUpstreamCommand ) ;
99100}
100101
102+ /** Upstream commands that canonicalize to a bare kind with no fields. */
103+ const BARE_UPSTREAM_CANONICAL : Record < string , CanonicalCommand > = {
104+ ScrollCommand : { kind : 'scroll' } ,
105+ BackPressCommand : { kind : 'back' } ,
106+ HideKeyboardCommand : { kind : 'hideKeyboard' } ,
107+ TakeScreenshotCommand : { kind : 'takeScreenshot' } ,
108+ StopAppCommand : { kind : 'stopApp' } ,
109+ RunScriptCommand : { kind : 'runScript' } ,
110+ } ;
111+
101112function canonicalizeUpstreamCommand ( command : UpstreamCommand ) : CanonicalCommand {
113+ const bare = BARE_UPSTREAM_CANONICAL [ command . type ] ;
114+ if ( bare ) return bare ;
102115 const f = command . fields ;
103116 switch ( command . type ) {
104117 case 'LaunchAppCommand' :
@@ -164,8 +177,6 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand
164177 }
165178 case 'SwipeCommand' :
166179 return dropUndefined ( { kind : 'swipe' , label : str ( f . label ) , gesture : upstreamGesture ( f ) } ) ;
167- case 'ScrollCommand' :
168- return { kind : 'scroll' } ;
169180 case 'ScrollUntilVisibleCommand' :
170181 return dropUndefined ( {
171182 kind : 'scrollUntilVisible' ,
@@ -185,19 +196,17 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand
185196 return dropUndefined ( { kind : 'openLink' , link : str ( f . link ) } ) ;
186197 case 'PressKeyCommand' :
187198 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' } ;
194199 case 'WaitForAnimationToEndCommand' :
195200 return dropUndefined ( {
196201 kind : 'waitForAnimationToEnd' ,
197202 timeout : numLike ( f . timeout ) ?? str ( f . timeout ) ,
198203 } ) ;
199- case 'StopAppCommand' :
200- return { kind : 'stopApp' } ;
204+ case 'SetPermissionsCommand' :
205+ return dropUndefined ( {
206+ kind : 'setPermissions' ,
207+ appId : str ( f . appId ) ,
208+ permissions : permissionsRecord ( f . permissions ) ,
209+ } ) ;
201210 case 'RepeatCommand' :
202211 return { kind : 'repeat' , times : numLike ( f . times ) ?? str ( f . times ) ?? '' } ;
203212 case 'RetryCommand' :
@@ -211,8 +220,6 @@ function canonicalizeUpstreamCommand(command: UpstreamCommand): CanonicalCommand
211220 label : str ( f . label ) ,
212221 source : f . sourceDescription != null ? 'file' : 'commands' ,
213222 } ) ;
214- case 'RunScriptCommand' :
215- return { kind : 'runScript' } ;
216223 default :
217224 return { kind : 'unsupported' , command : unsupportedName ( command . type ) } ;
218225 }
@@ -286,6 +293,18 @@ function lower(value: string | undefined): string | undefined {
286293 return value ?. toLowerCase ( ) ;
287294}
288295
296+ function permissionsRecord ( value : unknown ) : Record < string , string > | undefined {
297+ const record = asRecord ( value ) ;
298+ if ( ! record ) return undefined ;
299+ const permissions : Record < string , string > = { } ;
300+ for ( const [ key , entry ] of Object . entries ( record ) ) {
301+ const coerced = str ( entry ) ?. toLowerCase ( ) ;
302+ if ( coerced === undefined ) return undefined ;
303+ permissions [ key ] = coerced ;
304+ }
305+ return permissions ;
306+ }
307+
289308// ---------------------------------------------------------------------------
290309// agent-device engine IR → canonical
291310// ---------------------------------------------------------------------------
@@ -302,10 +321,27 @@ export function canonicalizeAgentCommands(
302321 return program . commands . map ( ( command ) => canonicalizeAgentCommand ( command , program . config ) ) ;
303322}
304323
324+ /** Agent commands that canonicalize to a bare kind with no fields. */
325+ const BARE_AGENT_CANONICAL = {
326+ scroll : { kind : 'scroll' } ,
327+ back : { kind : 'back' } ,
328+ hideKeyboard : { kind : 'hideKeyboard' } ,
329+ takeScreenshot : { kind : 'takeScreenshot' } ,
330+ stopApp : { kind : 'stopApp' } ,
331+ runScript : { kind : 'runScript' } ,
332+ } satisfies Record < string , CanonicalCommand > ;
333+
334+ type BareAgentCommand = Extract < MaestroCommand , { kind : keyof typeof BARE_AGENT_CANONICAL } > ;
335+
336+ function isBareAgentCommand ( command : MaestroCommand ) : command is BareAgentCommand {
337+ return command . kind in BARE_AGENT_CANONICAL ;
338+ }
339+
305340function canonicalizeAgentCommand (
306341 command : MaestroCommand ,
307342 config : MaestroProgram [ 'config' ] ,
308343) : CanonicalCommand {
344+ if ( isBareAgentCommand ( command ) ) return BARE_AGENT_CANONICAL [ command . kind ] ;
309345 switch ( command . kind ) {
310346 case 'launchApp' :
311347 return dropUndefined ( {
@@ -384,8 +420,6 @@ function canonicalizeAgentCommand(
384420 return dropUndefined ( { kind : 'eraseText' , count : numLike ( command . charactersToErase ) } ) ;
385421 case 'openLink' :
386422 return dropUndefined ( { kind : 'openLink' , link : command . link } ) ;
387- case 'scroll' :
388- return { kind : 'scroll' } ;
389423 case 'scrollUntilVisible' :
390424 // Upstream materializes the DOWN default onto the command at parse time;
391425 // our engine defers it to execution (runtime-port-commands.ts). Materialize
@@ -400,16 +434,14 @@ function canonicalizeAgentCommand(
400434 } ) ;
401435 case 'pressKey' :
402436 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' } ;
409437 case 'waitForAnimationToEnd' :
410438 return dropUndefined ( { kind : 'waitForAnimationToEnd' , timeout : numLike ( command . timeout ) } ) ;
411- case 'stopApp' :
412- return { kind : 'stopApp' } ;
439+ case 'setPermissions' :
440+ return dropUndefined ( {
441+ kind : 'setPermissions' ,
442+ appId : command . appId ?? config . appId ,
443+ permissions : command . permissions ,
444+ } ) ;
413445 case 'repeat' :
414446 return { kind : 'repeat' , times : numLike ( command . times ) ?? str ( command . times ) ?? '' } ;
415447 case 'retry' :
@@ -423,8 +455,6 @@ function canonicalizeAgentCommand(
423455 label : command . label ,
424456 source : command . include . kind === 'file' ? 'file' : 'commands' ,
425457 } ) ;
426- case 'runScript' :
427- return { kind : 'runScript' } ;
428458 default : {
429459 const exhaustive : never = command ;
430460 throw new Error ( `Unhandled agent command: ${ JSON . stringify ( exhaustive ) } ` ) ;
0 commit comments