From c167bd2029b42e99ba125d6927bc09f8d7591b51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 07:42:30 +0000 Subject: [PATCH 1/3] Initial plan From ad381bf126bf03a7119fd13994dc6b229b356aef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 08:03:19 +0000 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20complete=20Engine=20abstraction?= =?UTF-8?q?=20=E2=80=94=20move=20install,=20invocation,=20and=20paths=20be?= =?UTF-8?q?hind=20Engine=20enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move COPILOT_CLI_VERSION constant from compile/common.rs to engine.rs (re-export preserved for backward compatibility) - Add Engine::install_steps() — generates NuGet install YAML steps - Add Engine::invocation() — generates AWF agent command line - Add Engine::detection_invocation() — generates AWF detection command line - Add Engine::version() — returns engine CLI version string - Add Engine::home_config_dir() — returns engine home config path - Add Engine::log_dir() — returns engine log directory - Add Engine::mcp_config_path() — returns engine MCP config path - Replace hardcoded install steps in base.yml and 1es-base.yml with {{ engine_install }} marker - Replace hardcoded AWF commands with {{ engine_run }} and {{ engine_run_detection }} markers - Replace {{ copilot_version }} with {{ engine_version }} - Replace {{ copilot_params }} with engine_args (now baked into {{ engine_run }}) - Replace hardcoded ~/.copilot paths with {{ engine_home_config_dir }}, {{ engine_log_dir }}, {{ engine_mcp_config_path }} - Rename copilot_params variable to engine_args in compile_shared() and run.rs - Rename test_copilot_params_* test functions to test_engine_args_* - Update version updater workflow docs for new constant location Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/f7532250-9a9d-45aa-9043-2ea43eb2c870 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- .github/workflows/update-awf-version.md | 6 +- src/compile/common.rs | 71 ++++++---- src/data/1es-base.yml | 85 +++--------- src/data/base.yml | 83 +++-------- src/engine.rs | 176 ++++++++++++++++++++++-- src/run.rs | 10 +- tests/compiler_tests.rs | 6 +- 7 files changed, 263 insertions(+), 174 deletions(-) diff --git a/.github/workflows/update-awf-version.md b/.github/workflows/update-awf-version.md index 6a68205..ca523c6 100644 --- a/.github/workflows/update-awf-version.md +++ b/.github/workflows/update-awf-version.md @@ -29,7 +29,7 @@ There are four items to check: | Item | Upstream Source | Local Path | |------|---------------|------------| | `AWF_VERSION` | [github/gh-aw-firewall](https://github.com/github/gh-aw-firewall) latest release | `src/compile/common.rs` | -| `COPILOT_CLI_VERSION` | [github/copilot-cli](https://github.com/github/copilot-cli) latest release | `src/compile/common.rs` | +| `COPILOT_CLI_VERSION` | [github/copilot-cli](https://github.com/github/copilot-cli) latest release | `src/engine.rs` (re-exported in `src/compile/common.rs`) | | `MCPG_VERSION` | [github/gh-aw-mcpg](https://github.com/github/gh-aw-mcpg) latest release | `src/compile/common.rs` | | `ecosystem_domains.json` | [github/gh-aw](https://github.com/github/gh-aw) `pkg/workflow/data/ecosystem_domains.json` on `main` | `src/data/ecosystem_domains.json` | @@ -45,7 +45,7 @@ Fetch the latest release of the upstream repository. Record the tag name, stripp ### Step 2: Read the Current Version -Read the file `src/compile/common.rs` in this repository and find the corresponding constant: +Read the file `src/compile/common.rs` (for `AWF_VERSION` and `MCPG_VERSION`) or `src/engine.rs` (for `COPILOT_CLI_VERSION`) in this repository and find the corresponding constant: - `pub const AWF_VERSION: &str = "...";` - `pub const COPILOT_CLI_VERSION: &str = "...";` @@ -89,7 +89,7 @@ If the latest version is newer than the current constant: ```markdown ## Dependency Update - Updates the pinned `COPILOT_CLI_VERSION` constant in `src/compile/common.rs` from `` to ``. + Updates the pinned `COPILOT_CLI_VERSION` constant in `src/engine.rs` from `` to ``. ### Release diff --git a/src/compile/common.rs b/src/compile/common.rs index 5d4996d..f078e24 100644 --- a/src/compile/common.rs +++ b/src/compile/common.rs @@ -548,7 +548,11 @@ pub const AWF_VERSION: &str = "0.25.26"; /// Version of the GitHub Copilot CLI (Microsoft.Copilot.CLI.linux-x64) NuGet package to install. /// Update this when upgrading to a new Copilot CLI release. /// See: https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json -pub const COPILOT_CLI_VERSION: &str = "1.0.34"; +/// +/// Re-exported from [`crate::engine::COPILOT_CLI_VERSION`] for backward compatibility. +/// The canonical definition lives in `src/engine.rs`. +#[allow(unused_imports)] +pub use crate::engine::COPILOT_CLI_VERSION; /// Prefix used to identify agentic pipeline YAML files generated by ado-aw. pub const HEADER_MARKER: &str = "# @ado-aw"; @@ -1915,8 +1919,8 @@ pub async fn compile_shared( } } - // 4. Generate copilot params - let copilot_params = ctx.engine.args(ctx.front_matter, extensions)?; + // 4. Generate engine args + let engine_args = ctx.engine.args(ctx.front_matter, extensions)?; // 5. Compute workspace, working directory, triggers let effective_workspace = compute_effective_workspace( @@ -2014,10 +2018,26 @@ pub async fn compile_shared( // 14. Shared replacements let compiler_version = env!("CARGO_PKG_VERSION"); let integrity_check = generate_integrity_check(config.skip_integrity); + + // Engine-generated pipeline fragments + let engine_install = ctx.engine.install_steps(); + let engine_version = ctx.engine.version(); + let engine_run = ctx.engine.invocation(&engine_args); + let engine_run_detection = ctx.engine.detection_invocation(&engine_args); + let engine_home_config_dir = ctx.engine.home_config_dir(); + let engine_log_dir = ctx.engine.log_dir(); + let engine_mcp_config_path = ctx.engine.mcp_config_path(); + let replacements: Vec<(&str, &str)> = vec![ ("{{ parameters }}", ¶meters_yaml), ("{{ compiler_version }}", compiler_version), - ("{{ copilot_version }}", COPILOT_CLI_VERSION), + ("{{ engine_version }}", engine_version), + ("{{ engine_install }}", &engine_install), + ("{{ engine_run }}", &engine_run), + ("{{ engine_run_detection }}", &engine_run_detection), + ("{{ engine_home_config_dir }}", engine_home_config_dir), + ("{{ engine_log_dir }}", engine_log_dir), + ("{{ engine_mcp_config_path }}", engine_mcp_config_path), ("{{ pool }}", &pool), ("{{ setup_job }}", &setup_job), ("{{ teardown_job }}", &teardown_job), @@ -2035,7 +2055,6 @@ pub async fn compile_shared( ("{{ agent }}", &agent_name), ("{{ agent_name }}", &front_matter.name), ("{{ agent_description }}", &front_matter.description), - ("{{ copilot_params }}", &copilot_params), ("{{ source_path }}", &source_path), // integrity_check must come before pipeline_path because the // integrity step content itself contains {{ pipeline_path }}. @@ -2156,10 +2175,10 @@ mod tests { assert!(result.is_ok()); } - // ─── Engine::args (copilot params) ────────────────────────────────────── + // ─── Engine::args (engine args) ────────────────────────────────────── #[test] - fn test_copilot_params_bash_wildcard() { + fn test_engine_args_bash_wildcard() { let mut fm = minimal_front_matter(); fm.tools = Some(crate::compile::types::ToolsConfig { bash: Some(vec![":*".to_string()]), @@ -2173,7 +2192,7 @@ mod tests { } #[test] - fn test_copilot_params_bash_star_wildcard() { + fn test_engine_args_bash_star_wildcard() { let mut fm = minimal_front_matter(); fm.tools = Some(crate::compile::types::ToolsConfig { bash: Some(vec!["*".to_string()]), @@ -2187,7 +2206,7 @@ mod tests { } #[test] - fn test_copilot_params_bash_disabled() { + fn test_engine_args_bash_disabled() { let mut fm = minimal_front_matter(); fm.tools = Some(crate::compile::types::ToolsConfig { bash: Some(vec![]), @@ -2200,7 +2219,7 @@ mod tests { } #[test] - fn test_copilot_params_allow_all_paths_when_edit_enabled() { + fn test_engine_args_allow_all_paths_when_edit_enabled() { let fm = minimal_front_matter(); // edit defaults to true, bash defaults to wildcard let params = CompileContext::for_test(&fm).engine.args(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap(); assert!(params.contains("--allow-all-paths"), "edit enabled (default) should emit --allow-all-paths"); @@ -2209,7 +2228,7 @@ mod tests { } #[test] - fn test_copilot_params_no_allow_all_paths_when_edit_disabled() { + fn test_engine_args_no_allow_all_paths_when_edit_disabled() { let mut fm = minimal_front_matter(); fm.tools = Some(crate::compile::types::ToolsConfig { bash: None, @@ -2223,7 +2242,7 @@ mod tests { } #[test] - fn test_copilot_params_allow_all_tools_with_allow_all_paths() { + fn test_engine_args_allow_all_tools_with_allow_all_paths() { let mut fm = minimal_front_matter(); fm.tools = Some(crate::compile::types::ToolsConfig { bash: Some(vec![":*".to_string()]), @@ -2238,7 +2257,7 @@ mod tests { } #[test] - fn test_copilot_params_lean_adds_bash_commands() { + fn test_engine_args_lean_adds_bash_commands() { let mut fm = minimal_front_matter(); fm.tools = Some(crate::compile::types::ToolsConfig { bash: Some(vec!["cat".to_string()]), @@ -2258,7 +2277,7 @@ mod tests { } #[test] - fn test_copilot_params_lean_with_unrestricted_bash() { + fn test_engine_args_lean_with_unrestricted_bash() { let mut fm = minimal_front_matter(); fm.tools = Some(crate::compile::types::ToolsConfig { bash: Some(vec![":*".to_string()]), @@ -2276,7 +2295,7 @@ mod tests { } #[test] - fn test_copilot_params_custom_mcp_no_mcp_flag() { + fn test_engine_args_custom_mcp_no_mcp_flag() { let mut fm = minimal_front_matter(); fm.mcp_servers.insert( "my-tool".to_string(), @@ -2293,7 +2312,7 @@ mod tests { } #[test] - fn test_copilot_params_allow_tool_for_container_mcp() { + fn test_engine_args_allow_tool_for_container_mcp() { let mut fm = minimal_front_matter(); fm.tools = Some(crate::compile::types::ToolsConfig { bash: Some(vec!["cat".to_string()]), @@ -2313,7 +2332,7 @@ mod tests { } #[test] - fn test_copilot_params_allow_tool_for_url_mcp() { + fn test_engine_args_allow_tool_for_url_mcp() { let mut fm = minimal_front_matter(); fm.tools = Some(crate::compile::types::ToolsConfig { bash: Some(vec!["cat".to_string()]), @@ -2333,7 +2352,7 @@ mod tests { } #[test] - fn test_copilot_params_no_allow_tool_for_enabled_only_mcp() { + fn test_engine_args_no_allow_tool_for_enabled_only_mcp() { let mut fm = minimal_front_matter(); fm.mcp_servers.insert( "my-tool".to_string(), @@ -2344,7 +2363,7 @@ mod tests { } #[test] - fn test_copilot_params_allow_tool_mcps_sorted() { + fn test_engine_args_allow_tool_mcps_sorted() { let mut fm = minimal_front_matter(); fm.tools = Some(crate::compile::types::ToolsConfig { bash: Some(vec!["cat".to_string()]), @@ -2373,7 +2392,7 @@ mod tests { } #[test] - fn test_copilot_params_builtin_mcp_no_mcp_flag() { + fn test_engine_args_builtin_mcp_no_mcp_flag() { let mut fm = minimal_front_matter(); fm.mcp_servers .insert("ado".to_string(), McpConfig::Enabled(true)); @@ -2383,7 +2402,7 @@ mod tests { } #[test] - fn test_copilot_params_max_turns_ignored() { + fn test_engine_args_max_turns_ignored() { let (fm, _) = parse_markdown( "---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n max-turns: 50\n---\n", ) @@ -2393,14 +2412,14 @@ mod tests { } #[test] - fn test_copilot_params_no_max_turns_when_simple_engine() { + fn test_engine_args_no_max_turns_when_simple_engine() { let fm = minimal_front_matter(); let params = CompileContext::for_test(&fm).engine.args(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap(); assert!(!params.contains("--max-turns")); } #[test] - fn test_copilot_params_no_max_timeout() { + fn test_engine_args_no_max_timeout() { let (fm, _) = parse_markdown( "---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n timeout-minutes: 30\n---\n", ) @@ -2410,14 +2429,14 @@ mod tests { } #[test] - fn test_copilot_params_no_max_timeout_when_simple_engine() { + fn test_engine_args_no_max_timeout_when_simple_engine() { let fm = minimal_front_matter(); let params = CompileContext::for_test(&fm).engine.args(&fm, &crate::compile::extensions::collect_extensions(&fm)).unwrap(); assert!(!params.contains("--max-timeout")); } #[test] - fn test_copilot_params_max_turns_zero_not_emitted() { + fn test_engine_args_max_turns_zero_not_emitted() { let (fm, _) = parse_markdown( "---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n max-turns: 0\n---\n", ) @@ -2427,7 +2446,7 @@ mod tests { } #[test] - fn test_copilot_params_max_timeout_zero_not_emitted() { + fn test_engine_args_max_timeout_zero_not_emitted() { let (fm, _) = parse_markdown( "---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n timeout-minutes: 0\n---\n", ) diff --git a/src/data/1es-base.yml b/src/data/1es-base.yml index ab28da2..1319b73 100644 --- a/src/data/1es-base.yml +++ b/src/data/1es-base.yml @@ -1,5 +1,5 @@ # 1ES Pipeline Template for Agentic Pipelines -# This template extends the 1ES Unofficial Pipeline Template with Copilot CLI, +# This template extends the 1ES Unofficial Pipeline Template with engine CLI, # AWF network isolation, and MCP Gateway — matching the standalone pipeline model. name: {{ agent_name }}-$(BuildID) @@ -56,30 +56,7 @@ extends: {{ acquire_ado_token }} - - task: NuGetAuthenticate@1 - displayName: "Authenticate NuGet Feed" - - - task: NuGetCommand@2 - displayName: "Install Copilot CLI" - inputs: - command: 'custom' - arguments: 'install Microsoft.Copilot.CLI.linux-x64 -Source "https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json" -Version {{ copilot_version }} -OutputDirectory $(Agent.TempDirectory)/tools -ExcludeVersion -NonInteractive' - - - bash: | - ls -la "$(Agent.TempDirectory)/tools" - echo "##vso[task.prependpath]$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64" - - # Copy copilot binary to /tmp so it's accessible inside AWF container - # (AWF auto-mounts /tmp:/tmp:rw but not Agent.TempDirectory) - mkdir -p /tmp/awf-tools - cp "$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64/copilot" /tmp/awf-tools/copilot - chmod +x /tmp/awf-tools/copilot - displayName: "Add copilot to PATH" - - - bash: | - copilot --version - copilot -h - displayName: "Output copilot version" + {{ engine_install }} - bash: | COMPILER_VERSION="{{ compiler_version }}" @@ -129,7 +106,7 @@ extends: displayName: "Prepare MCPG config" - bash: | - mkdir -p "$HOME/.copilot" + mkdir -p "{{ engine_home_config_dir }}" mkdir -p /tmp/awf-tools/staging echo "HOME: $HOME" @@ -320,7 +297,7 @@ extends: echo "Gateway output:" cat "$GATEWAY_OUTPUT" - # Convert gateway output to Copilot CLI mcp-config.json. + # Convert gateway output to engine mcp-config.json. # Mirrors gh-aw's convert_gateway_config_copilot.cjs: # - Rewrite URLs from 127.0.0.1 to host.docker.internal (AWF container needs # host.docker.internal to reach MCPG on the host; 127.0.0.1 is container loopback) @@ -330,9 +307,9 @@ extends: '.mcpServers |= (to_entries | sort_by(.key) | map(.value.url |= sub("^http://[^/]+/"; "\($prefix)/") | .value.tools = ["*"]) | from_entries)' \ "$GATEWAY_OUTPUT" > /tmp/awf-tools/mcp-config.json - # Also write to $HOME/.copilot for host-side use - cp /tmp/awf-tools/mcp-config.json "$HOME/.copilot/mcp-config.json" - chmod 600 /tmp/awf-tools/mcp-config.json "$HOME/.copilot/mcp-config.json" + # Also write to engine home config dir for host-side use + cp /tmp/awf-tools/mcp-config.json "{{ engine_mcp_config_path }}" + chmod 600 /tmp/awf-tools/mcp-config.json "{{ engine_mcp_config_path }}" echo "Generated MCP config at: /tmp/awf-tools/mcp-config.json" cat /tmp/awf-tools/mcp-config.json @@ -354,7 +331,7 @@ extends: # AWF provides L7 domain whitelisting via Squid proxy + Docker containers. # --enable-host-access allows the AWF container to reach host services # (MCPG and SafeOutputs) via host.docker.internal. - # AWF auto-mounts /tmp:/tmp:rw into the container, so copilot binary, + # AWF auto-mounts /tmp:/tmp:rw into the container, so the engine binary, # agent prompt, and MCP config are placed under /tmp/awf-tools/. # Stream agent output in real-time while filtering VSO commands. # sed -u = unbuffered (line-by-line) so output appears immediately. @@ -368,7 +345,7 @@ extends: --container-workdir "{{ working_directory }}" \ --log-level info \ --proxy-logs-dir "$(Agent.TempDirectory)/staging/logs/firewall" \ - -- '/tmp/awf-tools/copilot --prompt "$(cat /tmp/awf-tools/agent-prompt.md)" --additional-mcp-config @/tmp/awf-tools/mcp-config.json {{ copilot_params }}' \ + -- {{ engine_run }} \ 2>&1 \ | sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \ | tee "$AGENT_OUTPUT_FILE" \ @@ -381,7 +358,7 @@ extends: fi exit $AGENT_EXIT_CODE - displayName: "Run copilot (AWF network isolated)" + displayName: "Run agent (AWF network isolated)" workingDirectory: {{ working_directory }} env: {{ engine_env }} @@ -415,8 +392,8 @@ extends: - bash: | # Copy all logs to output directory for artifact upload mkdir -p "$(Agent.TempDirectory)/staging/logs" - if [ -d ~/.copilot/logs ]; then - cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true + if [ -d {{ engine_log_dir }} ]; then + cp -r {{ engine_log_dir }}/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true fi if [ -d ~/.ado-aw/logs ]; then cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true @@ -448,29 +425,7 @@ extends: - download: current artifact: agent_outputs_$(Build.BuildId) - - task: NuGetAuthenticate@1 - displayName: "Authenticate NuGet Feed" - - - task: NuGetCommand@2 - displayName: "Install Copilot CLI" - inputs: - command: 'custom' - arguments: 'install Microsoft.Copilot.CLI.linux-x64 -Source "https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json" -Version {{ copilot_version }} -OutputDirectory $(Agent.TempDirectory)/tools -ExcludeVersion -NonInteractive' - - - bash: | - ls -la "$(Agent.TempDirectory)/tools" - echo "##vso[task.prependpath]$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64" - - # Copy copilot binary to /tmp so it's accessible inside AWF container - mkdir -p /tmp/awf-tools - cp "$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64/copilot" /tmp/awf-tools/copilot - chmod +x /tmp/awf-tools/copilot - displayName: "Add copilot to PATH" - - - bash: | - copilot --version - copilot -h - displayName: "Output copilot version" + {{ engine_install }} - bash: | COMPILER_VERSION="{{ compiler_version }}" @@ -556,7 +511,7 @@ extends: --container-workdir "{{ working_directory }}" \ --log-level info \ --proxy-logs-dir "$(Agent.TempDirectory)/threat-analysis-logs/firewall" \ - -- '/tmp/awf-tools/copilot --prompt "$(cat /tmp/awf-tools/threat-analysis-prompt.md)" {{ copilot_params }}' \ + -- {{ engine_run_detection }} \ 2>&1 \ | sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \ | tee "$THREAT_OUTPUT_FILE" \ @@ -634,9 +589,9 @@ extends: - bash: | # Copy all logs to analyzed outputs for artifact upload mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs" - if [ -d ~/.copilot/logs ]; then - mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot" - cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true + if [ -d {{ engine_log_dir }} ]; then + mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/engine" + cp -r {{ engine_log_dir }}/* "$(Agent.TempDirectory)/analyzed_outputs/logs/engine/" 2>/dev/null || true fi if [ -d ~/.ado-aw/logs ]; then mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw" @@ -716,9 +671,9 @@ extends: # Copy agent output log from analyzed_outputs for optimisation use cp "$(Pipeline.Workspace)/analyzed_outputs_$(Build.BuildId)/logs/agent-output.txt" \ "$(Agent.TempDirectory)/staging/logs/agent-output.txt" 2>/dev/null || true - if [ -d ~/.copilot/logs ]; then - mkdir -p "$(Agent.TempDirectory)/staging/logs/copilot" - cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/staging/logs/copilot/" 2>/dev/null || true + if [ -d {{ engine_log_dir }} ]; then + mkdir -p "$(Agent.TempDirectory)/staging/logs/engine" + cp -r {{ engine_log_dir }}/* "$(Agent.TempDirectory)/staging/logs/engine/" 2>/dev/null || true fi if [ -d ~/.ado-aw/logs ]; then mkdir -p "$(Agent.TempDirectory)/staging/logs/ado-aw" diff --git a/src/data/base.yml b/src/data/base.yml index e101836..7a70f0b 100644 --- a/src/data/base.yml +++ b/src/data/base.yml @@ -27,30 +27,7 @@ jobs: {{ acquire_ado_token }} - - task: NuGetAuthenticate@1 - displayName: "Authenticate NuGet Feed" - - - task: NuGetCommand@2 - displayName: "Install Copilot CLI" - inputs: - command: 'custom' - arguments: 'install Microsoft.Copilot.CLI.linux-x64 -Source "https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json" -Version {{ copilot_version }} -OutputDirectory $(Agent.TempDirectory)/tools -ExcludeVersion -NonInteractive' - - - bash: | - ls -la "$(Agent.TempDirectory)/tools" - echo "##vso[task.prependpath]$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64" - - # Copy copilot binary to /tmp so it's accessible inside AWF container - # (AWF auto-mounts /tmp:/tmp:rw but not Agent.TempDirectory) - mkdir -p /tmp/awf-tools - cp "$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64/copilot" /tmp/awf-tools/copilot - chmod +x /tmp/awf-tools/copilot - displayName: "Add copilot to PATH" - - - bash: | - copilot --version - copilot -h - displayName: "Output copilot version" + {{ engine_install }} - bash: | COMPILER_VERSION="{{ compiler_version }}" @@ -100,7 +77,7 @@ jobs: displayName: "Prepare MCPG config" - bash: | - mkdir -p "$HOME/.copilot" + mkdir -p "{{ engine_home_config_dir }}" mkdir -p /tmp/awf-tools/staging echo "HOME: $HOME" @@ -291,7 +268,7 @@ jobs: echo "Gateway output:" cat "$GATEWAY_OUTPUT" - # Convert gateway output to Copilot CLI mcp-config.json. + # Convert gateway output to engine mcp-config.json. # Mirrors gh-aw's convert_gateway_config_copilot.cjs: # - Rewrite URLs from 127.0.0.1 to host.docker.internal (AWF container needs # host.docker.internal to reach MCPG on the host; 127.0.0.1 is container loopback) @@ -301,9 +278,9 @@ jobs: '.mcpServers |= (to_entries | sort_by(.key) | map(.value.url |= sub("^http://[^/]+/"; "\($prefix)/") | .value.tools = ["*"]) | from_entries)' \ "$GATEWAY_OUTPUT" > /tmp/awf-tools/mcp-config.json - # Also write to $HOME/.copilot for host-side use - cp /tmp/awf-tools/mcp-config.json "$HOME/.copilot/mcp-config.json" - chmod 600 /tmp/awf-tools/mcp-config.json "$HOME/.copilot/mcp-config.json" + # Also write to engine home config dir for host-side use + cp /tmp/awf-tools/mcp-config.json "{{ engine_mcp_config_path }}" + chmod 600 /tmp/awf-tools/mcp-config.json "{{ engine_mcp_config_path }}" echo "Generated MCP config at: /tmp/awf-tools/mcp-config.json" cat /tmp/awf-tools/mcp-config.json @@ -325,7 +302,7 @@ jobs: # AWF provides L7 domain whitelisting via Squid proxy + Docker containers. # --enable-host-access allows the AWF container to reach host services # (MCPG and SafeOutputs) via host.docker.internal. - # AWF auto-mounts /tmp:/tmp:rw into the container, so copilot binary, + # AWF auto-mounts /tmp:/tmp:rw into the container, so the engine binary, # agent prompt, and MCP config are placed under /tmp/awf-tools/. # Stream agent output in real-time while filtering VSO commands. # sed -u = unbuffered (line-by-line) so output appears immediately. @@ -339,7 +316,7 @@ jobs: --container-workdir "{{ working_directory }}" \ --log-level info \ --proxy-logs-dir "$(Agent.TempDirectory)/staging/logs/firewall" \ - -- '/tmp/awf-tools/copilot --prompt "$(cat /tmp/awf-tools/agent-prompt.md)" --additional-mcp-config @/tmp/awf-tools/mcp-config.json {{ copilot_params }}' \ + -- {{ engine_run }} \ 2>&1 \ | sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \ | tee "$AGENT_OUTPUT_FILE" \ @@ -352,7 +329,7 @@ jobs: fi exit $AGENT_EXIT_CODE - displayName: "Run copilot (AWF network isolated)" + displayName: "Run agent (AWF network isolated)" workingDirectory: {{ working_directory }} env: {{ engine_env }} @@ -386,8 +363,8 @@ jobs: - bash: | # Copy all logs to output directory for artifact upload mkdir -p "$(Agent.TempDirectory)/staging/logs" - if [ -d ~/.copilot/logs ]; then - cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true + if [ -d {{ engine_log_dir }} ]; then + cp -r {{ engine_log_dir }}/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true fi if [ -d ~/.ado-aw/logs ]; then cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true @@ -417,29 +394,7 @@ jobs: - download: current artifact: agent_outputs_$(Build.BuildId) - - task: NuGetAuthenticate@1 - displayName: "Authenticate NuGet Feed" - - - task: NuGetCommand@2 - displayName: "Install Copilot CLI" - inputs: - command: 'custom' - arguments: 'install Microsoft.Copilot.CLI.linux-x64 -Source "https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json" -Version {{ copilot_version }} -OutputDirectory $(Agent.TempDirectory)/tools -ExcludeVersion -NonInteractive' - - - bash: | - ls -la "$(Agent.TempDirectory)/tools" - echo "##vso[task.prependpath]$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64" - - # Copy copilot binary to /tmp so it's accessible inside AWF container - mkdir -p /tmp/awf-tools - cp "$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64/copilot" /tmp/awf-tools/copilot - chmod +x /tmp/awf-tools/copilot - displayName: "Add copilot to PATH" - - - bash: | - copilot --version - copilot -h - displayName: "Output copilot version" + {{ engine_install }} - bash: | COMPILER_VERSION="{{ compiler_version }}" @@ -525,7 +480,7 @@ jobs: --container-workdir "{{ working_directory }}" \ --log-level info \ --proxy-logs-dir "$(Agent.TempDirectory)/threat-analysis-logs/firewall" \ - -- '/tmp/awf-tools/copilot --prompt "$(cat /tmp/awf-tools/threat-analysis-prompt.md)" {{ copilot_params }}' \ + -- {{ engine_run_detection }} \ 2>&1 \ | sed -u 's/##vso\[/[VSO-FILTERED] vso[/g; s/##\[/[VSO-FILTERED] [/g' \ | tee "$THREAT_OUTPUT_FILE" \ @@ -603,9 +558,9 @@ jobs: - bash: | # Copy all logs to analyzed outputs for artifact upload mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs" - if [ -d ~/.copilot/logs ]; then - mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot" - cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true + if [ -d {{ engine_log_dir }} ]; then + mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/engine" + cp -r {{ engine_log_dir }}/* "$(Agent.TempDirectory)/analyzed_outputs/logs/engine/" 2>/dev/null || true fi if [ -d ~/.ado-aw/logs ]; then mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw" @@ -684,9 +639,9 @@ jobs: # Copy agent output log from analyzed_outputs for optimisation use cp "$(Pipeline.Workspace)/analyzed_outputs_$(Build.BuildId)/logs/agent-output.txt" \ "$(Agent.TempDirectory)/staging/logs/agent-output.txt" 2>/dev/null || true - if [ -d ~/.copilot/logs ]; then - mkdir -p "$(Agent.TempDirectory)/staging/logs/copilot" - cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/staging/logs/copilot/" 2>/dev/null || true + if [ -d {{ engine_log_dir }} ]; then + mkdir -p "$(Agent.TempDirectory)/staging/logs/engine" + cp -r {{ engine_log_dir }}/* "$(Agent.TempDirectory)/staging/logs/engine/" 2>/dev/null || true fi if [ -d ~/.ado-aw/logs ]; then mkdir -p "$(Agent.TempDirectory)/staging/logs/ado-aw" diff --git a/src/engine.rs b/src/engine.rs index 81234ae..724b7d2 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -6,6 +6,11 @@ use crate::compile::types::{FrontMatter, McpConfig}; /// Default model used by the Copilot engine when no model is specified in front matter. pub const DEFAULT_COPILOT_MODEL: &str = "claude-opus-4.5"; +/// Version of the GitHub Copilot CLI (Microsoft.Copilot.CLI.linux-x64) NuGet package to install. +/// Update this when upgrading to a new Copilot CLI release. +/// See: https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json +pub const COPILOT_CLI_VERSION: &str = "1.0.34"; + /// Resolved engine — enum dispatch over supported engine identifiers. /// /// Currently only `Copilot` (GitHub Copilot CLI) is supported. New engines @@ -33,10 +38,7 @@ pub fn get_engine(engine_id: &str) -> Result { impl Engine { /// The default engine binary name (e.g., "copilot"). /// - /// Currently scaffolding — the pipeline templates hard-code the binary path - /// (`/tmp/awf-tools/copilot`). This will be wired into template substitution - /// when additional engines are added. Can be overridden per-agent via - /// `engine.command` in front matter. + /// Can be overridden per-agent via `engine.command` in front matter. #[allow(dead_code)] pub fn command(&self) -> &str { match self { @@ -44,6 +46,51 @@ impl Engine { } } + /// Return the engine CLI version string (e.g., "1.0.34"). + pub fn version(&self) -> &str { + match self { + Engine::Copilot => COPILOT_CLI_VERSION, + } + } + + /// Generate pipeline YAML steps that install the engine binary. + /// + /// The steps authenticate the NuGet feed, install the package, copy the + /// binary to `/tmp/awf-tools/`, and verify the installation. + pub fn install_steps(&self) -> String { + match self { + Engine::Copilot => copilot_install_steps(), + } + } + + /// Generate the shell command that AWF executes for the Agent job. + /// + /// Returns the full command string (including engine binary, prompt, + /// MCP config, and engine args) that goes after AWF's `--` flag. + /// `engine_args` is the output of [`Engine::args`]. + pub fn invocation(&self, engine_args: &str) -> String { + match self { + Engine::Copilot => format!( + "'/tmp/awf-tools/copilot --prompt \"$(cat /tmp/awf-tools/agent-prompt.md)\" \ + --additional-mcp-config @/tmp/awf-tools/mcp-config.json {}'", + engine_args, + ), + } + } + + /// Generate the shell command that AWF executes for the Detection job. + /// + /// Similar to [`Engine::invocation`] but uses the threat-analysis prompt + /// and omits the MCP config (Detection runs without MCPG). + pub fn detection_invocation(&self, engine_args: &str) -> String { + match self { + Engine::Copilot => format!( + "'/tmp/awf-tools/copilot --prompt \"$(cat /tmp/awf-tools/threat-analysis-prompt.md)\" {}'", + engine_args, + ), + } + } + /// Generate CLI arguments for the engine invocation. pub fn args( &self, @@ -61,6 +108,34 @@ impl Engine { Engine::Copilot => copilot_env(), } } + + /// Return the engine's home configuration directory (e.g., `$HOME/.copilot`). + /// + /// Used in pipeline steps that create the config dir and copy MCP config. + pub fn home_config_dir(&self) -> &str { + match self { + Engine::Copilot => "$HOME/.copilot", + } + } + + /// Return the engine's log directory (e.g., `~/.copilot/logs`). + /// + /// Used in pipeline steps that collect engine logs after the agent runs. + pub fn log_dir(&self) -> &str { + match self { + Engine::Copilot => "~/.copilot/logs", + } + } + + /// Return the engine-specific path where MCP config is copied in the home dir. + /// + /// The primary MCP config is always at `/tmp/awf-tools/mcp-config.json`; + /// this returns the secondary copy path for host-side use. + pub fn mcp_config_path(&self) -> &str { + match self { + Engine::Copilot => "$HOME/.copilot/mcp-config.json", + } + } } fn copilot_args( @@ -166,7 +241,7 @@ fn copilot_args( } for cmd in &bash_commands { - // Reject single quotes in bash commands — copilot_params are embedded inside + // Reject single quotes in bash commands — engine_args are embedded inside // a single-quoted bash string in the AWF command. if cmd.contains('\'') { anyhow::bail!( @@ -181,7 +256,7 @@ fn copilot_args( let mut params = Vec::new(); - // Validate model name to prevent shell injection — copilot_params are embedded + // Validate model name to prevent shell injection — engine_args are embedded // inside a single-quoted bash string in the AWF command. let model = front_matter.engine.model().unwrap_or(DEFAULT_COPILOT_MODEL); if model.is_empty() @@ -268,7 +343,7 @@ fn copilot_args( } else { for tool in allowed_tools { if tool.contains('(') || tool.contains(')') || tool.contains(' ') { - // Use double quotes - the copilot_params are embedded inside a single-quoted + // Use double quotes - the engine_args are embedded inside a single-quoted // bash string in the AWF command, so single quotes would break quoting. params.push(format!("--allow-tool \"{}\"", tool)); } else { @@ -286,6 +361,40 @@ fn copilot_args( Ok(params.join(" ")) } +/// Generate the Copilot CLI install steps as YAML pipeline steps. +/// +/// These steps: authenticate the NuGet feed, install the package, copy the +/// binary to /tmp/awf-tools/, and run a version check. +fn copilot_install_steps() -> String { + format!( + r###"- task: NuGetAuthenticate@1 + displayName: "Authenticate NuGet Feed" + +- task: NuGetCommand@2 + displayName: "Install Copilot CLI" + inputs: + command: 'custom' + arguments: 'install Microsoft.Copilot.CLI.linux-x64 -Source "https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json" -Version {version} -OutputDirectory $(Agent.TempDirectory)/tools -ExcludeVersion -NonInteractive' + +- bash: | + ls -la "$(Agent.TempDirectory)/tools" + echo "##vso[task.prependpath]$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64" + + # Copy copilot binary to /tmp so it's accessible inside AWF container + # (AWF auto-mounts /tmp:/tmp:rw but not Agent.TempDirectory) + mkdir -p /tmp/awf-tools + cp "$(Agent.TempDirectory)/tools/Microsoft.Copilot.CLI.linux-x64/copilot" /tmp/awf-tools/copilot + chmod +x /tmp/awf-tools/copilot + displayName: "Add copilot to PATH" + +- bash: | + copilot --version + copilot -h + displayName: "Output copilot version""###, + version = COPILOT_CLI_VERSION, + ) +} + fn copilot_env() -> String { let lines = [ "GITHUB_TOKEN: $(GITHUB_TOKEN)", @@ -299,7 +408,7 @@ fn copilot_env() -> String { #[cfg(test)] mod tests { - use super::{get_engine, Engine}; + use super::{get_engine, Engine, COPILOT_CLI_VERSION}; use crate::compile::{extensions::collect_extensions, parse_markdown}; #[test] @@ -307,6 +416,57 @@ mod tests { assert_eq!(Engine::Copilot.command(), "copilot"); } + #[test] + fn copilot_engine_version() { + assert_eq!(Engine::Copilot.version(), COPILOT_CLI_VERSION); + } + + #[test] + fn copilot_engine_install_steps() { + let steps = Engine::Copilot.install_steps(); + assert!(steps.contains("NuGetAuthenticate@1")); + assert!(steps.contains("Install Copilot CLI")); + assert!(steps.contains(COPILOT_CLI_VERSION)); + assert!(steps.contains("/tmp/awf-tools/copilot")); + assert!(steps.contains("copilot --version")); + } + + #[test] + fn copilot_engine_invocation() { + let inv = Engine::Copilot.invocation("--model claude-opus-4.5 --no-ask-user"); + assert!(inv.contains("/tmp/awf-tools/copilot")); + assert!(inv.contains("--prompt")); + assert!(inv.contains("agent-prompt.md")); + assert!(inv.contains("--additional-mcp-config")); + assert!(inv.contains("mcp-config.json")); + assert!(inv.contains("--model claude-opus-4.5")); + } + + #[test] + fn copilot_engine_detection_invocation() { + let inv = Engine::Copilot.detection_invocation("--model claude-opus-4.5 --no-ask-user"); + assert!(inv.contains("/tmp/awf-tools/copilot")); + assert!(inv.contains("--prompt")); + assert!(inv.contains("threat-analysis-prompt.md")); + assert!(!inv.contains("--additional-mcp-config"), "detection should not include MCP config"); + assert!(inv.contains("--model claude-opus-4.5")); + } + + #[test] + fn copilot_engine_home_config_dir() { + assert_eq!(Engine::Copilot.home_config_dir(), "$HOME/.copilot"); + } + + #[test] + fn copilot_engine_log_dir() { + assert_eq!(Engine::Copilot.log_dir(), "~/.copilot/logs"); + } + + #[test] + fn copilot_engine_mcp_config_path() { + assert_eq!(Engine::Copilot.mcp_config_path(), "$HOME/.copilot/mcp-config.json"); + } + #[test] fn copilot_engine_args() { let (front_matter, _) = parse_markdown("---\nname: test\ndescription: test\n---\n").unwrap(); diff --git a/src/run.rs b/src/run.rs index 14f862a..f2cfe43 100644 --- a/src/run.rs +++ b/src/run.rs @@ -926,8 +926,8 @@ pub async fn run(args: &RunArgs) -> Result<()> { .with_context(|| format!("Failed to write agent prompt: {}", prompt_path.display()))?; debug!("Agent prompt written to {}", prompt_path.display()); - // ── 7. Build and run copilot command ───────────────────────────── - let copilot_params = compile_ctx.engine.args(compile_ctx.front_matter, &extensions)?; + // ── 7. Build and run engine command ───────────────────────────── + let engine_args = compile_ctx.engine.args(compile_ctx.front_matter, &extensions)?; println!("\n=== Copilot CLI ==="); @@ -948,8 +948,8 @@ pub async fn run(args: &RunArgs) -> Result<()> { visible_args.push("--additional-mcp-config".into()); visible_args.push(mcp_config_ref.clone()); - // Parse copilot_params and add as args - for param in shell_words(&copilot_params) { + // Parse engine_args and add as args + for param in shell_words(&engine_args) { visible_args.push(param.clone()); cmd.arg(param); } @@ -990,7 +990,7 @@ pub async fn run(args: &RunArgs) -> Result<()> { " copilot --prompt @{} --additional-mcp-config @{} {}{}\n", prompt_path.display(), mcp_config_path.display(), - copilot_params, + engine_args, debug_flags, ); diff --git a/tests/compiler_tests.rs b/tests/compiler_tests.rs index e1120cc..0366d29 100644 --- a/tests/compiler_tests.rs +++ b/tests/compiler_tests.rs @@ -105,8 +105,8 @@ fn test_compiled_yaml_structure() { "Template should contain agent_name marker" ); assert!( - template_content.contains("{{ copilot_params }}"), - "Template should contain copilot_params marker" + template_content.contains("{{ engine_run }}"), + "Template should contain engine_run marker" ); assert!( template_content.contains("{{ compiler_version }}"), @@ -3039,7 +3039,7 @@ fn test_1es_compiled_output_is_valid_yaml() { ); assert!( compiled.contains("copilot --prompt"), - "1ES output should contain copilot invocation (copilot_params substituted)" + "1ES output should contain copilot invocation (engine_args substituted)" ); assert!( compiled.contains("threat-analysis"), From 0b287a0ceeebfef63b6ac66760a52901d9678076 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 22 Apr 2026 08:06:40 +0000 Subject: [PATCH 3/3] fix: use $HOME consistently instead of mixing ~ and $HOME in Engine paths Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/f7532250-9a9d-45aa-9043-2ea43eb2c870 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --- src/engine.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 724b7d2..56698f8 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -118,12 +118,12 @@ impl Engine { } } - /// Return the engine's log directory (e.g., `~/.copilot/logs`). + /// Return the engine's log directory (e.g., `$HOME/.copilot/logs`). /// /// Used in pipeline steps that collect engine logs after the agent runs. pub fn log_dir(&self) -> &str { match self { - Engine::Copilot => "~/.copilot/logs", + Engine::Copilot => "$HOME/.copilot/logs", } } @@ -459,7 +459,7 @@ mod tests { #[test] fn copilot_engine_log_dir() { - assert_eq!(Engine::Copilot.log_dir(), "~/.copilot/logs"); + assert_eq!(Engine::Copilot.log_dir(), "$HOME/.copilot/logs"); } #[test]