@@ -7,6 +7,7 @@ use std::path::Path;
77use super :: types:: { FrontMatter , PipelineParameter , Repository , TriggerConfig } ;
88use super :: extensions:: { CompilerExtension , Extension , McpgServerConfig , McpgGatewayConfig , McpgConfig , CompileContext } ;
99use crate :: compile:: types:: McpConfig ;
10+ use crate :: engine:: Engine as _;
1011use crate :: fuzzy_schedule;
1112use crate :: allowed_hosts:: { CORE_ALLOWED_HOSTS , mcp_required_hosts} ;
1213use crate :: ecosystem_domains:: { get_ecosystem_domains, is_ecosystem_identifier, is_known_ecosystem} ;
@@ -463,176 +464,7 @@ pub fn generate_copilot_params(
463464 front_matter : & FrontMatter ,
464465 extensions : & [ super :: extensions:: Extension ] ,
465466) -> Result < String > {
466- // Check if bash triggers --allow-all-tools. This happens when:
467- // 1. Bash has an explicit wildcard entry (":*" or "*"), OR
468- // 2. Bash is not specified at all (None) — ado-aw agents always run in AWF sandbox,
469- // and gh-aw defaults to bash: ["*"] when sandbox is enabled (applyDefaultTools).
470- //
471- // Note: wildcard detection requires exactly one entry (cmds.len() == 1). Mixing a
472- // wildcard with other commands (e.g. bash: [":*", "cat"]) is not supported and will
473- // fall through to the restricted path, emitting "shell(:*)" literally.
474- let bash_config = front_matter. tools . as_ref ( ) . and_then ( |t| t. bash . as_ref ( ) ) ;
475- let use_allow_all_tools = match bash_config {
476- Some ( cmds) if cmds. len ( ) == 1 && ( cmds[ 0 ] == ":*" || cmds[ 0 ] == "*" ) => true ,
477- None => true , // default: all tools (matches gh-aw sandbox default)
478- _ => false ,
479- } ;
480-
481- // Edit tool: enabled by default, can be disabled with `edit: false`
482- let edit_enabled = front_matter
483- . tools
484- . as_ref ( )
485- . and_then ( |t| t. edit )
486- . unwrap_or ( true ) ;
487-
488- // When --allow-all-tools is active, skip individual tool collection entirely.
489- // --allow-all-tools is a superset that permits all tool calls regardless.
490- let mut allowed_tools: Vec < String > = Vec :: new ( ) ;
491-
492- if !use_allow_all_tools {
493- // Collect tool permissions from extensions (github, safeoutputs, azure-devops, etc.)
494- for ext in extensions {
495- for tool in ext. allowed_copilot_tools ( ) {
496- if !allowed_tools. contains ( & tool) {
497- allowed_tools. push ( tool) ;
498- }
499- }
500- }
501-
502- // Collect tool permissions from user-defined MCP servers (sorted for deterministic output).
503- // Only add --allow-tool for MCPs that will actually produce an MCPG entry (i.e.,
504- // WithOptions that have a container or url). McpConfig::Enabled(true) has no backing
505- // server in MCPG, so granting the permission would cause confusing runtime errors.
506- let mut sorted_mcps: Vec < _ > = front_matter. mcp_servers . iter ( ) . collect ( ) ;
507- sorted_mcps. sort_by ( |( a, _) , ( b, _) | a. cmp ( b) ) ;
508- for ( name, config) in sorted_mcps {
509- // Skip servers already provided by extensions (case-insensitive to match
510- // generate_mcpg_config's eq_ignore_ascii_case guard for reserved names)
511- if allowed_tools. iter ( ) . any ( |t| t. eq_ignore_ascii_case ( name) ) {
512- continue ;
513- }
514- // Only add MCPs that have a backing server (container or url)
515- let has_backing_server = match config {
516- crate :: compile:: types:: McpConfig :: Enabled ( _) => false ,
517- crate :: compile:: types:: McpConfig :: WithOptions ( opts) => {
518- opts. enabled . unwrap_or ( true )
519- && ( opts. container . is_some ( ) || opts. url . is_some ( ) )
520- }
521- } ;
522- if !has_backing_server {
523- continue ;
524- }
525- allowed_tools. push ( name. clone ( ) ) ;
526- }
527-
528- // Intentional: with restricted bash, both --allow-tool write (tool identity)
529- // and --allow-all-paths (path scope) are emitted. --allow-all-tools subsumes
530- // --allow-tool write, so only --allow-all-paths is needed on that path.
531- if edit_enabled {
532- allowed_tools. push ( "write" . to_string ( ) ) ;
533- }
534-
535- // Bash tool: use the explicitly configured list.
536- // When bash is None (not specified), use_allow_all_tools is true and this
537- // block is skipped entirely (gh-aw sandbox default = wildcard).
538- let mut bash_commands: Vec < String > =
539- match front_matter. tools . as_ref ( ) . and_then ( |t| t. bash . as_ref ( ) ) {
540- Some ( cmds) if cmds. is_empty ( ) => {
541- // Explicitly disabled: no bash commands
542- vec ! [ ]
543- }
544- Some ( cmds) => {
545- // Explicit list of commands
546- cmds. clone ( )
547- }
548- None => {
549- // Invariant: bash=None → use_allow_all_tools=true → this block is
550- // skipped. Panic if the invariant is ever broken.
551- unreachable ! ( "bash=None should imply use_allow_all_tools=true" )
552- }
553- } ;
554-
555- // Auto-add extension-declared bash commands (runtimes + first-party tools)
556- for ext in extensions {
557- for cmd in ext. required_bash_commands ( ) {
558- if !bash_commands. contains ( & cmd) {
559- bash_commands. push ( cmd) ;
560- }
561- }
562- }
563-
564- for cmd in & bash_commands {
565- // Reject single quotes in bash commands — copilot_params are embedded inside
566- // a single-quoted bash string in the AWF command.
567- if cmd. contains ( '\'' ) {
568- anyhow:: bail!(
569- "Bash command '{}' contains a single quote, which is not allowed \
570- (would break AWF shell quoting).",
571- cmd
572- ) ;
573- }
574- allowed_tools. push ( format ! ( "shell({})" , cmd) ) ;
575- }
576- }
577-
578- let mut params = Vec :: new ( ) ;
579-
580- // Validate model name to prevent shell injection — copilot_params are embedded
581- // inside a single-quoted bash string in the AWF command.
582- let model = front_matter. engine . model ( ) ;
583- if model. is_empty ( )
584- || !model
585- . chars ( )
586- . all ( |c| c. is_ascii_alphanumeric ( ) || matches ! ( c, '.' | '_' | ':' | '-' ) )
587- {
588- anyhow:: bail!(
589- "Model name '{}' contains invalid characters. \
590- Only ASCII alphanumerics, '.', '_', ':', and '-' are allowed.",
591- model
592- ) ;
593- }
594- params. push ( format ! ( "--model {}" , model) ) ;
595- if front_matter. engine . max_turns ( ) . is_some ( ) {
596- eprintln ! (
597- "Warning: Agent '{}' has max-turns set, but max-turns is not supported by Copilot CLI \
598- and will be ignored. Consider removing it from the engine configuration.",
599- front_matter. name
600- ) ;
601- }
602- if let Some ( timeout_minutes) = front_matter. engine . timeout_minutes ( ) {
603- if timeout_minutes == 0 {
604- eprintln ! (
605- "Warning: Agent '{}' has timeout-minutes: 0, which means no time is allowed. \
606- The agent job will time out immediately. \
607- Consider setting timeout-minutes to at least 1.",
608- front_matter. name
609- ) ;
610- }
611- }
612- params. push ( "--disable-builtin-mcps" . to_string ( ) ) ;
613- params. push ( "--no-ask-user" . to_string ( ) ) ;
614-
615- if use_allow_all_tools {
616- params. push ( "--allow-all-tools" . to_string ( ) ) ;
617- } else {
618- for tool in allowed_tools {
619- if tool. contains ( '(' ) || tool. contains ( ')' ) || tool. contains ( ' ' ) {
620- // Use double quotes - the copilot_params are embedded inside a single-quoted
621- // bash string in the AWF command, so single quotes would break quoting.
622- params. push ( format ! ( "--allow-tool \" {}\" " , tool) ) ;
623- } else {
624- params. push ( format ! ( "--allow-tool {}" , tool) ) ;
625- }
626- }
627- }
628-
629- // --allow-all-paths when edit is enabled — lets the agent write to any file path.
630- // Emitted independently of --allow-all-tools (matches gh-aw behavior).
631- if edit_enabled {
632- params. push ( "--allow-all-paths" . to_string ( ) ) ;
633- }
634-
635- Ok ( params. join ( " " ) )
467+ crate :: engine:: GITHUB_COPILOT_CLI_ENGINE . generate_cli_params ( front_matter, extensions)
636468}
637469
638470/// Compute the effective workspace based on explicit setting and checkout configuration.
@@ -1082,11 +914,7 @@ pub fn generate_acquire_ado_token(service_connection: Option<&str>, variable_nam
1082914/// Uses the read-only token from the read service connection.
1083915/// When not configured, omits ADO access tokens entirely.
1084916pub fn generate_copilot_ado_env ( read_service_connection : Option < & str > ) -> String {
1085- match read_service_connection {
1086- Some ( _) => "AZURE_DEVOPS_EXT_PAT: $(SC_READ_TOKEN)\n SYSTEM_ACCESSTOKEN: $(SC_READ_TOKEN)"
1087- . to_string ( ) ,
1088- None => String :: new ( ) ,
1089- }
917+ crate :: engine:: GITHUB_COPILOT_CLI_ENGINE . generate_agent_ado_env ( read_service_connection)
1090918}
1091919
1092920/// Generate the env block entries for the executor step (Stage 3 Execution).
0 commit comments