diff --git a/.changeset/minor-copilot-sdk-driver.md b/.changeset/minor-copilot-sdk-driver.md
new file mode 100644
index 00000000000..4c1073bc3e6
--- /dev/null
+++ b/.changeset/minor-copilot-sdk-driver.md
@@ -0,0 +1,5 @@
+---
+"gh-aw": minor
+---
+
+Add the Copilot SDK-backed harness path for `copilot-sdk: true` workflows and the new smoke workflow.
diff --git a/.github/workflows/smoke-copilot-sdk.lock.yml b/.github/workflows/smoke-copilot-sdk.lock.yml
new file mode 100644
index 00000000000..cb392bfb247
--- /dev/null
+++ b/.github/workflows/smoke-copilot-sdk.lock.yml
@@ -0,0 +1,1553 @@
+# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4ec20cf53ea166c9409b8c93d883b5a0b612655de5f082265713c39f6f328ee3","body_hash":"c29aab69d3ce89bf57c600eccadae2604be0217feaf2b4247ae6c5a2d6c1d01d","strict":true,"agent_id":"copilot","agent_model":"gpt-5.4"}
+# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"de0fac2e4500dabe0009e67214ff5f5447ce83dd","version":"v6.0.2"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.25.58"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.22"},{"image":"ghcr.io/github/github-mcp-server:v1.1.0"},{"image":"node:lts-alpine","digest":"sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14","pinned_image":"node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14"}]}
+# ___ _ _
+# / _ \ | | (_)
+# | |_| | __ _ ___ _ __ | |_ _ ___
+# | _ |/ _` |/ _ \ '_ \| __| |/ __|
+# | | | | (_| | __/ | | | |_| | (__
+# \_| |_/\__, |\___|_| |_|\__|_|\___|
+# __/ |
+# _ _ |___/
+# | | | | / _| |
+# | | | | ___ _ __ _ __| |_| | _____ ____
+# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___|
+# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \
+# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/
+#
+# This file was automatically generated by gh-aw. DO NOT EDIT.
+#
+# To update this file, edit the corresponding .md file and run:
+# gh aw compile
+# Not all edits will cause changes to this file.
+#
+# For more information: https://github.github.com/gh-aw/introduction/overview/
+#
+# Smoke Copilot SDK
+#
+# Secrets used:
+# - COPILOT_GITHUB_TOKEN
+# - GH_AW_GITHUB_MCP_SERVER_TOKEN
+# - GH_AW_GITHUB_TOKEN
+# - GITHUB_TOKEN
+#
+# Custom actions used:
+# - actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+# - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
+# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+#
+# Container images used:
+# - ghcr.io/github/gh-aw-firewall/agent:0.25.58
+# - ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58
+# - ghcr.io/github/gh-aw-firewall/squid:0.25.58
+# - ghcr.io/github/gh-aw-mcpg:v0.3.22
+# - ghcr.io/github/github-mcp-server:v1.1.0
+# - node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14
+
+name: "Smoke Copilot SDK"
+on:
+ pull_request:
+ types:
+ - labeled
+ workflow_dispatch:
+ inputs:
+ aw_context:
+ default: ""
+ description: "Agent caller context (used internally by Agentic Workflows)."
+ required: false
+ type: string
+ item_number:
+ default: ""
+ description: The number of the issue, pull request, or discussion
+ required: false
+ type: string
+
+permissions: {}
+
+concurrency:
+ group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number || github.run_id }}-${{ github.event.label.name || github.run_id }}"
+
+run-name: "Smoke Copilot SDK"
+
+jobs:
+ activation:
+ needs: pre_activation
+ if: >
+ needs.pre_activation.outputs.activated == 'true' && (github.event_name == 'pull_request' && github.event.label.name == 'smoke-sdk' ||
+ !(github.event_name == 'pull_request'))
+ runs-on: ubuntu-slim
+ permissions:
+ actions: read
+ contents: read
+ issues: write
+ pull-requests: write
+ outputs:
+ comment_id: ${{ steps.add-comment.outputs.comment-id }}
+ comment_repo: ${{ steps.add-comment.outputs.comment-repo }}
+ comment_url: ${{ steps.add-comment.outputs.comment-url }}
+ engine_id: ${{ steps.generate_aw_info.outputs.engine_id }}
+ label_command: ${{ steps.remove_trigger_label.outputs.label_name }}
+ lockdown_check_failed: ${{ steps.generate_aw_info.outputs.lockdown_check_failed == 'true' }}
+ model: ${{ steps.generate_aw_info.outputs.model }}
+ secret_verification_result: ${{ steps.validate-secret.outputs.verification_result }}
+ setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }}
+ setup-span-id: ${{ steps.setup.outputs.span-id }}
+ setup-trace-id: ${{ steps.setup.outputs.trace-id }}
+ stale_lock_file_failed: ${{ steps.check-lock-file.outputs.stale_lock_file_failed == 'true' }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ repository: github/gh-aw
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ id: setup
+ uses: ./actions/setup
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.pre_activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.pre_activation.outputs.setup-parent-span-id || needs.pre_activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-copilot-sdk.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Generate agentic run info
+ id: generate_aw_info
+ env:
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI"
+ GH_AW_INFO_MODEL: "gpt-5.4"
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AGENT_VERSION: "1.0.55"
+ GH_AW_INFO_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_INFO_EXPERIMENTAL: "false"
+ GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true"
+ GH_AW_INFO_STAGED: "false"
+ GH_AW_INFO_ALLOWED_DOMAINS: '["defaults"]'
+ GH_AW_INFO_FIREWALL_ENABLED: "true"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_AWMG_VERSION: ""
+ GH_AW_INFO_FIREWALL_TYPE: "squid"
+ GH_AW_INFO_FRONTMATTER_EMOJI: "🔬"
+ GH_AW_COMPILED_STRICT: "true"
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs');
+ await main(core, context);
+ - name: Add eyes reaction for immediate feedback
+ id: react
+ if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_REACTION: "eyes"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/add_reaction.cjs');
+ await main();
+ - name: Validate COPILOT_GITHUB_TOKEN secret
+ id: validate-secret
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_multi_secret.sh" COPILOT_GITHUB_TOKEN 'GitHub Copilot CLI' https://github.github.com/gh-aw/reference/engines/#github-copilot-default
+ env:
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ - name: Checkout .github and .agents folders
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ sparse-checkout: |
+ .github
+ .agents
+ actions/setup
+ .antigravity
+ .claude
+ .codex
+ .crush
+ .gemini
+ .opencode
+ .pi
+ sparse-checkout-cone-mode: true
+ fetch-depth: 1
+ - name: Save agent config folders for base branch restoration
+ env:
+ GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi"
+ GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc"
+ # poutine:ignore untrusted_checkout_exec
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/save_base_github_folders.sh"
+ - name: Check workflow lock file
+ id: check-lock-file
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_WORKFLOW_FILE: "smoke-copilot-sdk.lock.yml"
+ GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs');
+ await main();
+ - name: Add comment with workflow run link
+ id: add-comment
+ if: github.event_name == 'issues' || github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || github.event_name == 'discussion' || github.event_name == 'discussion_comment' || github.event_name == 'pull_request' && github.event.pull_request.head.repo.id == github.repository_id
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_WORKFLOW_NAME: "Smoke Copilot SDK"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/add_workflow_run_comment.cjs');
+ await main();
+ - name: Remove trigger label
+ id: remove_trigger_label
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_LABEL_NAMES: "[\"smoke-sdk\"]"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/remove_trigger_label.cjs');
+ await main();
+ - name: Create prompt with built-in context
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl
+ GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ # poutine:ignore untrusted_checkout_exec
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh"
+ {
+ cat << 'GH_AW_PROMPT_621482cc32c8c45f_EOF'
+
+ GH_AW_PROMPT_621482cc32c8c45f_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/xpia.md"
+ cat "${RUNNER_TEMP}/gh-aw/prompts/temp_folder_prompt.md"
+ cat "${RUNNER_TEMP}/gh-aw/prompts/markdown.md"
+ cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_prompt.md"
+ cat << 'GH_AW_PROMPT_621482cc32c8c45f_EOF'
+
+ Tools: create_issue, missing_tool, missing_data, noop
+
+ GH_AW_PROMPT_621482cc32c8c45f_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md"
+ cat << 'GH_AW_PROMPT_621482cc32c8c45f_EOF'
+
+ The following GitHub context information is available for this workflow:
+ {{#if github.actor}}
+ - **actor**: __GH_AW_GITHUB_ACTOR__
+ {{/if}}
+ {{#if github.repository}}
+ - **repository**: __GH_AW_GITHUB_REPOSITORY__
+ {{/if}}
+ {{#if github.workspace}}
+ - **workspace**: __GH_AW_GITHUB_WORKSPACE__
+ {{/if}}
+ {{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}
+ - **issue-number**: #__GH_AW_EXPR_802A9F6A__
+ {{/if}}
+ {{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}
+ - **discussion-number**: #__GH_AW_EXPR_1A3A194A__
+ {{/if}}
+ {{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}
+ - **pull-request-number**: #__GH_AW_EXPR_463A214A__
+ {{/if}}
+ {{#if github.event.comment.id || github.aw.context.comment_id}}
+ - **comment-id**: __GH_AW_EXPR_FF1D34CE__
+ {{/if}}
+ {{#if github.run_id}}
+ - **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__
+ {{/if}}
+
+
+ GH_AW_PROMPT_621482cc32c8c45f_EOF
+ cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md"
+ cat << 'GH_AW_PROMPT_621482cc32c8c45f_EOF'
+
+ {{#runtime-import .github/workflows/smoke-copilot-sdk.md}}
+ GH_AW_PROMPT_621482cc32c8c45f_EOF
+ } > "$GH_AW_PROMPT"
+ - name: Interpolate variables and render templates
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }}
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs');
+ await main();
+ - name: Substitute placeholders
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_EXPR_1A3A194A: ${{ github.event.discussion.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'discussion' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_463A214A: ${{ github.event.pull_request.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'pull_request' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_802A9F6A: ${{ github.event.issue.number || (fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_type == 'issue' && fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').item_number) }}
+ GH_AW_EXPR_FF1D34CE: ${{ github.event.comment.id || fromJSON(github.event.inputs.aw_context || github.event.client_payload.aw_context || '{}').comment_id }}
+ GH_AW_GITHUB_ACTOR: ${{ github.actor }}
+ GH_AW_GITHUB_REPOSITORY: ${{ github.repository }}
+ GH_AW_GITHUB_RUN_ID: ${{ github.run_id }}
+ GH_AW_GITHUB_SERVER_URL: ${{ github.server_url }}
+ GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }}
+ GH_AW_MCP_CLI_SERVERS_LIST: '- `safeoutputs` — run `safeoutputs --help` to see available tools'
+ GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: ${{ needs.pre_activation.outputs.activated }}
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+
+ const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs');
+
+ // Call the substitution function
+ return await substitutePlaceholders({
+ file: process.env.GH_AW_PROMPT,
+ substitutions: {
+ GH_AW_EXPR_1A3A194A: process.env.GH_AW_EXPR_1A3A194A,
+ GH_AW_EXPR_463A214A: process.env.GH_AW_EXPR_463A214A,
+ GH_AW_EXPR_802A9F6A: process.env.GH_AW_EXPR_802A9F6A,
+ GH_AW_EXPR_FF1D34CE: process.env.GH_AW_EXPR_FF1D34CE,
+ GH_AW_GITHUB_ACTOR: process.env.GH_AW_GITHUB_ACTOR,
+ GH_AW_GITHUB_REPOSITORY: process.env.GH_AW_GITHUB_REPOSITORY,
+ GH_AW_GITHUB_RUN_ID: process.env.GH_AW_GITHUB_RUN_ID,
+ GH_AW_GITHUB_SERVER_URL: process.env.GH_AW_GITHUB_SERVER_URL,
+ GH_AW_GITHUB_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE,
+ GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST,
+ GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED: process.env.GH_AW_NEEDS_PRE_ACTIVATION_OUTPUTS_ACTIVATED
+ }
+ });
+ - name: Validate prompt placeholders
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ # poutine:ignore untrusted_checkout_exec
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/validate_prompt_placeholders.sh"
+ - name: Print prompt
+ env:
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ # poutine:ignore untrusted_checkout_exec
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh"
+ - name: Upload activation artifact
+ if: success()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: activation
+ include-hidden-files: true
+ path: |
+ /tmp/gh-aw/aw_info.json
+ /tmp/gh-aw/model_multipliers.json
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/aw-prompts/prompt-template.txt
+ /tmp/gh-aw/aw-prompts/prompt-import-tree.json
+ /tmp/gh-aw/github_rate_limits.jsonl
+ /tmp/gh-aw/base
+ /tmp/gh-aw/.github/agents
+ /tmp/gh-aw/.github/skills
+ if-no-files-found: ignore
+ retention-days: 1
+
+ agent:
+ needs: activation
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ env:
+ DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
+ GH_AW_ASSETS_ALLOWED_EXTS: ""
+ GH_AW_ASSETS_BRANCH: ""
+ GH_AW_ASSETS_MAX_SIZE_KB: 0
+ GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs
+ GH_AW_WORKFLOW_ID_SANITIZED: smokecopilotsdk
+ outputs:
+ agentic_engine_timeout: ${{ steps.detect-agent-errors.outputs.agentic_engine_timeout || 'false' }}
+ checkout_pr_success: ${{ steps.checkout-pr.outputs.checkout_pr_success || 'true' }}
+ effective_tokens: ${{ steps.parse-mcp-gateway.outputs.effective_tokens }}
+ effective_tokens_rate_limit_error: ${{ steps.parse-mcp-gateway.outputs.effective_tokens_rate_limit_error || 'false' }}
+ has_patch: ${{ steps.collect_output.outputs.has_patch }}
+ inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }}
+ mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }}
+ model: ${{ needs.activation.outputs.model }}
+ model_not_supported_error: ${{ steps.detect-agent-errors.outputs.model_not_supported_error || 'false' }}
+ output: ${{ steps.collect_output.outputs.output }}
+ output_types: ${{ steps.collect_output.outputs.output_types }}
+ setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }}
+ setup-span-id: ${{ steps.setup.outputs.span-id }}
+ setup-trace-id: ${{ steps.setup.outputs.trace-id }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ repository: github/gh-aw
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ id: setup
+ uses: ./actions/setup
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-copilot-sdk.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Set runtime paths
+ id: set-runtime-paths
+ run: |
+ {
+ echo "GH_AW_SAFE_OUTPUTS=${RUNNER_TEMP}/gh-aw/safeoutputs/outputs.jsonl"
+ echo "GH_AW_SAFE_OUTPUTS_CONFIG_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/config.json"
+ echo "GH_AW_SAFE_OUTPUTS_TOOLS_PATH=${RUNNER_TEMP}/gh-aw/safeoutputs/tools.json"
+ } >> "$GITHUB_OUTPUT"
+ - name: Checkout repository
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - name: Create gh-aw temp directory
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/create_gh_aw_tmp_dir.sh"
+ - name: Configure gh CLI for GitHub Enterprise
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/configure_gh_for_ghe.sh"
+ env:
+ GH_TOKEN: ${{ github.token }}
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Checkout PR branch
+ id: checkout-pr
+ if: |
+ github.event.pull_request || github.event.issue.pull_request
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs');
+ await main();
+ - name: Install GitHub Copilot CLI
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55
+ env:
+ GH_HOST: github.com
+ - name: Install AWF binary
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58
+ - name: Determine automatic lockdown mode for GitHub MCP Server
+ id: determine-automatic-lockdown
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 (source v9)
+ env:
+ GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ with:
+ script: |
+ const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs');
+ await determineAutomaticLockdown(github, context, core);
+ - name: Download activation artifact
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: activation
+ path: /tmp/gh-aw
+ - name: Restore agent config folders from base branch
+ if: steps.checkout-pr.outcome == 'success'
+ env:
+ GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .crush .gemini .github .opencode .pi"
+ GH_AW_AGENT_FILES: ".crush.json AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc"
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh"
+ - name: Restore inline sub-agents from activation artifact
+ env:
+ GH_AW_SUB_AGENT_DIR: ".github/agents"
+ GH_AW_SUB_AGENT_EXT: ".agent.md"
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_sub_agents.sh"
+ - name: Restore inline skills from activation artifact
+ env:
+ GH_AW_SKILL_DIR: ".github/skills"
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_inline_skills.sh"
+ - name: Download container images
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58 ghcr.io/github/gh-aw-mcpg:v0.3.22 ghcr.io/github/github-mcp-server:v1.1.0 node:lts-alpine@sha256:2bdb65ed1dab192432bc31c95f94155ca5ad7fc1392fb7eb7526ab682fa5bf14
+ - name: Generate Safe Outputs Config
+ run: |
+ mkdir -p "${RUNNER_TEMP}/gh-aw/safeoutputs"
+ mkdir -p /tmp/gh-aw/safeoutputs
+ mkdir -p /tmp/gh-aw/mcp-logs/safeoutputs
+ cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_da99e9e70250a2d0_EOF'
+ {"create_issue":{"close_older_issues":true,"close_older_key":"smoke-copilot-sdk","expires":2,"group":true,"labels":["automation","testing"],"max":1},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}}
+ GH_AW_SAFE_OUTPUTS_CONFIG_da99e9e70250a2d0_EOF
+ - name: Generate Safe Outputs Tools
+ env:
+ GH_AW_TOOLS_META_JSON: |
+ {
+ "description_suffixes": {
+ "create_issue": " CONSTRAINTS: Maximum 1 issue(s) can be created. Labels [\"automation\" \"testing\"] will be automatically added."
+ },
+ "repo_params": {},
+ "dynamic_tools": []
+ }
+ GH_AW_VALIDATION_JSON: |
+ {
+ "create_issue": {
+ "defaultMax": 1,
+ "fields": {
+ "body": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "fields": {
+ "type": "array"
+ },
+ "labels": {
+ "type": "array",
+ "itemType": "string",
+ "itemSanitize": true,
+ "itemMaxLength": 128
+ },
+ "parent": {
+ "issueOrPRNumber": true
+ },
+ "repo": {
+ "type": "string",
+ "maxLength": 256
+ },
+ "temporary_id": {
+ "type": "string"
+ },
+ "title": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ }
+ }
+ },
+ "missing_data": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "context": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "data_type": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ },
+ "reason": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ }
+ }
+ },
+ "missing_tool": {
+ "defaultMax": 20,
+ "fields": {
+ "alternatives": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 512
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 256
+ },
+ "tool": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 128
+ }
+ }
+ },
+ "noop": {
+ "defaultMax": 1,
+ "fields": {
+ "message": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ }
+ }
+ },
+ "report_incomplete": {
+ "defaultMax": 5,
+ "fields": {
+ "details": {
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 65000
+ },
+ "reason": {
+ "required": true,
+ "type": "string",
+ "sanitize": true,
+ "maxLength": 1024
+ }
+ }
+ }
+ }
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs');
+ await main();
+ - name: Generate Safe Outputs MCP Server Config
+ id: safe-outputs-config
+ run: |
+ # Generate a secure random API key (360 bits of entropy, 40+ chars)
+ # Mask immediately to prevent timing vulnerabilities
+ API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${API_KEY}"
+
+ PORT=3001
+
+ # Set outputs for next steps
+ {
+ echo "safe_outputs_api_key=${API_KEY}"
+ echo "safe_outputs_port=${PORT}"
+ } >> "$GITHUB_OUTPUT"
+
+ echo "Safe Outputs MCP server will run on port ${PORT}"
+
+ - name: Start Safe Outputs MCP HTTP Server
+ id: safe-outputs-start
+ env:
+ DEBUG: '*'
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-config.outputs.safe_outputs_port }}
+ GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-config.outputs.safe_outputs_api_key }}
+ GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/tools.json
+ GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ runner.temp }}/gh-aw/safeoutputs/config.json
+ GH_AW_MCP_LOG_DIR: /tmp/gh-aw/mcp-logs/safeoutputs
+ run: |
+ # Environment variables are set above to prevent template injection
+ export DEBUG
+ export GH_AW_SAFE_OUTPUTS
+ export GH_AW_SAFE_OUTPUTS_PORT
+ export GH_AW_SAFE_OUTPUTS_API_KEY
+ export GH_AW_SAFE_OUTPUTS_TOOLS_PATH
+ export GH_AW_SAFE_OUTPUTS_CONFIG_PATH
+ export GH_AW_MCP_LOG_DIR
+
+ bash "${RUNNER_TEMP}/gh-aw/actions/start_safe_outputs_server.sh"
+
+ - name: Start MCP Gateway
+ id: start-mcp-gateway
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_SAFE_OUTPUTS_API_KEY: ${{ steps.safe-outputs-start.outputs.api_key }}
+ GH_AW_SAFE_OUTPUTS_PORT: ${{ steps.safe-outputs-start.outputs.port }}
+ GITHUB_MCP_GUARD_MIN_INTEGRITY: ${{ steps.determine-automatic-lockdown.outputs.min_integrity }}
+ GITHUB_MCP_GUARD_REPOS: ${{ steps.determine-automatic-lockdown.outputs.repos }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ run: |
+ set -eo pipefail
+ mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config"
+
+ # Export gateway environment variables for MCP config and gateway script
+ export MCP_GATEWAY_PORT="8080"
+ export MCP_GATEWAY_DOMAIN="host.docker.internal"
+ export MCP_GATEWAY_HOST_DOMAIN="localhost"
+ MCP_GATEWAY_API_KEY=$(openssl rand -base64 45 | tr -d '/+=')
+ echo "::add-mask::${MCP_GATEWAY_API_KEY}"
+ export MCP_GATEWAY_API_KEY
+ export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads"
+ mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}"
+ export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288"
+ export DEBUG="*"
+
+ export GH_AW_ENGINE="copilot"
+ MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0')
+ MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0')
+ case "${DOCKER_HOST:-}" in
+ unix://* ) DOCKER_SOCK_PATH="${DOCKER_HOST#unix://}" ;;
+ /* ) DOCKER_SOCK_PATH="$DOCKER_HOST" ;;
+ * ) DOCKER_SOCK_PATH=/var/run/docker.sock ;;
+ esac
+ DOCKER_SOCK_GID=$(stat -c '%g' "$DOCKER_SOCK_PATH" 2>/dev/null || echo '0')
+ export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network host --add-host host.docker.internal:127.0.0.1 --user '"${MCP_GATEWAY_UID}"':'"${MCP_GATEWAY_GID}"' --group-add '"${DOCKER_SOCK_GID}"' -v '"${DOCKER_SOCK_PATH}"':/var/run/docker.sock -e MCP_GATEWAY_PORT -e MCP_GATEWAY_DOMAIN -e MCP_GATEWAY_API_KEY -e MCP_GATEWAY_PAYLOAD_DIR -e MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD -e DOCKER_HOST=unix:///var/run/docker.sock -e DEBUG -e MCP_GATEWAY_LOG_DIR -e GH_AW_MCP_LOG_DIR -e GH_AW_SAFE_OUTPUTS -e GH_AW_SAFE_OUTPUTS_CONFIG_PATH -e GH_AW_SAFE_OUTPUTS_TOOLS_PATH -e GH_AW_ASSETS_BRANCH -e GH_AW_ASSETS_MAX_SIZE_KB -e GH_AW_ASSETS_ALLOWED_EXTS -e DEFAULT_BRANCH -e GITHUB_MCP_SERVER_TOKEN -e GITHUB_MCP_GUARD_MIN_INTEGRITY -e GITHUB_MCP_GUARD_REPOS -e GITHUB_REPOSITORY -e GITHUB_SERVER_URL -e GITHUB_SHA -e GITHUB_WORKSPACE -e GITHUB_TOKEN -e GITHUB_RUN_ID -e GITHUB_RUN_NUMBER -e GITHUB_RUN_ATTEMPT -e GITHUB_JOB -e GITHUB_ACTION -e GITHUB_EVENT_NAME -e GITHUB_EVENT_PATH -e GITHUB_ACTOR -e GITHUB_ACTOR_ID -e GITHUB_TRIGGERING_ACTOR -e GITHUB_WORKFLOW -e GITHUB_WORKFLOW_REF -e GITHUB_WORKFLOW_SHA -e GITHUB_REF -e GITHUB_REF_NAME -e GITHUB_REF_TYPE -e GITHUB_HEAD_REF -e GITHUB_BASE_REF -e GH_AW_SAFE_OUTPUTS_PORT -e GH_AW_SAFE_OUTPUTS_API_KEY -v /tmp/gh-aw/mcp-payloads:/tmp/gh-aw/mcp-payloads:rw -v /opt:/opt:ro -v /tmp:/tmp:rw -v '"${GITHUB_WORKSPACE}"':'"${GITHUB_WORKSPACE}"':rw ghcr.io/github/gh-aw-mcpg:v0.3.22'
+
+ mkdir -p /home/runner/.copilot
+ GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node)
+ cat << GH_AW_MCP_CONFIG_fe219e884a7d092c_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs"
+ {
+ "mcpServers": {
+ "github": {
+ "type": "stdio",
+ "container": "ghcr.io/github/github-mcp-server:v1.1.0",
+ "env": {
+ "GITHUB_HOST": "\${GITHUB_SERVER_URL}",
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "\${GITHUB_MCP_SERVER_TOKEN}",
+ "GITHUB_READ_ONLY": "1",
+ "GITHUB_TOOLSETS": "context,repos,issues,pull_requests"
+ },
+ "guard-policies": {
+ "allow-only": {
+ "min-integrity": "$GITHUB_MCP_GUARD_MIN_INTEGRITY",
+ "repos": "$GITHUB_MCP_GUARD_REPOS"
+ }
+ }
+ },
+ "safeoutputs": {
+ "type": "http",
+ "url": "http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT",
+ "headers": {
+ "Authorization": "\${GH_AW_SAFE_OUTPUTS_API_KEY}"
+ },
+ "guard-policies": {
+ "write-sink": {
+ "accept": [
+ "*"
+ ]
+ }
+ }
+ }
+ },
+ "gateway": {
+ "port": $MCP_GATEWAY_PORT,
+ "domain": "${MCP_GATEWAY_DOMAIN}",
+ "apiKey": "${MCP_GATEWAY_API_KEY}",
+ "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}"
+ }
+ }
+ GH_AW_MCP_CONFIG_fe219e884a7d092c_EOF
+ - name: Mount MCP servers as CLIs
+ id: mount-mcp-clis
+ continue-on-error: true
+ env:
+ MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
+ MCP_GATEWAY_DOMAIN: ${{ steps.start-mcp-gateway.outputs.gateway-domain }}
+ MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs');
+ await main();
+ - name: Clean credentials
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/clean_git_credentials.sh"
+ - name: Audit pre-agent workspace
+ id: pre_agent_audit
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/audit_pre_agent_workspace.sh"
+ - name: Execute GitHub Copilot CLI
+ id: agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ timeout-minutes: 10
+ run: |
+ set -o pipefail
+ printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt
+ touch /tmp/gh-aw/agent-step-summary.md
+ GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true)
+ export GH_AW_NODE_BIN
+ export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK"
+ (umask 177 && touch /tmp/gh-aw/agent-stdio.log)
+ printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","api.snapcraft.io","archive.ubuntu.com","azure.archive.ubuntu.com","crl.geotrust.com","crl.globalsign.com","crl.identrust.com","crl.sectigo.com","crl.thawte.com","crl.usertrust.com","crl.verisign.com","crl3.digicert.com","crl4.digicert.com","crls.ssl.com","github.com","host.docker.internal","json-schema.org","json.schemastore.org","keyserver.ubuntu.com","ocsp.digicert.com","ocsp.geotrust.com","ocsp.globalsign.com","ocsp.identrust.com","ocsp.sectigo.com","ocsp.ssl.com","ocsp.thawte.com","ocsp.usertrust.com","ocsp.verisign.com","packagecloud.io","packages.cloud.google.com","packages.microsoft.com","ppa.launchpad.net","raw.githubusercontent.com","registry.npmjs.org","s.symcb.com","s.symcd.com","security.ubuntu.com","telemetry.enterprise.githubcopilot.com","ts-crl.ws.symantec.com","ts-ocsp.ws.symantec.com","www.googleapis.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000,"models":{"agent":["sonnet-6x","gpt-5.4","gpt-5.3","gemini-pro","any"],"antigravity":["copilot/antigravity*","google/antigravity*","gemini/antigravity*"],"any":["copilot/*","anthropic/*","openai/*","google/*","gemini/*"],"claude":["agent"],"codex":["agent"],"coding":["copilot/gpt-5*codex*","openai/gpt-5*codex*","gpt-5-codex"],"computer-use":["copilot/*computer-use*","google/*computer-use*","gemini/*computer-use*","openai/*computer-use*"],"copilot":["agent"],"deep-research":["copilot/deep-research*","copilot/o3-deep-research*","copilot/o4-mini-deep-research*","google/deep-research*","gemini/deep-research*","openai/o3-deep-research*","openai/o4-mini-deep-research*"],"gemini":["agent"],"gemini-3-flash":["copilot/gemini-3*flash*","google/gemini-3*flash*","gemini/gemini-3*flash*"],"gemini-3-pro":["copilot/gemini-3*pro*","google/gemini-3*pro*","gemini/gemini-3*pro*"],"gemini-3.1-flash":["copilot/gemini-3.1*flash*","google/gemini-3.1*flash*","gemini/gemini-3.1*flash*"],"gemini-3.1-pro":["copilot/gemini-3.1*pro*","google/gemini-3.1*pro*","gemini/gemini-3.1*pro*"],"gemini-3.5-flash":["copilot/gemini-3.5*flash*","google/gemini-3.5*flash*","gemini/gemini-3.5*flash*"],"gemini-flash":["copilot/gemini-*flash*","google/gemini-*flash*","gemini/gemini-*flash*"],"gemini-flash-lite":["copilot/gemini-*flash*lite*","google/gemini-*flash*lite*","gemini/gemini-*flash*lite*"],"gemini-pro":["copilot/gemini-*pro*","google/gemini-*pro*","gemini/gemini-*pro*"],"gemma":["copilot/gemma*","google/gemma*","gemini/gemma*"],"gpt-5":["copilot/gpt-5*","openai/gpt-5*"],"gpt-5-codex":["copilot/gpt-5*codex*","openai/gpt-5*codex*"],"gpt-5-mini":["copilot/gpt-5*mini*","openai/gpt-5*mini*"],"gpt-5-nano":["copilot/gpt-5*nano*","openai/gpt-5*nano*"],"gpt-5-pro":["copilot/gpt-5*pro*","openai/gpt-5*pro*"],"gpt-5.2":["copilot/gpt-5.2*","openai/gpt-5.2*"],"gpt-5.3":["copilot/gpt-5.3*","openai/gpt-5.3*"],"gpt-5.4":["copilot/gpt-5.4*","openai/gpt-5.4*"],"gpt-5.5":["copilot/gpt-5.5*","openai/gpt-5.5*"],"haiku":["copilot/*haiku*","anthropic/*haiku*"],"large":["sonnet","gpt-5-pro","gpt-5","gemini-pro"],"mini":["haiku","gpt-5-mini","gpt-5-nano","gemini-flash-lite"],"opus":["copilot/*opus*","anthropic/*opus*"],"opusplan":["opus?effort=high"],"reasoning":["copilot/o1*","copilot/o3*","copilot/o4*","openai/o1*","openai/o3*","openai/o4*"],"robotics":["copilot/*robotics*","google/*robotics*","gemini/*robotics*"],"small":["mini"],"sonnet":["copilot/*sonnet*","anthropic/*sonnet*"],"sonnet-6x":["copilot/*sonnet-4.5*","copilot/*sonnet-4.6*","copilot/*sonnet-4-5-*","anthropic/*sonnet-4-5-*","copilot/*sonnet-4-6*","anthropic/*sonnet-4-6*"],"summarization":["haiku","gpt-5-mini","gemini-flash-lite","mini"],"vision":["copilot/gemini-*image*","gemini/gemini-*image*","copilot/gemini-*flash*","gemini/gemini-*flash*"]}},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json"
+ GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs"
+ cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS=""
+ if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw"
+ fi
+ GH_AW_TOOL_CACHE_MOUNT=""
+ GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"
+ if [ -d "$GH_AW_TOOL_CACHE" ]; then
+ if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then
+ GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro"
+ fi
+ elif [ -d "/home/runner/work/_tool" ]; then
+ GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro"
+ fi
+ # shellcheck disable=SC1003
+ sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \
+ -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && printf '\''%s'\'' '\''{"promptFile":"/tmp/gh-aw/aw-prompts/prompt.txt","serverArgs":["--headless","--no-auto-update","--port","3002","--add-dir","/tmp/gh-aw/","--log-level","all","--log-dir","/tmp/gh-aw/sandbox/agent/logs/","--disable-builtin-mcps","--no-ask-user","--allow-all-tools","--allow-all-paths","--no-custom-instructions"],"addWorkspaceDir":true}'\'' | GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log
+ env:
+ AWF_REFLECT_ENABLED: 1
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: gpt-5.4
+ COPILOT_SDK_URI: http://127.0.0.1:3002
+ GH_AW_MCP_CONFIG: /home/runner/.copilot/mcp-config.json
+ GH_AW_PHASE: agent
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_VERSION: dev
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ RUNNER_TEMP: ${{ runner.temp }}
+ XDG_CONFIG_HOME: /home/runner
+ - name: Detect agent errors
+ if: always()
+ id: detect-agent-errors
+ continue-on-error: true
+ run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs"
+ - name: Configure Git credentials
+ env:
+ REPO_NAME: ${{ github.repository }}
+ SERVER_URL: ${{ github.server_url }}
+ GITHUB_TOKEN: ${{ github.token }}
+ run: |
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git config --global user.name "github-actions[bot]"
+ git config --global am.keepcr true
+ # Re-authenticate git with GitHub token
+ SERVER_URL_STRIPPED="${SERVER_URL#https://}"
+ git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@${SERVER_URL_STRIPPED}/${REPO_NAME}.git"
+ echo "Git configured with standard GitHub Actions identity"
+ - name: Copy Copilot session state files to logs
+ if: always()
+ continue-on-error: true
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/copy_copilot_session_state.sh"
+ - name: Stop MCP Gateway
+ if: always()
+ continue-on-error: true
+ env:
+ MCP_GATEWAY_PORT: ${{ steps.start-mcp-gateway.outputs.gateway-port }}
+ MCP_GATEWAY_API_KEY: ${{ steps.start-mcp-gateway.outputs.gateway-api-key }}
+ GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }}
+ run: |
+ bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID"
+ - name: Redact secrets in logs
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs');
+ await main();
+ env:
+ GH_AW_SECRET_NAMES: 'COPILOT_GITHUB_TOKEN,GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN'
+ SECRET_COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ SECRET_GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }}
+ SECRET_GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }}
+ SECRET_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ - name: Append agent step summary
+ if: always()
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/append_agent_step_summary.sh"
+ - name: Copy Safe Outputs
+ if: always()
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ run: |
+ mkdir -p /tmp/gh-aw
+ cp "$GH_AW_SAFE_OUTPUTS" /tmp/gh-aw/safeoutputs.jsonl 2>/dev/null || true
+ - name: Ingest agent output
+ id: collect_output
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }}
+ GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs');
+ await main();
+ - name: Parse agent logs for step summary
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: /tmp/gh-aw/sandbox/agent/logs/
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs');
+ await main();
+ - name: Parse MCP Gateway logs for step summary
+ if: always()
+ id: parse-mcp-gateway
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs');
+ await main();
+ - name: Print firewall logs
+ if: always()
+ continue-on-error: true
+ env:
+ AWF_LOGS_DIR: /tmp/gh-aw/sandbox/firewall/logs
+ run: |
+ # Fix permissions on firewall logs/audit dirs so they can be uploaded as artifacts
+ # AWF runs with sudo, creating files owned by root
+ sudo chmod -R a+rX /tmp/gh-aw/sandbox/firewall 2>/dev/null || true
+ # Only run awf logs summary if awf command exists (it may not be installed if workflow failed before install step)
+ if command -v awf &> /dev/null; then
+ awf logs summary | tee -a "$GITHUB_STEP_SUMMARY"
+ else
+ echo 'AWF binary not installed, skipping firewall log summary'
+ fi
+ - name: Parse token usage for step summary
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs');
+ await main();
+ - name: Print AWF reflect summary
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs');
+ await main();
+ - name: Write agent output placeholder if missing
+ if: always()
+ run: |
+ if [ ! -f /tmp/gh-aw/agent_output.json ]; then
+ echo '{"items":[]}' > /tmp/gh-aw/agent_output.json
+ fi
+ - name: Upload agent artifacts
+ if: always()
+ continue-on-error: true
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: agent
+ path: |
+ /tmp/gh-aw/aw-prompts/prompt.txt
+ /tmp/gh-aw/sandbox/agent/logs/
+ /tmp/gh-aw/redacted-urls.log
+ /tmp/gh-aw/mcp-logs/
+ /tmp/gh-aw/agent_usage.json
+ /tmp/gh-aw/agent-stdio.log
+ /tmp/gh-aw/pre-agent-audit.txt
+ /tmp/gh-aw/agent/
+ /tmp/gh-aw/github_rate_limits.jsonl
+ /tmp/gh-aw/safeoutputs.jsonl
+ /tmp/gh-aw/agent_output.json
+ /tmp/gh-aw/aw-*.patch
+ /tmp/gh-aw/aw-*.bundle
+ /tmp/gh-aw/awf-config.json
+ /tmp/gh-aw/sandbox/firewall/logs/
+ /tmp/gh-aw/sandbox/firewall/audit/
+ /tmp/gh-aw/sandbox/firewall/awf-reflect.json
+ if-no-files-found: ignore
+
+ conclusion:
+ needs:
+ - activation
+ - agent
+ - detection
+ - safe_outputs
+ if: >
+ always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
+ needs.activation.outputs.stale_lock_file_failed == 'true')
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ issues: write
+ concurrency:
+ group: "gh-aw-conclusion-smoke-copilot-sdk"
+ cancel-in-progress: false
+ queue: max
+ outputs:
+ incomplete_count: ${{ steps.report_incomplete.outputs.incomplete_count }}
+ noop_message: ${{ steps.noop.outputs.noop_message }}
+ tools_reported: ${{ steps.missing_tool.outputs.tools_reported }}
+ total_count: ${{ steps.missing_tool.outputs.total_count }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ repository: github/gh-aw
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ id: setup
+ uses: ./actions/setup
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-copilot-sdk.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ run: |
+ mkdir -p /tmp/gh-aw/
+ find "/tmp/gh-aw/" -type f -print
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
+ - name: Process no-op messages
+ id: noop
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_NOOP_MAX: "1"
+ GH_AW_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/smoke-copilot-sdk.md"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
+ GH_AW_NOOP_REPORT_AS_ISSUE: "true"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs');
+ await main();
+ - name: Log detection run
+ id: detection_runs
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/smoke-copilot-sdk.md"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }}
+ GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs');
+ await main();
+ - name: Record missing tool
+ id: missing_tool
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_MISSING_TOOL_CREATE_ISSUE: "true"
+ GH_AW_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/smoke-copilot-sdk.md"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs');
+ await main();
+ - name: Record incomplete
+ id: report_incomplete
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_REPORT_INCOMPLETE_CREATE_ISSUE: "true"
+ GH_AW_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/smoke-copilot-sdk.md"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs');
+ await main();
+ - name: Handle agent failure
+ id: handle_agent_failure
+ if: always()
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/smoke-copilot-sdk.md"
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
+ GH_AW_WORKFLOW_ID: "smoke-copilot-sdk"
+ GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "12"
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_SECRET_VERIFICATION_RESULT: ${{ needs.activation.outputs.secret_verification_result }}
+ GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }}
+ GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }}
+ GH_AW_EFFECTIVE_TOKENS_RATE_LIMIT_ERROR: ${{ needs.agent.outputs.effective_tokens_rate_limit_error || 'false' }}
+ GH_AW_INFERENCE_ACCESS_ERROR: ${{ needs.agent.outputs.inference_access_error }}
+ GH_AW_MCP_POLICY_ERROR: ${{ needs.agent.outputs.mcp_policy_error }}
+ GH_AW_AGENTIC_ENGINE_TIMEOUT: ${{ needs.agent.outputs.agentic_engine_timeout }}
+ GH_AW_MODEL_NOT_SUPPORTED_ERROR: ${{ needs.agent.outputs.model_not_supported_error }}
+ GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com"
+ GH_AW_LOCKDOWN_CHECK_FAILED: ${{ needs.activation.outputs.lockdown_check_failed }}
+ GH_AW_STALE_LOCK_FILE_FAILED: ${{ needs.activation.outputs.stale_lock_file_failed }}
+ GH_AW_GROUP_REPORTS: "false"
+ GH_AW_FAILURE_REPORT_AS_ISSUE: "true"
+ GH_AW_MISSING_TOOL_REPORT_AS_FAILURE: "true"
+ GH_AW_MISSING_DATA_REPORT_AS_FAILURE: "true"
+ GH_AW_TIMEOUT_MINUTES: "10"
+ GH_AW_MAX_EFFECTIVE_TOKENS: "25000000"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs');
+ await main();
+ - name: Update reaction comment with completion status
+ id: conclusion
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }}
+ GH_AW_COMMENT_REPO: ${{ needs.activation.outputs.comment_repo }}
+ GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ GH_AW_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_AGENT_CONCLUSION: ${{ needs.agent.result }}
+ GH_AW_SAFE_OUTPUTS_RESULT: ${{ needs.safe_outputs.result }}
+ GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }}
+ GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }}
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/notify_comment_error.cjs');
+ await main();
+
+ detection:
+ needs:
+ - activation
+ - agent
+ if: >
+ always() && needs.agent.result != 'skipped' && (needs.agent.outputs.output_types != '' || needs.agent.outputs.has_patch == 'true')
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ detection_conclusion: ${{ steps.detection_conclusion.outputs.conclusion }}
+ detection_reason: ${{ steps.detection_conclusion.outputs.reason }}
+ detection_success: ${{ steps.detection_conclusion.outputs.success }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ repository: github/gh-aw
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ id: setup
+ uses: ./actions/setup
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-copilot-sdk.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ run: |
+ mkdir -p /tmp/gh-aw/
+ find "/tmp/gh-aw/" -type f -print
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
+ - name: Checkout repository for patch context
+ if: needs.agent.outputs.has_patch == 'true'
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ # --- Threat Detection ---
+ - name: Clean stale firewall files from agent artifact
+ run: |
+ rm -rf /tmp/gh-aw/sandbox/firewall/logs
+ rm -rf /tmp/gh-aw/sandbox/firewall/audit
+ - name: Download container images
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.25.58 ghcr.io/github/gh-aw-firewall/api-proxy:0.25.58 ghcr.io/github/gh-aw-firewall/squid:0.25.58
+ - name: Check if detection needed
+ id: detection_guard
+ if: always()
+ env:
+ OUTPUT_TYPES: ${{ needs.agent.outputs.output_types }}
+ HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
+ run: |
+ if [[ -n "$OUTPUT_TYPES" || "$HAS_PATCH" == "true" ]]; then
+ echo "run_detection=true" >> "$GITHUB_OUTPUT"
+ echo "Detection will run: output_types=$OUTPUT_TYPES, has_patch=$HAS_PATCH"
+ else
+ echo "run_detection=false" >> "$GITHUB_OUTPUT"
+ echo "Detection skipped: no agent outputs or patches to analyze"
+ fi
+ - name: Clear MCP Config for detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ rm -f "${RUNNER_TEMP}/gh-aw/mcp-config/mcp-servers.json"
+ rm -f /home/runner/.copilot/mcp-config.json
+ rm -f "$GITHUB_WORKSPACE/.gemini/settings.json"
+ - name: Prepare threat detection files
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ mkdir -p /tmp/gh-aw/threat-detection/aw-prompts
+ cp /tmp/gh-aw/aw-prompts/prompt.txt /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt 2>/dev/null || true
+ if [ ! -s /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt ]; then
+ echo "::warning::ERR_VALIDATION: Missing or empty detection context prompt at /tmp/gh-aw/threat-detection/aw-prompts/prompt.txt. Ensure the agent artifact includes /tmp/gh-aw/aw-prompts/prompt.txt. Detection will continue with fallback workflow context."
+ fi
+ cp /tmp/gh-aw/agent_output.json /tmp/gh-aw/threat-detection/agent_output.json 2>/dev/null || true
+ for f in /tmp/gh-aw/aw-*.patch; do
+ [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ done
+ for f in /tmp/gh-aw/aw-*.bundle; do
+ [ -f "$f" ] && cp "$f" /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ done
+ echo "Prepared threat detection files:"
+ ls -la /tmp/gh-aw/threat-detection/ 2>/dev/null || true
+ - name: Setup threat detection
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ WORKFLOW_NAME: "Smoke Copilot SDK"
+ WORKFLOW_DESCRIPTION: "Smoke Copilot SDK"
+ HAS_PATCH: ${{ needs.agent.outputs.has_patch }}
+ with:
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs');
+ await main();
+ - name: Ensure threat-detection directory and log
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ run: |
+ mkdir -p /tmp/gh-aw/threat-detection
+ touch /tmp/gh-aw/threat-detection/detection.log
+ - name: Setup Node.js
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
+ with:
+ node-version: '24'
+ package-manager-cache: false
+ - name: Install GitHub Copilot CLI
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.55
+ env:
+ GH_HOST: github.com
+ - name: Install AWF binary
+ run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.25.58
+ - name: Execute GitHub Copilot CLI
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ continue-on-error: true
+ id: detection_agentic_execution
+ # Copilot CLI tool arguments (sorted):
+ timeout-minutes: 20
+ run: |
+ set -o pipefail
+ printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt
+ touch /tmp/gh-aw/agent-step-summary.md
+ GH_AW_NODE_BIN=$(command -v node 2>/dev/null || true)
+ export GH_AW_NODE_BIN
+ export COPILOT_API_KEY="$COPILOT_DUMMY_BYOK"
+ (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log)
+ printf '%s\n' '{"$schema":"https://github.com/github/gh-aw-firewall/releases/download/v0.25.58/awf-config.schema.json","network":{"allowDomains":["api.business.githubcopilot.com","api.enterprise.githubcopilot.com","api.github.com","api.githubcopilot.com","api.individual.githubcopilot.com","github.com","host.docker.internal","registry.npmjs.org","telemetry.enterprise.githubcopilot.com"]},"apiProxy":{"enabled":true,"enableTokenSteering":true,"maxRuns":500,"maxEffectiveTokens":25000000},"container":{"imageTag":"0.25.58"}}' > "${RUNNER_TEMP}/gh-aw/awf-config.json"
+ GH_AW_MODEL_MULTIPLIERS_PATH="/tmp/gh-aw/model_multipliers.json" node "${RUNNER_TEMP}/gh-aw/actions/merge_awf_model_multipliers.cjs"
+ cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS=""
+ if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then
+ GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS="--docker-host-path-prefix /tmp/gh-aw"
+ fi
+ GH_AW_TOOL_CACHE_MOUNT=""
+ GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"
+ if [ -d "$GH_AW_TOOL_CACHE" ]; then
+ if [[ "$GH_AW_TOOL_CACHE" != /opt/* ]]; then
+ GH_AW_TOOL_CACHE_MOUNT="$GH_AW_TOOL_CACHE:$GH_AW_TOOL_CACHE:ro"
+ fi
+ elif [ -d "/home/runner/work/_tool" ]; then
+ GH_AW_TOOL_CACHE_MOUNT="/home/runner/work/_tool:/home/runner/work/_tool:ro"
+ fi
+ # shellcheck disable=SC1003
+ sudo -E awf --config "${RUNNER_TEMP}/gh-aw/awf-config.json" --container-workdir "${GITHUB_WORKSPACE}" --mount "${RUNNER_TEMP}/gh-aw:${RUNNER_TEMP}/gh-aw:ro" --mount "${RUNNER_TEMP}/gh-aw:/host${RUNNER_TEMP}/gh-aw:ro" ${GH_AW_TOOL_CACHE_MOUNT:+--mount "$GH_AW_TOOL_CACHE_MOUNT"} ${GH_AW_DOCKER_HOST_PATH_PREFIX_ARGS} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --proxy-logs-dir /tmp/gh-aw/sandbox/firewall/logs --audit-dir /tmp/gh-aw/sandbox/firewall/audit --enable-host-access --allow-host-ports 80,443,8080 --skip-pull \
+ -- /bin/bash -c 'set +o histexpand; GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:-/opt/hostedtoolcache}"; export PATH="$(find "$GH_AW_TOOL_CACHE" /opt/hostedtoolcache /home/runner/work/_tool -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true && GH_AW_NODE_EXEC="${GH_AW_NODE_BIN:-}"; if [ -z "$GH_AW_NODE_EXEC" ] || [ ! -x "$GH_AW_NODE_EXEC" ]; then GH_AW_NODE_EXEC="$(command -v node 2>/dev/null || true)"; fi; if [ -z "$GH_AW_NODE_EXEC" ]; then echo "node runtime missing on this runner — check runtimes.node in workflow YAML" >&2; exit 127; fi; "$GH_AW_NODE_EXEC" ${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs /usr/local/bin/copilot --add-dir /tmp/gh-aw/ --log-level all --log-dir /tmp/gh-aw/sandbox/agent/logs/ --disable-builtin-mcps --no-ask-user --allow-all-tools --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log
+ env:
+ AWF_REFLECT_ENABLED: 1
+ COPILOT_AGENT_RUNNER_TYPE: STANDALONE
+ COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode
+ COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
+ COPILOT_MODEL: gpt-5.4
+ GH_AW_PHASE: detection
+ GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt
+ GH_AW_VERSION: dev
+ GITHUB_API_URL: ${{ github.api_url }}
+ GITHUB_AW: true
+ GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows
+ GITHUB_HEAD_REF: ${{ github.head_ref }}
+ GITHUB_REF_NAME: ${{ github.ref_name }}
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_STEP_SUMMARY: /tmp/gh-aw/agent-step-summary.md
+ GITHUB_WORKSPACE: ${{ github.workspace }}
+ GIT_AUTHOR_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_AUTHOR_NAME: github-actions[bot]
+ GIT_COMMITTER_EMAIL: github-actions[bot]@users.noreply.github.com
+ GIT_COMMITTER_NAME: github-actions[bot]
+ RUNNER_TEMP: ${{ runner.temp }}
+ XDG_CONFIG_HOME: /home/runner
+ - name: Upload threat detection log
+ if: always() && steps.detection_guard.outputs.run_detection == 'true'
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: detection
+ path: /tmp/gh-aw/threat-detection/detection.log
+ if-no-files-found: ignore
+ - name: Parse and conclude threat detection
+ id: detection_conclusion
+ if: always()
+ continue-on-error: true
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ RUN_DETECTION: ${{ steps.detection_guard.outputs.run_detection }}
+ DETECTION_AGENTIC_EXECUTION_OUTCOME: ${{ steps.detection_agentic_execution.outcome }}
+ GH_AW_DETECTION_CONTINUE_ON_ERROR: "true"
+ with:
+ script: |
+ try {
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_threat_detection_results.cjs');
+ await main();
+ } catch (loadErr) {
+ const continueOnError = process.env.GH_AW_DETECTION_CONTINUE_ON_ERROR !== 'false';
+ const detectionExecutionFailed = process.env.DETECTION_AGENTIC_EXECUTION_OUTCOME === 'failure';
+ const msg = 'ERR_SYSTEM: \u274C Unexpected error loading threat detection module: ' + (loadErr && loadErr.message ? loadErr.message : String(loadErr));
+ core.error(msg);
+ core.setOutput('reason', 'parse_error');
+ if (continueOnError && !detectionExecutionFailed) {
+ core.warning('\u26A0\uFE0F ' + msg);
+ core.setOutput('conclusion', 'warning');
+ core.setOutput('success', 'false');
+ } else {
+ core.setOutput('conclusion', 'failure');
+ core.setOutput('success', 'false');
+ core.setFailed(msg);
+ }
+ }
+
+ pre_activation:
+ if: github.event_name == 'pull_request' && github.event.label.name == 'smoke-sdk' || !(github.event_name == 'pull_request')
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ outputs:
+ activated: ${{ steps.check_membership.outputs.is_team_member == 'true' }}
+ matched_command: ''
+ setup-parent-span-id: ${{ steps.setup.outputs.parent-span-id || steps.setup.outputs.span-id }}
+ setup-span-id: ${{ steps.setup.outputs.span-id }}
+ setup-trace-id: ${{ steps.setup.outputs.trace-id }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ repository: github/gh-aw
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ id: setup
+ uses: ./actions/setup
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-copilot-sdk.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Check team membership for workflow
+ id: check_membership
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_REQUIRED_ROLES: "admin,maintainer,write"
+ with:
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs');
+ await main();
+
+ safe_outputs:
+ needs:
+ - activation
+ - agent
+ - detection
+ if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success'
+ runs-on: ubuntu-slim
+ permissions:
+ contents: read
+ issues: write
+ timeout-minutes: 15
+ env:
+ GH_AW_CALLER_WORKFLOW_ID: "${{ github.repository }}/smoke-copilot-sdk"
+ GH_AW_DETECTION_CONCLUSION: ${{ needs.detection.outputs.detection_conclusion }}
+ GH_AW_DETECTION_REASON: ${{ needs.detection.outputs.detection_reason }}
+ GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }}
+ GH_AW_ENGINE_ID: "copilot"
+ GH_AW_ENGINE_MODEL: "gpt-5.4"
+ GH_AW_ENGINE_VERSION: "1.0.55"
+ GH_AW_WORKFLOW_EMOJI: "🔬"
+ GH_AW_WORKFLOW_ID: "smoke-copilot-sdk"
+ GH_AW_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/smoke-copilot-sdk.md"
+ outputs:
+ code_push_failure_count: ${{ steps.process_safe_outputs.outputs.code_push_failure_count }}
+ code_push_failure_errors: ${{ steps.process_safe_outputs.outputs.code_push_failure_errors }}
+ create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }}
+ create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }}
+ created_issue_number: ${{ steps.process_safe_outputs.outputs.created_issue_number }}
+ created_issue_url: ${{ steps.process_safe_outputs.outputs.created_issue_url }}
+ process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }}
+ process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }}
+ steps:
+ - name: Checkout actions folder
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ repository: github/gh-aw
+ sparse-checkout: |
+ actions
+ persist-credentials: false
+ - name: Setup Scripts
+ id: setup
+ uses: ./actions/setup
+ with:
+ destination: ${{ runner.temp }}/gh-aw/actions
+ job-name: ${{ github.job }}
+ trace-id: ${{ needs.activation.outputs.setup-trace-id }}
+ parent-span-id: ${{ needs.activation.outputs.setup-parent-span-id || needs.activation.outputs.setup-span-id }}
+ env:
+ GH_AW_SETUP_WORKFLOW_NAME: "Smoke Copilot SDK"
+ GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/smoke-copilot-sdk.lock.yml@${{ github.ref }}
+ GH_AW_INFO_VERSION: "1.0.55"
+ GH_AW_INFO_AWF_VERSION: "v0.25.58"
+ GH_AW_INFO_ENGINE_ID: "copilot"
+ - name: Download agent output artifact
+ id: download-agent-output
+ continue-on-error: true
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
+ with:
+ name: agent
+ path: /tmp/gh-aw/
+ - name: Setup agent output environment variable
+ id: setup-agent-output-env
+ if: steps.download-agent-output.outcome == 'success'
+ run: |
+ mkdir -p /tmp/gh-aw/
+ find "/tmp/gh-aw/" -type f -print
+ echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT"
+ - name: Configure GH_HOST for enterprise compatibility
+ id: ghes-host-config
+ shell: bash
+ run: |
+ # Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
+ # GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
+ GH_HOST="${GITHUB_SERVER_URL#https://}"
+ GH_HOST="${GH_HOST#http://}"
+ echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
+ - name: Process Safe Outputs
+ id: process_safe_outputs
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ env:
+ GH_AW_AGENT_OUTPUT: ${{ steps.setup-agent-output-env.outputs.GH_AW_AGENT_OUTPUT }}
+ GH_AW_COMMENT_ID: ${{ needs.activation.outputs.comment_id }}
+ GH_AW_ALLOWED_DOMAINS: "api.business.githubcopilot.com,api.enterprise.githubcopilot.com,api.github.com,api.githubcopilot.com,api.individual.githubcopilot.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,crl.geotrust.com,crl.globalsign.com,crl.identrust.com,crl.sectigo.com,crl.thawte.com,crl.usertrust.com,crl.verisign.com,crl3.digicert.com,crl4.digicert.com,crls.ssl.com,github.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,ocsp.digicert.com,ocsp.geotrust.com,ocsp.globalsign.com,ocsp.identrust.com,ocsp.sectigo.com,ocsp.ssl.com,ocsp.thawte.com,ocsp.usertrust.com,ocsp.verisign.com,packagecloud.io,packages.cloud.google.com,packages.microsoft.com,ppa.launchpad.net,raw.githubusercontent.com,registry.npmjs.org,s.symcb.com,s.symcd.com,security.ubuntu.com,telemetry.enterprise.githubcopilot.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com"
+ GITHUB_SERVER_URL: ${{ github.server_url }}
+ GITHUB_API_URL: ${{ github.api_url }}
+ GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_issue\":{\"close_older_issues\":true,\"close_older_key\":\"smoke-copilot-sdk\",\"expires\":2,\"group\":true,\"labels\":[\"automation\",\"testing\"],\"max\":1},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}"
+ with:
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+ script: |
+ const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs');
+ setupGlobals(core, github, context, exec, io, getOctokit);
+ const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs');
+ await main();
+ - name: Upload Safe Outputs Items
+ if: always()
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ with:
+ name: safe-outputs-items
+ path: |
+ /tmp/gh-aw/safe-output-items.jsonl
+ /tmp/gh-aw/temporary-id-map.json
+ if-no-files-found: ignore
+
diff --git a/.github/workflows/smoke-copilot-sdk.md b/.github/workflows/smoke-copilot-sdk.md
new file mode 100644
index 00000000000..6fdeed86852
--- /dev/null
+++ b/.github/workflows/smoke-copilot-sdk.md
@@ -0,0 +1,53 @@
+---
+emoji: "🔬"
+description: Smoke Copilot SDK
+on:
+ workflow_dispatch:
+ label_command:
+ name: smoke-sdk
+ events: [pull_request]
+ github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
+permissions:
+ contents: read
+name: Smoke Copilot SDK
+engine:
+ id: copilot
+ copilot-sdk: true
+ model: gpt-5.4
+ bare: true
+tools:
+ bash:
+ - "*"
+ edit:
+safe-outputs:
+ create-issue:
+ expires: 2h
+ group: true
+ close-older-issues: true
+ close-older-key: "smoke-copilot-sdk"
+ labels: [automation, testing]
+timeout-minutes: 10
+---
+
+# Smoke Test: Copilot SDK Engine Validation
+
+**IMPORTANT: Keep all outputs extremely short and concise.**
+
+## Tasks
+
+1. **File Writing**: Create a file `/tmp/smoke-copilot-sdk-${{ github.run_id }}.txt` with the content:
+ ```
+ Copilot SDK smoke test passed at
+ ```
+ Create the directory if it does not exist.
+
+2. **Verify**: Read the file back with `cat` and confirm it contains the phrase "smoke test passed".
+
+3. **Bash calculation**: Run a bash command to compute `echo $((6 * 7))` and confirm the output is `42`.
+
+## Output
+
+Create an issue titled **"Smoke Test: Copilot SDK - ${{ github.run_id }}"** with:
+- ✅ or ❌ for each task above
+- Overall status: PASS or FAIL
+- Run URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs
index 9b96e6f1188..3d7e44b4d87 100644
--- a/actions/setup/js/copilot_harness.cjs
+++ b/actions/setup/js/copilot_harness.cjs
@@ -1,16 +1,17 @@
// @ts-check
/**
- * Copilot CLI Harness with Retry Logic
+ * Copilot Harness with Retry Logic
*
- * Wraps the Copilot CLI command with retry logic for failures that occur after the session
- * has been partially executed. Passes all arguments to the copilot subprocess, transparently
- * forwarding stdin/stdout/stderr.
+ * Wraps the Copilot CLI command (or @github/copilot-sdk session in SDK mode) with retry logic
+ * for failures that occur after the session has been partially executed. Passes all arguments
+ * to the copilot subprocess, transparently forwarding stdin/stdout/stderr.
*
- * Retry policy:
+ * Retry policy (shared by CLI and SDK modes):
* - If the process produced any output (hasOutput) and exits with a non-zero code, the
- * session is considered partially executed. The driver retries with --continue so the
- * Copilot CLI can continue from where it left off.
+ * session is considered partially executed and is retried.
+ * - CLI mode: retries with --continue so the Copilot CLI can continue from on-disk state.
+ * - SDK mode: retries always restart the session fresh (--continue is a CLI concept).
* - CAPIError 400 is a well-known transient failure mode and is logged explicitly, but
* any partial-execution failure is retried — not just CAPIError 400.
* - If the process produced no output (failed to start / auth error before any work), the
@@ -42,6 +43,7 @@ const fs = require("fs");
const path = require("path");
const { runProcess, formatDuration, sleep, isCopilotSDKEnabled, buildCopilotSDKEnv } = require("./process_runner.cjs");
const { buildCopilotSDKServerArgs, getCopilotSDKServerPort, startCopilotSDKServer, stopCopilotSDKServer, waitForCopilotSDKServer } = require("./copilot_sdk_sidecar.cjs");
+const { extractPromptFromArgs, runWithCopilotSDK } = require("./copilot_sdk_driver.cjs");
const { isMaxEffectiveTokensExceededError } = require("./effective_tokens_hard_rail.cjs");
const {
AWF_API_PROXY_REFLECT_URL,
@@ -392,6 +394,36 @@ async function checkCommandAccessible(command) {
}
}
+/**
+ * Read and parse the JSON options payload piped to stdin by the engine command.
+ * Called in SDK mode where the Go engine pipes options via `printf '%s' '{"promptFile":"...","serverArgs":[...]}'
+ * | node harness`.
+ * Returns null when stdin is a TTY, empty, or contains invalid JSON.
+ * @returns {Promise<{promptFile?: string, serverArgs?: string[], addWorkspaceDir?: boolean} | null>}
+ */
+async function readSDKOptionsFromStdin() {
+ if (process.stdin.isTTY) return null;
+ return new Promise(resolve => {
+ /** @type {Buffer[]} */
+ const chunks = [];
+ process.stdin.on("data", chunk => chunks.push(/** @type {Buffer} */ (chunk)));
+ process.stdin.on("end", () => {
+ const text = Buffer.concat(chunks).toString("utf8").trim();
+ if (!text) {
+ resolve(null);
+ return;
+ }
+ try {
+ resolve(JSON.parse(text));
+ } catch {
+ log(`warning: failed to parse SDK options from stdin: ${text.slice(0, 100)}`);
+ resolve(null);
+ }
+ });
+ process.stdin.on("error", () => resolve(null));
+ });
+}
+
/**
* Build a compact fallback prompt that asks the agent to read instructions from disk.
* @param {string} promptFile
@@ -463,7 +495,6 @@ async function main() {
log(`starting: command=${command} maxRetries=${MAX_RETRIES} initialDelayMs=${INITIAL_DELAY_MS}` + ` backoffMultiplier=${BACKOFF_MULTIPLIER} maxDelayMs=${MAX_DELAY_MS}` + ` nodeVersion=${process.version} platform=${process.platform}`);
await checkCommandAccessible(command);
- const resolvedArgs = resolvePromptFileArgs(args);
// Build SDK env additions. When COPILOT_SDK_URI is set the harness will start a separate
// headless Copilot CLI sidecar and this helper merges COPILOT_SDK_URI into the child
@@ -479,6 +510,24 @@ async function main() {
// runProcess inherits the full process.env (the common case).
const childEnv = Object.keys(sdkEnv).length > 0 ? { ...process.env, ...sdkEnv } : undefined;
+ // In SDK mode, the engine pipes a JSON options payload via stdin containing the promptFile
+ // path, serverArgs (complete CLI argument list for the headless server), and optionally addWorkspaceDir.
+ // Read it before doing anything else so stdin is consumed before the process runs.
+ // In CLI mode, args are resolved normally (--prompt-file is inlined into -p ).
+ /** @type {{promptFile?: string, serverArgs?: string[], addWorkspaceDir?: boolean} | null} */
+ let sdkOptions = null;
+ let resolvedArgs;
+ if (copilotSDKMode) {
+ sdkOptions = await readSDKOptionsFromStdin();
+ if (sdkOptions) {
+ log(`sdk-options: promptFile=${sdkOptions.promptFile || "(none)"} serverArgs=${(sdkOptions.serverArgs || []).length} addWorkspaceDir=${!!sdkOptions.addWorkspaceDir}`);
+ }
+ // SDK mode does not use CLI prompt args; pass args through unmodified.
+ resolvedArgs = args;
+ } else {
+ resolvedArgs = resolvePromptFileArgs(args);
+ }
+
// Fetch AWF API proxy reflection data before running the agent to capture initial proxy state.
// This is best-effort: failures are logged but do not affect the agent run.
// Skip when AWF_REFLECT_ENABLED is not "1" (e.g. sandbox.agent: false — no api-proxy running).
@@ -503,187 +552,235 @@ async function main() {
agenticEngineTimeout: false,
modelNotSupportedError: false,
};
+ // In SDK mode the prompt is required; read it from the promptFile in sdkOptions (piped via
+ // stdin by the engine command). Fall back to extracting from CLI args for backward compatibility.
+ let sdkPrompt = null;
+ if (copilotSDKMode) {
+ if (sdkOptions && sdkOptions.promptFile) {
+ try {
+ sdkPrompt = fs.readFileSync(sdkOptions.promptFile, "utf8");
+ log(`sdk-mode: read prompt from ${sdkOptions.promptFile} (${sdkPrompt.length} chars)`);
+ } catch (err) {
+ const readErr = /** @type {Error} */ err;
+ log(`sdk-mode: failed to read prompt from ${sdkOptions.promptFile}: ${readErr.message}`);
+ }
+ }
+ if (!sdkPrompt) {
+ // Fallback: try to extract from CLI args (backward compatibility with older engine versions)
+ sdkPrompt = extractPromptFromArgs(resolvedArgs);
+ if (sdkPrompt) {
+ log("sdk-mode: prompt extracted from CLI args (fallback)");
+ } else {
+ log("sdk-mode: no prompt found in stdin JSON payload or CLI args");
+ }
+ }
+ }
/** @type {Awaited>} */
let copilotSDKServer = null;
try {
if (copilotSDKMode) {
- copilotSDKServer = await startCopilotSDKServer({
- command,
- env: childEnv ?? process.env,
- logger: log,
- });
+ if (!sdkPrompt) {
+ log("copilot-sdk mode: no prompt found (expected promptFile in stdin JSON payload or -p/--prompt in args)");
+ lastExitCode = 1;
+ } else {
+ // Build the server args from the stdin JSON payload.
+ // serverArgs carries the complete CLI argument list for the headless server (--headless,
+ // --no-auto-update, --port, --add-dir, --log-level, etc.) generated by the Go engine.
+ // addWorkspaceDir signals that the GITHUB_WORKSPACE env var should be appended at runtime.
+ const serverArgs = [...(sdkOptions?.serverArgs ?? [])];
+ if (sdkOptions?.addWorkspaceDir && process.env.GITHUB_WORKSPACE) {
+ serverArgs.push("--add-dir", process.env.GITHUB_WORKSPACE);
+ }
+ copilotSDKServer = await startCopilotSDKServer({
+ command,
+ env: childEnv ?? process.env,
+ serverArgs: serverArgs.length > 0 ? serverArgs : undefined,
+ logger: log,
+ });
+ }
}
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
- // Add --continue flag on retries so the copilot session continues from where it left off
- const currentArgs = attempt > 0 && useContinueOnRetry ? [...resolvedArgs, "--continue"] : resolvedArgs;
+ // CLI mode always enters the retry loop. SDK mode only enters when a prompt was found;
+ // the missing-prompt case is handled above and results in lastExitCode=1 with no loop.
+ if (!copilotSDKMode || sdkPrompt) {
+ // Unified retry loop for both SDK and CLI modes.
+ // --continue is a CLI concept; in SDK mode retries always restart the session fresh.
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
+ // Add --continue flag on CLI retries so the copilot session continues from where it left off
+ const currentArgs = !copilotSDKMode && attempt > 0 && useContinueOnRetry ? [...resolvedArgs, "--continue"] : resolvedArgs;
+
+ if (attempt > 0) {
+ const retryMode = !copilotSDKMode && useContinueOnRetry ? "--continue" : "fresh run";
+ log(`retry ${attempt}/${MAX_RETRIES}: sleeping ${delay}ms before next attempt (${retryMode})`);
+ await sleep(delay);
+ delay = Math.min(delay * BACKOFF_MULTIPLIER, MAX_DELAY_MS);
+ log(`retry ${attempt}/${MAX_RETRIES}: woke up, next delay cap will be ${Math.min(delay * BACKOFF_MULTIPLIER, MAX_DELAY_MS)}ms`);
+ }
- if (attempt > 0) {
- const retryMode = useContinueOnRetry ? "--continue" : "fresh run";
- log(`retry ${attempt}/${MAX_RETRIES}: sleeping ${delay}ms before next attempt (${retryMode})`);
- await sleep(delay);
- delay = Math.min(delay * BACKOFF_MULTIPLIER, MAX_DELAY_MS);
- log(`retry ${attempt}/${MAX_RETRIES}: woke up, next delay cap will be ${Math.min(delay * BACKOFF_MULTIPLIER, MAX_DELAY_MS)}ms`);
- }
+ // Redact --prompt / -p value from logs to avoid leaking prompt content
+ const safeArgs = currentArgs.map((arg, i) => (currentArgs[i - 1] === "--prompt" || currentArgs[i - 1] === "-p" ? "" : arg));
+ const result = copilotSDKMode
+ ? await runWithCopilotSDK({ sdkUri: sdkEnv.COPILOT_SDK_URI ?? process.env.COPILOT_SDK_URI ?? "", prompt: sdkPrompt, logger: log, attempt })
+ : await runProcess({ command, args: currentArgs, attempt, log, logArgs: safeArgs, env: childEnv });
+ lastExitCode = result.exitCode;
+ const attemptDetections = detectCopilotErrors(result.output);
+ detectedCopilotErrors.inferenceAccessError ||= attemptDetections.inferenceAccessError;
+ detectedCopilotErrors.mcpPolicyError ||= attemptDetections.mcpPolicyError;
+ detectedCopilotErrors.agenticEngineTimeout ||= attemptDetections.agenticEngineTimeout;
+ detectedCopilotErrors.modelNotSupportedError ||= attemptDetections.modelNotSupportedError;
+
+ // Success — record exit code and stop retrying
+ if (result.exitCode === 0) {
+ log(`success on attempt ${attempt + 1}: totalDuration=${formatDuration(Date.now() - driverStartTime)}`);
+ lastExitCode = 0;
+ break;
+ }
- // Redact --prompt / -p value from logs to avoid leaking prompt content
- const safeArgs = currentArgs.map((arg, i) => (currentArgs[i - 1] === "--prompt" || currentArgs[i - 1] === "-p" ? "" : arg));
- const result = await runProcess({ command, args: currentArgs, attempt, log, logArgs: safeArgs, env: childEnv });
- lastExitCode = result.exitCode;
- const attemptDetections = detectCopilotErrors(result.output);
- detectedCopilotErrors.inferenceAccessError ||= attemptDetections.inferenceAccessError;
- detectedCopilotErrors.mcpPolicyError ||= attemptDetections.mcpPolicyError;
- detectedCopilotErrors.agenticEngineTimeout ||= attemptDetections.agenticEngineTimeout;
- detectedCopilotErrors.modelNotSupportedError ||= attemptDetections.modelNotSupportedError;
-
- // Success — record exit code and stop retrying
- if (result.exitCode === 0) {
- log(`success on attempt ${attempt + 1}: totalDuration=${formatDuration(Date.now() - driverStartTime)}`);
- lastExitCode = 0;
- break;
- }
+ // Determine whether to retry.
+ // Retry whenever the session was partially executed (hasOutput).
+ // - CLI mode: retry with --continue so the Copilot CLI can continue from on-disk state.
+ // - SDK mode: retry always restarts fresh — there is no CLI on-disk state to resume.
+ // CAPIError 400 is the well-known transient case, but any partial-execution failure is
+ // eligible for a retry.
+ // Exceptions:
+ // - MCP policy errors and model-not-supported errors are persistent configuration issues.
+ // - Auth errors trigger a one-time fallback to a fresh run; after that --continue is
+ // permanently disabled.
+ // - Null-type tool_call 400 errors poison conversation history — always restart fresh and
+ // permanently disable --continue so the corrupt state is never reloaded.
+ const isCAPIError = isTransientCAPIError(result.output);
+ const isMCPPolicy = isMCPPolicyError(result.output);
+ const isModelNotSupported = isModelNotSupportedError(result.output);
+ const isAuthErr = isNoAuthInfoError(result.output);
+ const isAuthenticationFailed = isAuthenticationFailedError(result.output);
+ const isNullTypeToolCall = isNullTypeToolCallError(result.output);
+ const isMaxEffectiveTokensExceeded = isMaxEffectiveTokensExceededError(result.output);
+ const permissionDeniedCount = countPermissionDeniedIssues(result.output);
+ const hasNumerousPermissionDenied = hasNumerousPermissionDeniedIssues(result.output);
+ log(
+ `attempt ${attempt + 1} failed:` +
+ ` exitCode=${result.exitCode}` +
+ ` isCAPIError400=${isCAPIError}` +
+ ` isMCPPolicyError=${isMCPPolicy}` +
+ ` isModelNotSupportedError=${isModelNotSupported}` +
+ ` isNullTypeToolCallError=${isNullTypeToolCall}` +
+ ` isMaxEffectiveTokensExceededError=${isMaxEffectiveTokensExceeded}` +
+ ` isAuthError=${isAuthErr}` +
+ ` isAuthenticationFailedError=${isAuthenticationFailed}` +
+ ` permissionDeniedCount=${permissionDeniedCount}` +
+ ` hasNumerousPermissionDenied=${hasNumerousPermissionDenied}` +
+ ` hasOutput=${result.hasOutput}` +
+ ` retriesRemaining=${MAX_RETRIES - attempt}`
+ );
+
+ if (attempt === 0 && isAuthenticationFailed) {
+ log(`attempt ${attempt + 1}: authentication failed — not retrying (first-attempt auth failure is non-retryable)`);
+ break;
+ }
- // Determine whether to retry.
- // Retry whenever the session was partially executed (hasOutput), using --continue so that
- // the Copilot CLI can continue from where it left off. CAPIError 400 is the well-known
- // transient case, but any partial-execution failure is eligible for a continue retry.
- // Exceptions:
- // - MCP policy errors and model-not-supported errors are persistent configuration issues.
- // - Auth errors trigger a one-time fallback to a fresh run; after that --continue is
- // permanently disabled.
- // - Null-type tool_call 400 errors poison conversation history — always restart fresh and
- // permanently disable --continue so the corrupt state is never reloaded.
- const isCAPIError = isTransientCAPIError(result.output);
- const isMCPPolicy = isMCPPolicyError(result.output);
- const isModelNotSupported = isModelNotSupportedError(result.output);
- const isAuthErr = isNoAuthInfoError(result.output);
- const isAuthenticationFailed = isAuthenticationFailedError(result.output);
- const isNullTypeToolCall = isNullTypeToolCallError(result.output);
- const isMaxEffectiveTokensExceeded = isMaxEffectiveTokensExceededError(result.output);
- const permissionDeniedCount = countPermissionDeniedIssues(result.output);
- const hasNumerousPermissionDenied = hasNumerousPermissionDeniedIssues(result.output);
- log(
- `attempt ${attempt + 1} failed:` +
- ` exitCode=${result.exitCode}` +
- ` isCAPIError400=${isCAPIError}` +
- ` isMCPPolicyError=${isMCPPolicy}` +
- ` isModelNotSupportedError=${isModelNotSupported}` +
- ` isNullTypeToolCallError=${isNullTypeToolCall}` +
- ` isMaxEffectiveTokensExceededError=${isMaxEffectiveTokensExceeded}` +
- ` isAuthError=${isAuthErr}` +
- ` isAuthenticationFailedError=${isAuthenticationFailed}` +
- ` permissionDeniedCount=${permissionDeniedCount}` +
- ` hasNumerousPermissionDenied=${hasNumerousPermissionDenied}` +
- ` hasOutput=${result.hasOutput}` +
- ` retriesRemaining=${MAX_RETRIES - attempt}`
- );
-
- if (attempt === 0 && isAuthenticationFailed) {
- log(`attempt ${attempt + 1}: authentication failed — not retrying (first-attempt auth failure is non-retryable)`);
- break;
- }
+ if (hasNumerousPermissionDenied) {
+ const deniedCommands = extractDeniedCommands(result.output);
+ emitMissingToolPermissionIssue({ deniedCommands, logger: log });
+ log(`attempt ${attempt + 1}: detected numerous permission-denied issues — not retrying (classified as missing tool/permission issue)`);
+ break;
+ }
- if (hasNumerousPermissionDenied) {
- const deniedCommands = extractDeniedCommands(result.output);
- emitMissingToolPermissionIssue({ deniedCommands, logger: log });
- log(`attempt ${attempt + 1}: detected numerous permission-denied issues — not retrying (classified as missing tool/permission issue)`);
- break;
- }
+ // MCP policy errors are persistent — retrying will not help.
+ if (isMCPPolicy) {
+ log(`attempt ${attempt + 1}: MCP servers blocked by policy — not retrying (this is a policy configuration issue, not a transient error)`);
+ break;
+ }
- // MCP policy errors are persistent — retrying will not help.
- if (isMCPPolicy) {
- log(`attempt ${attempt + 1}: MCP servers blocked by policy — not retrying (this is a policy configuration issue, not a transient error)`);
- break;
- }
+ // Model-not-supported errors are persistent — retrying will not help.
+ if (isModelNotSupported) {
+ if (!modelNotSupportedReflectRetryAttempted && attempt < MAX_RETRIES && isDetectionPhase(process.env.GH_AW_PHASE) && process.env.AWF_REFLECT_ENABLED === "1") {
+ const configuredModel = process.env.COPILOT_MODEL || "";
+ modelNotSupportedReflectRetryAttempted = true;
+ log(`attempt ${attempt + 1}: model not supported during detection — refreshing awf-reflect to rule out startup registry race`);
+ await fetchAWFReflect({ logger: log });
+ if (isModelAvailableInReflectFile(configuredModel, { logger: log })) {
+ useContinueOnRetry = false;
+ continueDisabledPermanently = true;
+ log(`attempt ${attempt + 1}: refreshed awf-reflect now includes model '${configuredModel}' — retrying once as fresh run`);
+ continue;
+ }
+ log(`attempt ${attempt + 1}: refreshed awf-reflect does not include model '${configuredModel || "(none)"}' — treating as non-retryable`);
+ }
+ log(`attempt ${attempt + 1}: model not supported — not retrying (the requested model is unavailable for this subscription tier; specify a supported model in the workflow frontmatter)`);
+ break;
+ }
- // Model-not-supported errors are persistent — retrying will not help.
- if (isModelNotSupported) {
- if (!modelNotSupportedReflectRetryAttempted && attempt < MAX_RETRIES && isDetectionPhase(process.env.GH_AW_PHASE) && process.env.AWF_REFLECT_ENABLED === "1") {
- const configuredModel = process.env.COPILOT_MODEL || "";
- modelNotSupportedReflectRetryAttempted = true;
- log(`attempt ${attempt + 1}: model not supported during detection — refreshing awf-reflect to rule out startup registry race`);
- await fetchAWFReflect({ logger: log });
- if (isModelAvailableInReflectFile(configuredModel, { logger: log })) {
+ if (isMaxEffectiveTokensExceeded) {
+ log(`attempt ${attempt + 1}: AWF effective-token hard rail hit — not retrying or continuing (further inference will be refused until budget resets)`);
+ break;
+ }
+
+ // Auth error: behavior depends on whether this was a --continue attempt (CLI mode only).
+ // On a --continue attempt: the Copilot CLI's on-disk session credential written by the
+ // interrupted run may be incomplete/invalid. Fall back to a fresh run (without --continue)
+ // once so env-var auth can succeed. Mid-stream context is lost but the job can recover.
+ // On a fresh run: the auth token is genuinely absent or invalid — retrying will not help.
+ if (isAuthErr) {
+ if (useContinueOnRetry && attempt < MAX_RETRIES) {
useContinueOnRetry = false;
continueDisabledPermanently = true;
- log(`attempt ${attempt + 1}: refreshed awf-reflect now includes model '${configuredModel}' — retrying once as fresh run`);
+ log(`attempt ${attempt + 1}: auth error on --continue — retrying as fresh run (session credential may be corrupted; context will be lost)`);
continue;
}
- log(`attempt ${attempt + 1}: refreshed awf-reflect does not include model '${configuredModel || "(none)"}' — treating as non-retryable`);
+ log(`attempt ${attempt + 1}: no authentication information found — not retrying (COPILOT_GITHUB_TOKEN, GH_TOKEN, and GITHUB_TOKEN are all absent or invalid)`);
+ break;
}
- log(`attempt ${attempt + 1}: model not supported — not retrying (the requested model is unavailable for this subscription tier; specify a supported model in the workflow frontmatter)`);
- break;
- }
- if (isMaxEffectiveTokensExceeded) {
- log(`attempt ${attempt + 1}: AWF effective-token hard rail hit — not retrying or continuing (further inference will be refused until budget resets)`);
- break;
- }
+ // Null-type tool_call error: the model emitted a malformed tool call that poisons the
+ // conversation history. Retrying with --continue re-injects the same broken history and
+ // produces the same 400 on every subsequent attempt. Restart fresh to discard the poisoned
+ // history, and permanently disable --continue so the corrupt state is never re-loaded.
+ if (isNullTypeToolCall) {
+ if (attempt < MAX_RETRIES && result.hasOutput) {
+ const priorMode = attempt > 0 && useContinueOnRetry ? "--continue" : "fresh run";
+ useContinueOnRetry = false;
+ continueDisabledPermanently = true;
+ log(`attempt ${attempt + 1}: null-type tool_call error (${priorMode}) — restarting fresh (poisoned history discarded; --continue disabled permanently)`);
+ continue;
+ }
+ }
- // Auth error: behavior depends on whether this was a --continue attempt.
- // On a --continue attempt: the Copilot CLI's on-disk session credential written by the
- // interrupted run may be incomplete/invalid. Fall back to a fresh run (without --continue)
- // once so env-var auth can succeed. Mid-stream context is lost but the job can recover.
- // On a fresh run: the auth token is genuinely absent or invalid — retrying will not help.
- if (isAuthErr) {
- if (useContinueOnRetry && attempt < MAX_RETRIES) {
+ // Scheduled runs: retry once on exit code 2 even when no output was produced.
+ // This specifically targets transient Copilot API outages at startup where there is no
+ // partial session state to continue from.
+ if (isScheduledRun && result.exitCode === 2 && !result.hasOutput && scheduledExit2Retries < MAX_SCHEDULED_EXIT2_RETRIES && attempt < MAX_RETRIES) {
+ scheduledExit2Retries += 1;
+ scheduledExit2RetryAttempted = true;
useContinueOnRetry = false;
- continueDisabledPermanently = true;
- log(`attempt ${attempt + 1}: auth error on --continue — retrying as fresh run (session credential may be corrupted; context will be lost)`);
+ log(`attempt ${attempt + 1}: scheduled startup interruption (exit code 2, no output)` + ` — retrying once as fresh run (startupRetry=${scheduledExit2Retries}/${MAX_SCHEDULED_EXIT2_RETRIES})`);
continue;
}
- log(`attempt ${attempt + 1}: no authentication information found — not retrying (COPILOT_GITHUB_TOKEN, GH_TOKEN, and GITHUB_TOKEN are all absent or invalid)`);
- break;
- }
+ if (isScheduledRun && result.exitCode === 2 && !result.hasOutput && scheduledExit2Retries < MAX_SCHEDULED_EXIT2_RETRIES && attempt >= MAX_RETRIES) {
+ log(`attempt ${attempt + 1}: scheduled startup interruption detected but retry budget exhausted — no attempts remain`);
+ }
- // Null-type tool_call error: the model emitted a malformed tool call that poisons the
- // conversation history. Retrying with --continue re-injects the same broken history and
- // produces the same 400 on every subsequent attempt. Restart fresh to discard the poisoned
- // history, and permanently disable --continue so the corrupt state is never re-loaded.
- if (isNullTypeToolCall) {
if (attempt < MAX_RETRIES && result.hasOutput) {
- const priorMode = attempt > 0 && useContinueOnRetry ? "--continue" : "fresh run";
- useContinueOnRetry = false;
- continueDisabledPermanently = true;
- log(`attempt ${attempt + 1}: null-type tool_call error (${priorMode}) — restarting fresh (poisoned history discarded; --continue disabled permanently)`);
+ const reason = isCAPIError ? "CAPIError 400 (transient)" : "partial execution";
+ // --continue is only meaningful in CLI mode; SDK mode always restarts fresh.
+ useContinueOnRetry = !copilotSDKMode && !continueDisabledPermanently;
+ const retryMode = useContinueOnRetry ? "--continue" : copilotSDKMode ? "fresh run" : "fresh run (--continue permanently disabled)";
+ log(`attempt ${attempt + 1}: ${reason} — will retry with ${retryMode} (attempt ${attempt + 2}/${MAX_RETRIES + 1})`);
continue;
}
- }
- // Scheduled runs: retry once on exit code 2 even when no output was produced.
- // This specifically targets transient Copilot API outages at startup where there is no
- // partial session state to continue from.
- if (isScheduledRun && result.exitCode === 2 && !result.hasOutput && scheduledExit2Retries < MAX_SCHEDULED_EXIT2_RETRIES && attempt < MAX_RETRIES) {
- scheduledExit2Retries += 1;
- scheduledExit2RetryAttempted = true;
- useContinueOnRetry = false;
- log(`attempt ${attempt + 1}: scheduled startup interruption (exit code 2, no output)` + ` — retrying once as fresh run (startupRetry=${scheduledExit2Retries}/${MAX_SCHEDULED_EXIT2_RETRIES})`);
- continue;
- }
- if (isScheduledRun && result.exitCode === 2 && !result.hasOutput && scheduledExit2Retries < MAX_SCHEDULED_EXIT2_RETRIES && attempt >= MAX_RETRIES) {
- log(`attempt ${attempt + 1}: scheduled startup interruption detected but retry budget exhausted — no attempts remain`);
- }
+ if (attempt >= MAX_RETRIES) {
+ log(`all ${MAX_RETRIES} retries exhausted — giving up (exitCode=${lastExitCode})`);
+ } else {
+ log(`attempt ${attempt + 1}: no output produced — not retrying` + ` (possible causes: binary not found, permission denied, auth failure, or silent startup crash)`);
+ }
- if (attempt < MAX_RETRIES && result.hasOutput) {
- const reason = isCAPIError ? "CAPIError 400 (transient)" : "partial execution";
- useContinueOnRetry = !continueDisabledPermanently;
- const retryMode = useContinueOnRetry ? "--continue" : "fresh run (--continue permanently disabled)";
- log(`attempt ${attempt + 1}: ${reason} — will retry with ${retryMode} (attempt ${attempt + 2}/${MAX_RETRIES + 1})`);
- continue;
+ // Non-retryable error or retries exhausted — propagate exit code
+ break;
}
- if (attempt >= MAX_RETRIES) {
- log(`all ${MAX_RETRIES} retries exhausted — giving up (exitCode=${lastExitCode})`);
- } else {
- log(`attempt ${attempt + 1}: no output produced — not retrying` + ` (possible causes: binary not found, permission denied, auth failure, or silent startup crash)`);
+ if (isScheduledRun && lastExitCode === 2 && scheduledExit2RetryAttempted) {
+ emitInfrastructureIncomplete("Copilot API interruption (exit code 2) persisted after automatic retry in scheduled workflow run.");
}
-
- // Non-retryable error or retries exhausted — propagate exit code
- break;
- }
-
- if (isScheduledRun && lastExitCode === 2 && scheduledExit2RetryAttempted) {
- emitInfrastructureIncomplete("Copilot API interruption (exit code 2) persisted after automatic retry in scheduled workflow run.");
}
// Fetch AWF API proxy reflection data and persist to disk for post-run step summary.
@@ -736,6 +833,9 @@ if (typeof module !== "undefined" && module.exports) {
waitForCopilotSDKServer,
writeCopilotOutputs,
resolvePromptFileArgs,
+ extractPromptFromArgs,
+ readSDKOptionsFromStdin,
+ runWithCopilotSDK,
};
}
diff --git a/actions/setup/js/copilot_harness.test.cjs b/actions/setup/js/copilot_harness.test.cjs
index 5a442e95e54..674f65d75ac 100644
--- a/actions/setup/js/copilot_harness.test.cjs
+++ b/actions/setup/js/copilot_harness.test.cjs
@@ -35,7 +35,9 @@ const {
GEMINI_MODEL_NAME_PREFIX,
PROMPT_FILE_INLINE_THRESHOLD_BYTES,
resolvePromptFileArgs,
+ runWithCopilotSDK,
writeCopilotOutputs,
+ readSDKOptionsFromStdin,
} = require("./copilot_harness.cjs");
describe("copilot_harness.cjs", () => {
@@ -197,6 +199,84 @@ describe("copilot_harness.cjs", () => {
).toBe("3002");
});
+ describe("copilot-sdk driver lifecycle", () => {
+ it("disconnects session and stops client on success", async () => {
+ const disconnect = vi.fn().mockResolvedValue(undefined);
+ const stop = vi.fn().mockResolvedValue(undefined);
+ let onEvent = () => {};
+ const session = {
+ sessionId: "session-success",
+ on: handler => {
+ onEvent = handler;
+ },
+ sendAndWait: vi.fn().mockImplementation(async () => {
+ onEvent({
+ type: "assistant.message",
+ ephemeral: false,
+ timestamp: new Date().toISOString(),
+ data: { content: "hello from sdk" },
+ });
+ return { data: { content: "hello from sdk" } };
+ }),
+ disconnect,
+ };
+ class FakeCopilotClient {
+ start = vi.fn().mockResolvedValue(undefined);
+ createSession = vi.fn().mockResolvedValue(session);
+ stop = stop;
+ }
+
+ const result = await runWithCopilotSDK({
+ sdkUri: "http://127.0.0.1:3002",
+ prompt: "test prompt",
+ logger: () => {},
+ sdkModule: {
+ CopilotClient: FakeCopilotClient,
+ RuntimeConnection: { forUri: vi.fn(() => ({})) },
+ approveAll: () => "allow",
+ },
+ });
+
+ expect(result.exitCode).toBe(0);
+ expect(result.hasOutput).toBe(true);
+ expect(result.output).toContain("hello from sdk");
+ expect(disconnect).toHaveBeenCalledTimes(1);
+ expect(stop).toHaveBeenCalledTimes(1);
+ });
+
+ it("disconnects session and stops client on send failure", async () => {
+ const disconnect = vi.fn().mockResolvedValue(undefined);
+ const stop = vi.fn().mockResolvedValue(undefined);
+ const session = {
+ sessionId: "session-failure",
+ on: () => {},
+ sendAndWait: vi.fn().mockRejectedValue(new Error("send failed")),
+ disconnect,
+ };
+ class FakeCopilotClient {
+ start = vi.fn().mockResolvedValue(undefined);
+ createSession = vi.fn().mockResolvedValue(session);
+ stop = stop;
+ }
+
+ const result = await runWithCopilotSDK({
+ sdkUri: "http://127.0.0.1:3002",
+ prompt: "test prompt",
+ logger: () => {},
+ sdkModule: {
+ CopilotClient: FakeCopilotClient,
+ RuntimeConnection: { forUri: vi.fn(() => ({})) },
+ approveAll: () => "allow",
+ },
+ });
+
+ expect(result.exitCode).toBe(1);
+ expect(result.output).toContain("send failed");
+ expect(disconnect).toHaveBeenCalledTimes(1);
+ expect(stop).toHaveBeenCalledTimes(1);
+ });
+ });
+
it("builds headless Copilot CLI sidecar args", () => {
expect(
buildCopilotSDKServerArgs({
@@ -282,6 +362,80 @@ describe("copilot_harness.cjs", () => {
expect(child.listenerCount("exit")).toBe(0);
});
+ it("forwards extraArgs to the headless server when provided", async () => {
+ const child = new EventEmitter();
+ child.stdout = new PassThrough();
+ child.stderr = new PassThrough();
+ child.pid = 5678;
+ child.exitCode = null;
+ child.signalCode = null;
+ child.kill = vi.fn();
+ const spawnImpl = vi.fn(() => child);
+ const waitForReady = vi.fn().mockResolvedValue(undefined);
+
+ await startCopilotSDKServer({
+ command: "copilot",
+ env: { COPILOT_SDK_URI: "http://127.0.0.1:3002" },
+ extraArgs: ["--add-dir", "/tmp/gh-aw/", "--log-level", "all", "--disable-builtin-mcps"],
+ logger: () => {},
+ spawnImpl,
+ waitForReady,
+ });
+
+ expect(spawnImpl).toHaveBeenCalledWith(
+ "copilot",
+ ["--headless", "--no-auto-update", "--port", "3002", "--add-dir", "/tmp/gh-aw/", "--log-level", "all", "--disable-builtin-mcps"],
+ expect.objectContaining({ stdio: ["ignore", "pipe", "pipe"] })
+ );
+ });
+
+ it("uses engine-generated serverArgs directly when provided", async () => {
+ const child = new EventEmitter();
+ child.stdout = new PassThrough();
+ child.stderr = new PassThrough();
+ child.pid = 5680;
+ child.exitCode = null;
+ child.signalCode = null;
+ child.kill = vi.fn();
+ const spawnImpl = vi.fn(() => child);
+ const waitForReady = vi.fn().mockResolvedValue(undefined);
+
+ const engineGeneratedArgs = ["--headless", "--no-auto-update", "--port", "3002", "--add-dir", "/tmp/gh-aw/", "--log-level", "all", "--disable-builtin-mcps", "--no-ask-user"];
+ await startCopilotSDKServer({
+ command: "copilot",
+ env: { COPILOT_SDK_URI: "http://127.0.0.1:3002" },
+ serverArgs: engineGeneratedArgs,
+ logger: () => {},
+ spawnImpl,
+ waitForReady,
+ });
+
+ expect(spawnImpl).toHaveBeenCalledWith("copilot", engineGeneratedArgs, expect.objectContaining({ stdio: ["ignore", "pipe", "pipe"] }));
+ });
+
+ it("uses only base headless args when extraArgs is empty or omitted", async () => {
+ const child = new EventEmitter();
+ child.stdout = new PassThrough();
+ child.stderr = new PassThrough();
+ child.pid = 5679;
+ child.exitCode = null;
+ child.signalCode = null;
+ child.kill = vi.fn();
+ const spawnImpl = vi.fn(() => child);
+ const waitForReady = vi.fn().mockResolvedValue(undefined);
+
+ await startCopilotSDKServer({
+ command: "copilot",
+ env: { COPILOT_SDK_URI: "http://127.0.0.1:3002" },
+ extraArgs: [],
+ logger: () => {},
+ spawnImpl,
+ waitForReady,
+ });
+
+ expect(spawnImpl).toHaveBeenCalledWith("copilot", ["--headless", "--no-auto-update", "--port", "3002"], expect.objectContaining({ stdio: ["ignore", "pipe", "pipe"] }));
+ });
+
it("stops the headless Copilot CLI sidecar with SIGTERM", async () => {
const child = new EventEmitter();
child.pid = 4321;
@@ -1228,6 +1382,183 @@ describe("copilot_harness.cjs", () => {
});
});
+ describe("SDK mode retry policy", () => {
+ // In SDK mode, --continue is a CLI concept and must never be used.
+ // Retries always restart the session fresh.
+ // The retry eligibility rules (hasOutput, MAX_RETRIES) are otherwise shared.
+ const MAX_RETRIES = 3;
+
+ /**
+ * Mirrors the blended retry decision from copilot_harness.cjs (the
+ * `attempt < MAX_RETRIES && result.hasOutput` branch plus the
+ * `useContinueOnRetry = !copilotSDKMode && !continueDisabledPermanently` assignment).
+ * Keep this helper in sync with the production logic.
+ *
+ * @param {{hasOutput: boolean, exitCode: number, output: string}} result
+ * @param {number} attempt
+ * @param {boolean} copilotSDKMode
+ * @param {boolean} continueDisabledPermanently
+ * @returns {{ shouldRetry: boolean, useContinueOnRetry: boolean }}
+ */
+ function blendedRetryDecision(result, attempt, copilotSDKMode, continueDisabledPermanently = false) {
+ if (result.exitCode === 0) return { shouldRetry: false, useContinueOnRetry: false };
+ if (hasNumerousPermissionDeniedIssues(result.output)) return { shouldRetry: false, useContinueOnRetry: false };
+ if (isMaxEffectiveTokensExceededError(result.output)) return { shouldRetry: false, useContinueOnRetry: false };
+ if (attempt >= MAX_RETRIES || !result.hasOutput) return { shouldRetry: false, useContinueOnRetry: false };
+ // --continue is only enabled in CLI mode and only when not permanently disabled.
+ const useContinueOnRetry = !copilotSDKMode && !continueDisabledPermanently;
+ return { shouldRetry: true, useContinueOnRetry };
+ }
+
+ it("retries on partial execution in SDK mode (fresh run, not --continue)", () => {
+ const result = { exitCode: 1, hasOutput: true, output: "Error: connection reset" };
+ const { shouldRetry, useContinueOnRetry } = blendedRetryDecision(result, 0, true);
+ expect(shouldRetry).toBe(true);
+ expect(useContinueOnRetry).toBe(false);
+ });
+
+ it("retries on CAPIError 400 in SDK mode (fresh run, not --continue)", () => {
+ const result = { exitCode: 1, hasOutput: true, output: "CAPIError: 400 Bad Request" };
+ const { shouldRetry, useContinueOnRetry } = blendedRetryDecision(result, 0, true);
+ expect(shouldRetry).toBe(true);
+ expect(useContinueOnRetry).toBe(false);
+ });
+
+ it("never sets useContinueOnRetry=true in SDK mode regardless of error type", () => {
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
+ const result = { exitCode: 1, hasOutput: true, output: "Error: partial execution" };
+ const { useContinueOnRetry } = blendedRetryDecision(result, attempt, /* copilotSDKMode */ true);
+ expect(useContinueOnRetry).toBe(false);
+ }
+ });
+
+ it("does not retry in SDK mode when no output was produced", () => {
+ const result = { exitCode: 1, hasOutput: false, output: "" };
+ const { shouldRetry } = blendedRetryDecision(result, 0, true);
+ expect(shouldRetry).toBe(false);
+ });
+
+ it("does not retry in SDK mode after retries are exhausted", () => {
+ const result = { exitCode: 1, hasOutput: true, output: "Error: partial execution" };
+ const { shouldRetry } = blendedRetryDecision(result, MAX_RETRIES, true);
+ expect(shouldRetry).toBe(false);
+ });
+
+ it("CLI mode still enables --continue on partial execution when not disabled", () => {
+ const result = { exitCode: 1, hasOutput: true, output: "Error: connection reset" };
+ const { shouldRetry, useContinueOnRetry } = blendedRetryDecision(result, 0, /* copilotSDKMode */ false);
+ expect(shouldRetry).toBe(true);
+ expect(useContinueOnRetry).toBe(true);
+ });
+
+ it("CLI mode respects continueDisabledPermanently", () => {
+ const result = { exitCode: 1, hasOutput: true, output: "Error: connection reset" };
+ const { shouldRetry, useContinueOnRetry } = blendedRetryDecision(result, 0, /* copilotSDKMode */ false, /* continueDisabledPermanently */ true);
+ expect(shouldRetry).toBe(true);
+ expect(useContinueOnRetry).toBe(false);
+ });
+
+ it("currentArgs never appends --continue in SDK mode", () => {
+ const resolvedArgs = ["--prompt", "hello"];
+ // Simulate the blended loop's currentArgs logic for multiple attempts in SDK mode
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
+ const useContinueOnRetry = false; // always false in SDK mode
+ const copilotSDKMode = true;
+ const currentArgs = !copilotSDKMode && attempt > 0 && useContinueOnRetry ? [...resolvedArgs, "--continue"] : resolvedArgs;
+ expect(currentArgs).not.toContain("--continue");
+ }
+ });
+
+ it("currentArgs appends --continue in CLI mode when useContinueOnRetry=true", () => {
+ const resolvedArgs = ["--prompt", "hello"];
+ const copilotSDKMode = false;
+ const useContinueOnRetry = true;
+ // attempt > 0 is when --continue kicks in
+ const currentArgs = !copilotSDKMode && 1 > 0 && useContinueOnRetry ? [...resolvedArgs, "--continue"] : resolvedArgs;
+ expect(currentArgs).toContain("--continue");
+ });
+ });
+
+ describe("readSDKOptionsFromStdin", () => {
+ const { PassThrough } = require("stream");
+
+ /**
+ * Helper: run readSDKOptionsFromStdin with a fake stdin backed by a PassThrough stream.
+ * @param {string | null} data - JSON string to push into stdin, or null to simulate TTY.
+ * @param {boolean} [isTTY]
+ */
+ async function runWithFakeStdin(data, isTTY = false) {
+ const fakeStdin = new PassThrough();
+ fakeStdin.isTTY = isTTY;
+
+ const originalStdin = process.stdin;
+ // Replace process.stdin temporarily by patching the relevant properties
+ Object.defineProperty(process, "stdin", { value: fakeStdin, writable: true, configurable: true });
+ try {
+ const promise = readSDKOptionsFromStdin();
+ if (data !== null) {
+ fakeStdin.push(data);
+ }
+ fakeStdin.end();
+ return await promise;
+ } finally {
+ Object.defineProperty(process, "stdin", { value: originalStdin, writable: true, configurable: true });
+ }
+ }
+
+ it("returns null when stdin is a TTY", async () => {
+ const result = await runWithFakeStdin(null, /* isTTY */ true);
+ expect(result).toBeNull();
+ });
+
+ it("parses valid JSON payload with promptFile", async () => {
+ const result = await runWithFakeStdin('{"promptFile":"/tmp/gh-aw/aw-prompts/prompt.txt"}');
+ expect(result).toEqual({ promptFile: "/tmp/gh-aw/aw-prompts/prompt.txt" });
+ });
+
+ it("parses full payload with promptFile, serverArgs, and addWorkspaceDir", async () => {
+ const payload = JSON.stringify({
+ promptFile: "/tmp/gh-aw/aw-prompts/prompt.txt",
+ serverArgs: ["--headless", "--no-auto-update", "--port", "3002", "--add-dir", "/tmp/gh-aw/", "--log-level", "all", "--disable-builtin-mcps", "--no-ask-user"],
+ addWorkspaceDir: true,
+ });
+ const result = await runWithFakeStdin(payload);
+ expect(result).toEqual({
+ promptFile: "/tmp/gh-aw/aw-prompts/prompt.txt",
+ serverArgs: ["--headless", "--no-auto-update", "--port", "3002", "--add-dir", "/tmp/gh-aw/", "--log-level", "all", "--disable-builtin-mcps", "--no-ask-user"],
+ addWorkspaceDir: true,
+ });
+ });
+
+ it("parses payload with serverArgs but no addWorkspaceDir (non-sandbox mode)", async () => {
+ const payload = JSON.stringify({
+ promptFile: "/tmp/gh-aw/aw-prompts/prompt.txt",
+ serverArgs: ["--headless", "--no-auto-update", "--port", "3002", "--add-dir", "/tmp/", "--log-level", "all"],
+ });
+ const result = await runWithFakeStdin(payload);
+ expect(result).toMatchObject({
+ promptFile: "/tmp/gh-aw/aw-prompts/prompt.txt",
+ serverArgs: ["--headless", "--no-auto-update", "--port", "3002", "--add-dir", "/tmp/", "--log-level", "all"],
+ });
+ expect(result.addWorkspaceDir).toBeUndefined();
+ });
+
+ it("returns null on empty stdin", async () => {
+ const result = await runWithFakeStdin("");
+ expect(result).toBeNull();
+ });
+
+ it("returns null on invalid JSON", async () => {
+ const result = await runWithFakeStdin("not-json{");
+ expect(result).toBeNull();
+ });
+
+ it("handles extra whitespace around JSON", async () => {
+ const result = await runWithFakeStdin('\n {"promptFile":"/tmp/prompt.txt"} \n');
+ expect(result).toEqual({ promptFile: "/tmp/prompt.txt" });
+ });
+ });
+
describe("fetchAWFReflect enriches models via fallback", () => {
afterEach(() => {
vi.unstubAllGlobals();
diff --git a/actions/setup/js/copilot_sdk_driver.cjs b/actions/setup/js/copilot_sdk_driver.cjs
new file mode 100644
index 00000000000..523f9f4b351
--- /dev/null
+++ b/actions/setup/js/copilot_sdk_driver.cjs
@@ -0,0 +1,241 @@
+// @ts-check
+
+/**
+ * Copilot SDK Driver
+ *
+ * Uses @github/copilot-sdk to drive a Copilot session against a running headless
+ * Copilot CLI server (started by copilot_sdk_sidecar.cjs). Serializes all SDK
+ * session events to a JSONL file so that unified_timeline.cjs can render them.
+ *
+ * Event mapping:
+ * SDK "user.message" → JSONL "user.message"
+ * SDK "tool.execution_start" → JSONL "tool.execution_start" (toolName, mcpServerName)
+ * SDK "tool.execution_complete" → JSONL "tool.execution_complete" (toolName, mcpServerName, success)
+ * SDK "assistant.message" → JSONL "assistant.message" (content)
+ *
+ * The JSONL file is written to:
+ * /tmp/gh-aw/sandbox/agent/logs/copilot-session-state/{sessionId}/events.jsonl
+ * which mirrors the path that copy_copilot_session_state.sh produces and that
+ * unified_timeline.cjs reads.
+ */
+
+"use strict";
+
+const fs = require("fs");
+const path = require("path");
+const os = require("os");
+
+// Default timeout for a single sendAndWait call: 10 minutes.
+// This is intentionally generous — the headless Copilot CLI has its own internal
+// timeouts for individual tool calls and model inference.
+// Override via the COPILOT_SDK_SEND_TIMEOUT_MS environment variable.
+const SDK_SEND_TIMEOUT_MS_DEFAULT = 10 * 60 * 1000;
+
+/**
+ * Extract the prompt text from a resolved args array.
+ * Looks for the first occurrence of "-p " or "--prompt ".
+ *
+ * @param {string[]} args - Resolved args (after resolvePromptFileArgs has run).
+ * @returns {string | null} The prompt text, or null if not found.
+ */
+function extractPromptFromArgs(args) {
+ for (let i = 0; i < args.length - 1; i++) {
+ if (args[i] === "-p" || args[i] === "--prompt") {
+ return args[i + 1];
+ }
+ }
+ return null;
+}
+
+/**
+ * Run a Copilot agentic session using the @github/copilot-sdk.
+ *
+ * Connects to the already-running headless Copilot CLI server at sdkUri, creates
+ * a session, sends the prompt, waits for the session to go idle, and returns a
+ * result shape that mirrors what runProcess() returns so that callers can treat
+ * both modes uniformly.
+ *
+ * All SDK events are serialised to a JSONL file under the session state directory
+ * so that unified_timeline.cjs can render them in the step summary.
+ *
+ * @param {{
+ * sdkUri: string,
+ * prompt: string,
+ * logger: (msg: string) => void,
+ * attempt?: number,
+ * sdkModule?: {
+ * CopilotClient: typeof import("@github/copilot-sdk").CopilotClient,
+ * RuntimeConnection: typeof import("@github/copilot-sdk").RuntimeConnection,
+ * approveAll: typeof import("@github/copilot-sdk").approveAll
+ * },
+ * }} options
+ * @returns {Promise<{exitCode: number, output: string, hasOutput: boolean, durationMs: number}>}
+ */
+async function runWithCopilotSDK({ sdkUri, prompt, logger, attempt = 0, sdkModule }) {
+ // Lazy-require to avoid loading the SDK when it is not needed.
+ // The SDK is large and has side-effects on import (worker threads, etc.).
+ const { CopilotClient, RuntimeConnection, approveAll } = sdkModule ?? require("@github/copilot-sdk");
+
+ const startTime = Date.now();
+ let output = "";
+ let hasOutput = false;
+
+ const log = msg => logger(`[sdk-driver] ${msg}`);
+ log(`attempt ${attempt + 1}: connecting to Copilot SDK at ${sdkUri}`);
+
+ // Session state directory — mirrors the target path used by unified_timeline.cjs.
+ // /tmp/gh-aw/sandbox/agent/logs/copilot-session-state/{sessionId}/events.jsonl
+ const sessionStateBase = path.join(os.tmpdir(), "gh-aw", "sandbox", "agent", "logs", "copilot-session-state");
+
+ /** @type {ReadonlyArray>} */
+ const VALID_LOG_LEVELS = ["none", "error", "warning", "info", "debug", "all"];
+ const rawLogLevel = process.env.COPILOT_SDK_LOG_LEVEL ?? "";
+ /** @type {import("@github/copilot-sdk").CopilotClientOptions["logLevel"]} */
+ const logLevel = /** @type {any} */ VALID_LOG_LEVELS.includes(/** @type {any} */ rawLogLevel) ? rawLogLevel : "warning";
+
+ const client = new CopilotClient({
+ connection: RuntimeConnection.forUri(sdkUri, {}),
+ workingDirectory: process.env.GITHUB_WORKSPACE || process.cwd(),
+ logLevel,
+ });
+ let session = null;
+ /** @type {fs.WriteStream | null} */
+ let eventsStream = null;
+ let clientStarted = false;
+
+ try {
+ await client.start();
+ clientStarted = true;
+ log("client started");
+
+ session = await client.createSession({
+ model: process.env.COPILOT_MODEL || undefined,
+ onPermissionRequest: approveAll,
+ });
+ log(`session created: sessionId=${session.sessionId}`);
+
+ // Prepare JSONL output file for this session.
+ const sessionDir = path.join(sessionStateBase, session.sessionId);
+ fs.mkdirSync(sessionDir, { recursive: true });
+ const eventsPath = path.join(sessionDir, "events.jsonl");
+ eventsStream = fs.createWriteStream(eventsPath, { flags: "a" });
+ log(`serialising SDK events to ${eventsPath}`);
+
+ /**
+ * Map from toolCallId → {toolName, mcpServerName} so that tool.execution_complete
+ * events (which carry no mcpServerName) can be enriched from the matching start event.
+ * @type {Map}
+ */
+ const pendingToolCalls = new Map();
+
+ /**
+ * Write one JSONL entry to the events file.
+ * Uses the event's own ISO-8601 timestamp when available.
+ *
+ * @param {string} type
+ * @param {object} data
+ * @param {string | undefined} [timestamp]
+ */
+ function writeEvent(type, data, timestamp) {
+ const entry = { type, timestamp: timestamp ?? new Date().toISOString(), data };
+ eventsStream.write(JSON.stringify(entry) + "\n");
+ }
+
+ // Subscribe to all session events and serialise the ones we care about.
+ session.on(event => {
+ // Skip transient events that are not persisted by the server.
+ if (event.ephemeral) return;
+
+ switch (event.type) {
+ case "user.message":
+ writeEvent("user.message", {}, event.timestamp);
+ break;
+
+ case "tool.execution_start": {
+ const toolName = event.data?.toolName ?? "unknown";
+ const mcpServerName = event.data?.mcpServerName ?? "";
+ const toolCallId = event.data?.toolCallId;
+ if (toolCallId) {
+ pendingToolCalls.set(toolCallId, { toolName, mcpServerName });
+ }
+ writeEvent("tool.execution_start", { toolName, mcpServerName }, event.timestamp);
+ break;
+ }
+
+ case "tool.execution_complete": {
+ const toolCallId = event.data?.toolCallId;
+ // Resolve toolName/mcpServerName from the matching start event when available.
+ const pending = toolCallId ? pendingToolCalls.get(toolCallId) : undefined;
+ const toolName = pending?.toolName ?? event.data?.toolDescription?.name ?? "unknown";
+ const mcpServerName = pending?.mcpServerName ?? "";
+ if (toolCallId) pendingToolCalls.delete(toolCallId);
+ const success = event.data?.success ?? !event.data?.error;
+ writeEvent("tool.execution_complete", { toolName, mcpServerName, success }, event.timestamp);
+ break;
+ }
+
+ case "assistant.message": {
+ const content = event.data?.content ?? "";
+ if (content) {
+ hasOutput = true;
+ output += content;
+ }
+ writeEvent("assistant.message", { content }, event.timestamp);
+ break;
+ }
+
+ default:
+ // Other event types are not consumed by unified_timeline.cjs; skip them.
+ break;
+ }
+ });
+
+ log("sending prompt...");
+ const sendTimeoutMs = Number(process.env.COPILOT_SDK_SEND_TIMEOUT_MS) || SDK_SEND_TIMEOUT_MS_DEFAULT;
+ const result = await session.sendAndWait({ prompt }, sendTimeoutMs);
+
+ // sendAndWait returns the last assistant.message event; capture its content
+ // as a fallback in case the on() handler missed it.
+ if (result && !hasOutput) {
+ const content = result.data?.content ?? "";
+ if (content) {
+ output = content;
+ hasOutput = true;
+ }
+ }
+
+ const durationMs = Date.now() - startTime;
+ log(`session completed: hasOutput=${hasOutput} durationMs=${durationMs}`);
+
+ return { exitCode: 0, output, hasOutput, durationMs };
+ } catch (err) {
+ const durationMs = Date.now() - startTime;
+ log(`error: ${err instanceof Error ? err.message : String(err)}`);
+ return {
+ exitCode: 1,
+ output: err instanceof Error ? err.message : String(err),
+ hasOutput: false,
+ durationMs,
+ };
+ } finally {
+ if (eventsStream) {
+ await new Promise(resolve => eventsStream.end(resolve));
+ }
+ if (session) {
+ try {
+ await session.disconnect();
+ } catch {
+ // best-effort cleanup
+ }
+ }
+ if (clientStarted) {
+ try {
+ await client.stop();
+ } catch {
+ // best-effort cleanup
+ }
+ }
+ }
+}
+
+module.exports = { extractPromptFromArgs, runWithCopilotSDK };
diff --git a/actions/setup/js/copilot_sdk_sidecar.cjs b/actions/setup/js/copilot_sdk_sidecar.cjs
index 0825696f112..fa64731484b 100644
--- a/actions/setup/js/copilot_sdk_sidecar.cjs
+++ b/actions/setup/js/copilot_sdk_sidecar.cjs
@@ -96,6 +96,8 @@ async function waitForCopilotSDKServer(options) {
* command: string,
* env?: NodeJS.ProcessEnv,
* logger?: (message: string) => void,
+ * serverArgs?: string[], // Complete CLI argument list generated by the engine (--headless, --no-auto-update, --port, config flags). Takes precedence over extraArgs.
+ * extraArgs?: string[], // Additional Copilot CLI flags to append to base headless args (legacy; prefer serverArgs).
* spawnImpl?: typeof spawn,
* waitForReady?: typeof waitForCopilotSDKServer
* }} options
@@ -106,9 +108,21 @@ async function startCopilotSDKServer(options) {
const logger = options.logger ?? (() => {});
const serverURL = getCopilotSDKServerURL(env);
if (!serverURL) return null;
- const args = buildCopilotSDKServerArgs(env);
- if (args.length === 0) {
- throw new Error("copilot-sdk enabled but COPILOT_SDK_URI does not include a usable port");
+
+ let args;
+ if (options.serverArgs && options.serverArgs.length > 0) {
+ // Engine-generated complete args: already includes --headless, --no-auto-update, --port,
+ // and all configuration flags. Use them directly without further construction.
+ args = options.serverArgs;
+ } else {
+ // Legacy path: build base headless args from env and merge with optional extra flags.
+ const baseArgs = buildCopilotSDKServerArgs(env);
+ if (baseArgs.length === 0) {
+ throw new Error("copilot-sdk enabled but COPILOT_SDK_URI does not include a usable port");
+ }
+ // Append caller-supplied flags (e.g. --add-dir, --log-level, --disable-builtin-mcps) after
+ // the standard headless flags so they configure the server the same way the CLI would be.
+ args = options.extraArgs && options.extraArgs.length > 0 ? [...baseArgs, ...options.extraArgs] : baseArgs;
}
const spawnImpl = options.spawnImpl ?? spawn;
diff --git a/actions/setup/js/package-lock.json b/actions/setup/js/package-lock.json
index a594f7d2ae0..60d7b178c72 100644
--- a/actions/setup/js/package-lock.json
+++ b/actions/setup/js/package-lock.json
@@ -12,6 +12,7 @@
"@actions/github-script": "github:actions/github-script#v9.0.0",
"@actions/glob": "^0.7.0",
"@actions/io": "^3.0.2",
+ "@github/copilot-sdk": "^1.0.0-beta.9",
"@types/node": "^25.9.1",
"@vitest/coverage-v8": "^4.1.7",
"@vitest/ui": "^4.1.7",
@@ -611,6 +612,192 @@
"tslib": "^2.4.0"
}
},
+ "node_modules/@github/copilot": {
+ "version": "1.0.57",
+ "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.57.tgz",
+ "integrity": "sha512-7dpOu9/qiodmFohZVpTxYmTcjbcXfstWeHof0Ka5RkhguKMkbS3c+sW23a7TTjtlViTV73z+IZFfFW1ru621kw==",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE.md",
+ "dependencies": {
+ "detect-libc": "^2.1.2"
+ },
+ "bin": {
+ "copilot": "npm-loader.js"
+ },
+ "optionalDependencies": {
+ "@github/copilot-darwin-arm64": "1.0.57",
+ "@github/copilot-darwin-x64": "1.0.57",
+ "@github/copilot-linux-arm64": "1.0.57",
+ "@github/copilot-linux-x64": "1.0.57",
+ "@github/copilot-linuxmusl-arm64": "1.0.57",
+ "@github/copilot-linuxmusl-x64": "1.0.57",
+ "@github/copilot-win32-arm64": "1.0.57",
+ "@github/copilot-win32-x64": "1.0.57"
+ }
+ },
+ "node_modules/@github/copilot-darwin-arm64": {
+ "version": "1.0.57",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.57.tgz",
+ "integrity": "sha512-ZmsojZbitPSRfgw3W9wBrHGLRDsBvMCjGsGnJ7xXOU6qxeF/IyWHADxEv1WKfDw8BdCM+LE5yITPXB8bcvCdqQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE.md",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "bin": {
+ "copilot-darwin-arm64": "copilot"
+ }
+ },
+ "node_modules/@github/copilot-darwin-x64": {
+ "version": "1.0.57",
+ "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.57.tgz",
+ "integrity": "sha512-F4TFDOdORy4oSHJS4DE+3sTk09uk1lohOloe0jfvoEVxJSU6jdQcJLNGoo+BQljcG7a1HEBrmB04iAWG1UXVfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE.md",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "bin": {
+ "copilot-darwin-x64": "copilot"
+ }
+ },
+ "node_modules/@github/copilot-linux-arm64": {
+ "version": "1.0.57",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.57.tgz",
+ "integrity": "sha512-6apNY/v7CMxKk45CctUZLzQnddBpIG9keSendFKYN+kBIEBSdy//s/Cz/4YQX1iERnklpgZRP7FvcwaKs0/7YA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "SEE LICENSE IN LICENSE.md",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "bin": {
+ "copilot-linux-arm64": "copilot"
+ }
+ },
+ "node_modules/@github/copilot-linux-x64": {
+ "version": "1.0.57",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.57.tgz",
+ "integrity": "sha512-EOOnU4Y+vZHfxVl8eBAP7JtSTmu5d4ZDUC9wCGpAA5k703lEnpu8UOv04mTHRn8KTzb8gj+ijNhxDWe3Xljbaw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "SEE LICENSE IN LICENSE.md",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "bin": {
+ "copilot-linux-x64": "copilot"
+ }
+ },
+ "node_modules/@github/copilot-linuxmusl-arm64": {
+ "version": "1.0.57",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.57.tgz",
+ "integrity": "sha512-FCAaaJLX5T2ZpMeS1TCNnhQuGqyH9WVZndFdN1VOEnN/iWeSSaVF3lM4TPyRHHnWDVxzZtB+VLqOSjINZntD6g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "SEE LICENSE IN LICENSE.md",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "bin": {
+ "copilot-linuxmusl-arm64": "copilot"
+ }
+ },
+ "node_modules/@github/copilot-linuxmusl-x64": {
+ "version": "1.0.57",
+ "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.57.tgz",
+ "integrity": "sha512-AMIBN830yOvNcrj2Q0tGMImqat/V24wZS/4m5BaUssELM7r7KrT9ZBnBs+nWDZYeQaRoblFWL3f4AfxE3t94lQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "SEE LICENSE IN LICENSE.md",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "bin": {
+ "copilot-linuxmusl-x64": "copilot"
+ }
+ },
+ "node_modules/@github/copilot-sdk": {
+ "version": "1.0.0-beta.9",
+ "resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-1.0.0-beta.9.tgz",
+ "integrity": "sha512-D4yiGL4/faFCjL7bozhX7bgxt/x1wp2LZ2p9Tw+xrA5hbcLh5Be5kPen+bFA8NbVfgt1G2djDYFZlrZjXXmcBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@github/copilot": "^1.0.55-5",
+ "vscode-jsonrpc": "^8.2.1",
+ "zod": "^4.3.6"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@github/copilot-win32-arm64": {
+ "version": "1.0.57",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.57.tgz",
+ "integrity": "sha512-3TL2bd1/p/sYbNgDIqbnjES//zlXP5b0sPEXKQRrpVF9ZLN3vjQ1tmBWx8Qx7zn2J3oywH2dG7qKjuxWTJRXKA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE.md",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "bin": {
+ "copilot-win32-arm64": "copilot.exe"
+ }
+ },
+ "node_modules/@github/copilot-win32-x64": {
+ "version": "1.0.57",
+ "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.57.tgz",
+ "integrity": "sha512-zuKqRn0pIF+ZvuiMXbZkYK1AMlrV21kFTpyf5l7gdI1dzJuwHNI0Qfe0gzaZYaU1B4htbzMk9MhEbjR1PQcoJg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE.md",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "bin": {
+ "copilot-win32-x64": "copilot.exe"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -3700,6 +3887,16 @@
}
}
},
+ "node_modules/vscode-jsonrpc": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
+ "integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3861,6 +4058,16 @@
"engines": {
"node": ">= 14"
}
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/actions/setup/js/package.json b/actions/setup/js/package.json
index 44d48d620fe..a098519935f 100644
--- a/actions/setup/js/package.json
+++ b/actions/setup/js/package.json
@@ -7,6 +7,7 @@
"@actions/github-script": "github:actions/github-script#v9.0.0",
"@actions/glob": "^0.7.0",
"@actions/io": "^3.0.2",
+ "@github/copilot-sdk": "^1.0.0-beta.9",
"@types/node": "^25.9.1",
"@vitest/coverage-v8": "^4.1.7",
"@vitest/ui": "^4.1.7",
diff --git a/pkg/workflow/copilot_engine_execution.go b/pkg/workflow/copilot_engine_execution.go
index 5c0d4655bc7..e74071d3b30 100644
--- a/pkg/workflow/copilot_engine_execution.go
+++ b/pkg/workflow/copilot_engine_execution.go
@@ -22,6 +22,7 @@
package workflow
import (
+ "encoding/json"
"fmt"
"maps"
"strconv"
@@ -33,6 +34,23 @@ import (
"github.com/github/gh-aw/pkg/workflow/compilerenv"
)
+// copilotSDKStdinOptions is the JSON payload piped to the harness via stdin when copilot-sdk: true.
+// All options needed to start and configure the headless Copilot CLI sidecar are included so that
+// the JS harness does not need to parse Copilot CLI argument syntax itself.
+type copilotSDKStdinOptions struct {
+ // PromptFile is the path on disk to the prompt text file.
+ PromptFile string `json:"promptFile"`
+ // ServerArgs is the complete CLI argument list for the headless Copilot CLI server process.
+ // It includes the server control flags (--headless, --no-auto-update, --port) followed by
+ // all configuration flags (--add-dir, --log-level, --disable-builtin-mcps, etc.).
+ // The JS harness passes these directly to the spawned process without any parsing.
+ ServerArgs []string `json:"serverArgs,omitempty"`
+ // AddWorkspaceDir instructs the harness to append --add-dir ${GITHUB_WORKSPACE} to the
+ // server args at runtime. This is needed in sandbox (AWF) mode where the workspace is
+ // only known via the environment variable at execution time.
+ AddWorkspaceDir bool `json:"addWorkspaceDir,omitempty"`
+}
+
var copilotExecLog = logger.New("workflow:copilot_engine_execution")
const customEngineCommandScriptPath = "/tmp/gh-aw/engine-command.sh"
@@ -203,7 +221,42 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st
execPrefix = commandName
}
- if sandboxEnabled {
+ isCopilotSDKMode := workflowData.EngineConfig != nil && workflowData.EngineConfig.CopilotSDK
+
+ if isCopilotSDKMode {
+ // SDK mode: all Copilot CLI options are bundled into a JSON payload piped via stdin.
+ // This avoids passing copilot CLI flags as harness CLI args and lets the harness pass
+ // them directly to the headless sidecar server without any argument parsing.
+ //
+ // serverArgs: the complete CLI argument list for the headless Copilot CLI server process.
+ // Includes the server control flags followed by all configuration flags.
+ // addWorkspaceDir: signals the harness to append --add-dir $GITHUB_WORKSPACE at runtime
+ // (needed in sandbox/AWF mode; $GITHUB_WORKSPACE is only known at execution time).
+ serverArgs := append(
+ []string{"--headless", "--no-auto-update", "--port", strconv.Itoa(constants.DefaultCopilotSDKPort)},
+ copilotArgs...,
+ )
+ sdkOptions := copilotSDKStdinOptions{
+ PromptFile: "/tmp/gh-aw/aw-prompts/prompt.txt",
+ ServerArgs: serverArgs,
+ AddWorkspaceDir: sandboxEnabled,
+ }
+ optionsJSON, err := json.Marshal(sdkOptions)
+ if err != nil {
+ // This should never happen with a plain struct of strings and booleans,
+ // but log and fall back to a minimal payload so the run is not blocked.
+ copilotExecLog.Printf("warning: failed to marshal SDK stdin options: %v; falling back to minimal payload", err)
+ optionsJSON = []byte(`{"promptFile":"/tmp/gh-aw/aw-prompts/prompt.txt"}`)
+ }
+ // Escape single quotes in the JSON for safe embedding in a single-quoted shell string.
+ // JSON marshaling never produces actual newlines, null bytes, or backslash sequences that
+ // would confuse `printf '%s'`; single quotes are the only character that can appear in a
+ // JSON string (from user-supplied args) and that breaks single-quote shell quoting.
+ jsonStr := strings.ReplaceAll(string(optionsJSON), "'", `'\''`)
+ // No copilot CLI args are appended to the harness invocation: all options live in the
+ // JSON payload, so the harness command is simply `node harness copilot`.
+ copilotCommand = fmt.Sprintf(`printf '%%s' '%s' | %s`, jsonStr, execPrefix)
+ } else if sandboxEnabled {
// Sandbox mode: add workspace dir and pass prompt file path directly
copilotCommand = fmt.Sprintf(`%s %s --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt`, execPrefix, shellJoinArgs(copilotArgs))
} else {
diff --git a/pkg/workflow/copilot_engine_test.go b/pkg/workflow/copilot_engine_test.go
index eab7ae22819..4501ea0e717 100644
--- a/pkg/workflow/copilot_engine_test.go
+++ b/pkg/workflow/copilot_engine_test.go
@@ -248,6 +248,46 @@ func TestCopilotEngineExecutionStepsWithCopilotSDK(t *testing.T) {
if !strings.Contains(stepContent, expectedURI) {
t.Fatalf("Expected %s in step env, got:\n%s", expectedURI, stepContent)
}
+
+ // SDK mode pipes a JSON options payload via stdin.
+ // The payload must include promptFile and serverArgs (complete CLI arg list for the headless server).
+ if !strings.Contains(stepContent, `"promptFile":"/tmp/gh-aw/aw-prompts/prompt.txt"`) {
+ t.Fatalf("Expected SDK mode JSON payload to include promptFile, got:\n%s", stepContent)
+ }
+ if !strings.Contains(stepContent, `"serverArgs":[`) {
+ t.Fatalf("Expected SDK mode JSON payload to include serverArgs, got:\n%s", stepContent)
+ }
+ // serverArgs must include the server control flags generated by the engine.
+ if !strings.Contains(stepContent, `"--headless"`) {
+ t.Fatalf("Expected serverArgs to include --headless, got:\n%s", stepContent)
+ }
+ if !strings.Contains(stepContent, `"--port"`) {
+ t.Fatalf("Expected serverArgs to include --port, got:\n%s", stepContent)
+ }
+ // Known configuration flags must appear inside the JSON payload.
+ if !strings.Contains(stepContent, `"--disable-builtin-mcps"`) {
+ t.Fatalf("Expected serverArgs to include --disable-builtin-mcps, got:\n%s", stepContent)
+ }
+ if !strings.Contains(stepContent, `"--no-ask-user"`) {
+ t.Fatalf("Expected serverArgs to include --no-ask-user, got:\n%s", stepContent)
+ }
+ // --prompt-file must never appear: the prompt is read from the promptFile path.
+ if strings.Contains(stepContent, "--prompt-file") {
+ t.Fatalf("Expected SDK mode to omit --prompt-file CLI arg (prompt is read from stdin JSON), got:\n%s", stepContent)
+ }
+ // Copilot CLI args must NOT be passed as CLI args to the harness after the command name.
+ // In SDK mode the harness invocation is `... copilot` with no trailing flags.
+ // Verify by checking that known CLI flags do not appear *after* the pipe character.
+ pipeIdx := strings.LastIndex(stepContent, "| ")
+ if pipeIdx >= 0 {
+ afterPipe := stepContent[pipeIdx:]
+ if strings.Contains(afterPipe, "--add-dir") {
+ t.Fatalf("Expected SDK mode to not pass --add-dir as a harness CLI arg (should be in serverArgs JSON), got:\n%s", afterPipe)
+ }
+ if strings.Contains(afterPipe, "--log-level") {
+ t.Fatalf("Expected SDK mode to not pass --log-level as a harness CLI arg (should be in serverArgs JSON), got:\n%s", afterPipe)
+ }
+ }
}
func TestCopilotEngineExecutionStepsAlwaysInjectsIntegrationIDAfterEnvMerges(t *testing.T) {