diff --git a/.github/workflows/update-awf-version.md b/.github/workflows/update-awf-version.md index 6a68205..3fc846b 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` | | `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,11 +45,11 @@ 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 corresponding file in this repository and find the constant: -- `pub const AWF_VERSION: &str = "...";` -- `pub const COPILOT_CLI_VERSION: &str = "...";` -- `pub const MCPG_VERSION: &str = "...";` +- `pub const AWF_VERSION: &str = "...";` in `src/compile/common.rs` +- `pub const COPILOT_CLI_VERSION: &str = "...";` in `src/engine.rs` +- `pub const MCPG_VERSION: &str = "...";` in `src/compile/common.rs` Extract the version string. @@ -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 b2c793d..9c04cbf 100644 --- a/src/compile/common.rs +++ b/src/compile/common.rs @@ -459,14 +459,25 @@ pub fn validate_checkout_list(repositories: &[Repository], checkout: &[String]) Ok(()) } -/// Generate copilot CLI params from front matter configuration -pub fn generate_copilot_params( +/// Generate engine CLI args from front matter configuration. +/// +/// Delegates to the Engine implementation (currently Copilot CLI). +pub fn generate_engine_args( front_matter: &FrontMatter, extensions: &[super::extensions::Extension], ) -> Result { crate::engine::GITHUB_COPILOT_CLI_ENGINE.generate_cli_params(front_matter, extensions) } +/// Backwards-compatible alias for `generate_engine_args`. +#[allow(dead_code)] +pub fn generate_copilot_params( + front_matter: &FrontMatter, + extensions: &[super::extensions::Extension], +) -> Result { + generate_engine_args(front_matter, extensions) +} + /// Compute the effective workspace based on explicit setting and checkout configuration. pub fn compute_effective_workspace( explicit_workspace: &Option, @@ -592,7 +603,10 @@ 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` for backwards compatibility. +#[allow(dead_code)] +pub const COPILOT_CLI_VERSION: &str = crate::engine::COPILOT_CLI_VERSION; /// Prefix used to identify agentic pipeline YAML files generated by ado-aw. pub const HEADER_MARKER: &str = "# @ado-aw"; @@ -730,7 +744,7 @@ pub fn generate_debug_pipeline_replacements(debug: bool) -> Vec<(String, String) - bash: | echo "=== Probing MCP backends ===" PROBE_FAILED=false - for server in $(jq -r '.mcpServers | keys[]' /tmp/awf-tools/mcp-config.json); do + for server in $(jq -r '.mcpServers | keys[]' {{ engine_mcp_config_path }}); do echo "" echo "--- Probing: $server ---" # MCP requires initialize handshake before tools/list. @@ -910,13 +924,19 @@ pub fn generate_acquire_ado_token(service_connection: Option<&str>, variable_nam } } -/// Generate the env block entries for the copilot AWF step (Stage 1 agent). +/// Generate the env block entries for the engine AWF step (Stage 1 agent). /// Uses the read-only token from the read service connection. /// When not configured, omits ADO access tokens entirely. -pub fn generate_copilot_ado_env(read_service_connection: Option<&str>) -> String { +pub fn generate_engine_ado_env(read_service_connection: Option<&str>) -> String { crate::engine::GITHUB_COPILOT_CLI_ENGINE.generate_agent_ado_env(read_service_connection) } +/// Backwards-compatible alias for `generate_engine_ado_env`. +#[allow(dead_code)] +pub fn generate_copilot_ado_env(read_service_connection: Option<&str>) -> String { + generate_engine_ado_env(read_service_connection) +} + /// Generate the env block entries for the executor step (Stage 3 Execution). /// Uses the write token from the write service connection. /// When not configured, omits ADO access tokens entirely. @@ -1966,8 +1986,9 @@ pub async fn compile_shared( } } - // 4. Generate copilot params - let copilot_params = generate_copilot_params(front_matter, extensions)?; + // 4. Generate engine args + let engine = &crate::engine::GITHUB_COPILOT_CLI_ENGINE; + let engine_args = generate_engine_args(front_matter, extensions)?; // 5. Compute workspace, working directory, triggers let effective_workspace = compute_effective_workspace( @@ -2015,7 +2036,7 @@ pub async fn compile_shared( .and_then(|p| p.read.as_deref()), "SC_READ_TOKEN", ); - let copilot_ado_env = generate_copilot_ado_env( + let engine_ado_env = generate_engine_ado_env( front_matter .permissions .as_ref() @@ -2067,13 +2088,41 @@ pub async fn compile_shared( template = replace_with_indent(&template, placeholder, replacement); } - // 14. Shared replacements + // 14. Generate engine-specific template values let compiler_version = env!("CARGO_PKG_VERSION"); let integrity_check = generate_integrity_check(config.skip_integrity); + let engine_install = engine.install_steps(); + let engine_version = engine.version(); + let engine_run = engine.invocation( + "/tmp/awf-tools/agent-prompt.md", + Some(engine.mcp_config_path()), + &engine_args, + ); + let engine_run_detection = engine.invocation( + "/tmp/awf-tools/threat-analysis-prompt.md", + None, + &engine_args, + ); + let engine_home_config_dir = engine.home_config_dir(); + let engine_log_dir = engine.log_dir(); + let engine_mcp_config_path = engine.mcp_config_path(); let replacements: Vec<(&str, &str)> = vec![ ("{{ parameters }}", ¶meters_yaml), ("{{ compiler_version }}", compiler_version), - ("{{ copilot_version }}", COPILOT_CLI_VERSION), + // Engine-specific replacements (new markers) + ("{{ engine_install }}", &engine_install), + ("{{ engine_version }}", engine_version), + ("{{ engine_args }}", &engine_args), + ("{{ engine_run }}", &engine_run), + ("{{ engine_run_detection }}", &engine_run_detection), + ("{{ engine_ado_env }}", &engine_ado_env), + ("{{ engine_home_config_dir }}", engine_home_config_dir), + ("{{ engine_log_dir }}", engine_log_dir), + ("{{ engine_mcp_config_path }}", engine_mcp_config_path), + // Legacy markers (kept for backwards compatibility during transition) + ("{{ copilot_version }}", engine_version), + ("{{ copilot_params }}", &engine_args), + ("{{ copilot_ado_env }}", &engine_ado_env), ("{{ pool }}", &pool), ("{{ setup_job }}", &setup_job), ("{{ teardown_job }}", &teardown_job), @@ -2091,7 +2140,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 }}. @@ -2101,7 +2149,6 @@ pub async fn compile_shared( ("{{ workspace }}", &working_directory), ("{{ agent_content }}", markdown_body), ("{{ acquire_ado_token }}", &acquire_read_token), - ("{{ copilot_ado_env }}", &copilot_ado_env), ("{{ acquire_write_token }}", &acquire_write_token), ("{{ executor_ado_env }}", &executor_ado_env), ]; diff --git a/src/compile/mod.rs b/src/compile/mod.rs index 499bc0a..89ae9bd 100644 --- a/src/compile/mod.rs +++ b/src/compile/mod.rs @@ -19,10 +19,13 @@ use std::path::{Path, PathBuf}; pub use common::parse_markdown; pub use common::HEADER_MARKER; +pub use common::generate_engine_args; +#[allow(unused_imports)] pub use common::generate_copilot_params; pub use common::generate_mcpg_config; pub use common::MCPG_IMAGE; pub use common::MCPG_VERSION; +#[allow(unused_imports)] pub use common::MCPG_PORT; pub use types::{CompileTarget, FrontMatter}; diff --git a/src/data/1es-base.yml b/src/data/1es-base.yml index 4239d89..c2408b8 100644 --- a/src/data/1es-base.yml +++ b/src/data/1es-base.yml @@ -58,30 +58,7 @@ extends: {{ cancel_previous_builds }} - - 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 }}" @@ -131,7 +108,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" @@ -322,7 +299,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,14 +307,14 @@ extends: # - Preserve all other fields (headers, type, etc.) jq --arg prefix "http://$(MCP_GATEWAY_DOMAIN):$(MCP_GATEWAY_PORT)" \ '.mcpServers |= (to_entries | sort_by(.key) | map(.value.url |= sub("^http://[^/]+/"; "\($prefix)/") | .value.tools = ["*"]) | from_entries)' \ - "$GATEWAY_OUTPUT" > /tmp/awf-tools/mcp-config.json + "$GATEWAY_OUTPUT" > {{ engine_mcp_config_path }} - # 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 {{ engine_mcp_config_path }} "{{ engine_home_config_dir }}/mcp-config.json" + chmod 600 {{ engine_mcp_config_path }} "{{ engine_home_config_dir }}/mcp-config.json" - echo "Generated MCP config at: /tmp/awf-tools/mcp-config.json" - cat /tmp/awf-tools/mcp-config.json + echo "Generated MCP config at: {{ engine_mcp_config_path }}" + cat {{ engine_mcp_config_path }} displayName: "Start MCP Gateway (MCPG)" {{ mcpg_step_env }} @@ -370,7 +347,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" \ @@ -383,10 +360,10 @@ extends: fi exit $AGENT_EXIT_CODE - displayName: "Run copilot (AWF network isolated)" + displayName: "Run agent (AWF network isolated)" workingDirectory: {{ working_directory }} env: - {{ copilot_ado_env }} + {{ engine_ado_env }} GITHUB_TOKEN: $(GITHUB_TOKEN) GITHUB_READ_ONLY: 1 COPILOT_OTEL_ENABLED: "true" @@ -422,8 +399,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 @@ -455,29 +432,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 }}" @@ -563,7 +518,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" \ @@ -641,9 +596,9 @@ extends: - bash: | # Copy all logs to analyzed outputs for artifact upload mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs" - if [ -d ~/.copilot/logs ]; then + if [ -d {{ engine_log_dir }} ]; then mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot" - cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true + cp -r {{ engine_log_dir }}/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true fi if [ -d ~/.ado-aw/logs ]; then mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw" @@ -723,9 +678,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 + if [ -d {{ engine_log_dir }} ]; then mkdir -p "$(Agent.TempDirectory)/staging/logs/copilot" - cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/staging/logs/copilot/" 2>/dev/null || true + cp -r {{ engine_log_dir }}/* "$(Agent.TempDirectory)/staging/logs/copilot/" 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 ec51af2..b4c6ae0 100644 --- a/src/data/base.yml +++ b/src/data/base.yml @@ -29,30 +29,7 @@ jobs: {{ cancel_previous_builds }} - - 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 }}" @@ -102,7 +79,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" @@ -293,7 +270,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,14 +278,14 @@ jobs: # - Preserve all other fields (headers, type, etc.) jq --arg prefix "http://$(MCP_GATEWAY_DOMAIN):$(MCP_GATEWAY_PORT)" \ '.mcpServers |= (to_entries | sort_by(.key) | map(.value.url |= sub("^http://[^/]+/"; "\($prefix)/") | .value.tools = ["*"]) | from_entries)' \ - "$GATEWAY_OUTPUT" > /tmp/awf-tools/mcp-config.json + "$GATEWAY_OUTPUT" > {{ engine_mcp_config_path }} - # 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 {{ engine_mcp_config_path }} "{{ engine_home_config_dir }}/mcp-config.json" + chmod 600 {{ engine_mcp_config_path }} "{{ engine_home_config_dir }}/mcp-config.json" - echo "Generated MCP config at: /tmp/awf-tools/mcp-config.json" - cat /tmp/awf-tools/mcp-config.json + echo "Generated MCP config at: {{ engine_mcp_config_path }}" + cat {{ engine_mcp_config_path }} displayName: "Start MCP Gateway (MCPG)" {{ mcpg_step_env }} @@ -341,7 +318,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" \ @@ -354,10 +331,10 @@ jobs: fi exit $AGENT_EXIT_CODE - displayName: "Run copilot (AWF network isolated)" + displayName: "Run agent (AWF network isolated)" workingDirectory: {{ working_directory }} env: - {{ copilot_ado_env }} + {{ engine_ado_env }} GITHUB_TOKEN: $(GITHUB_TOKEN) GITHUB_READ_ONLY: 1 COPILOT_OTEL_ENABLED: "true" @@ -393,8 +370,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 @@ -424,29 +401,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 }}" @@ -532,7 +487,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" \ @@ -610,9 +565,9 @@ jobs: - bash: | # Copy all logs to analyzed outputs for artifact upload mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs" - if [ -d ~/.copilot/logs ]; then + if [ -d {{ engine_log_dir }} ]; then mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot" - cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true + cp -r {{ engine_log_dir }}/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true fi if [ -d ~/.ado-aw/logs ]; then mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw" @@ -691,9 +646,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 + if [ -d {{ engine_log_dir }} ]; then mkdir -p "$(Agent.TempDirectory)/staging/logs/copilot" - cp -r ~/.copilot/logs/* "$(Agent.TempDirectory)/staging/logs/copilot/" 2>/dev/null || true + cp -r {{ engine_log_dir }}/* "$(Agent.TempDirectory)/staging/logs/copilot/" 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 63b85ef..9433465 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -4,19 +4,48 @@ use crate::compile::extensions::{CompilerExtension, Extension}; use crate::compile::types::{FrontMatter, McpConfig}; pub trait Engine { + /// Generate CLI argument string (e.g., `--model claude-opus-4.5 --disable-builtin-mcps ...`). fn generate_cli_params( &self, front_matter: &FrontMatter, extensions: &[Extension], ) -> Result; + /// Generate ADO environment variable entries for the agent step (Stage 1). fn generate_agent_ado_env(&self, read_service_connection: Option<&str>) -> String; + + /// Version of the engine binary/package to install. + fn version(&self) -> &str; + + /// Generate pipeline YAML steps to install the engine binary. + /// Returns raw YAML step(s) that will be indented by the template engine. + fn install_steps(&self) -> String; + + /// Generate the AWF command string for the agent run. + /// `prompt_path` is the path to the agent prompt file inside the container. + /// `mcp_config_path` is the path to the MCP config file (Some for Agent job, None for Detection). + /// `engine_args` is the generated CLI argument string from `generate_cli_params`. + fn invocation(&self, prompt_path: &str, mcp_config_path: Option<&str>, engine_args: &str) -> String; + + /// Home config directory for the engine (e.g., `$HOME/.copilot`). + fn home_config_dir(&self) -> &str; + + /// Log directory for the engine (e.g., `~/.copilot/logs`). + fn log_dir(&self) -> &str; + + /// Path where the MCP config is stored inside the AWF container (e.g., `/tmp/awf-tools/mcp-config.json`). + fn mcp_config_path(&self) -> &str; } pub struct GitHubCopilotCliEngine; pub const GITHUB_COPILOT_CLI_ENGINE: GitHubCopilotCliEngine = GitHubCopilotCliEngine; +/// 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"; + impl Engine for GitHubCopilotCliEngine { fn generate_cli_params( &self, @@ -204,11 +233,74 @@ impl Engine for GitHubCopilotCliEngine { None => String::new(), } } + + fn version(&self) -> &str { + COPILOT_CLI_VERSION + } + + fn install_steps(&self) -> String { + let version = self.version(); + [ + "- task: NuGetAuthenticate@1", + " displayName: \"Authenticate NuGet Feed\"", + "", + "- task: NuGetCommand@2", + " displayName: \"Install Copilot CLI\"", + " inputs:", + " command: 'custom'", + &format!( + " arguments: 'install Microsoft.Copilot.CLI.linux-x64 -Source \ + \"https://pkgs.dev.azure.com/msazuresphere/_packaging/Guardian1ESPTUpstreamOrgFeed/nuget/v3/index.json\" \ + -Version {} -OutputDirectory $(Agent.TempDirectory)/tools -ExcludeVersion -NonInteractive'", + version + ), + "", + "- 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\"", + ] + .join("\n") + } + + fn invocation(&self, prompt_path: &str, mcp_config_path: Option<&str>, engine_args: &str) -> String { + match mcp_config_path { + Some(mcp_path) => format!( + "/tmp/awf-tools/copilot --prompt \"$(cat {prompt_path})\" --additional-mcp-config @{mcp_path} {engine_args}" + ), + None => format!( + "/tmp/awf-tools/copilot --prompt \"$(cat {prompt_path})\" {engine_args}" + ), + } + } + + fn home_config_dir(&self) -> &str { + "$HOME/.copilot" + } + + fn log_dir(&self) -> &str { + "~/.copilot/logs" + } + + fn mcp_config_path(&self) -> &str { + "/tmp/awf-tools/mcp-config.json" + } } #[cfg(test)] mod tests { - use super::{Engine, GITHUB_COPILOT_CLI_ENGINE}; + use super::{Engine, GITHUB_COPILOT_CLI_ENGINE, COPILOT_CLI_VERSION}; use crate::compile::{extensions::collect_extensions, parse_markdown}; #[test] @@ -232,4 +324,51 @@ mod tests { fn copilot_engine_generates_empty_ado_env_without_service_connection() { assert!(GITHUB_COPILOT_CLI_ENGINE.generate_agent_ado_env(None).is_empty()); } + + #[test] + fn copilot_engine_version() { + assert_eq!(GITHUB_COPILOT_CLI_ENGINE.version(), COPILOT_CLI_VERSION); + } + + #[test] + fn copilot_engine_install_steps_contain_nuget() { + let steps = GITHUB_COPILOT_CLI_ENGINE.install_steps(); + assert!(steps.contains("NuGetAuthenticate@1")); + assert!(steps.contains("NuGetCommand@2")); + assert!(steps.contains(COPILOT_CLI_VERSION)); + assert!(steps.contains("/tmp/awf-tools/copilot")); + assert!(steps.contains("copilot --version")); + } + + #[test] + fn copilot_engine_invocation_with_mcp_config() { + let inv = GITHUB_COPILOT_CLI_ENGINE.invocation( + "/tmp/awf-tools/agent-prompt.md", + Some("/tmp/awf-tools/mcp-config.json"), + "--model claude-opus-4.5 --disable-builtin-mcps", + ); + assert!(inv.contains("/tmp/awf-tools/copilot")); + assert!(inv.contains("--prompt")); + assert!(inv.contains("--additional-mcp-config @/tmp/awf-tools/mcp-config.json")); + assert!(inv.contains("--model claude-opus-4.5")); + } + + #[test] + fn copilot_engine_invocation_without_mcp_config() { + let inv = GITHUB_COPILOT_CLI_ENGINE.invocation( + "/tmp/awf-tools/threat-analysis-prompt.md", + None, + "--model claude-opus-4.5 --disable-builtin-mcps", + ); + assert!(inv.contains("/tmp/awf-tools/copilot")); + assert!(inv.contains("--prompt")); + assert!(!inv.contains("--additional-mcp-config")); + } + + #[test] + fn copilot_engine_paths() { + assert_eq!(GITHUB_COPILOT_CLI_ENGINE.home_config_dir(), "$HOME/.copilot"); + assert_eq!(GITHUB_COPILOT_CLI_ENGINE.log_dir(), "~/.copilot/logs"); + assert_eq!(GITHUB_COPILOT_CLI_ENGINE.mcp_config_path(), "/tmp/awf-tools/mcp-config.json"); + } } diff --git a/src/run.rs b/src/run.rs index 8c8c9d6..fe801f9 100644 --- a/src/run.rs +++ b/src/run.rs @@ -924,7 +924,7 @@ pub async fn run(args: &RunArgs) -> Result<()> { debug!("Agent prompt written to {}", prompt_path.display()); // ── 7. Build and run copilot command ───────────────────────────── - let copilot_params = compile::generate_copilot_params(&front_matter, &extensions)?; + let engine_args = compile::generate_engine_args(&front_matter, &extensions)?; println!("\n=== Copilot CLI ==="); @@ -945,8 +945,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); } @@ -987,7 +987,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, ); @@ -1071,7 +1071,7 @@ pub async fn run(args: &RunArgs) -> Result<()> { /// Does NOT handle backslash escapes, single quotes, or nested quotes. /// /// This is safe because the input is compiler-controlled output from -/// `generate_copilot_params()`, which only produces double-quoted values +/// `generate_engine_args()`, which only produces double-quoted values /// with no escapes. If params ever gain more complex quoting, consider /// using the `shell-words` crate. fn shell_words(s: &str) -> Vec { diff --git a/tests/compiler_tests.rs b/tests/compiler_tests.rs index d8eb0b0..d67e801 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_install }}"), + "Template should contain engine_install marker" ); assert!( template_content.contains("{{ compiler_version }}"), @@ -3040,7 +3040,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_run substituted)" ); assert!( compiled.contains("threat-analysis"),