diff --git a/.gitattributes b/.gitattributes index 2a92ef0172..8d991b1183 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -.github/workflows/*.lock.yml linguist-generated=true merge=ours +.github/workflows/*.lock.yml linguist-generated=true # Cross-platform tools rewrite these files, so keep their output deterministic. java/**/*.java text eol=lf diff --git a/.github/actions/setup-copilot/action.yml b/.github/actions/setup-copilot/action.yml index 3769bc3751..a9c39a2a0a 100644 --- a/.github/actions/setup-copilot/action.yml +++ b/.github/actions/setup-copilot/action.yml @@ -4,6 +4,9 @@ outputs: cli-path: description: "Path to the Copilot CLI" value: ${{ steps.cli-path.outputs.path }} + javascript-cli-path: + description: "Path to the JavaScript Copilot CLI entrypoint" + value: ${{ steps.cli-path.outputs.javascript-path }} runs: using: "composite" steps: @@ -23,16 +26,19 @@ runs: - name: Set CLI path id: cli-path run: | - # As of CLI 1.0.64-1 the @github/copilot package is a thin loader; the - # runnable index.js ships in the installed platform package - # (e.g. @github/copilot-linux-x64). Exactly one is installed. - cli_path=$(ls "$(pwd)"/nodejs/node_modules/@github/copilot-*/index.js 2>/dev/null | head -n1) + cli_path=$(npm --prefix "$(pwd)/nodejs" run --silent prepare:runtime -- --print-path) if [ -z "$cli_path" ]; then - echo "Could not find @github/copilot platform package (index.js) under nodejs/node_modules" >&2 + echo "Could not prepare the Copilot CLI runtime" >&2 + exit 1 + fi + javascript_cli_path=$(npm --prefix "$(pwd)/nodejs" run --silent prepare:runtime -- --print-legacy-path) + if [ -z "$javascript_cli_path" ]; then + echo "Could not prepare the Copilot CLI JavaScript entrypoint" >&2 exit 1 fi echo "path=$cli_path" >> $GITHUB_OUTPUT + echo "javascript-path=$javascript_cli_path" >> $GITHUB_OUTPUT shell: bash - name: Verify CLI works - run: node ${{ steps.cli-path.outputs.path }} --version + run: node "${{ steps.cli-path.outputs.javascript-path }}" --version shell: bash diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json index 4be7dfbd5c..23d2b6242f 100644 --- a/.github/aw/actions-lock.json +++ b/.github/aw/actions-lock.json @@ -20,15 +20,10 @@ "version": "v7.0.1", "sha": "043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" }, - "github/gh-aw-actions/setup-cli@v0.83.1": { - "repo": "github/gh-aw-actions/setup-cli", - "version": "v0.83.1", - "sha": "8bdba8075360648fe6802302a5b4e016361dc6ac" - }, - "github/gh-aw-actions/setup@v0.83.1": { + "github/gh-aw-actions/setup@v0.88.2": { "repo": "github/gh-aw-actions/setup", - "version": "v0.83.1", - "sha": "8bdba8075360648fe6802302a5b4e016361dc6ac" + "version": "v0.88.2", + "sha": "9271a1804551c0dc4fb0085a97979950aa2f8489" } } } diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a9bc22d0ec..476f4e5689 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -47,7 +47,7 @@ - Tools: each SDK has helper APIs to expose functions as tools; prefer the language's `DefineTool`/`@define_tool`/`CopilotTool.DefineTool` patterns (see language READMEs). - Infinite sessions are enabled by default and persist workspace state to `~/.copilot/session-state/{sessionId}`; compaction events are emitted (`session.compaction_start`, `session.compaction_complete`). See language READMEs for usage. - Streaming: when `streaming`/`Streaming=true` you receive delta events (`assistant.message_delta`, `assistant.reasoning_delta`) and final events (`assistant.message`, `assistant.reasoning`) — tests expect this behavior. -- Type generation is centralized in `nodejs/scripts/generate-session-types.ts` and requires the `@github/copilot` schema to be present (often via `npm link` or installed package). +- Type generation is centralized in `scripts/codegen/` and downloads schemas from the pinned `github/copilot-cli` release. - Java code style: 4-space indent (Spotless + Eclipse formatter), fluent setter pattern for config classes, Javadoc required on public APIs (enforced by Checkstyle, except `json`/`events` packages). - Java handlers return `CompletableFuture` (the Java equivalent of C# `async/await`). When porting from .NET: convert properties → getters/fluent setters, use Jackson (`ObjectMapper`, `@JsonProperty`) for serialization. @@ -64,7 +64,7 @@ - SDK code: `nodejs/src`, `python/copilot`, `go`, `dotnet/src`, `rust/src`, `java/sdk/src/main/java` - Unit tests: `nodejs/test`, `python/*`, `go/*`, `dotnet/test`, `rust/tests`, `java/sdk/src/test/java` - E2E tests: `*/e2e/` folders that use the shared replay proxy and `test/snapshots/`, `java/sdk/src/test/java/**/e2e/` -- Generated types: update schema in `@github/copilot` then run `cd nodejs && npm run generate:session-types` and commit generated files in `src/generated` or language generated location. Java generated types: `java/sdk/src/generated/java` +- Generated types: update the pinned Copilot CLI version, run `cd nodejs && npm run generate`, and commit generated files in each language's generated location. Java generated types: `java/sdk/src/generated/java` ## Boundaries — files you must NOT hand-edit ⛔ diff --git a/.github/skills/agentic-workflows/SKILL.md b/.github/skills/agentic-workflows/SKILL.md index acec3f146c..4c983cc5ac 100644 --- a/.github/skills/agentic-workflows/SKILL.md +++ b/.github/skills/agentic-workflows/SKILL.md @@ -15,6 +15,8 @@ Repository overlay (optional): Read only the files you need: Load these files from `github/gh-aw` (they are not available locally). +- `.github/aw/action-container-substitutions.md` +- `.github/aw/agent-runtime-instructions.md` - `.github/aw/agentic-chat.md` - `.github/aw/agentic-workflows-mcp.md` - `.github/aw/asciicharts.md` @@ -22,6 +24,7 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/charts-trending.md` - `.github/aw/charts.md` - `.github/aw/cli-commands.md` +- `.github/aw/compat.md` - `.github/aw/configure-agentic-engine.md` - `.github/aw/context.md` - `.github/aw/create-agentic-workflow-trigger-details.md` @@ -30,15 +33,24 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/debug-agentic-workflow.md` - `.github/aw/dependabot.md` - `.github/aw/deployment-status.md` +- `.github/aw/designer-mappings.md` - `.github/aw/designer.md` +- `.github/aw/drive-memory.md` +- `.github/aw/enclaves.md` - `.github/aw/evals.md` - `.github/aw/experiments.md` - `.github/aw/github-agentic-workflows.md` +- `.github/aw/github-mcp-server-pagination.md` +- `.github/aw/github-mcp-server-tools.md` - `.github/aw/github-mcp-server.md` - `.github/aw/instructions.md` +- `.github/aw/intent.md` +- `.github/aw/jobs.md` +- `.github/aw/linter-workflows.md` - `.github/aw/llms.md` - `.github/aw/loop.md` - `.github/aw/lsp.md` +- `.github/aw/maintainer.md` - `.github/aw/mcp-clis.md` - `.github/aw/memory-stateful-patterns.md` - `.github/aw/memory.md` @@ -47,7 +59,9 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/network.md` - `.github/aw/optimize-agentic-workflow.md` - `.github/aw/patterns.md` +- `.github/aw/playwright.md` - `.github/aw/pr-reviewer.md` +- `.github/aw/release-workflow.md` - `.github/aw/report.md` - `.github/aw/reuse.md` - `.github/aw/safe-outputs-automation.md` @@ -61,10 +75,13 @@ Load these files from `github/gh-aw` (they are not available locally). - `.github/aw/subagents.md` - `.github/aw/syntax-agentic.md` - `.github/aw/syntax-core.md` +- `.github/aw/syntax-engine.md` - `.github/aw/syntax-tools-imports.md` - `.github/aw/syntax.md` - `.github/aw/test-coverage.md` - `.github/aw/test-expression.md` +- `.github/aw/token-optimization-caching-budgets.md` +- `.github/aw/token-optimization-observability.md` - `.github/aw/token-optimization.md` - `.github/aw/triggers.md` - `.github/aw/update-agentic-workflow.md` @@ -90,5 +107,6 @@ After loading the matching workflow prompt or skill, follow it directly: - Choose workflow architecture and patterns: `.github/aw/patterns.md` - Optimize token usage and cost: `.github/aw/token-optimization.md` - Design long-running multi-agent research workflows: `.github/aw/multi-agent-research.md` +- Add skills or agent plugins requested by the user (`skills:` / `plugins:` frontmatter, never on-the-fly installs): `.github/aw/skills.md` When the task involves OTEL, OTLP, traces, observability backends, or telemetry-driven analysis, also read and follow `skills/otel-queries/SKILL.md` after loading the matching workflow prompt or skill. diff --git a/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md b/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md index d034b20377..a0fd94b943 100644 --- a/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md +++ b/.github/skills/new-java-e2e-test-yaml-and-test/SKILL.md @@ -33,7 +33,7 @@ The format is: ```yaml models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/.github/skills/new-java-e2e-test-yaml-and-test/examples.md b/.github/skills/new-java-e2e-test-yaml-and-test/examples.md index af82ef4dba..12971244a2 100644 --- a/.github/skills/new-java-e2e-test-yaml-and-test/examples.md +++ b/.github/skills/new-java-e2e-test-yaml-and-test/examples.md @@ -8,7 +8,7 @@ File: `test/snapshots/system_message_sections/should_use_replaced_identity_secti ```yaml models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -73,7 +73,7 @@ File: `test/snapshots/system_message_transform/should_invoke_transform_callbacks ```yaml models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: # First exchange: model decides to call tools - messages: diff --git a/.github/workflows/agentics-maintenance.yml b/.github/workflows/agentics-maintenance.yml deleted file mode 100644 index 28c4e67caf..0000000000 --- a/.github/workflows/agentics-maintenance.yml +++ /dev/null @@ -1,633 +0,0 @@ -# This file was automatically generated by pkg/workflow/maintenance_workflow.go (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md -# -# ___ _ _ -# / _ \ | | (_) -# | |_| | __ _ ___ _ __ | |_ _ ___ -# | _ |/ _` |/ _ \ '_ \| __| |/ __| -# | | | | (_| | __/ | | | |_| | (__ -# \_| |_/\__, |\___|_| |_|\__|_|\___| -# __/ | -# _ _ |___/ -# | | | | / _| | -# | | | | ___ _ __ _ __| |_| | _____ ____ -# | |/\| |/ _ \ '__| |/ /| _| |/ _ \ \ /\ / / ___| -# \ /\ / (_) | | | | ( | | | | (_) \ V V /\__ \ -# \/ \/ \___/|_| |_|\_\|_| |_|\___/ \_/\_/ |___/ -# -# -# To regenerate this workflow, run: -# gh aw compile -# Not all edits will cause changes to this file. -# -# For more information: https://github.github.com/gh-aw/introduction/overview/ -# -# This file defines the generated agentic maintenance workflow for this repository. -# It runs scheduled cleanup for expiring safe outputs and supports manual maintenance operations. -# -# This workflow is generated automatically when workflows use expiring safe outputs -# or when repository maintenance features are enabled in .github/workflows/aw.json. -# -# To disable maintenance workflow generation, set in .github/workflows/aw.json: -# {"maintenance": false} -# -# Agentic maintenance docs: -# https://github.github.com/gh-aw/reference/ephemerals/#manual-maintenance-operations -# -name: Agentic Maintenance - -on: - schedule: - - cron: "37 0 * * *" # Daily (based on minimum expires: 30 days) - workflow_dispatch: - inputs: - operation: - description: 'Optional maintenance operation to run' - required: false - type: choice - default: '' - options: - - '' - - 'disable' - - 'enable' - - 'update' - - 'upgrade' - - 'safe_outputs' - - 'create_labels' - - 'activity_report' - - 'close_agentic_workflows_issues' - - 'clean_cache_memories' - - 'update_pull_request_branches' - - 'validate' - - 'forecast' - run_url: - description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.' - required: false - type: string - default: '' - workflow_call: - inputs: - operation: - description: 'Optional maintenance operation to run (disable, enable, update, upgrade, safe_outputs, create_labels, activity_report, close_agentic_workflows_issues, clean_cache_memories, update_pull_request_branches, validate, forecast)' - required: false - type: string - default: '' - run_url: - description: 'Run URL or run ID to replay safe outputs from (e.g. https://github.com/owner/repo/actions/runs/12345 or 12345). Required when operation is safe_outputs.' - required: false - type: string - default: '' - outputs: - operation_completed: - description: 'The maintenance operation that was completed (empty when none ran or a scheduled job ran)' - value: ${{ jobs.run_operation.outputs.operation || inputs.operation }} - applied_run_url: - description: 'The run URL that safe outputs were applied from' - value: ${{ jobs.apply_safe_outputs.outputs.run_url }} - -permissions: {} - -jobs: - close-expired-discussions: - if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} - runs-on: ubuntu-slim - permissions: - discussions: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Close expired discussions - 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/close_expired_discussions.cjs'); - await main(); - close-expired-issues: - if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} - runs-on: ubuntu-slim - permissions: - issues: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Close expired issues - 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/close_expired_issues.cjs'); - await main(); - close-expired-pull-requests: - if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '') }} - runs-on: ubuntu-slim - permissions: - pull-requests: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Close expired pull requests - 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/close_expired_pull_requests.cjs'); - await main(); - - cleanup-cache-memory: - if: ${{ (!(github.event.repository.fork)) && github.event_name != 'push' && (github.event_name != 'workflow_dispatch' && github.event_name != 'workflow_call' || inputs.operation == '' || inputs.operation == 'clean_cache_memories') }} - runs-on: ubuntu-slim - permissions: - actions: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Cleanup outdated cache-memory entries - 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/cleanup_cache_memory.cjs'); - await main(); - - run_operation: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation != '' && inputs.operation != 'safe_outputs' && inputs.operation != 'create_labels' && inputs.operation != 'activity_report' && inputs.operation != 'close_agentic_workflows_issues' && inputs.operation != 'clean_cache_memories' && inputs.operation != 'update_pull_request_branches' && inputs.operation != 'validate' && inputs.operation != 'forecast' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - permissions: - actions: write - contents: write - pull-requests: write - outputs: - operation: ${{ steps.record.outputs.operation }} - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - 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_team_member.cjs'); - await main(); - - - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - version: v0.83.1 - - - name: Run operation - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_OPERATION: ${{ inputs.operation }} - GH_AW_CMD_PREFIX: gh aw - 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/run_operation_update_upgrade.cjs'); - await main(); - - - name: Record outputs - id: record - env: - GH_AW_OPERATION: ${{ inputs.operation }} - run: echo "operation=$GH_AW_OPERATION" >> "$GITHUB_OUTPUT" - - update_pull_request_branches: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'update_pull_request_branches' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - permissions: - contents: write - pull-requests: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - 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_team_member.cjs'); - await main(); - - - name: Update pull request branches - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - 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/update_pull_request_branches.cjs'); - await main(); - - apply_safe_outputs: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'safe_outputs' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - permissions: - actions: read - contents: write - discussions: write - issues: write - pull-requests: write - outputs: - run_url: ${{ steps.record.outputs.run_url }} - steps: - - name: Checkout actions folder - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - sparse-checkout: | - actions - clean: false - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - 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_team_member.cjs'); - await main(); - - - name: Apply Safe Outputs - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_RUN_URL: ${{ inputs.run_url }} - 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/apply_safe_outputs_replay.cjs'); - await main(); - - - name: Record outputs - id: record - env: - GH_AW_RUN_URL: ${{ inputs.run_url }} - run: echo "run_url=$GH_AW_RUN_URL" >> "$GITHUB_OUTPUT" - - create_labels: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'create_labels' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - 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_team_member.cjs'); - await main(); - - - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - version: v0.83.1 - - - name: Create missing labels - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_CMD_PREFIX: gh aw - 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/create_labels.cjs'); - await main(); - - activity_report: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'activity_report' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - timeout-minutes: 120 - permissions: - actions: read - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - 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_team_member.cjs'); - await main(); - - - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - version: v0.83.1 - - - name: Restore activity report logs cache - id: activity_report_logs_cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ./.cache/gh-aw/activity-report-logs - key: ${{ runner.os }}-activity-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} - restore-keys: | - ${{ runner.os }}-activity-report-logs-${{ github.repository }}- - ${{ runner.os }}-activity-report-logs- - - name: Download activity report logs - timeout-minutes: 20 - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GH_AW_CMD_PREFIX: gh aw - run: | - ${GH_AW_CMD_PREFIX} logs \ - --repo "$GITHUB_REPOSITORY" \ - --start-date -1w \ - --count 500 \ - --output ./.cache/gh-aw/activity-report-logs \ - --format markdown \ - --report-file ./.cache/gh-aw/activity-report-logs/report.md - - - name: Save activity report logs cache - if: ${{ always() }} - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ./.cache/gh-aw/activity-report-logs - key: ${{ steps.activity_report_logs_cache.outputs.cache-primary-key }} - - - name: Generate activity report issue - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('node:fs'); - const reportPath = './.cache/gh-aw/activity-report-logs/report.md'; - if (!fs.existsSync(reportPath)) { - core.warning('Activity report markdown not found at ' + reportPath + '; skipping issue creation.'); - return; - } - let reportBody = ''; - try { - reportBody = fs.readFileSync(reportPath, 'utf8').trim(); - } catch (error) { - core.warning('Failed to read activity report markdown at ' + reportPath + ': ' + error.message); - return; - } - if (!reportBody) { - core.warning('Activity report markdown is empty at ' + reportPath + '; skipping issue creation.'); - return; - } - const repoSlug = context.repo.owner + '/' + context.repo.repo; - const body = [ - '### Agentic workflow activity report', - '', - 'Repository: ' + repoSlug, - 'Generated at: ' + new Date().toISOString(), - '', - reportBody, - ].join('\n'); - const createdIssue = await github.rest.issues.create({ - owner: context.repo.owner, - repo: context.repo.repo, - title: '[aw] agentic status report', - body, - labels: ['agentic-workflows'], - }); - core.info('Created issue #' + createdIssue.data.number + ': ' + createdIssue.data.html_url); - - forecast_report: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'forecast' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - timeout-minutes: 60 - permissions: - actions: read - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - 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_team_member.cjs'); - await main(); - - - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - version: v0.83.1 - - - name: Restore forecast report logs cache - id: forecast_report_logs_cache - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ./.github/aw/logs - key: ${{ runner.os }}-forecast-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} - restore-keys: | - ${{ runner.os }}-forecast-report-logs-${{ github.repository }}- - ${{ runner.os }}-forecast-report-logs- - - - name: Generate forecast report - id: generate_forecast_report - timeout-minutes: 30 - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEBUG: "*" - GH_AW_CMD_PREFIX: gh aw - run: | - mkdir -p ./.cache/gh-aw/forecast - set +e - ${GH_AW_CMD_PREFIX} forecast --repo "$GITHUB_REPOSITORY" --timeout 30 --verbose --json > ./.cache/gh-aw/forecast/report.json - forecast_exit_code=$? - set -e - if [ "${forecast_exit_code}" -eq 124 ]; then - echo '{"outcome":"timeout","message":"Forecast computation timed out after 30 minutes."}' > ./.cache/gh-aw/forecast/error.json - echo "::error::Forecast computation timed out after 30 minutes." - exit 1 - fi - if [ "${forecast_exit_code}" -ne 0 ]; then - echo '{"outcome":"error","message":"Forecast computation failed before producing a report."}' > ./.cache/gh-aw/forecast/error.json - echo "::error::Forecast computation failed with exit code ${forecast_exit_code}." - exit 1 - fi - - - name: Debug forecast logs folder - if: ${{ always() }} - shell: bash - run: | - if [ ! -d ./.github/aw/logs ]; then - echo "Logs directory not found: ./.github/aw/logs" - exit 0 - fi - echo "Files under ./.github/aw/logs:" - find ./.github/aw/logs -type f | sort - - - name: Save forecast report logs cache - if: ${{ always() }} - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ./.github/aw/logs - key: ${{ runner.os }}-forecast-report-logs-${{ github.repository }}-${{ github.ref_name }}-${{ github.run_id }} - - - name: Generate forecast issue - if: ${{ always() }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - FORECAST_STEP_OUTCOME: ${{ steps.generate_forecast_report.outcome }} - 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/create_forecast_issue.cjs'); - await main(); - - close_agentic_workflows_issues: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'close_agentic_workflows_issues' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-slim - permissions: - issues: write - steps: - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - 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_team_member.cjs'); - await main(); - - - name: Close no-repro agentic-workflows issues - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - 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/close_agentic_workflows_issues.cjs'); - await main(); - - validate_workflows: - if: ${{ (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call') && inputs.operation == 'validate' && (!(github.event.repository.fork)) }} - runs-on: ubuntu-latest - permissions: - contents: read - issues: write - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - - - name: Setup Scripts - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - destination: ${{ runner.temp }}/gh-aw/actions - - - name: Check admin/maintainer permissions - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - 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_team_member.cjs'); - await main(); - - - name: Install gh-aw - uses: github/gh-aw-actions/setup-cli@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 - with: - version: v0.83.1 - - - name: Validate workflows and file issue on findings - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_CMD_PREFIX: gh aw - 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/run_validate_workflows.cjs'); - await main(); diff --git a/.github/workflows/cross-repo-issue-analysis.lock.yml b/.github/workflows/cross-repo-issue-analysis.lock.yml index 510618f041..f5c636eb05 100644 --- a/.github/workflows/cross-repo-issue-analysis.lock.yml +++ b/.github/workflows/cross-repo-issue-analysis.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"16de319b1db6be0d409d3055ca8fa9f619f2c0120dad678248a45dce07b880b4","body_hash":"653dfb46c89df98eca22ddfb802149d6ade32e9a7ad40dbdc51bfb6b0ba1c4a3","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","RUNTIME_TRIAGE_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"16de319b1db6be0d409d3055ca8fa9f619f2c0120dad678248a45dce07b880b4","body_hash":"653dfb46c89df98eca22ddfb802149d6ade32e9a7ad40dbdc51bfb6b0ba1c4a3","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN","RUNTIME_TRIAGE_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_labels","create_issue","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -35,22 +35,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "SDK Runtime Triage" on: @@ -73,9 +71,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}" + queue: max run-name: "SDK Runtime Triage" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.cross-repo-issue-analysis + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=SDK%20Runtime%20Triage,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: needs: pre_activation @@ -93,6 +100,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -108,7 +116,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -118,34 +126,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "SDK Runtime Triage" 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.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -167,9 +180,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -187,38 +202,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -227,38 +241,47 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Compute current body text id: sanitized uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + const { main } = require(path.join(actionsDir, 'compute_text.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -270,63 +293,22 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_41a978c1ce3777a8_EOF' - - GH_AW_PROMPT_41a978c1ce3777a8_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_41a978c1ce3777a8_EOF' - - Tools: create_issue, add_labels(max:3), missing_tool, missing_data, noop - - GH_AW_PROMPT_41a978c1ce3777a8_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_41a978c1ce3777a8_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_41a978c1ce3777a8_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_41a978c1ce3777a8_EOF' - - {{#runtime-import .github/workflows/cross-repo-issue-analysis.md}} - GH_AW_PROMPT_41a978c1ce3777a8_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: create_issue, add_labels(max:3), missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/cross-repo-issue-analysis.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} GH_AW_GITHUB_EVENT_ISSUE_TITLE: ${{ github.event.issue.title }} @@ -334,14 +316,16 @@ jobs: GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -357,10 +341,12 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -383,16 +369,20 @@ jobs: }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -419,12 +409,19 @@ jobs: copilot-requests: write issues: read pull-requests: read + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: crossrepoissueanalysis outputs: @@ -438,7 +435,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -446,11 +446,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -459,19 +460,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -507,16 +517,19 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -525,13 +538,15 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -543,15 +558,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_f9dc8569c195ba44_EOF' - {"add_labels":{"allowed":["runtime","sdk-fix-only","needs-investigation"],"max":3,"target":"triggering"},"create_issue":{"labels":["upstream-from-sdk","ai-triaged"],"max":1,"target-repo":"github/copilot-agent-runtime","title_prefix":"[copilot-sdk] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_f9dc8569c195ba44_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_labels\":{\"allowed\":[\"runtime\",\"sdk-fix-only\",\"needs-investigation\"],\"max\":3,\"target\":\"triggering\"},\"create_issue\":{\"labels\":[\"upstream-from-sdk\",\"ai-triaged\"],\"max\":1,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -584,6 +610,7 @@ jobs: "create_issue": { "defaultMax": 1, "fields": { + "blocked_by": {}, "body": { "required": true, "type": "string", @@ -695,9 +722,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -706,6 +735,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} 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.RUNTIME_TRIAGE_TOKEN }} @@ -713,33 +743,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_46604863f3d8e286_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -772,6 +814,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -783,7 +833,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -791,25 +841,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF + GH_AW_MCP_CONFIG_46604863f3d8e286_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -850,18 +907,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\",\"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\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -879,14 +951,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(cat:*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find:*)'\'' --allow-tool '\''shell(github:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(grep:*)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(head:*)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(ls:*)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tail:*)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(wc:*)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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-tool github --allow-tool safeoutputs --allow-tool '\''shell(cat)'\'' --allow-tool '\''shell(cat:*)'\'' --allow-tool '\''shell(date)'\'' --allow-tool '\''shell(echo)'\'' --allow-tool '\''shell(find:*)'\'' --allow-tool '\''shell(github:*)'\'' --allow-tool '\''shell(grep)'\'' --allow-tool '\''shell(grep:*)'\'' --allow-tool '\''shell(head)'\'' --allow-tool '\''shell(head:*)'\'' --allow-tool '\''shell(ls)'\'' --allow-tool '\''shell(ls:*)'\'' --allow-tool '\''shell(printf)'\'' --allow-tool '\''shell(pwd)'\'' --allow-tool '\''shell(safeoutputs:*)'\'' --allow-tool '\''shell(sort)'\'' --allow-tool '\''shell(tail)'\'' --allow-tool '\''shell(tail:*)'\'' --allow-tool '\''shell(uniq)'\'' --allow-tool '\''shell(wc)'\'' --allow-tool '\''shell(wc:*)'\'' --allow-tool '\''shell(yq)'\'' --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -894,7 +971,7 @@ jobs: 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_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -915,7 +992,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 20 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -931,7 +1019,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -940,9 +1028,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN,RUNTIME_TRIAGE_TOKEN' @@ -966,15 +1056,17 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" GH_AW_ALLOWED_GITHUB_REFS: "repo,github/copilot-agent-runtime" 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -984,9 +1076,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -994,9 +1088,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -1010,9 +1106,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1020,16 +1118,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1046,6 +1169,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1065,10 +1190,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1085,7 +1210,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1094,15 +1219,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" 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 + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1110,42 +1236,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1159,6 +1272,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1181,9 +1296,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1213,7 +1330,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/cross-repo-issue-analysis.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" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1221,9 +1338,11 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1238,9 +1357,11 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1253,9 +1374,11 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1268,9 +1391,11 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1283,7 +1408,7 @@ jobs: 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: "cross-repo-issue-analysis" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1297,6 +1422,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} 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_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1312,9 +1441,30 @@ jobs: with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "SDK Runtime Triage" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/cross-repo-issue-analysis.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1326,6 +1476,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1336,7 +1487,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1345,15 +1496,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1361,10 +1519,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1373,7 +1533,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1397,21 +1557,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1419,82 +1565,52 @@ jobs: WORKFLOW_NAME: "SDK Runtime Triage" WORKFLOW_DESCRIPTION: "Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1510,58 +1626,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "SDK Runtime Triage" + WORKFLOW_DESCRIPTION: "Analyzes copilot-sdk issues to determine if a fix is needed in copilot-agent-runtime, then opens a linked issue there" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact 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 + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json pre_activation: if: github.event_name == 'workflow_dispatch' || github.event.label.name == 'runtime triage' @@ -1577,15 +1740,15 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Check team membership for workflow id: check_membership @@ -1595,9 +1758,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_membership.cjs'); + const { main } = require(path.join(actionsDir, 'check_membership.cjs')); await main(); safe_outputs: @@ -1608,7 +1773,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1622,7 +1786,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "cross-repo-issue-analysis" @@ -1635,12 +1798,20 @@ jobs: 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_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1649,15 +1820,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Runtime Triage" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/cross-repo-issue-analysis.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1665,7 +1839,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1681,16 +1857,18 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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: "{\"add_labels\":{\"allowed\":[\"runtime\",\"sdk-fix-only\",\"needs-investigation\"],\"max\":3,\"target\":\"triggering\"},\"create_issue\":{\"labels\":[\"upstream-from-sdk\",\"ai-triaged\"],\"max\":1,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_labels\":{\"allowed\":[\"runtime\",\"sdk-fix-only\",\"needs-investigation\"],\"max\":3,\"target\":\"triggering\"},\"create_issue\":{\"labels\":[\"upstream-from-sdk\",\"ai-triaged\"],\"max\":1,\"target-repo\":\"github/copilot-agent-runtime\",\"title_prefix\":\"[copilot-sdk] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" with: github-token: ${{ secrets.RUNTIME_TRIAGE_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1700,4 +1878,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/dotnet-sdk-tests.yml b/.github/workflows/dotnet-sdk-tests.yml index 4695cae7f2..9b7216ad20 100644 --- a/.github/workflows/dotnet-sdk-tests.yml +++ b/.github/workflows/dotnet-sdk-tests.yml @@ -1,9 +1,6 @@ name: ".NET SDK Tests" on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -11,6 +8,40 @@ permissions: contents: read jobs: + validate: + name: ".NET SDK Build and Format" + if: github.event.repository.fork == false + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: ./dotnet + steps: + - uses: actions/checkout@v6.0.2 + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: "10.0.x" + - uses: actions/setup-node@v6 + with: + node-version: "22" + + - name: Restore .NET dependencies + run: dotnet restore + + - name: Run dotnet format check + run: | + if ! dotnet format --no-restore --verify-no-changes; then + echo "❌ dotnet format produced changes. Please run 'dotnet format' in dotnet" + exit 1 + fi + echo "✅ dotnet format produced no changes" + + # Build the whole solution once to validate every SDK target framework. + # Matrix cells below only need to build frameworks consumed by the tests. + - name: Build SDK + run: dotnet build --no-restore + test: name: ".NET SDK Tests (${{ matrix.os }}, ${{ matrix.transport }}, ${{ matrix.backend }}, ${{ matrix.shard }})" if: github.event.repository.fork == false @@ -73,7 +104,65 @@ jobs: - os: macos-latest transport: default backend: capi - shard: "2b" + shard: "2b-pending" + - os: macos-latest + transport: default + backend: capi + shard: "2b-permission" + - os: macos-latest + transport: default + backend: capi + shard: "2b-auth" + - os: macos-latest + transport: default + backend: capi + shard: "2b-hooks" + - os: macos-latest + transport: default + backend: capi + shard: "2b-unit-p" + - os: macos-latest + transport: default + backend: capi + shard: "2b-provider" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-additional" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-agent" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-event-log" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-event-effects" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-mcp-skills" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-mcp-config" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-mcp-lifecycle" + - os: macos-latest + transport: default + backend: capi + shard: "2b-rpc-q-z" + # Standalone extension tests require the legacy JavaScript entrypoint. + # Keep its subprocess tree isolated from the rest of the O/P/R shard. + - os: macos-latest + transport: default + backend: capi + shard: extensions - os: macos-latest transport: default backend: capi @@ -97,7 +186,7 @@ jobs: # A hung test used to run until the runner died (~50 min) and the dying # runner never uploaded its logs, so the failures were undiagnosable. # Every healthy cell finishes well under 15 min. - timeout-minutes: 30 + timeout-minutes: 20 defaults: run: shell: bash @@ -111,27 +200,16 @@ jobs: with: node-version: "22" cache: "npm" - cache-dependency-path: "./nodejs/package-lock.json" + cache-dependency-path: | + ./nodejs/package-lock.json + ./test/harness/package-lock.json - - name: Install Node.js dependencies (for CLI version extraction) + - name: Install Node.js dependencies working-directory: ./nodejs run: npm ci --ignore-scripts - name: Restore .NET dependencies - run: dotnet restore - - - name: Run dotnet format check - if: runner.os == 'Linux' - run: | - dotnet format --verify-no-changes - if [ $? -ne 0 ]; then - echo "❌ dotnet format produced changes. Please run 'dotnet format' in dotnet" - exit 1 - fi - echo "✅ dotnet format produced no changes" - - - name: Build SDK - run: dotnet build --no-restore + run: dotnet restore test/GitHub.Copilot.SDK.Test.csproj - name: Install test harness dependencies working-directory: ./test/harness @@ -153,26 +231,95 @@ jobs: # --blame-hang names the offending test instead of letting it wedge # the runner. No single test legitimately runs for 10 minutes; the # whole suite normally finishes in about five. - args=(--no-build -v n --blame-hang --blame-hang-timeout 10m --blame-hang-dump-type none) + # The validation job performs the full analyzer-enabled SDK build. + # Build only test-consumed frameworks here and do not repeat analyzers. + args=( + --no-restore + -v n + --blame-hang + --blame-hang-timeout 10m + --blame-hang-dump-type none + --logger "trx;LogFilePrefix=test-results" + --results-directory "$GITHUB_WORKSPACE/dotnet/TestResults" + -p:RunAnalyzers=false + ) filter="$DOTNET_TEST_FILTER" if [[ "$DOTNET_TEST_SHARD" != "full" ]]; then case "$DOTNET_TEST_SHARD" in 1) - initials=(A C D H I J K L N Q S U W Y) + # M moved here after recent runs showed shard 2 was about two + # minutes slower; this keeps Windows and macOS balanced. + initials=(A C D H I J K L M N Q S U W Y) shard_filter="FullyQualifiedName~GitHub.Copilot.Test.ConnectionToken" ;; 2) - initials=(B E F G M O P R T V X Z) + initials=(B E F G O P R T V X Z) shard_filter="" ;; 2a) initials=(B E F G) shard_filter="" ;; - 2b) - initials=(M O P R) - shard_filter="" + 2b-pending) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PendingWorkResumeE2ETests" + ;; + 2b-permission) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PermissionE2ETests" + ;; + 2b-auth) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PerSessionAuthE2ETests" + ;; + 2b-hooks) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.PreMcpToolCallHookE2ETests" + ;; + 2b-unit-p) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.Unit.P" + ;; + 2b-provider) + initials=(O) + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.ProviderEndpointE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RewindE2ETests|FullyQualifiedName~GitHub.Copilot.Test.Unit.R" + ;; + 2b-rpc-additional) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcAdditionalEdgeCasesE2ETests" + ;; + 2b-rpc-agent) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcAgentE2ETests" + ;; + 2b-rpc-event-log) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcEventLogE2ETests" + ;; + 2b-rpc-event-effects) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcEventSideEffectsE2ETests" + ;; + 2b-rpc-mcp-skills) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpAndSkillsE2ETests" + ;; + 2b-rpc-mcp-config) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpConfigE2ETests" + ;; + 2b-rpc-mcp-lifecycle) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcMcpLifecycleE2ETests" + ;; + 2b-rpc-q-z) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcQueueE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcRemoteE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcScheduleE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerMiscE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerPluginsE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcServerRemoteControlE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcSessionStateE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcSessionStateExtrasE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellAndFleetE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellEdgeCaseE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcShellUserRequestedE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcTasksAndHandlersE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcUiEphemeralQueryE2ETests|FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcWorkspaceCheckpointsE2ETests" + ;; + extensions) + initials=() + shard_filter="FullyQualifiedName~GitHub.Copilot.Test.E2E.RpcExtensionsLoadedE2ETests" ;; 2c) initials=(T V X Z) @@ -192,4 +339,13 @@ jobs: if [[ -n "$filter" ]]; then args+=(--filter "$filter") fi - dotnet test "${args[@]}" + dotnet test test/GitHub.Copilot.SDK.Test.csproj "${args[@]}" + + - name: Upload .NET test diagnostics + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: dotnet-test-diagnostics-${{ matrix.os }}-${{ matrix.transport }}-${{ matrix.backend }}-${{ matrix.shard }}-${{ github.run_attempt }} + path: dotnet/TestResults/ + if-no-files-found: warn + retention-days: 7 diff --git a/.github/workflows/go-sdk-tests.yml b/.github/workflows/go-sdk-tests.yml index 61d74d257e..4fb6fd0184 100644 --- a/.github/workflows/go-sdk-tests.yml +++ b/.github/workflows/go-sdk-tests.yml @@ -1,9 +1,6 @@ name: "Go SDK Tests" on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -22,6 +19,7 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] transport: ["default", "inprocess"] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash diff --git a/.github/workflows/handle-bug.lock.yml b/.github/workflows/handle-bug.lock.yml index 153e882c44..6d2c916a4f 100644 --- a/.github/workflows/handle-bug.lock.yml +++ b/.github/workflows/handle-bug.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"df4e7ec5b346a28c7de5353cbfb15d329d03eb7092f2ded784ee6602108461e6","body_hash":"376c982b907760113954510ef1aff70d22dcb172c7bb851b2fa3d82121bdbc1c","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c5b0e0eada050c6349c6630c62a7d91425c57a49758a3d8937e851eb3275e5f9","body_hash":"376c982b907760113954510ef1aff70d22dcb172c7bb851b2fa3d82121bdbc1c","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Handles issues classified as bugs by the triage classifier # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Bug Handler" on: @@ -74,7 +72,7 @@ on: description: URL of the first added comment value: ${{ jobs.safe_outputs.outputs.comment_url }} secrets: - COPILOT_GITHUB_TOKEN: + GH_AW_DEFAULT_OTLP_HEADERS: required: false GH_AW_GITHUB_MCP_SERVER_TOKEN: required: false @@ -85,9 +83,18 @@ permissions: {} concurrency: group: "gh-aw-handle-bug-${{ github.run_id }}" + queue: max run-name: "Bug Handler" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.handle-bug + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Bug%20Handler,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -102,6 +109,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -119,7 +127,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -127,10 +135,12 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Bug Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-bug.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Resolve host repo for activation checkout id: resolve-host-repo uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -141,9 +151,11 @@ jobs: JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + const { main } = require(path.join(actionsDir, 'resolve_host_repo.cjs')); await main(); - name: Compute artifact prefix id: artifact-prefix @@ -155,27 +167,30 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Bug Handler" 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.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -197,9 +212,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -217,15 +234,16 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Print cross-repo setup guidance @@ -236,7 +254,7 @@ jobs: echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" - name: Checkout .github and .agents folders if: steps.resolve-host-repo.outputs.target_repo == github.repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -244,20 +262,18 @@ jobs: sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -266,27 +282,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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) }} @@ -296,75 +319,36 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_04d69bd6df5739b0_EOF' - - GH_AW_PROMPT_04d69bd6df5739b0_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_04d69bd6df5739b0_EOF' - - Tools: add_comment, add_labels, missing_tool, missing_data, noop - - GH_AW_PROMPT_04d69bd6df5739b0_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_04d69bd6df5739b0_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_04d69bd6df5739b0_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_04d69bd6df5739b0_EOF' - - {{#runtime-import .github/workflows/handle-bug.md}} - GH_AW_PROMPT_04d69bd6df5739b0_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, add_labels, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/handle-bug.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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) }} @@ -374,13 +358,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -394,22 +379,25 @@ jobs: 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_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER } }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.artifact-prefix.outputs.prefix }}activation @@ -439,12 +427,19 @@ jobs: concurrency: group: "gh-aw-copilot-handle-bug-${{ inputs.issue_number }}" queue: max + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handlebug outputs: @@ -459,7 +454,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -467,11 +465,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -480,20 +479,29 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Bug Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-bug.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -523,16 +531,19 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -542,7 +553,9 @@ jobs: GH_AW_GITHUB_MIN_INTEGRITY: 'none' with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars @@ -554,8 +567,8 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -567,15 +580,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_18a2d1564ec59c01_EOF' - {"add_comment":{"max":1,"target":"*"},"add_labels":{"allowed":["bug","enhancement","question","documentation"],"max":1,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_18a2d1564ec59c01_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -598,9 +622,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -608,6 +641,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -704,9 +747,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -715,38 +760,51 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -782,6 +840,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -793,7 +859,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -801,25 +867,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF + GH_AW_MCP_CONFIG_137b94c134aafdc9_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -831,22 +904,40 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool write timeout-minutes: 20 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\",\"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\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -864,14 +955,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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-tool github --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -879,7 +975,7 @@ jobs: 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_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -900,7 +996,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 20 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -916,7 +1023,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -925,9 +1032,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -950,14 +1059,16 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -967,9 +1078,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -977,9 +1090,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -993,9 +1108,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1003,16 +1120,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1031,6 +1173,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1050,10 +1194,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1070,7 +1214,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1079,8 +1223,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Bug Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-bug.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1088,7 +1232,8 @@ jobs: continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1096,42 +1241,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + pattern: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1145,6 +1277,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1167,9 +1301,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1199,7 +1335,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-bug.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" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1207,9 +1343,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1224,9 +1362,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1239,9 +1379,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1254,9 +1396,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1269,7 +1413,7 @@ jobs: 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: "handle-bug" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1283,6 +1427,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} 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_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1298,9 +1446,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "Bug Handler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-bug.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1312,6 +1481,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1322,7 +1492,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1331,16 +1501,23 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Bug Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-bug.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.agent.outputs.artifact_prefix }}agent + pattern: "{${{ needs.agent.outputs.artifact_prefix }}agent,${{ needs.agent.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1348,10 +1525,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1360,7 +1539,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1384,21 +1563,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1406,82 +1571,52 @@ jobs: WORKFLOW_NAME: "Bug Handler" WORKFLOW_DESCRIPTION: "Handles issues classified as bugs by the triage classifier" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1497,58 +1632,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Bug Handler" + WORKFLOW_DESCRIPTION: "Handles issues classified as bugs by the triage classifier" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ needs.agent.outputs.artifact_prefix }}detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1558,7 +1740,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1572,7 +1753,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-bug" @@ -1585,12 +1765,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1599,16 +1787,19 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Bug Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-bug.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1616,7 +1807,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1632,16 +1825,18 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1651,4 +1846,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/handle-bug.md b/.github/workflows/handle-bug.md index 8d426ce5d6..21f9532bc2 100644 --- a/.github/workflows/handle-bug.md +++ b/.github/workflows/handle-bug.md @@ -18,6 +18,8 @@ permissions: pull-requests: read copilot-requests: write tools: + bash: [] + cli-proxy: false github: toolsets: [default] min-integrity: none @@ -62,4 +64,4 @@ Based on your investigation, take **one** of the following actions: **Always leave a comment** explaining your findings, even when confirming the issue is a bug. Include: - What you investigated (which files/code paths you looked at) - What you found (is the behavior intentional or not) -- Why you applied the label you chose +- Why you applied the label you chose \ No newline at end of file diff --git a/.github/workflows/handle-documentation.lock.yml b/.github/workflows/handle-documentation.lock.yml index 0a5e68efe3..ea7cd2121d 100644 --- a/.github/workflows/handle-documentation.lock.yml +++ b/.github/workflows/handle-documentation.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6c3b9dc8d0f7b54d44175db209cda34e37ea8c635d01ee0a5cba13675053f6cd","body_hash":"81c8287f5691cdc10ae8f60c004bb671d9b4942740d73fcc9646e28fbcd8790e","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"6624c9bc7f3d51508aa278e0086a6066843a0331d8220f477c10f156f26f7960","body_hash":"81c8287f5691cdc10ae8f60c004bb671d9b4942740d73fcc9646e28fbcd8790e","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Handles issues classified as documentation-related by the triage classifier # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Documentation Handler" on: @@ -74,7 +72,7 @@ on: description: URL of the first added comment value: ${{ jobs.safe_outputs.outputs.comment_url }} secrets: - COPILOT_GITHUB_TOKEN: + GH_AW_DEFAULT_OTLP_HEADERS: required: false GH_AW_GITHUB_MCP_SERVER_TOKEN: required: false @@ -85,9 +83,18 @@ permissions: {} concurrency: group: "gh-aw-handle-documentation-${{ github.run_id }}" + queue: max run-name: "Documentation Handler" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.handle-documentation + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Documentation%20Handler,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -102,6 +109,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -119,7 +127,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -127,10 +135,12 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Documentation Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-documentation.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Resolve host repo for activation checkout id: resolve-host-repo uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -141,9 +151,11 @@ jobs: JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + const { main } = require(path.join(actionsDir, 'resolve_host_repo.cjs')); await main(); - name: Compute artifact prefix id: artifact-prefix @@ -155,27 +167,30 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Documentation Handler" 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.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -197,9 +212,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -217,15 +234,16 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Print cross-repo setup guidance @@ -236,7 +254,7 @@ jobs: echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" - name: Checkout .github and .agents folders if: steps.resolve-host-repo.outputs.target_repo == github.repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -244,20 +262,18 @@ jobs: sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -266,27 +282,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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) }} @@ -296,75 +319,36 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_EOF' - - GH_AW_PROMPT_b3d8e6ce75517df8_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_b3d8e6ce75517df8_EOF' - - Tools: add_comment, add_labels, missing_tool, missing_data, noop - - GH_AW_PROMPT_b3d8e6ce75517df8_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_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_b3d8e6ce75517df8_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_b3d8e6ce75517df8_EOF' - - {{#runtime-import .github/workflows/handle-documentation.md}} - GH_AW_PROMPT_b3d8e6ce75517df8_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, add_labels, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/handle-documentation.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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) }} @@ -374,13 +358,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -394,22 +379,25 @@ jobs: 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_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER } }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.artifact-prefix.outputs.prefix }}activation @@ -439,12 +427,19 @@ jobs: concurrency: group: "gh-aw-copilot-handle-documentation-${{ inputs.issue_number }}" queue: max + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handledocumentation outputs: @@ -459,7 +454,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -467,11 +465,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -480,20 +479,29 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Documentation Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-documentation.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -523,16 +531,19 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -542,7 +553,9 @@ jobs: GH_AW_GITHUB_MIN_INTEGRITY: 'none' with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars @@ -554,8 +567,8 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -567,15 +580,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_6c1b251bac7b1edb_EOF' - {"add_comment":{"max":1,"target":"*"},"add_labels":{"allowed":["documentation"],"max":1,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_6c1b251bac7b1edb_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -598,9 +622,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -608,6 +641,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -704,9 +747,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -715,38 +760,51 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -782,6 +840,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -793,7 +859,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -801,25 +867,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF + GH_AW_MCP_CONFIG_137b94c134aafdc9_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -831,22 +904,40 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool write timeout-minutes: 5 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\",\"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\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -864,14 +955,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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-tool github --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -879,7 +975,7 @@ jobs: 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_TIMEOUT_MINUTES: 5 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -900,7 +996,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 5 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -916,7 +1023,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -925,9 +1032,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -950,14 +1059,16 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -967,9 +1078,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -977,9 +1090,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -993,9 +1108,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1003,16 +1120,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1031,6 +1173,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1050,10 +1194,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1070,7 +1214,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1079,8 +1223,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Documentation Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-documentation.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1088,7 +1232,8 @@ jobs: continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1096,42 +1241,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + pattern: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1145,6 +1277,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1167,9 +1301,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1199,7 +1335,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-documentation.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" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1207,9 +1343,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1224,9 +1362,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1239,9 +1379,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1254,9 +1396,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1269,7 +1413,7 @@ jobs: 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: "handle-documentation" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1283,6 +1427,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} 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_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1298,9 +1446,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "Documentation Handler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-documentation.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1312,6 +1481,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1322,7 +1492,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1331,16 +1501,23 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Documentation Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-documentation.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.agent.outputs.artifact_prefix }}agent + pattern: "{${{ needs.agent.outputs.artifact_prefix }}agent,${{ needs.agent.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1348,10 +1525,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1360,7 +1539,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1384,21 +1563,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1406,82 +1571,52 @@ jobs: WORKFLOW_NAME: "Documentation Handler" WORKFLOW_DESCRIPTION: "Handles issues classified as documentation-related by the triage classifier" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1497,58 +1632,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Documentation Handler" + WORKFLOW_DESCRIPTION: "Handles issues classified as documentation-related by the triage classifier" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ needs.agent.outputs.artifact_prefix }}detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1558,7 +1740,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1572,7 +1753,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-documentation" @@ -1585,12 +1765,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1599,16 +1787,19 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Documentation Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-documentation.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1616,7 +1807,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1632,16 +1825,18 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"documentation\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1651,4 +1846,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/handle-documentation.md b/.github/workflows/handle-documentation.md index 12449a85ca..543d7f71d5 100644 --- a/.github/workflows/handle-documentation.md +++ b/.github/workflows/handle-documentation.md @@ -18,6 +18,8 @@ permissions: pull-requests: read copilot-requests: write tools: + bash: [] + cli-proxy: false github: toolsets: [default] min-integrity: none @@ -44,4 +46,4 @@ You are an AI agent that handles issues classified as documentation-related in t 4. Leave a comment that includes: - A summary of the documentation gap (what is missing, incorrect, or unclear) - Which documentation pages, files, or sections are affected - - A brief description of what content should be added or improved to resolve the issue + - A brief description of what content should be added or improved to resolve the issue \ No newline at end of file diff --git a/.github/workflows/handle-enhancement.lock.yml b/.github/workflows/handle-enhancement.lock.yml index 5943203871..b87f3c22a3 100644 --- a/.github/workflows/handle-enhancement.lock.yml +++ b/.github/workflows/handle-enhancement.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"f194730258943dec72121a119dba066ff5fee588d79f69fddddd4ca29b56ffd4","body_hash":"624219976b9b7078c6bb11c4177925478cfd8316fe8de535a581bdd176eda825","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c7efbd4a51f5a1e6f8b189c420a07337135cdb5f25290588d167dc9b4ae1c117","body_hash":"624219976b9b7078c6bb11c4177925478cfd8316fe8de535a581bdd176eda825","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Handles issues classified as enhancements by the triage classifier # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Enhancement Handler" on: @@ -74,7 +72,7 @@ on: description: URL of the first added comment value: ${{ jobs.safe_outputs.outputs.comment_url }} secrets: - COPILOT_GITHUB_TOKEN: + GH_AW_DEFAULT_OTLP_HEADERS: required: false GH_AW_GITHUB_MCP_SERVER_TOKEN: required: false @@ -85,9 +83,18 @@ permissions: {} concurrency: group: "gh-aw-handle-enhancement-${{ github.run_id }}" + queue: max run-name: "Enhancement Handler" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.handle-enhancement + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Enhancement%20Handler,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -102,6 +109,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -119,7 +127,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -127,10 +135,12 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Enhancement Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-enhancement.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Resolve host repo for activation checkout id: resolve-host-repo uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -141,9 +151,11 @@ jobs: JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + const { main } = require(path.join(actionsDir, 'resolve_host_repo.cjs')); await main(); - name: Compute artifact prefix id: artifact-prefix @@ -155,27 +167,30 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Enhancement Handler" 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.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -197,9 +212,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -217,15 +234,16 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Print cross-repo setup guidance @@ -236,7 +254,7 @@ jobs: echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" - name: Checkout .github and .agents folders if: steps.resolve-host-repo.outputs.target_repo == github.repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -244,20 +262,18 @@ jobs: sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -266,27 +282,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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) }} @@ -296,75 +319,36 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_EOF' - - GH_AW_PROMPT_e59da2f8e25b61b4_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_e59da2f8e25b61b4_EOF' - - Tools: add_comment, add_labels, missing_tool, missing_data, noop - - GH_AW_PROMPT_e59da2f8e25b61b4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_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_e59da2f8e25b61b4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_e59da2f8e25b61b4_EOF' - - {{#runtime-import .github/workflows/handle-enhancement.md}} - GH_AW_PROMPT_e59da2f8e25b61b4_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, add_labels, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/handle-enhancement.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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) }} @@ -374,13 +358,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -394,22 +379,25 @@ jobs: 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_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER } }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.artifact-prefix.outputs.prefix }}activation @@ -439,12 +427,19 @@ jobs: concurrency: group: "gh-aw-copilot-handle-enhancement-${{ inputs.issue_number }}" queue: max + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handleenhancement outputs: @@ -459,7 +454,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -467,11 +465,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -480,20 +479,29 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Enhancement Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-enhancement.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -523,16 +531,19 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -542,7 +553,9 @@ jobs: GH_AW_GITHUB_MIN_INTEGRITY: 'none' with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars @@ -554,8 +567,8 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -567,15 +580,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_a36bb21781ffc2fb_EOF' - {"add_comment":{"max":1,"target":"*"},"add_labels":{"allowed":["enhancement"],"max":1,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_a36bb21781ffc2fb_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"enhancement\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -598,9 +622,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -608,6 +641,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -704,9 +747,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -715,38 +760,51 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -782,6 +840,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -793,7 +859,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -801,25 +867,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF + GH_AW_MCP_CONFIG_137b94c134aafdc9_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -831,22 +904,40 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool write timeout-minutes: 5 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\",\"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\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -864,14 +955,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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-tool github --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -879,7 +975,7 @@ jobs: 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_TIMEOUT_MINUTES: 5 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -900,7 +996,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 5 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -916,7 +1023,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -925,9 +1032,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -950,14 +1059,16 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -967,9 +1078,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -977,9 +1090,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -993,9 +1108,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1003,16 +1120,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1031,6 +1173,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1050,10 +1194,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1070,7 +1214,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1079,8 +1223,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Enhancement Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-enhancement.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1088,7 +1232,8 @@ jobs: continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1096,42 +1241,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + pattern: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1145,6 +1277,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1167,9 +1301,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1199,7 +1335,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-enhancement.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" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1207,9 +1343,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1224,9 +1362,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1239,9 +1379,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1254,9 +1396,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1269,7 +1413,7 @@ jobs: 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: "handle-enhancement" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1283,6 +1427,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} 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_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1298,9 +1446,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "Enhancement Handler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-enhancement.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1312,6 +1481,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1322,7 +1492,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1331,16 +1501,23 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Enhancement Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-enhancement.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.agent.outputs.artifact_prefix }}agent + pattern: "{${{ needs.agent.outputs.artifact_prefix }}agent,${{ needs.agent.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1348,10 +1525,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1360,7 +1539,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1384,21 +1563,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1406,82 +1571,52 @@ jobs: WORKFLOW_NAME: "Enhancement Handler" WORKFLOW_DESCRIPTION: "Handles issues classified as enhancements by the triage classifier" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1497,58 +1632,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Enhancement Handler" + WORKFLOW_DESCRIPTION: "Handles issues classified as enhancements by the triage classifier" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ needs.agent.outputs.artifact_prefix }}detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1558,7 +1740,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1572,7 +1753,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-enhancement" @@ -1585,12 +1765,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1599,16 +1787,19 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Enhancement Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-enhancement.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1616,7 +1807,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1632,16 +1825,18 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"enhancement\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"enhancement\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1651,4 +1846,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/handle-enhancement.md b/.github/workflows/handle-enhancement.md index 9043c181c1..b7c06c1ba9 100644 --- a/.github/workflows/handle-enhancement.md +++ b/.github/workflows/handle-enhancement.md @@ -18,6 +18,8 @@ permissions: pull-requests: read copilot-requests: write tools: + bash: [] + cli-proxy: false github: toolsets: [default] min-integrity: none @@ -34,4 +36,4 @@ timeout-minutes: 5 # Enhancement Handler -Add the `enhancement` label to issue #${{ inputs.issue_number }}. +Add the `enhancement` label to issue #${{ inputs.issue_number }}. \ No newline at end of file diff --git a/.github/workflows/handle-question.lock.yml b/.github/workflows/handle-question.lock.yml index 0093edce0e..2c319aa805 100644 --- a/.github/workflows/handle-question.lock.yml +++ b/.github/workflows/handle-question.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"92158ba4f7e373cb65457b060c7645569b32c756c13c025837491f6809cf694f","body_hash":"1bdd19aae2095beb6e3fcf7af755cd102d424de3a8727ef6e4674815950c7e8b","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"d091d519856c3b08a2355cc9d035c3a6e249aa088c107cdfb5b00e8e9aea67bc","body_hash":"1bdd19aae2095beb6e3fcf7af755cd102d424de3a8727ef6e4674815950c7e8b","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Handles issues classified as questions by the triage classifier # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Question Handler" on: @@ -74,7 +72,7 @@ on: description: URL of the first added comment value: ${{ jobs.safe_outputs.outputs.comment_url }} secrets: - COPILOT_GITHUB_TOKEN: + GH_AW_DEFAULT_OTLP_HEADERS: required: false GH_AW_GITHUB_MCP_SERVER_TOKEN: required: false @@ -85,9 +83,18 @@ permissions: {} concurrency: group: "gh-aw-handle-question-${{ github.run_id }}" + queue: max run-name: "Question Handler" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.handle-question + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Question%20Handler,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -102,6 +109,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -119,7 +127,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -127,10 +135,12 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Question Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-question.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Resolve host repo for activation checkout id: resolve-host-repo uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -141,9 +151,11 @@ jobs: JOB_WORKFLOW_FILE_PATH: ${{ job.workflow_file_path }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/resolve_host_repo.cjs'); + const { main } = require(path.join(actionsDir, 'resolve_host_repo.cjs')); await main(); - name: Compute artifact prefix id: artifact-prefix @@ -155,27 +167,30 @@ jobs: env: GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_INFO_ENGINE_NAME: "GitHub Copilot CLI" - GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Question Handler" 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.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" GH_AW_COMPILED_STRICT: "true" GH_AW_INFO_TARGET_REPO: ${{ steps.resolve-host-repo.outputs.target_repo }} uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -197,9 +212,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -217,15 +234,16 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Print cross-repo setup guidance @@ -236,7 +254,7 @@ jobs: echo "::error::See: https://github.github.com/gh-aw/patterns/central-repo-ops/#cross-repo-setup" - name: Checkout .github and .agents folders if: steps.resolve-host-repo.outputs.target_repo == github.repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false repository: ${{ steps.resolve-host-repo.outputs.target_repo }} @@ -244,20 +262,18 @@ jobs: sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -266,27 +282,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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) }} @@ -296,75 +319,36 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_EOF' - - GH_AW_PROMPT_a6cfd5b92b97c528_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_a6cfd5b92b97c528_EOF' - - Tools: add_comment, add_labels, missing_tool, missing_data, noop - - GH_AW_PROMPT_a6cfd5b92b97c528_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_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_a6cfd5b92b97c528_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_a6cfd5b92b97c528_EOF' - - {{#runtime-import .github/workflows/handle-question.md}} - GH_AW_PROMPT_a6cfd5b92b97c528_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, add_labels, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/handle-question.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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) }} @@ -374,13 +358,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -394,22 +379,25 @@ jobs: 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_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER } }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ steps.artifact-prefix.outputs.prefix }}activation @@ -439,12 +427,19 @@ jobs: concurrency: group: "gh-aw-copilot-handle-question-${{ inputs.issue_number }}" queue: max + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: handlequestion outputs: @@ -459,7 +454,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -467,11 +465,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -480,20 +479,29 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Question Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-question.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -523,16 +531,19 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -542,7 +553,9 @@ jobs: GH_AW_GITHUB_MIN_INTEGRITY: 'none' with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars @@ -554,8 +567,8 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -567,15 +580,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_564a988b74c338ef_EOF' - {"add_comment":{"max":1,"target":"*"},"add_labels":{"allowed":["question"],"max":1,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_564a988b74c338ef_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"question\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -598,9 +622,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -608,6 +641,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -704,9 +747,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -715,38 +760,51 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -782,6 +840,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -793,7 +859,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -801,25 +867,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF + GH_AW_MCP_CONFIG_137b94c134aafdc9_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -831,22 +904,40 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool write timeout-minutes: 5 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\",\"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\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -864,14 +955,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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-tool github --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -879,7 +975,7 @@ jobs: 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_TIMEOUT_MINUTES: 5 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -900,7 +996,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 5 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -916,7 +1023,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -925,9 +1032,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -950,14 +1059,16 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -967,9 +1078,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -977,9 +1090,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -993,9 +1108,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1003,16 +1120,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1031,6 +1173,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1050,10 +1194,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1070,7 +1214,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1079,8 +1223,8 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Question Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-question.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} - name: Download agent output artifact @@ -1088,7 +1232,8 @@ jobs: continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1096,42 +1241,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + pattern: ${{ needs.activation.outputs.artifact_prefix }}safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1145,6 +1277,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1167,9 +1301,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1199,7 +1335,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-question.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" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1207,9 +1343,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1224,9 +1362,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1239,9 +1379,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1254,9 +1396,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1269,7 +1413,7 @@ jobs: 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: "handle-question" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1283,6 +1427,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} 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_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1298,9 +1446,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "Question Handler" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/handle-question.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1312,6 +1481,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1322,7 +1492,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1331,16 +1501,23 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Question Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-question.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.activation.outputs.artifact_prefix }}activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.agent.outputs.artifact_prefix }}agent + pattern: "{${{ needs.agent.outputs.artifact_prefix }}agent,${{ needs.agent.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1348,10 +1525,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1360,7 +1539,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1384,21 +1563,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1406,82 +1571,52 @@ jobs: WORKFLOW_NAME: "Question Handler" WORKFLOW_DESCRIPTION: "Handles issues classified as questions by the triage classifier" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1497,58 +1632,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Question Handler" + WORKFLOW_DESCRIPTION: "Handles issues classified as questions by the triage classifier" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ needs.agent.outputs.artifact_prefix }}detection - path: /tmp/gh-aw/threat-detection/detection.log + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1558,7 +1740,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1572,7 +1753,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "handle-question" @@ -1585,12 +1765,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1599,16 +1787,19 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Question Handler" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/handle-question.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" GH_AW_SETUP_AW_CONTEXT: ${{ inputs.aw_context }} + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ needs.activation.outputs.artifact_prefix }}agent + pattern: "{${{ needs.activation.outputs.artifact_prefix }}agent,${{ needs.activation.outputs.artifact_prefix }}agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1616,7 +1807,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1632,16 +1825,18 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"question\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"*\"},\"add_labels\":{\"allowed\":[\"question\"],\"max\":1,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1651,4 +1846,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/handle-question.md b/.github/workflows/handle-question.md index 21a10d468a..eb40378636 100644 --- a/.github/workflows/handle-question.md +++ b/.github/workflows/handle-question.md @@ -18,6 +18,8 @@ permissions: pull-requests: read copilot-requests: write tools: + bash: [] + cli-proxy: false github: toolsets: [default] min-integrity: none @@ -34,4 +36,4 @@ timeout-minutes: 5 # Question Handler -Add the `question` label to issue #${{ inputs.issue_number }}. +Add the `question` label to issue #${{ inputs.issue_number }}. \ No newline at end of file diff --git a/.github/workflows/issue-classification.lock.yml b/.github/workflows/issue-classification.lock.yml index 041fb94b73..c0020170f3 100644 --- a/.github/workflows/issue-classification.lock.yml +++ b/.github/workflows/issue-classification.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"797f7487a67c2fa4465cb3fd31e17c9f0620bb232b2a4fb693c62cb76d5d5a36","body_hash":"8e7ac9b7bb6ab07630a10a4a016108ba59f70feadf82a7391ca0ba5504e14bff","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c68d65ddf05c945729544eaf3436f32818047cc55a9f8699db993f5b3db744bc","body_hash":"8e7ac9b7bb6ab07630a10a4a016108ba59f70feadf82a7391ca0ba5504e14bff","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","call_workflow","handle_bug","handle_documentation","handle_enhancement","handle_question","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Classifies newly opened issues and delegates to type-specific handler workflows # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Issue Classification Agent" on: @@ -73,9 +71,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}" + queue: max run-name: "Issue Classification Agent" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.issue-classification + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Issue%20Classification%20Agent,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -90,6 +97,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -105,7 +113,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -113,34 +121,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-classification.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Issue Classification Agent" 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.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -162,9 +175,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -182,38 +197,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -222,38 +236,47 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Compute current body text id: sanitized uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + const { main } = require(path.join(actionsDir, 'compute_text.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -265,63 +288,22 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_2b9644ded951c90e_EOF' - - GH_AW_PROMPT_2b9644ded951c90e_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_2b9644ded951c90e_EOF' - - Tools: add_comment, call_workflow, missing_tool, missing_data, noop - - GH_AW_PROMPT_2b9644ded951c90e_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_2b9644ded951c90e_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_2b9644ded951c90e_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_2b9644ded951c90e_EOF' - - {{#runtime-import .github/workflows/issue-classification.md}} - GH_AW_PROMPT_2b9644ded951c90e_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, handle_bug, handle_enhancement, handle_question, handle_documentation, missing_tool, missing_data, noop\nShared budgets: call-workflow [handle_bug, handle_enhancement, handle_question, handle_documentation](max:1 total)\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/issue-classification.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} GH_AW_GITHUB_EVENT_ISSUE_TITLE: ${{ github.event.issue.title }} @@ -329,14 +311,16 @@ jobs: GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -348,13 +332,14 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -370,22 +355,25 @@ jobs: 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_WORKSPACE: process.env.GH_AW_GITHUB_WORKSPACE, - GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER, - GH_AW_MCP_CLI_SERVERS_LIST: process.env.GH_AW_MCP_CLI_SERVERS_LIST + GH_AW_INPUTS_ISSUE_NUMBER: process.env.GH_AW_INPUTS_ISSUE_NUMBER } }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -412,12 +400,19 @@ jobs: copilot-requests: write issues: read pull-requests: read + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: issueclassification outputs: @@ -431,7 +426,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -439,11 +437,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -452,19 +451,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-classification.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -494,16 +502,19 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -513,7 +524,9 @@ jobs: GH_AW_GITHUB_MIN_INTEGRITY: 'none' with: script: | - const determineAutomaticLockdown = require('${{ runner.temp }}/gh-aw/actions/determine_automatic_lockdown.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Parse integrity filter lists id: parse-guard-vars @@ -525,8 +538,8 @@ jobs: - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -538,15 +551,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_3e3303377640f8c6_EOF' - {"add_comment":{"max":1,"target":"triggering"},"call_workflow":{"max":1,"workflow_files":{"handle-bug":"./.github/workflows/handle-bug.lock.yml","handle-documentation":"./.github/workflows/handle-documentation.lock.yml","handle-enhancement":"./.github/workflows/handle-enhancement.lock.yml","handle-question":"./.github/workflows/handle-question.lock.yml"},"workflows":["handle-bug","handle-enhancement","handle-question","handle-documentation"]},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_3e3303377640f8c6_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"triggering\"},\"call_workflow\":{\"max\":1,\"workflow_files\":{\"handle-bug\":\"./.github/workflows/handle-bug.lock.yml\",\"handle-documentation\":\"./.github/workflows/handle-documentation.lock.yml\",\"handle-enhancement\":\"./.github/workflows/handle-enhancement.lock.yml\",\"handle-question\":\"./.github/workflows/handle-question.lock.yml\"},\"workflows\":[\"handle-bug\",\"handle-enhancement\",\"handle-question\",\"handle-documentation\"]},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -677,9 +701,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -687,6 +720,33 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" + } + } + }, + "call_workflow": { + "defaultMax": 1, + "fields": { + "inputs": { + "type": "object" + }, + "workflow_name": { + "required": true, + "type": "string", + "sanitize": true, + "maxLength": 256, + "minLength": 1, + "pattern": ".*\\S.*", + "patternError": "must not be empty" } } }, @@ -767,9 +827,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -778,38 +840,51 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN || secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_137b94c134aafdc9_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -845,6 +920,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -856,7 +939,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -864,25 +947,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_9424afbf8aa8e8ef_EOF + GH_AW_MCP_CONFIG_137b94c134aafdc9_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -894,22 +984,40 @@ jobs: - name: Execute GitHub Copilot CLI id: agentic_execution # Copilot CLI tool arguments (sorted): + # --allow-tool github + # --allow-tool safeoutputs + # --allow-tool write timeout-minutes: 10 run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\",\"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\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -927,14 +1035,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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-tool github --allow-tool safeoutputs --allow-tool write --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -942,7 +1055,7 @@ jobs: 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_TIMEOUT_MINUTES: 10 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -963,7 +1076,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 10 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -979,7 +1103,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -988,9 +1112,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -1013,14 +1139,16 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -1030,9 +1158,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -1040,9 +1170,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -1056,9 +1188,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1066,16 +1200,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1094,6 +1253,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1121,7 +1282,7 @@ jobs: issue_number: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).issue_number }} payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }} secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_DEFAULT_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} @@ -1142,7 +1303,7 @@ jobs: issue_number: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).issue_number }} payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }} secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_DEFAULT_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} @@ -1163,7 +1324,7 @@ jobs: issue_number: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).issue_number }} payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }} secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_DEFAULT_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} @@ -1184,7 +1345,7 @@ jobs: issue_number: ${{ fromJSON(needs.safe_outputs.outputs.call_workflow_payload).issue_number }} payload: ${{ needs.safe_outputs.outputs.call_workflow_payload }} secrets: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + GH_AW_DEFAULT_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} @@ -1201,10 +1362,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1221,7 +1382,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1230,15 +1391,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-classification.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" 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 + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1246,42 +1408,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1295,6 +1444,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1317,9 +1468,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1349,7 +1502,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-classification.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" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1357,9 +1510,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1374,9 +1529,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1389,9 +1546,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1404,9 +1563,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1419,7 +1580,7 @@ jobs: 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: "issue-classification" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1433,6 +1594,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} 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_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1448,9 +1613,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "Issue Classification Agent" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-classification.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1462,6 +1648,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1472,7 +1659,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1481,15 +1668,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-classification.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1497,10 +1691,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1509,7 +1705,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1533,21 +1729,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1555,82 +1737,52 @@ jobs: WORKFLOW_NAME: "Issue Classification Agent" WORKFLOW_DESCRIPTION: "Classifies newly opened issues and delegates to type-specific handler workflows" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1646,58 +1798,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Issue Classification Agent" + WORKFLOW_DESCRIPTION: "Classifies newly opened issues and delegates to type-specific handler workflows" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact 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 + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1707,7 +1906,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1721,7 +1919,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "issue-classification" @@ -1736,12 +1933,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1750,15 +1955,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Classification Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-classification.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1766,7 +1974,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1782,16 +1992,18 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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: "{\"add_comment\":{\"max\":1,\"target\":\"triggering\"},\"call_workflow\":{\"max\":1,\"workflow_files\":{\"handle-bug\":\"./.github/workflows/handle-bug.lock.yml\",\"handle-documentation\":\"./.github/workflows/handle-documentation.lock.yml\",\"handle-enhancement\":\"./.github/workflows/handle-enhancement.lock.yml\",\"handle-question\":\"./.github/workflows/handle-question.lock.yml\"},\"workflows\":[\"handle-bug\",\"handle-enhancement\",\"handle-question\",\"handle-documentation\"]},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":1,\"target\":\"triggering\"},\"call_workflow\":{\"max\":1,\"workflow_files\":{\"handle-bug\":\"./.github/workflows/handle-bug.lock.yml\",\"handle-documentation\":\"./.github/workflows/handle-documentation.lock.yml\",\"handle-enhancement\":\"./.github/workflows/handle-enhancement.lock.yml\",\"handle-question\":\"./.github/workflows/handle-question.lock.yml\"},\"workflows\":[\"handle-bug\",\"handle-enhancement\",\"handle-question\",\"handle-documentation\"]},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1801,4 +2013,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/issue-classification.md b/.github/workflows/issue-classification.md index b1e3345f91..8b111cd1db 100644 --- a/.github/workflows/issue-classification.md +++ b/.github/workflows/issue-classification.md @@ -16,6 +16,8 @@ permissions: pull-requests: read copilot-requests: write tools: + bash: [] + cli-proxy: false github: toolsets: [default] min-integrity: none @@ -123,4 +125,4 @@ Common areas of issues: - Issue number: ${{ github.event.issue.number || inputs.issue_number }} - Issue title: ${{ github.event.issue.title }} -Use the GitHub tools to fetch the full issue details, especially when triggered manually via `workflow_dispatch`. +Use the GitHub tools to fetch the full issue details, especially when triggered manually via `workflow_dispatch`. \ No newline at end of file diff --git a/.github/workflows/issue-triage.lock.yml b/.github/workflows/issue-triage.lock.yml index e24584f26d..e6b1410607 100644 --- a/.github/workflows/issue-triage.lock.yml +++ b/.github/workflows/issue-triage.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b343e48e59d56a2461bafa8c08f4f37d4242a56fa662b83c2f0cad16262682e5","body_hash":"30994be7c5c23b102c12a56a325ac313e413a2507dff11d0dc695899379bfbd0","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b343e48e59d56a2461bafa8c08f4f37d4242a56fa662b83c2f0cad16262682e5","body_hash":"30994be7c5c23b102c12a56a325ac313e413a2507dff11d0dc695899379bfbd0","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","add_labels","close_issue","missing_data","missing_tool","noop","update_issue"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Triages newly opened issues by labeling, acknowledging, requesting clarification, and closing duplicates # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Issue Triage Agent" on: @@ -73,9 +71,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.event.issue.number || github.run_id }}" + queue: max run-name: "Issue Triage Agent" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.issue-triage + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Issue%20Triage%20Agent,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -90,6 +97,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -105,7 +113,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -113,34 +121,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Issue Triage Agent" 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.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -162,9 +175,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -182,38 +197,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -222,38 +236,47 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Compute current body text id: sanitized uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + const { main } = require(path.join(actionsDir, 'compute_text.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -265,63 +288,22 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_39930e94844c6d8f_EOF' - - GH_AW_PROMPT_39930e94844c6d8f_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_39930e94844c6d8f_EOF' - - Tools: add_comment(max:2), close_issue, update_issue, add_labels(max:10), missing_tool, missing_data, noop - - GH_AW_PROMPT_39930e94844c6d8f_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_39930e94844c6d8f_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_39930e94844c6d8f_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_39930e94844c6d8f_EOF' - - {{#runtime-import .github/workflows/issue-triage.md}} - GH_AW_PROMPT_39930e94844c6d8f_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment(max:2), close_issue, update_issue, add_labels(max:10), missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/issue-triage.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_EXPR_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} GH_AW_GITHUB_EVENT_ISSUE_TITLE: ${{ github.event.issue.title }} @@ -329,14 +311,16 @@ jobs: GH_AW_INPUTS_ISSUE_NUMBER: ${{ inputs.issue_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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_54492A5B: ${{ github.event.issue.number || inputs.issue_number }} @@ -351,10 +335,12 @@ jobs: GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -376,16 +362,20 @@ jobs: }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -412,12 +402,19 @@ jobs: copilot-requests: write issues: read pull-requests: read + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: issuetriage outputs: @@ -431,7 +428,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -439,11 +439,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -452,19 +453,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -494,16 +504,19 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -512,13 +525,15 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -530,15 +545,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_ee19492f88e4cc0b_EOF' - {"add_comment":{"max":2},"add_labels":{"allowed":["bug","enhancement","question","documentation","sdk/dotnet","sdk/go","sdk/java","sdk/nodejs","sdk/python","priority/high","priority/low","testing","security","needs-info","duplicate"],"issue_intent":true,"max":10,"target":"triggering"},"close_issue":{"issue_intent":true,"max":1,"target":"triggering"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"update_issue":{"allow_body":true,"max":1,"target":"triggering"}} - GH_AW_SAFE_OUTPUTS_CONFIG_ee19492f88e4cc0b_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\",\"sdk/dotnet\",\"sdk/go\",\"sdk/java\",\"sdk/nodejs\",\"sdk/python\",\"priority/high\",\"priority/low\",\"testing\",\"security\",\"needs-info\",\"duplicate\"],\"issue_intent\":true,\"max\":10,\"target\":\"triggering\"},\"close_issue\":{\"issue_intent\":true,\"max\":1,\"target\":\"triggering\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"triggering\"}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -582,9 +608,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -592,6 +627,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -628,6 +673,9 @@ jobs: ], "x-strip-on-error": true }, + "duplicate_of": { + "issueOrPRNumber": true + }, "issue_number": { "optionalPositiveInteger": true }, @@ -774,9 +822,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -785,6 +835,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} 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 }} @@ -792,33 +843,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_46604863f3d8e286_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -851,6 +914,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -862,7 +933,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -870,25 +941,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF + GH_AW_MCP_CONFIG_46604863f3d8e286_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -904,18 +982,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\",\"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\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -933,14 +1026,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -948,7 +1046,7 @@ jobs: 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_TIMEOUT_MINUTES: 10 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -969,7 +1067,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 10 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -985,7 +1094,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -994,9 +1103,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -1019,14 +1130,16 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -1036,9 +1149,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -1046,9 +1161,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -1062,9 +1179,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -1072,16 +1191,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1098,6 +1242,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1117,10 +1263,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1137,7 +1283,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1146,15 +1292,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" 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 + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1162,42 +1309,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1211,6 +1345,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1233,9 +1369,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1265,7 +1403,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.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" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1273,9 +1411,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1290,9 +1430,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1305,9 +1447,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1320,9 +1464,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1335,7 +1481,7 @@ jobs: 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: "issue-triage" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1349,6 +1495,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} 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_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1364,9 +1514,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "Issue Triage Agent" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/issue-triage.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1378,6 +1549,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1388,7 +1560,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1397,15 +1569,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1413,10 +1592,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1425,7 +1606,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1449,21 +1630,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1471,82 +1638,52 @@ jobs: WORKFLOW_NAME: "Issue Triage Agent" WORKFLOW_DESCRIPTION: "Triages newly opened issues by labeling, acknowledging, requesting clarification, and closing duplicates" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1562,58 +1699,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Issue Triage Agent" + WORKFLOW_DESCRIPTION: "Triages newly opened issues by labeling, acknowledging, requesting clarification, and closing duplicates" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact 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 + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1623,7 +1807,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1637,7 +1820,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "issue-triage" @@ -1650,12 +1832,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1664,15 +1854,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Issue Triage Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/issue-triage.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1680,7 +1873,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1696,16 +1891,18 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\",\"sdk/dotnet\",\"sdk/go\",\"sdk/java\",\"sdk/nodejs\",\"sdk/python\",\"priority/high\",\"priority/low\",\"testing\",\"security\",\"needs-info\",\"duplicate\"],\"issue_intent\":true,\"max\":10,\"target\":\"triggering\"},\"close_issue\":{\"issue_intent\":true,\"max\":1,\"target\":\"triggering\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"triggering\"}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":2},\"add_labels\":{\"allowed\":[\"bug\",\"enhancement\",\"question\",\"documentation\",\"sdk/dotnet\",\"sdk/go\",\"sdk/java\",\"sdk/nodejs\",\"sdk/python\",\"priority/high\",\"priority/low\",\"testing\",\"security\",\"needs-info\",\"duplicate\"],\"issue_intent\":true,\"max\":10,\"target\":\"triggering\"},\"close_issue\":{\"issue_intent\":true,\"max\":1,\"target\":\"triggering\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_issue\":{\"allow_body\":true,\"max\":1,\"target\":\"triggering\"}}" with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1715,4 +1912,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml b/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml index e94e0775c0..7554b0bb76 100644 --- a/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml +++ b/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a5f19a89f89b0693f86ca89ea90e3a633fe19c17bf4d27214fa9124429cdc156","body_hash":"8db09798070cbcba22c42c50a316ae45c8e8c650eeb23c556b44fde8d519550a","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"03beaef805d9bdf4b878b9fd1dd47c62793f30f179373406e081b88a9817b182","body_hash":"f535cb24328c6e9b3963e72de09404b08a4f4580a8174bc6ad580c0f005837ae","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_release_by_tag","get_tag","list_branches","list_commits","list_releases","list_starred_repositories","list_tags","search_code","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","missing_data","missing_tool","noop","push_to_pull_request_branch"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -24,12 +24,12 @@ # For more information: https://github.github.com/gh-aw/introduction/overview/ # # Adapt handwritten Java SDK code to work with regenerated types after a -# @github/copilot version bump. Assumes codegen succeeded and generated code +# Copilot CLI release update. Assumes codegen succeeded and generated code # compiles. Fixes handwritten source and tests only. # # Secrets used: -# - COPILOT_GITHUB_TOKEN # - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -37,22 +37,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Java Handwritten Code Adaptation After CLI Upgrade" on: @@ -76,9 +74,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.ref || github.run_id }}" + queue: max run-name: "Java Handwritten Code Adaptation After CLI Upgrade" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.java-adapt-handwritten-code-to-accept-upgrade-changes + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Java%20Handwritten%20Code%20Adaptation%20After%20CLI%20Upgrade,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -92,6 +99,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -105,7 +113,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -113,34 +121,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -162,9 +175,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -182,38 +197,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -222,27 +236,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"file\":\"safe_outputs_push_to_pr_branch.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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) }} @@ -253,79 +274,37 @@ jobs: GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_BRANCH: ${{ inputs.branch }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' - - GH_AW_PROMPT_67432b380d9d8ebb_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_67432b380d9d8ebb_EOF' - - Tools: add_comment(max:10), push_to_pull_request_branch, missing_tool, missing_data, noop - GH_AW_PROMPT_67432b380d9d8ebb_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' - - GH_AW_PROMPT_67432b380d9d8ebb_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_67432b380d9d8ebb_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_67432b380d9d8ebb_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_67432b380d9d8ebb_EOF' - - {{#runtime-import .github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md}} - GH_AW_PROMPT_67432b380d9d8ebb_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment(max:10), push_to_pull_request_branch, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_BRANCH: ${{ inputs.branch }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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) }} @@ -339,10 +318,12 @@ jobs: GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -363,16 +344,20 @@ jobs: }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -398,12 +383,19 @@ jobs: actions: read contents: read copilot-requests: write + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: javaadapthandwrittencodetoacceptupgradechanges outputs: @@ -417,7 +409,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -425,11 +420,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -438,19 +434,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -480,16 +485,19 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -498,13 +506,15 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -516,15 +526,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_fcd407b1cd819e9a_EOF' - {"add_comment":{"max":10,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"if_no_changes":"warn","max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"required_labels":["dependencies","sdk/java"],"target":"*"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_fcd407b1cd819e9a_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"required_labels\":[\"dependencies\",\"sdk/java\"],\"target\":\"*\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -546,9 +567,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -556,6 +586,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -632,6 +672,10 @@ jobs: }, "pull_request_number": { "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 } } }, @@ -655,9 +699,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -666,6 +712,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} 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 }} @@ -673,33 +720,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_58d53a00b5a25078_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_33dc71f92a389606_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -732,6 +791,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -743,7 +810,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -751,25 +818,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_58d53a00b5a25078_EOF + GH_AW_MCP_CONFIG_33dc71f92a389606_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -785,18 +859,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"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\",\"codeload.github.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\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.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\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"codeload.github.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\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.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\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -814,14 +903,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -829,7 +923,7 @@ jobs: 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_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -850,7 +944,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 60 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -866,7 +971,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -875,9 +980,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -900,14 +1007,16 @@ jobs: 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: "*.githubusercontent.com,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,codeload.github.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.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,patch-diff.githubusercontent.com,patchdiff.githubusercontent.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" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.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,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,s.symcb.com,s.symcd.com,security.ubuntu.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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -917,9 +1026,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -927,9 +1038,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -943,9 +1056,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -953,16 +1068,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -979,6 +1119,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -998,9 +1140,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: + actions: read contents: write issues: write pull-requests: write @@ -1018,7 +1161,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1027,15 +1170,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" 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 + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1043,42 +1187,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1092,6 +1223,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1114,9 +1247,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1154,9 +1289,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1171,9 +1308,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1186,9 +1325,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1201,9 +1342,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1216,7 +1359,7 @@ jobs: 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: "java-adapt-handwritten-code-to-accept-upgrade-changes" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1230,6 +1373,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} @@ -1247,9 +1394,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "Java Handwritten Code Adaptation After CLI Upgrade" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1261,6 +1429,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1271,7 +1440,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1280,15 +1449,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1296,10 +1472,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1308,7 +1486,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1332,104 +1510,60 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" - WORKFLOW_DESCRIPTION: "Adapt handwritten Java SDK code to work with regenerated types after a\n@github/copilot version bump. Assumes codegen succeeded and generated code\ncompiles. Fixes handwritten source and tests only." + WORKFLOW_DESCRIPTION: "Adapt handwritten Java SDK code to work with regenerated types after a\nCopilot CLI release update. Assumes codegen succeeded and generated code\ncompiles. Fixes handwritten source and tests only." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1445,58 +1579,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" + WORKFLOW_DESCRIPTION: "Adapt handwritten Java SDK code to work with regenerated types after a\nCopilot CLI release update. Assumes codegen succeeded and generated code\ncompiles. Fixes handwritten source and tests only." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact 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 + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1520,7 +1701,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "java-adapt-handwritten-code-to-accept-upgrade-changes" @@ -1533,14 +1713,22 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1549,15 +1737,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Handwritten Code Adaptation After CLI Upgrade" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1565,7 +1756,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Download patch artifact continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -1574,7 +1767,7 @@ jobs: path: /tmp/gh-aw/ - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1600,17 +1793,19 @@ jobs: 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: "*.githubusercontent.com,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,codeload.github.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.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,patch-diff.githubusercontent.com,patchdiff.githubusercontent.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" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.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,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,s.symcb.com,s.symcd.com,security.ubuntu.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: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"required_labels\":[\"dependencies\",\"sdk/java\"],\"target\":\"*\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":10,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"required_labels\":[\"dependencies\",\"sdk/java\"],\"target\":\"*\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1620,4 +1815,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md b/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md index dd1bfe2bbc..91f11d1f86 100644 --- a/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md +++ b/.github/workflows/java-adapt-handwritten-code-to-accept-upgrade-changes.md @@ -1,7 +1,7 @@ --- description: | Adapt handwritten Java SDK code to work with regenerated types after a - @github/copilot version bump. Assumes codegen succeeded and generated code + Copilot CLI release update. Assumes codegen succeeded and generated code compiles. Fixes handwritten source and tests only. on: @@ -45,14 +45,13 @@ safe-outputs: # Java Handwritten Code Adaptation After CLI Upgrade -You are an automation agent that fixes handwritten Java SDK source and test code after a `@github/copilot` version bump has regenerated the typed schemas. +You are an automation agent that fixes handwritten Java SDK source and test code after a Copilot CLI release update has regenerated the typed schemas. ## Assumptions - The branch `${{ inputs.branch }}` already has: - - Updated `java/scripts/codegen/package.json` with the new version + - Updated the shared CLI release pin in `nodejs/package.json` - Regenerated `java/sdk/src/generated/java/` code that compiles successfully - - Updated the Java POM CLI/version pin property - Your job is ONLY to fix **handwritten** code, NOT generated code. ## Boundaries @@ -147,7 +146,7 @@ If this passes, commit and push: ```bash git add java/sdk/src/main/java java/sdk/src/test/java -git commit -m "Fix handwritten Java code for @github/copilot schema changes +git commit -m "Fix handwritten Java code for CLI schema changes Adapt constructor calls, enum references, and test assertions to match regenerated types after CLI version bump." diff --git a/.github/workflows/java-codegen-check.yml b/.github/workflows/java-codegen-check.yml index f2f4527966..e490a5cf4e 100644 --- a/.github/workflows/java-codegen-check.yml +++ b/.github/workflows/java-codegen-check.yml @@ -5,11 +5,13 @@ on: branches: - main paths: + - 'nodejs/package.json' - 'java/scripts/codegen/**' - 'java/sdk/src/generated/**' - '.github/workflows/java-codegen-check.yml' pull_request: paths: + - 'nodejs/package.json' - 'java/scripts/codegen/**' - 'java/sdk/src/generated/**' - '.github/workflows/java-codegen-check.yml' @@ -48,9 +50,13 @@ jobs: working-directory: ./java/scripts/codegen run: npm ci + - name: Test schema fetcher + working-directory: ./java/scripts/codegen + run: npm test + - name: Run codegen working-directory: ./java/scripts/codegen - run: npx tsx java.ts + run: npm run generate - name: Check for uncommitted changes id: check-changes @@ -68,7 +74,7 @@ jobs: - name: Fail on stale generated files (push to main) if: steps.check-changes.outputs.changed == 'true' && github.event_name != 'pull_request' run: | - echo "::error::Generated files are out of date. Run 'cd java/scripts/codegen && npx tsx java.ts' and commit the changes." + echo "::error::Generated files are out of date. Run 'cd java/scripts/codegen && npm run generate' and commit the changes." git diff exit 1 @@ -93,7 +99,7 @@ jobs: if: steps.push-regen.outcome == 'failure' run: | echo "::error::Could not push regenerated files to the PR branch. This is expected for Dependabot PRs (read-only token) and fork PRs." - echo "To fix: check out this PR branch locally, run 'cd java/scripts/codegen && npx tsx java.ts', commit, and push." + echo "To fix: check out this PR branch locally, run 'cd java/scripts/codegen && npm run generate', commit, and push." exit 1 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5 diff --git a/.github/workflows/java-codegen-fix.lock.yml b/.github/workflows/java-codegen-fix.lock.yml index 1d8d28c439..fa64a0fe88 100644 --- a/.github/workflows/java-codegen-fix.lock.yml +++ b/.github/workflows/java-codegen-fix.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b0390c9ab9beb0d7e106314299e89e486269ab7f64d8489d5021132d79aa6b9b","body_hash":"63d6ce13a5131b158ddffb10a469aa59e0fdc2278eec4d8de7f6763e0b6f2ea2","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"b0390c9ab9beb0d7e106314299e89e486269ab7f64d8489d5021132d79aa6b9b","body_hash":"c7cc7984b1d512e871371a7fd99bc4f7e4615d4192348170eca763cf584f4855","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_release_by_tag","get_tag","list_branches","list_commits","list_releases","list_starred_repositories","list_tags","search_code","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","missing_data","missing_tool","noop","push_to_pull_request_branch"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -27,8 +27,8 @@ # mvn verify fails after code generation changes. # # Secrets used: -# - COPILOT_GITHUB_TOKEN # - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -36,22 +36,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Java Codegen Agentic Fix" on: @@ -79,9 +77,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}-${{ github.ref || github.run_id }}" + queue: max run-name: "Java Codegen Agentic Fix" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.java-codegen-fix + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Java%20Codegen%20Agentic%20Fix,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -95,6 +102,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -108,7 +116,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -116,34 +124,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-codegen-fix.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_INFO_EXPERIMENTAL: "false" GH_AW_INFO_SUPPORTS_TOOLS_ALLOWLIST: "true" GH_AW_INFO_STAGED: "false" GH_AW_INFO_ALLOWED_DOMAINS: '["defaults","github"]' GH_AW_INFO_FIREWALL_ENABLED: "true" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -165,9 +178,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -185,38 +200,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -225,27 +239,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"file\":\"safe_outputs_push_to_pr_branch.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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) }} @@ -257,80 +278,38 @@ jobs: GH_AW_INPUTS_BRANCH: ${{ inputs.branch }} GH_AW_INPUTS_ERROR_SUMMARY: ${{ inputs.error_summary }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' - - GH_AW_PROMPT_7834a0b5f08e9149_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_7834a0b5f08e9149_EOF' - - Tools: add_comment(max:5), push_to_pull_request_branch, missing_tool, missing_data, noop - GH_AW_PROMPT_7834a0b5f08e9149_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_push_to_pr_branch.md" - cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' - - GH_AW_PROMPT_7834a0b5f08e9149_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_7834a0b5f08e9149_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_7834a0b5f08e9149_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_7834a0b5f08e9149_EOF' - - {{#runtime-import .github/workflows/java-codegen-fix.md}} - GH_AW_PROMPT_7834a0b5f08e9149_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment(max:5), push_to_pull_request_branch, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/java-codegen-fix.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_INPUTS_BRANCH: ${{ inputs.branch }} GH_AW_INPUTS_ERROR_SUMMARY: ${{ inputs.error_summary }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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) }} @@ -345,10 +324,12 @@ jobs: GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -370,16 +351,20 @@ jobs: }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -405,12 +390,19 @@ jobs: actions: read contents: read copilot-requests: write + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: javacodegenfix outputs: @@ -424,7 +416,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -432,11 +427,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -445,19 +441,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-codegen-fix.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -487,16 +492,19 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -505,13 +513,15 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -523,15 +533,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_cf285131e299ca5f_EOF' - {"add_comment":{"max":5,"target":"*"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"false"},"push_to_pull_request_branch":{"if_no_changes":"warn","max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"required_labels":["dependencies"],"target":"*"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_cf285131e299ca5f_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"required_labels\":[\"dependencies\"],\"target\":\"*\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -553,9 +574,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -563,6 +593,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -639,6 +679,10 @@ jobs: }, "pull_request_number": { "issueOrPRNumber": true + }, + "repo": { + "type": "string", + "maxLength": 256 } } }, @@ -662,9 +706,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -673,6 +719,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} 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 }} @@ -680,33 +727,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_58d53a00b5a25078_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_33dc71f92a389606_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -739,6 +798,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -750,7 +817,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -758,25 +825,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_58d53a00b5a25078_EOF + GH_AW_MCP_CONFIG_33dc71f92a389606_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -792,18 +866,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"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\",\"codeload.github.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\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"host.docker.internal\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.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\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"*.githubusercontent.com\",\"api.snapcraft.io\",\"archive.ubuntu.com\",\"azure.archive.ubuntu.com\",\"codeload.github.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\",\"docs.github.com\",\"github-cloud.githubusercontent.com\",\"github-cloud.s3.amazonaws.com\",\"github.blog\",\"github.com\",\"github.githubassets.com\",\"json-schema.org\",\"json.schemastore.org\",\"keyserver.ubuntu.com\",\"lfs.github.com\",\"objects.githubusercontent.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\",\"patch-diff.githubusercontent.com\",\"patchdiff.githubusercontent.com\",\"ppa.launchpad.net\",\"raw.githubusercontent.com\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -821,14 +910,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -836,7 +930,7 @@ jobs: 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_TIMEOUT_MINUTES: 60 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -857,7 +951,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 60 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -873,7 +978,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -882,9 +987,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -907,14 +1014,16 @@ jobs: 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: "*.githubusercontent.com,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,codeload.github.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.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,patch-diff.githubusercontent.com,patchdiff.githubusercontent.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" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.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,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,s.symcb.com,s.symcd.com,security.ubuntu.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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -924,9 +1033,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -934,9 +1045,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -950,9 +1063,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -960,16 +1075,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -986,6 +1126,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1005,9 +1147,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: + actions: read contents: write issues: write pull-requests: write @@ -1025,7 +1168,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1034,15 +1177,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-codegen-fix.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" 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 + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1050,42 +1194,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1099,6 +1230,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1121,9 +1254,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1161,9 +1296,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1178,9 +1315,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1193,9 +1332,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1208,9 +1349,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1223,7 +1366,7 @@ jobs: 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: "java-codegen-fix" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1237,6 +1380,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} @@ -1254,9 +1401,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "Java Codegen Agentic Fix" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/java-codegen-fix.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1268,6 +1436,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1278,7 +1447,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1287,15 +1456,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-codegen-fix.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1303,10 +1479,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1315,7 +1493,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1339,21 +1517,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1361,82 +1525,52 @@ jobs: WORKFLOW_NAME: "Java Codegen Agentic Fix" WORKFLOW_DESCRIPTION: "Agentic fix for Java codegen-related build/test failures. Invoked when\nmvn verify fails after code generation changes." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1452,58 +1586,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Java Codegen Agentic Fix" + WORKFLOW_DESCRIPTION: "Agentic fix for Java codegen-related build/test failures. Invoked when\nmvn verify fails after code generation changes." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact 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 + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1527,7 +1708,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "java-codegen-fix" @@ -1540,14 +1720,22 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} push_commit_sha: ${{ steps.process_safe_outputs.outputs.push_commit_sha }} push_commit_url: ${{ steps.process_safe_outputs.outputs.push_commit_url }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1556,15 +1744,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Java Codegen Agentic Fix" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/java-codegen-fix.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1572,7 +1763,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Download patch artifact continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -1581,7 +1774,7 @@ jobs: path: /tmp/gh-aw/ - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'push_to_pull_request_branch') - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1607,17 +1800,19 @@ jobs: 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: "*.githubusercontent.com,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,codeload.github.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,host.docker.internal,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.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,patch-diff.githubusercontent.com,patchdiff.githubusercontent.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" + GH_AW_ALLOWED_DOMAINS: "*.githubusercontent.com,api.snapcraft.io,archive.ubuntu.com,azure.archive.ubuntu.com,codeload.github.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,docs.github.com,github-cloud.githubusercontent.com,github-cloud.s3.amazonaws.com,github.blog,github.com,github.githubassets.com,json-schema.org,json.schemastore.org,keyserver.ubuntu.com,lfs.github.com,objects.githubusercontent.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,patch-diff.githubusercontent.com,patchdiff.githubusercontent.com,ppa.launchpad.net,raw.githubusercontent.com,s.symcb.com,s.symcd.com,security.ubuntu.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: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"required_labels\":[\"dependencies\"],\"target\":\"*\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"max\":5,\"target\":\"*\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"push_to_pull_request_branch\":{\"if_no_changes\":\"warn\",\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"required_labels\":[\"dependencies\"],\"target\":\"*\"},\"report_incomplete\":{}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1627,4 +1822,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/java-codegen-fix.md b/.github/workflows/java-codegen-fix.md index b1dcb1f636..8f6a845433 100644 --- a/.github/workflows/java-codegen-fix.md +++ b/.github/workflows/java-codegen-fix.md @@ -52,7 +52,7 @@ You are an automation agent that fixes Java compilation and test failures caused ## Context -A Dependabot PR bumped the `@github/copilot` npm dependency in `java/scripts/codegen/package.json`. The `java-codegen-check` workflow ran the code generator (`java/scripts/codegen/java.ts`) against the new schemas and `mvn verify` subsequently failed. Your job is to fix **both** the code generator script (if needed) and the handwritten SDK/test source code so the build passes. +A Copilot CLI release pin update fetched new schemas from GitHub Releases. The `java-codegen-check` workflow ran the code generator (`java/scripts/codegen/java.ts`) against those schemas and `mvn verify` subsequently failed. Your job is to fix **both** the code generator script (if needed) and the handwritten SDK/test source code so the build passes. **❌❌❌ YOU MUST NEVER EDIT any of the java source code in `java/sdk/src/generated/` directly.** ✅✅Rather, the way to affect changes in these files is to change the code generator script and re-generate the classes in `java/sdk/src/generated`. @@ -66,9 +66,9 @@ ${{ inputs.error_summary }} ## Architecture overview -The code generator (`java/scripts/codegen/java.ts`) reads JSON schemas from `node_modules/@github/copilot/schemas/` and produces Java source files under `java/sdk/src/generated/java/`. These generated types are consumed by handwritten code in `java/sdk/src/main/java/` (primarily `CopilotSession.java`) and tested by handwritten tests in `java/sdk/src/test/java/`. +The code generator (`java/scripts/codegen/java.ts`) reads JSON schemas from `java/scripts/codegen/target/schemas/`. The schemas are extracted from the pinned `github-copilot--linux-x64.tgz` GitHub Release asset by `fetch-schemas.mjs`. The generator produces Java source files under `java/sdk/src/generated/java/`. These generated types are consumed by handwritten code in `java/sdk/src/main/java/` (primarily `CopilotSession.java`) and tested by handwritten tests in `java/sdk/src/test/java/`. -When `@github/copilot` is bumped, the schemas may change in ways the code generator does not yet handle. Common schema changes include: +When the Copilot CLI release pin is bumped, the schemas may change in ways the code generator does not yet handle. Common schema changes include: - **`$ref` references**: Inline nested type definitions replaced with `$ref` pointers to `#/definitions/` entries. The code generator must resolve these references and emit standalone Java types instead of nested records. - **Field type changes**: Numeric fields changing between `double`, `Long`, `int`, etc. @@ -97,10 +97,10 @@ mvn --version node --version ``` -Install codegen dependencies: +Install codegen dependencies and fetch the pinned release schemas: ```bash -cd java/scripts/codegen && npm ci && cd ../../.. +cd java/scripts/codegen && npm ci && npm run fetch:schemas && cd ../../.. ``` ### Step 1: Reproduce the failure @@ -135,13 +135,13 @@ To diagnose, compare the current schemas with the generated output: ```bash # List available schemas -ls java/scripts/codegen/node_modules/@github/copilot/schemas/ +ls java/scripts/codegen/target/schemas/ # Check for $ref usage in schemas (indicates the codegen may need $ref resolution) -grep -r '"$ref"' java/scripts/codegen/node_modules/@github/copilot/schemas/ | head -20 +grep -r '"$ref"' java/scripts/codegen/target/schemas/ | head -20 # Look at a specific schema that relates to failing types -cat java/scripts/codegen/node_modules/@github/copilot/schemas/.json | head -80 +head -80 java/scripts/codegen/target/schemas/.json ``` ### Step 3: Fix the code generator (if needed) @@ -157,7 +157,7 @@ If the diagnosis shows the code generator does not handle the new schema format: 3. **Re-run code generation** to produce updated generated files: ```bash - cd java/scripts/codegen && npx tsx java.ts && cd ../../.. + cd java/scripts/codegen && npm run generate && cd ../../.. ``` 4. **Verify the generated output** looks reasonable: @@ -213,7 +213,7 @@ After `mvn verify` passes, commit all changes and use the `push-to-pull-request- ```bash git add -A -git commit -m "Fix Java codegen and build failures after @github/copilot update +git commit -m "Fix Java codegen and build failures after CLI update Automated fix applied by java-codegen-fix workflow." ``` @@ -236,7 +236,7 @@ Do **NOT** push broken code. ## Important constraints -- **NEVER** hand-edit files under `java/sdk/src/generated/java/` — these are auto-generated. They are updated by running `cd java/scripts/codegen && npx tsx java.ts`. +- **NEVER** hand-edit files under `java/sdk/src/generated/java/` — these are auto-generated. They are updated by running `cd java/scripts/codegen && npm run generate`. - **NEVER** modify `java/sdk/pom.xml` — build config is not in scope - **NEVER** modify `java/scripts/codegen/package.json` or `java/scripts/codegen/package-lock.json` — dependency versions are not in scope - **NEVER** modify files under `.github/` — workflow files are not in scope diff --git a/.github/workflows/java-publish-maven.yml b/.github/workflows/java-publish-maven.yml index fd07aae155..d1ef78f66b 100644 --- a/.github/workflows/java-publish-maven.yml +++ b/.github/workflows/java-publish-maven.yml @@ -90,12 +90,13 @@ jobs: development_version: ${{ steps.versions.outputs.development_version }} release_tag: ${{ steps.release-identity.outputs.release_tag }} tag_commit: ${{ steps.release-identity.outputs.tag_commit }} - pre_prepare_commit: ${{ steps.pre-prepare.outputs.pre_prepare_commit }} + pre_prepare_commit: ${{ steps.update-docs.outputs.pre_prepare_commit }} post_prepare_commit: ${{ steps.release-identity.outputs.post_prepare_commit }} docs_commit: ${{ steps.update-docs.outputs.docs_commit_sha }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: + ref: main fetch-depth: 0 token: ${{ secrets.JAVA_RELEASE_TOKEN }} @@ -150,16 +151,34 @@ jobs: run: | VERSION="${{ steps.versions.outputs.release_version }}" DEV_VERSION="${{ steps.versions.outputs.development_version }}" - ./scripts/test-update-documentation-versions.sh - ./scripts/update-documentation-versions.sh "$VERSION" "$DEV_VERSION" README.md sdk/jbang-example.java - git add README.md sdk/jbang-example.java - git commit -m "docs: update version references to ${VERSION}" - echo "docs_commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - git push origin main - - - name: Record rollback base - id: pre-prepare - run: echo "pre_prepare_commit=$(git rev-parse HEAD^)" >> "$GITHUB_OUTPUT" + + for attempt in 1 2 3; do + git fetch origin main + git reset --hard origin/main + if [ -z "${{ inputs.releaseVersion }}" ]; then + LIVE_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) + if [ "${LIVE_VERSION%-SNAPSHOT}" != "$VERSION" ]; then + echo "::error::main advanced from release version $VERSION to ${LIVE_VERSION%-SNAPSHOT}; restart the release with the current version." + exit 1 + fi + fi + ./scripts/test-update-documentation-versions.sh + ./scripts/update-documentation-versions.sh "$VERSION" "$DEV_VERSION" README.md sdk/jbang-example.java + git add README.md sdk/jbang-example.java + git commit -m "docs: update version references to ${VERSION}" + + if git push origin HEAD:main; then + echo "pre_prepare_commit=$(git rev-parse HEAD^)" >> "$GITHUB_OUTPUT" + echo "docs_commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "::error::main continued to advance while preparing the Java release." + exit 1 + fi + echo "main advanced during release preparation; retrying from the latest commit." + done - name: Prepare Release run: | diff --git a/.github/workflows/java-sdk-tests.yml b/.github/workflows/java-sdk-tests.yml index 36310d26b5..514f17c758 100644 --- a/.github/workflows/java-sdk-tests.yml +++ b/.github/workflows/java-sdk-tests.yml @@ -1,21 +1,17 @@ name: "Java SDK Tests" on: - push: - branches: - - main - paths: - - "java/**" - - "test/**" - - ".github/workflows/java-sdk-tests.yml" - - ".github/actions/setup-copilot/**" - - ".github/actions/java-test-report/**" workflow_dispatch: workflow_call: permissions: contents: read +env: + MAVEN_OPTS: >- + -Daether.connector.http.retryHandler.count=3 + -Daether.connector.http.retryHandler.serviceUnavailable=429,502,503 + jobs: java-sdk-inprocess: name: "Java SDK InProcess Tests (${{ matrix.classifier }})" @@ -63,7 +59,7 @@ jobs: run: mvn clean verify -Pinprocess ${{ matrix.maven-args }} - name: Generate Test Report Summary - if: always() + if: failure() uses: ./.github/actions/java-test-report with: title: "Copilot Java SDK :: Test Results InProcess" @@ -130,11 +126,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-linux-arm64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-linux-arm64.jar java/copilot-native/target/linux-arm64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-windows: @@ -187,11 +184,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-win32-x64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-x64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-win32-x64.jar java/copilot-native/target/win32-x64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-windows-arm64: @@ -244,11 +242,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-win32-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-arm64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-win32-arm64.jar java/copilot-native/target/win32-arm64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-darwin: @@ -302,11 +301,12 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: java-native-publication-darwin-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-darwin-arm64-${{ github.run_id }} path: | java/copilot-native/target/copilot-sdk-java-runtime-${{ steps.build.outputs.version }}-darwin-arm64.jar java/copilot-native/target/darwin-arm64-${{ steps.build.outputs.version }}.sha256 if-no-files-found: error + overwrite: true retention-days: 1 java-native-publication-assembly: @@ -342,22 +342,22 @@ jobs: - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-linux-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-linux-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/linux-arm64 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-win32-x64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-x64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/windows - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-win32-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-win32-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/windows-arm64 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: - name: java-native-publication-darwin-arm64-${{ github.run_id }}-${{ github.run_attempt }} + name: java-native-publication-darwin-arm64-${{ github.run_id }} path: ${{ github.workspace }}/java/native-publication-input/darwin - name: Verify native inputs and deploy the complete local release @@ -410,6 +410,7 @@ jobs: mvn -B -pl copilot-native deploy -Prelease -DskipTests \ -Dcopilot.native.libc=glibc \ -Dcopilot.native.test.local.publication=true \ + -DskipPublishing=true \ "-Dcopilot.native.external.linux.arm64.classifier.path=$LINUX_ARM64_JAR" \ "-Dcopilot.native.external.win32.classifier.path=$WINDOWS_JAR" \ "-Dcopilot.native.external.win32.arm64.classifier.path=$WINDOWS_ARM64_JAR" \ @@ -458,7 +459,12 @@ jobs: run: mvn javadoc:javadoc -q - name: Verify CLI works - run: node ../nodejs/node_modules/@github/copilot/npm-loader.js --version + run: | + npm --prefix ../nodejs ci --ignore-scripts + cli_path=$(npm --prefix ../nodejs run --silent prepare:runtime -- --print-path) + test -x "$cli_path" + legacy_cli=$(npm --prefix ../nodejs run --silent prepare:runtime -- --print-legacy-path) + node "$legacy_cli" --version - name: Run spotless check if: matrix.test-jdk == '25' @@ -481,9 +487,8 @@ jobs: if: matrix.test-jdk == '25' env: CI: "true" - run: | - node copilot-native/scripts/validate-native-host.mjs linux-x64 - mvn verify -Dskip.test.harness=true -Dcopilot.native.libc=glibc -Dcopilot.native.skip.download=false + # Native artifacts are built and exercised by every classifier job. + run: mvn -pl sdk verify -Dskip.test.harness=true - name: Switch to JDK 17 if: matrix.test-jdk == '17' @@ -502,7 +507,7 @@ jobs: mvn -pl sdk jacoco:prepare-agent@wire-up-coverage-instrumentation antrun:run@print-test-jdk-banner surefire:test failsafe:integration-test failsafe:verify jacoco:report@build-coverage-report-from-tests -Denforcer.skip=true - name: Upload test results for site generation - if: success() && github.ref == 'refs/heads/main' && matrix.test-jdk == '25' + if: success() && github.event_name == 'merge_group' && matrix.test-jdk == '25' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: test-results-for-site diff --git a/.github/workflows/java-smoke-test.yml b/.github/workflows/java-smoke-test.yml index e7e9a417d2..5f808f8d0d 100644 --- a/.github/workflows/java-smoke-test.yml +++ b/.github/workflows/java-smoke-test.yml @@ -29,27 +29,8 @@ jobs: distribution: "microsoft" cache: "maven" - - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v6 - with: - node-version: 22 - - - name: Read pinned @github/copilot version from pom.xml - id: cli-version - run: | - PROP="readonly-copilot-sdk-ref-impl-version-from-lastmerge-file-updated-by-reference-impl-sync" - VERSION=$(sed -n "s|.*<${PROP}>\(.*\).*|\1|p" pom.xml | head -n 1 | tr -d '[:space:]') - if [[ -z "$VERSION" || "$VERSION" == "PRIMER_TO_REPLACE" ]]; then - echo "::error::Could not read pinned @github/copilot version from pom.xml property <${PROP}>" >&2 - exit 1 - fi - echo "Pinned @github/copilot version: $VERSION" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Install Copilot CLI globally (pinned to pom.xml version) - run: npm install -g "@github/copilot@${{ steps.cli-version.outputs.version }}" - - - name: Verify CLI works - run: copilot --version + - uses: ./.github/actions/setup-copilot + id: setup-copilot - name: Build SDK and install to local repo run: mvn -DskipTests -Pskip-test-harness clean install @@ -57,6 +38,7 @@ jobs: - name: Create and run smoke test via Copilot CLI env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_CLI_JS: ${{ steps.setup-copilot.outputs.javascript-cli-path }} run: | cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF' You are running inside the copilot-sdk monorepo, in the java/ subdirectory. @@ -74,7 +56,7 @@ jobs: If any step fails, exit with a non-zero exit code. Do not silently fix errors. PROMPT_EOF - copilot --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" + node "$COPILOT_CLI_JS" --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" - name: Run smoke test jar env: @@ -102,27 +84,8 @@ jobs: distribution: "microsoft" cache: "maven" - - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v6 - with: - node-version: 22 - - - name: Read pinned @github/copilot version from pom.xml - id: cli-version - run: | - PROP="readonly-copilot-sdk-ref-impl-version-from-lastmerge-file-updated-by-reference-impl-sync" - VERSION=$(sed -n "s|.*<${PROP}>\(.*\).*|\1|p" pom.xml | head -n 1 | tr -d '[:space:]') - if [[ -z "$VERSION" || "$VERSION" == "PRIMER_TO_REPLACE" ]]; then - echo "::error::Could not read pinned @github/copilot version from pom.xml property <${PROP}>" >&2 - exit 1 - fi - echo "Pinned @github/copilot version: $VERSION" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Install Copilot CLI globally (pinned to pom.xml version) - run: npm install -g "@github/copilot@${{ steps.cli-version.outputs.version }}" - - - name: Verify CLI works - run: copilot --version + - uses: ./.github/actions/setup-copilot + id: setup-copilot - name: Build SDK and install to local repo run: mvn -DskipTests -Pskip-test-harness clean install @@ -130,6 +93,7 @@ jobs: - name: Create and run smoke test via Copilot CLI env: COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} + COPILOT_CLI_JS: ${{ steps.setup-copilot.outputs.javascript-cli-path }} run: | cat > /tmp/smoke-test-prompt.txt << 'PROMPT_EOF' You are running inside the copilot-sdk monorepo, in the java/ subdirectory. @@ -150,7 +114,7 @@ jobs: If any step fails, exit with a non-zero exit code. Do not silently fix errors. PROMPT_EOF - copilot --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" + node "$COPILOT_CLI_JS" --yolo --prompt "$(cat /tmp/smoke-test-prompt.txt)" - name: Run smoke test jar env: diff --git a/.github/workflows/nodejs-sdk-tests.yml b/.github/workflows/nodejs-sdk-tests.yml index 4c31f79cc4..894f3b5d0d 100644 --- a/.github/workflows/nodejs-sdk-tests.yml +++ b/.github/workflows/nodejs-sdk-tests.yml @@ -4,9 +4,6 @@ env: HUSKY: 0 on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -52,10 +49,26 @@ jobs: - name: Build SDK run: npm run build + - name: Build and verify release packages + if: runner.os == 'Linux' && matrix.transport == 'default' + run: | + npm run pack:release + npm run verify:release-packages + - name: Install test harness dependencies working-directory: ./test/harness run: npm ci --ignore-scripts + - name: Run test harness tests + if: runner.os == 'Linux' && matrix.transport == 'default' + working-directory: ./test/harness + run: npm test + + - name: Prepare Copilot CLI runtime + run: | + runtime_path=$(npm run --silent prepare:runtime -- --print-path) + echo "COPILOT_CLI_PATH=$runtime_path" >> "$GITHUB_ENV" + - name: Warm up PowerShell if: runner.os == 'Windows' run: pwsh.exe -Command "Write-Host 'PowerShell ready'" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 98bf236900..5e1d277259 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -107,19 +107,19 @@ jobs: - name: Build run: npm run build - name: Pack - id: pack run: | - TARBALL="$(npm pack . --json | jq -r '.[0].filename')" - if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then - echo "::error::npm pack did not produce a tarball." + npm run pack:release + TARBALL_COUNT="$(find . -maxdepth 1 -name 'github-copilot-sdk-*.tgz' | wc -l | tr -d ' ')" + if [ "$TARBALL_COUNT" -ne 9 ]; then + echo "::error::Expected nine Node.js package tarballs, found $TARBALL_COUNT." exit 1 fi - echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT" + npm run verify:release-packages - name: Upload artifact uses: actions/upload-artifact@v7.0.0 with: name: nodejs-package - path: nodejs/${{ steps.pack.outputs.tarball }} + path: nodejs/github-copilot-sdk-*.tgz if-no-files-found: error publish-nodejs: @@ -150,12 +150,29 @@ jobs: set -euo pipefail shopt -s nullglob TARBALLS=(./dist/*.tgz) - if [ "${#TARBALLS[@]}" -ne 1 ]; then - echo "::error::Expected exactly one Node.js package tarball, found ${#TARBALLS[@]}." + if [ "${#TARBALLS[@]}" -ne 9 ]; then + echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}." + exit 1 + fi + MAIN_TARBALL="" + for TARBALL in "${TARBALLS[@]}"; do + PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)" + if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then + MAIN_TARBALL="$TARBALL" + continue + fi + node nodejs/scripts/npm-release.js publish \ + "$TARBALL" \ + "$DIST_TAG" \ + https://registry.npmjs.org \ + public + done + if [ -z "$MAIN_TARBALL" ]; then + echo "::error::Main @github/copilot-sdk tarball not found." exit 1 fi node nodejs/scripts/npm-release.js publish \ - "${TARBALLS[0]}" \ + "$MAIN_TARBALL" \ "$DIST_TAG" \ https://registry.npmjs.org \ public @@ -209,12 +226,29 @@ jobs: fi shopt -s nullglob TARBALLS=(./dist/*.tgz) - if [ "${#TARBALLS[@]}" -ne 1 ]; then - echo "::error::Expected exactly one Node.js package tarball, found ${#TARBALLS[@]}." + if [ "${#TARBALLS[@]}" -ne 9 ]; then + echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}." + exit 1 + fi + MAIN_TARBALL="" + for TARBALL in "${TARBALLS[@]}"; do + PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)" + if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then + MAIN_TARBALL="$TARBALL" + continue + fi + node nodejs/scripts/npm-release.js publish \ + "$TARBALL" \ + "$DIST_TAG" \ + "$FEED_URL" \ + azure + done + if [ -z "$MAIN_TARBALL" ]; then + echo "::error::Main @github/copilot-sdk tarball not found." exit 1 fi node nodejs/scripts/npm-release.js publish \ - "${TARBALLS[0]}" \ + "$MAIN_TARBALL" \ "$DIST_TAG" \ "$FEED_URL" \ azure @@ -327,9 +361,6 @@ jobs: node-version: "22.x" - name: Set up uv uses: astral-sh/setup-uv@v7 - - name: Install Node.js dependencies (for CLI version) - working-directory: ./nodejs - run: npm ci --ignore-scripts - name: Set version run: sed -i "s/^version = .*/version = \"${{ needs.version.outputs.version }}\"/" pyproject.toml - name: Inject CLI version diff --git a/.github/workflows/python-sdk-tests.yml b/.github/workflows/python-sdk-tests.yml index 1ea9739756..aa6e803372 100644 --- a/.github/workflows/python-sdk-tests.yml +++ b/.github/workflows/python-sdk-tests.yml @@ -4,9 +4,6 @@ env: PYTHONUTF8: 1 on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -27,6 +24,7 @@ jobs: python-version: ["3.11"] transport: ["default", "inprocess"] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash @@ -52,7 +50,7 @@ jobs: - name: Install Node.js dependencies (for CLI in tests) working-directory: ./nodejs - run: npm ci --ignore-scripts + run: npm ci --ignore-scripts --fetch-retries=4 --fetch-retry-mintimeout=10000 --fetch-retry-maxtimeout=60000 - name: Run ruff format check run: uv run ruff format --check . @@ -65,7 +63,7 @@ jobs: - name: Install test harness dependencies working-directory: ./test/harness - run: npm ci --ignore-scripts + run: npm ci --ignore-scripts --fetch-retries=4 --fetch-retry-mintimeout=10000 --fetch-retry-maxtimeout=60000 - name: Warm up PowerShell if: runner.os == 'Windows' diff --git a/.github/workflows/release-changelog.lock.yml b/.github/workflows/release-changelog.lock.yml index 23b19d9b82..558cdc2e27 100644 --- a/.github/workflows/release-changelog.lock.yml +++ b/.github/workflows/release-changelog.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a4a0859e0103be270433c7fe1926a346c46271b4b530b2c08fa5d606d0ab75c4","body_hash":"490b25b529910b1b087df624fd59eaef52e142e84b9503ca1cf87631f4c36b53","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_CI_TRIGGER_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}]} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"a4a0859e0103be270433c7fe1926a346c46271b4b530b2c08fa5d606d0ab75c4","body_hash":"490b25b529910b1b087df624fd59eaef52e142e84b9503ca1cf87631f4c36b53","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["create_pull_request","missing_data","missing_tool","noop","update_release"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,8 +26,8 @@ # Generates release notes from merged PRs/commits. Triggered by the publish workflow or manually via workflow_dispatch. # # Secrets used: -# - COPILOT_GITHUB_TOKEN # - GH_AW_CI_TRIGGER_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -35,22 +35,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "Release Changelog Generator" on: @@ -70,9 +68,18 @@ permissions: {} concurrency: group: "gh-aw-${{ github.workflow }}" + queue: max run-name: "Release Changelog Generator" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.release-changelog + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=Release%20Changelog%20Generator,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: runs-on: ubuntu-slim @@ -86,6 +93,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -99,7 +107,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -107,34 +115,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/release-changelog.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "Release Changelog Generator" 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.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -156,9 +169,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -176,38 +191,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -216,27 +230,34 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"file\":\"safe_outputs_create_pull_request.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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) }} @@ -246,79 +267,37 @@ jobs: GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} 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_c642707f673b9ac4_EOF' - - GH_AW_PROMPT_c642707f673b9ac4_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_c642707f673b9ac4_EOF' - - Tools: create_pull_request, update_release, missing_tool, missing_data, noop - GH_AW_PROMPT_c642707f673b9ac4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/safe_outputs_create_pull_request.md" - cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' - - GH_AW_PROMPT_c642707f673b9ac4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_c642707f673b9ac4_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_c642707f673b9ac4_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_c642707f673b9ac4_EOF' - - {{#runtime-import .github/workflows/release-changelog.md}} - GH_AW_PROMPT_c642707f673b9ac4_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: create_pull_request, update_release, missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/release-changelog.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_GITHUB_EVENT_INPUTS_TAG: ${{ github.event.inputs.tag }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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) }} @@ -331,10 +310,12 @@ jobs: GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -354,16 +335,20 @@ jobs: }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -391,12 +376,19 @@ jobs: copilot-requests: write issues: read pull-requests: read + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: releasechangelog outputs: @@ -410,7 +402,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -418,11 +413,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -431,19 +427,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/release-changelog.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -473,16 +478,19 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -491,13 +499,15 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -509,15 +519,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_269226b895ff9733_EOF' - {"create_pull_request":{"draft":false,"labels":["automation","changelog"],"max":1,"max_patch_files":100,"max_patch_size":4096,"protect_top_level_dot_folders":true,"protected_files":["package.json","bun.lockb","bunfig.toml","deno.json","deno.jsonc","deno.lock","global.json","NuGet.Config","Directory.Packages.props","mix.exs","mix.lock","go.mod","go.sum","stack.yaml","stack.yaml.lock","pom.xml","build.gradle","build.gradle.kts","settings.gradle","settings.gradle.kts","gradle.properties","package-lock.json","yarn.lock","pnpm-lock.yaml","npm-shrinkwrap.json","requirements.txt","Pipfile","Pipfile.lock","pyproject.toml","setup.py","setup.cfg","Gemfile","Gemfile.lock","uv.lock","CODEOWNERS","DESIGN.md","README.md","CONTRIBUTING.md","CHANGELOG.md","SECURITY.md","CODE_OF_CONDUCT.md","AGENTS.md","CLAUDE.md","GEMINI.md"],"protected_files_policy":"request_review","title_prefix":"[changelog] "},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{},"update_release":{"max":1}} - GH_AW_SAFE_OUTPUTS_CONFIG_269226b895ff9733_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"labels\":[\"automation\",\"changelog\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[changelog] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_release\":{\"max\":1}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -551,6 +572,12 @@ jobs: "sanitize": true, "maxLength": 256 }, + "dependencies": { + "type": "array", + "itemType": "string", + "itemSanitize": true, + "itemMaxLength": 256 + }, "draft": { "type": "boolean" }, @@ -564,6 +591,18 @@ jobs: "type": "string", "maxLength": 256 }, + "stack_position": { + "optionalPositiveInteger": true + }, + "stack_root": { + "type": "string", + "sanitize": true, + "maxLength": 256 + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" + }, "title": { "required": true, "type": "string", @@ -675,9 +714,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -686,6 +727,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} 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 }} @@ -693,33 +735,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_46604863f3d8e286_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -752,6 +806,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -763,7 +825,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -771,25 +833,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF + GH_AW_MCP_CONFIG_46604863f3d8e286_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -805,18 +874,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\",\"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\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -834,14 +918,19 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} @@ -849,7 +938,7 @@ jobs: 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_TIMEOUT_MINUTES: 15 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -870,7 +959,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 15 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -886,7 +986,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -895,9 +995,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -920,14 +1022,16 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -937,9 +1041,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -947,9 +1053,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -963,9 +1071,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -973,16 +1083,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -999,6 +1134,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1018,9 +1155,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: + actions: read contents: write issues: write pull-requests: write @@ -1038,7 +1176,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1047,15 +1185,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/release-changelog.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" 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 + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1063,42 +1202,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1112,6 +1238,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1134,9 +1262,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1166,7 +1296,7 @@ jobs: GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/release-changelog.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" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1174,9 +1304,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1191,9 +1323,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1206,9 +1340,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1221,9 +1357,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1236,7 +1374,7 @@ jobs: 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: "release-changelog" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1250,6 +1388,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} GH_AW_ENGINE_API_HOSTS: "api.enterprise.githubcopilot.com,api.githubcopilot.com,api.business.githubcopilot.com,api.individual.githubcopilot.com" GH_AW_CODE_PUSH_FAILURE_ERRORS: ${{ needs.safe_outputs.outputs.code_push_failure_errors }} GH_AW_CODE_PUSH_FAILURE_COUNT: ${{ needs.safe_outputs.outputs.code_push_failure_count }} @@ -1267,9 +1409,30 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "Release Changelog Generator" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/release-changelog.md" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1281,6 +1444,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1291,7 +1455,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1300,15 +1464,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/release-changelog.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1316,10 +1487,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1328,7 +1501,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1352,21 +1525,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1374,82 +1533,52 @@ jobs: WORKFLOW_NAME: "Release Changelog Generator" WORKFLOW_DESCRIPTION: "Generates release notes from merged PRs/commits. Triggered by the publish workflow or manually via workflow_dispatch." HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: detection + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1465,58 +1594,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "Release Changelog Generator" + WORKFLOW_DESCRIPTION: "Generates release notes from merged PRs/commits. Triggered by the publish workflow or manually via workflow_dispatch." + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact 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 + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1540,7 +1716,6 @@ jobs: GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens }} GH_AW_ENGINE_ID: "copilot" GH_AW_ENGINE_MODEL: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_WORKFLOW_ID: "release-changelog" @@ -1553,12 +1728,20 @@ jobs: create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} created_pr_number: ${{ steps.process_safe_outputs.outputs.created_pr_number }} created_pr_url: ${{ steps.process_safe_outputs.outputs.created_pr_url }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1567,15 +1750,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "Release Changelog Generator" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/release-changelog.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1583,7 +1769,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Download patch artifact continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -1592,7 +1780,7 @@ jobs: path: /tmp/gh-aw/ - name: Checkout repository if: (!cancelled()) && needs.agent.result != 'skipped' && contains(needs.agent.outputs.output_types, 'create_pull_request') - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: true token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} @@ -1618,17 +1806,19 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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_pull_request\":{\"draft\":false,\"labels\":[\"automation\",\"changelog\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\",\"CLAUDE.md\",\"GEMINI.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[changelog] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{},\"update_release\":{\"max\":1}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"create_pull_request\":{\"draft\":false,\"labels\":[\"automation\",\"changelog\"],\"max\":1,\"max_patch_files\":100,\"max_patch_size\":4096,\"protect_top_level_dot_folders\":true,\"protected_files\":[\"package.json\",\"bun.lockb\",\"bunfig.toml\",\"deno.json\",\"deno.jsonc\",\"deno.lock\",\"global.json\",\"NuGet.Config\",\"Directory.Packages.props\",\"mix.exs\",\"mix.lock\",\"go.mod\",\"go.sum\",\"stack.yaml\",\"stack.yaml.lock\",\"pom.xml\",\"build.gradle\",\"build.gradle.kts\",\"settings.gradle\",\"settings.gradle.kts\",\"gradle.properties\",\"package-lock.json\",\"yarn.lock\",\"pnpm-lock.yaml\",\"npm-shrinkwrap.json\",\"requirements.txt\",\"Pipfile\",\"Pipfile.lock\",\"pyproject.toml\",\"setup.py\",\"setup.cfg\",\"Gemfile\",\"Gemfile.lock\",\"uv.lock\",\"CODEOWNERS\",\"DESIGN.md\",\"README.md\",\"CONTRIBUTING.md\",\"CHANGELOG.md\",\"SECURITY.md\",\"CODE_OF_CONDUCT.md\",\"AGENTS.md\"],\"protected_files_policy\":\"request_review\",\"title_prefix\":\"[changelog] \"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{},\"update_release\":{\"max\":1}}" GH_AW_CI_TRIGGER_TOKEN: ${{ secrets.GH_AW_CI_TRIGGER_TOKEN }} with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1638,4 +1828,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/required-checks.yml b/.github/workflows/required-checks.yml index dbd79fd4cf..0fb8acf70b 100644 --- a/.github/workflows/required-checks.yml +++ b/.github/workflows/required-checks.yml @@ -10,6 +10,10 @@ permissions: contents: read pull-requests: read +concurrency: + group: sdk-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: changes: name: Select SDK workflows @@ -33,42 +37,41 @@ jobs: - '.github/workflows/required-checks.yml' nodejs: - '{nodejs/**,test/**,.github/workflows/nodejs-sdk-tests.yml}' - - '!nodejs/scripts/**' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' - '!**/.editorconfig' - '!**/*.{png,jpg,jpeg,gif,svg}' python: - - '{python/**,test/**,nodejs/package.json,.github/workflows/python-sdk-tests.yml}' + - '{python/**,test/**,nodejs/package.json,nodejs/scripts/**,.github/workflows/python-sdk-tests.yml}' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' - '!**/.editorconfig' - '!**/*.{png,jpg,jpeg,gif,svg}' go: - - '{go/**,test/**,nodejs/package.json,.github/workflows/go-sdk-tests.yml,.github/actions/setup-copilot/**}' + - '{go/**,test/**,nodejs/package.json,nodejs/scripts/**,.github/workflows/go-sdk-tests.yml,.github/actions/setup-copilot/**}' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' - '!**/.editorconfig' - '!**/*.{png,jpg,jpeg,gif,svg}' dotnet: - - '{dotnet/**,test/**,nodejs/package.json,.github/workflows/dotnet-sdk-tests.yml}' + - '{dotnet/**,test/**,nodejs/package.json,nodejs/scripts/**,.github/workflows/dotnet-sdk-tests.yml}' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' - '!**/.editorconfig' - '!**/*.{png,jpg,jpeg,gif,svg}' java: - - '{java/**,test/**,.github/workflows/java-sdk-tests.yml,.github/actions/setup-copilot/**,.github/actions/java-test-report/**}' + - '{java/**,test/**,nodejs/package.json,nodejs/scripts/**,.github/workflows/java-sdk-tests.yml,.github/actions/setup-copilot/**,.github/actions/java-test-report/**}' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' - '!**/.editorconfig' - '!**/*.{png,jpg,jpeg,gif,svg}' rust: - - '{rust/**,test/**,nodejs/package.json,.github/workflows/rust-sdk-tests.yml,.github/actions/setup-copilot/**}' + - '{rust/**,test/**,nodejs/package.json,nodejs/scripts/**,.github/workflows/rust-sdk-tests.yml,.github/actions/setup-copilot/**}' - '!**/*.md' - '!**/LICENSE*' - '!**/.gitignore' diff --git a/.github/workflows/rust-sdk-tests.yml b/.github/workflows/rust-sdk-tests.yml index 7fdac3b818..828459126b 100644 --- a/.github/workflows/rust-sdk-tests.yml +++ b/.github/workflows/rust-sdk-tests.yml @@ -1,9 +1,6 @@ name: "Rust SDK Tests" on: - push: - branches: - - main workflow_dispatch: workflow_call: @@ -23,6 +20,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash @@ -38,17 +36,6 @@ jobs: uses: dtolnay/rust-toolchain@stable with: toolchain: "1.94.0" - components: rustfmt, clippy - - # Nightly rustfmt for unstable format options (group_imports, - # imports_granularity, reorder_impl_items) — pinned in - # `.rustfmt.nightly.toml`. - - name: Install nightly rustfmt - if: runner.os == 'Linux' - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2026-04-14 - components: rustfmt - uses: Swatinem/rust-cache@v2 with: @@ -56,11 +43,11 @@ jobs: prefix-key: v1-rust-no-bin cache-bin: false - - name: Read pinned @github/copilot CLI version + - name: Read pinned Copilot CLI version id: cli-version working-directory: ./nodejs run: | - version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + version=$(node -p "require('./package.json').copilotCliVersion") echo "version=$version" >> "$GITHUB_OUTPUT" echo "Pinned CLI version: $version" @@ -73,23 +60,6 @@ jobs: path: ./rust/.bundled-cli-cache key: bundled-cli-${{ matrix.os }}-${{ steps.cli-version.outputs.version }} - - name: cargo fmt --check (nightly) - if: runner.os == 'Linux' - run: cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml --check - - - name: cargo clippy - if: runner.os == 'Linux' - env: - BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache - run: cargo clippy --all-targets --features test-support,bundled-in-process -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type - - - name: cargo doc - if: runner.os == 'Linux' - env: - RUSTDOCFLAGS: "-D warnings" - BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache - run: cargo doc --no-deps --all-features - - name: Install test harness dependencies working-directory: ./test/harness run: npm ci --ignore-scripts @@ -112,6 +82,112 @@ jobs: # The dedicated `bundle` job below exercises the embed pipeline. run: cargo test --no-default-features --features test-support -- --test-threads=4 --nocapture + clippy: + name: "Rust SDK Format and Clippy" + if: github.event.repository.fork == false + env: + POWERSHELL_UPDATECHECK: Off + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: ./rust + steps: + - uses: actions/checkout@v6.0.2 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: "1.94.0" + components: clippy + + # Nightly rustfmt for unstable format options (group_imports, + # imports_granularity, reorder_impl_items) — pinned in + # `.rustfmt.nightly.toml`. + - name: Install nightly rustfmt + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 + with: + toolchain: nightly-2026-04-14 + components: rustfmt + + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 + with: + workspaces: "rust" + prefix-key: v1-rust-no-bin + cache-bin: false + + - name: Read pinned Copilot CLI version + id: cli-version + working-directory: ./nodejs + run: | + version=$(node -p "require('./package.json').copilotCliVersion") + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Pinned CLI version: $version" + + - name: Cache bundled CLI archives + uses: actions/cache@v4 + with: + path: ./rust/.bundled-cli-cache + key: bundled-cli-ubuntu-latest-${{ steps.cli-version.outputs.version }} + + - name: cargo fmt --check (nightly) + run: cargo +nightly-2026-04-14 fmt --all -- --config-path .rustfmt.nightly.toml --check + + - name: cargo clippy + env: + BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache + run: cargo clippy --all-targets --features test-support,bundled-in-process -- --no-deps -D warnings -D clippy::unwrap_used -D clippy::disallowed_macros -D clippy::await_holding_invalid_type + + doc: + name: "Rust SDK Docs" + if: github.event.repository.fork == false + env: + POWERSHELL_UPDATECHECK: Off + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + runs-on: ubuntu-latest + timeout-minutes: 20 + defaults: + run: + shell: bash + working-directory: ./rust + steps: + - uses: actions/checkout@v6.0.2 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + with: + toolchain: "1.94.0" + + - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 + with: + workspaces: "rust" + prefix-key: v1-rust-no-bin + cache-bin: false + + - name: Read pinned Copilot CLI version + id: cli-version + working-directory: ./nodejs + run: | + version=$(node -p "require('./package.json').copilotCliVersion") + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "Pinned CLI version: $version" + + - name: Cache bundled CLI archives + uses: actions/cache@v4 + with: + path: ./rust/.bundled-cli-cache + key: bundled-cli-ubuntu-latest-${{ steps.cli-version.outputs.version }} + + - name: cargo doc + env: + RUSTDOCFLAGS: "-D warnings" + BUNDLED_CLI_CACHE_DIR: ${{ github.workspace }}/rust/.bundled-cli-cache + run: cargo doc --no-deps --all-features + # Exercises the in-process FFI transport (`Transport::InProcess`, the Rust # analogue of the .NET `RuntimeConnection.ForInProcess()`), mirroring the # `inprocess` transport cell in dotnet-sdk-tests.yml. Sets @@ -133,6 +209,7 @@ jobs: # TODO: Re-enable Windows after fixing the napi-oop peer shutdown crash. os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash @@ -154,11 +231,11 @@ jobs: prefix-key: v1-rust-no-bin cache-bin: false - - name: Read pinned @github/copilot CLI version + - name: Read pinned Copilot CLI version id: cli-version working-directory: ./nodejs run: | - version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + version=$(node -p "require('./package.json').copilotCliVersion") echo "version=$version" >> "$GITHUB_OUTPUT" echo "Pinned CLI version: $version" @@ -207,6 +284,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 20 defaults: run: shell: bash @@ -229,11 +307,11 @@ jobs: prefix-key: v1-rust-no-bin cache-bin: false - - name: Read pinned @github/copilot CLI version + - name: Read pinned Copilot CLI version id: cli-version working-directory: ./nodejs run: | - version=$(node -p "require('./package-lock.json').packages['node_modules/@github/copilot'].version") + version=$(node -p "require('./package.json').copilotCliVersion") echo "version=$version" >> "$GITHUB_OUTPUT" echo "Pinned CLI version: $version" diff --git a/.github/workflows/sdk-canary.yml b/.github/workflows/sdk-canary.yml index 95f8b1c926..7425f8e720 100644 --- a/.github/workflows/sdk-canary.yml +++ b/.github/workflows/sdk-canary.yml @@ -1,391 +1,428 @@ -name: "SDK Canary Test/Publish" - -# Nightly-style canary pipeline. First installs an explicit version of the -# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite -# against it to prove runtime <-> SDK compatibility. When that gate passes (and -# mode allows), publishes an SDK canary pinned to the tested runtime to the -# internal Azure Artifacts feed only (never public npm). - -env: - HUSKY: 0 - # Internal org-scoped Azure Artifacts feed — single source of truth so the - # feed name isn't repeated across steps. The SDK canary publishes here and - # (when runtime_source=internal) installs the runtime from here; it must NEVER - # reach public npm (@github/copilot-sdk is a live public package). - FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/ - # Azure DevOps resource ID used to mint an ADO access token for the feed. - ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 - -on: - workflow_dispatch: - inputs: - runtime_version: - description: "Exact @github/copilot version to test (e.g. 1.0.69 or 1.0.70-canary.)" - required: true - type: string - runtime_source: - description: "Where to install the runtime from" - required: true - type: choice - options: - - public - - internal - default: public - mode: - description: "publish (tests must pass), publish-force (publish even if tests fail), or tests-only (run gate, never publish)" - required: false - type: choice - default: publish - options: - - publish - - publish-force - - tests-only - repository_dispatch: - types: [runtime-canary] - -permissions: - contents: read - id-token: write - -# Serialize runs per ref so two overlapping canary runs can't race the feed -# publish. cancel-in-progress: false — never kill an in-flight publish. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - resolve: - name: "Resolve runtime inputs" - if: github.event.repository.fork == false - runs-on: ubuntu-latest - permissions: {} - outputs: - RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} - RUNTIME_SOURCE: ${{ steps.normalize.outputs.RUNTIME_SOURCE }} - PUBLISH_MODE: ${{ steps.normalize.outputs.PUBLISH_MODE }} - steps: - # Normalize whichever trigger fired into a single (RUNTIME_VERSION, - # RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step - # references. workflow_dispatch reads the human-supplied inputs; - # repository_dispatch reads client_payload and forces source=internal - # (a runtime canary only exists on the feed), defaulting mode to publish. - - name: Normalize inputs - id: normalize - env: - EVENT_NAME: ${{ github.event_name }} - INPUT_VERSION: ${{ inputs.runtime_version }} - INPUT_SOURCE: ${{ inputs.runtime_source }} - INPUT_MODE: ${{ inputs.mode }} - PAYLOAD_VERSION: ${{ github.event.client_payload.runtime_version }} - PAYLOAD_SOURCE: ${{ github.event.client_payload.runtime_source }} - PAYLOAD_MODE: ${{ github.event.client_payload.mode }} - run: | - set -euo pipefail - case "$EVENT_NAME" in - workflow_dispatch) - VERSION="$INPUT_VERSION" - SOURCE="$INPUT_SOURCE" - MODE="$INPUT_MODE" - ;; - repository_dispatch) - VERSION="$PAYLOAD_VERSION" - # A runtime canary only ever exists on the internal feed. - SOURCE="${PAYLOAD_SOURCE:-internal}" - MODE="${PAYLOAD_MODE:-publish}" - ;; - *) - echo "::error::Unsupported event '$EVENT_NAME'." - exit 1 - ;; - esac - if [ -z "$VERSION" ]; then echo "::error::Could not determine runtime version."; exit 1; fi - if [ -z "$SOURCE" ]; then SOURCE="public"; fi - case "$SOURCE" in - public|internal) ;; - *) echo "::error::Invalid runtime source '$SOURCE'. Expected one of: public, internal."; exit 1 ;; - esac - if [ -z "$MODE" ]; then MODE="publish"; fi - case "$MODE" in - publish|publish-force|tests-only) ;; - *) echo "::error::Invalid publish mode '$MODE'. Expected one of: publish, publish-force, tests-only."; exit 1 ;; - esac - echo "Resolved RUNTIME_VERSION=$VERSION RUNTIME_SOURCE=$SOURCE PUBLISH_MODE=$MODE" - echo "RUNTIME_VERSION=$VERSION" >> "$GITHUB_OUTPUT" - echo "RUNTIME_SOURCE=$SOURCE" >> "$GITHUB_OUTPUT" - echo "PUBLISH_MODE=$MODE" >> "$GITHUB_OUTPUT" - - - name: Validate runtime version (semver) - env: - RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} - run: | - if [[ ! "$RUNTIME_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then - echo "::error::Invalid runtime version '$RUNTIME_VERSION'. Expected semver (e.g. 1.0.69 or 1.0.70-canary.abc123)." - exit 1 - fi - - test: - name: "E2E tests (${{ matrix.os }})" - needs: resolve - if: github.event.repository.fork == false - environment: cicd - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - runs-on: ${{ matrix.os }} - env: - POWERSHELL_UPDATECHECK: Off - RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }} - RUNTIME_SOURCE: ${{ needs.resolve.outputs.RUNTIME_SOURCE }} - defaults: - run: - shell: bash - working-directory: ./nodejs - steps: - - uses: actions/checkout@v6.0.2 - - - uses: actions/setup-node@v6 - with: - cache: "npm" - cache-dependency-path: "./nodejs/package-lock.json" - node-version: 22 - - - name: Install SDK dependencies - run: npm ci --ignore-scripts - - - name: Install test harness dependencies - working-directory: ./test/harness - run: npm ci --ignore-scripts - - - name: Azure Login (OIDC -> id-cpd-ci) - if: env.RUNTIME_SOURCE == 'internal' - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci - tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" - allow-no-subscriptions: true - - # Route ONLY @github/* (the runtime + its 8 platform packages) to the - # internal feed via a scoped registry. All other deps (e.g. detect-libc) - # still resolve from public npm. A global --registry would break because - # detect-libc is not on the feed. - - name: Configure canary feed (.npmrc) - if: env.RUNTIME_SOURCE == 'internal' - run: | - set -euo pipefail - TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" - echo "::add-mask::$TOKEN" - # Derive the protocol-relative auth scopes from FEED_URL so the feed - # name lives in exactly one place (the workflow-level env). - FEED_AUTH_REGISTRY="${FEED_URL#https:}" - FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" - NPMRC="$(printf '%s\n' \ - "@github:registry=${FEED_URL}" \ - "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ - "${FEED_AUTH_BASE}:_authToken=${TOKEN}")" - printf '%s\n' "$NPMRC" > .npmrc - echo "Wrote scoped @github registry .npmrc to ./nodejs" - - - name: Override runtime version - run: | - set -euo pipefail - echo "Installing @github/copilot@${RUNTIME_VERSION} (source: ${RUNTIME_SOURCE})" - npm install "@github/copilot@${RUNTIME_VERSION}" --save-exact --ignore-scripts - - - name: Verify installed runtime - run: | - set -euo pipefail - node -e ' - const fs = require("fs"); - const expected = process.env.RUNTIME_VERSION; - const pkg = require("./node_modules/@github/copilot/package.json"); - if (pkg.version !== expected) { - console.error(`::error::Installed @github/copilot version ${pkg.version} does not match requested ${expected}`); - process.exit(1); - } - const dir = "./node_modules/@github"; - const entries = fs.readdirSync(dir).filter((d) => d.startsWith("copilot-")); - const plat = process.platform === "win32" ? "win32" : process.platform === "darwin" ? "darwin" : "linux"; - const arch = process.arch; - const match = entries.find((d) => d.includes(plat) && d.includes(arch)); - if (!match) { - console.error(`::error::No @github/copilot platform optional dep for ${plat}-${arch}. Present: ${entries.join(", ") || "(none)"}`); - process.exit(1); - } - const platPkg = require(`${dir}/${match}/package.json`); - if (platPkg.version !== expected) { - console.error(`::error::Platform package @github/${match} version ${platPkg.version} does not match requested ${expected}`); - process.exit(1); - } - console.log(`Verified @github/copilot@${pkg.version} with platform package @github/${match}@${platPkg.version}`); - ' - - - name: Build SDK - run: npm run build - - - name: Warm up PowerShell - if: runner.os == 'Windows' - run: pwsh.exe -Command "Write-Host 'PowerShell ready'" - - - name: Run Node.js SDK e2e tests - env: - COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} - run: npm test - - publish: - name: "Publish SDK canary (internal feed)" - needs: [resolve, test] - # Publish runs only when the gate permits it. Mode governs behavior: - # - tests-only: never publish (skips this job entirely). - # - publish: publish only when the e2e gate is green (the default for both - # the human and automated triggers). - # - publish-force: publish even on a non-green gate — a human-acknowledged - # flake override, audited via the ::warning:: step below and the run actor. - # publish-force only skips the e2e *signal* — the publish job still runs the - # build (so a broken build can't publish) and enforces the feed-only guards. - if: > - !cancelled() && - github.event.repository.fork == false && - needs.resolve.result == 'success' && - needs.resolve.outputs.PUBLISH_MODE != 'tests-only' && - (needs.test.result == 'success' || - needs.resolve.outputs.PUBLISH_MODE == 'publish-force') - environment: cicd - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - env: - RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }} - defaults: - run: - shell: bash - working-directory: ./nodejs - steps: - - name: Warn — publishing despite failed e2e gate (publish-force) - # always() so this audit is never skipped by prior-step status; it fires - # specifically when publish proceeded on a non-green gate via publish-force. - # Runs at the workspace root because it executes before checkout, so the - # job's default working-directory (./nodejs) does not exist yet. - if: always() && needs.test.result != 'success' && needs.resolve.outputs.PUBLISH_MODE == 'publish-force' - working-directory: ${{ github.workspace }} - run: | - echo "::warning title=e2e gate bypassed::Publishing SDK canary despite a non-passing e2e gate (test job result: ${{ needs.test.result }}) via publish-force. Triggered by '${{ github.actor }}' through '${{ github.event_name }}'. The e2e signal was bypassed; build + feed-only guards still apply." - - - uses: actions/checkout@v6.0.2 - - - uses: actions/setup-node@v6 - with: - node-version: 22 - - # Default public registry: installs build deps and the currently pinned - # runtime. Do NOT write any feed .npmrc or scoped @github:registry line - # here, or npm ci would try to fetch the runtime from the upstream-less - # feed and 404. - - name: Install SDK dependencies - run: npm ci --ignore-scripts - - - name: Compute SDK canary version - id: sdkver - env: - RUN_NUMBER: ${{ github.run_number }} - SHA: ${{ github.sha }} - run: | - set -euo pipefail - SHORT_SHA="${SHA:0:7}" - # Base the canary on the NEXT patch of the public SDK latest so canaries - # correlate with public releases: they sort ABOVE the current public - # latest and BELOW the eventual real release of that next patch (a - # prerelease of X.Y.Z always sorts below X.Y.Z), so a canary can never - # shadow the real release when it ships. - # Reuse the repo's own version helper (scripts/get-version.js) so this - # stays consistent with publish.yml: `current` returns the latest public - # dist-tag version, read-only from public npm (never the feed), then - # we bump the patch ourselves to keep strict patch+1 semantics. - PUBLIC_LATEST="$(node scripts/get-version.js current || true)" - BASE="${PUBLIC_LATEST%%-*}"; BASE="${BASE%%+*}" - if [[ "$BASE" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - NEXT="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$(( BASH_REMATCH[3] + 1 ))" - else - echo "::error::Could not resolve public SDK latest version (got '$PUBLIC_LATEST'); refusing to publish a canary with an unknown base." - exit 1 - fi - SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.g${SHORT_SHA}" - if [[ ! "$SDK_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then - echo "::error::Computed SDK canary version '$SDK_VERSION' is not valid semver." - exit 1 - fi - echo "SDK canary version: $SDK_VERSION" - echo "SDK_VERSION=$SDK_VERSION" >> "$GITHUB_OUTPUT" - - - name: Set package version and pin runtime dependency - env: - SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }} - run: | - set -euo pipefail - npm version "$SDK_VERSION" --no-git-tag-version --allow-same-version - # Exact pin (no caret) so the published SDK canary depends on precisely - # the runtime version that was just tested by the e2e gate. - npm pkg set "dependencies.@github/copilot=$RUNTIME_VERSION" - echo "Pinned @github/copilot to $(npm pkg get dependencies.@github/copilot)" - - - name: Build SDK - run: npm run build - - - name: Azure Login (OIDC -> id-cpd-ci) - uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 - with: - client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci - tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" - allow-no-subscriptions: true - - # Auth-only .npmrc: just the two token lines, NO scoped registry line. - # The publish target is supplied explicitly via publishConfig + --registry. - - name: Configure feed auth (.npmrc) - run: | - set -euo pipefail - TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" - echo "::add-mask::$TOKEN" - # Derive the protocol-relative auth scopes from FEED_URL (single source - # of truth). NO scoped @github:registry line here — publish target is - # supplied explicitly via publishConfig + --registry. - FEED_AUTH_REGISTRY="${FEED_URL#https:}" - FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" - printf '%s\n' \ - "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ - "${FEED_AUTH_BASE}:_authToken=${TOKEN}" > .npmrc - echo "Wrote auth-only .npmrc to ./nodejs" - - # Belt and suspenders (2 of 3): pin the publish target in the package too. - - name: Set publishConfig registry - run: npm pkg set "publishConfig.registry=$FEED_URL" - - # Belt and suspenders (3 of 3): fail loudly unless the effective publish - # target is the internal feed. Guards against ever reaching public npm. - - name: Assert publish target is the internal feed - run: | - set -euo pipefail - EFFECTIVE="$(npm pkg get publishConfig.registry | tr -d '"')" - echo "Effective publishConfig.registry: $EFFECTIVE" - if [ "$EFFECTIVE" != "$FEED_URL" ]; then - echo "::error::publishConfig.registry ('$EFFECTIVE') is not the internal feed ('$FEED_URL'). Refusing to publish." - exit 1 - fi - - - name: Publish SDK canary to internal feed - run: npm publish --registry "$FEED_URL" - - - name: Summarize published canary - env: - SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }} - run: | - set -euo pipefail - { - echo "## SDK canary published" - echo "" - echo "| | |" - echo "| --- | --- |" - echo "| Runtime consumed | \`@github/copilot@${RUNTIME_VERSION}\` |" - echo "| Canary SDK produced | \`@github/copilot-sdk@${SDK_VERSION}\` |" - echo "| Feed | ${FEED_URL} |" - } >> "$GITHUB_STEP_SUMMARY" +name: "SDK Canary Test/Publish" + +# Nightly-style canary pipeline. First installs an explicit version of the +# @github/copilot runtime, builds the Node SDK, and runs the Node e2e suite +# against it to prove runtime <-> SDK compatibility. When that gate passes (and +# mode allows), publishes an SDK canary pinned to the tested runtime to the +# internal Azure Artifacts feed only (never public npm). + +env: + HUSKY: 0 + # Internal org-scoped Azure Artifacts feed — single source of truth so the + # feed name isn't repeated across steps. The SDK canary publishes here and + # (when runtime_source=internal) installs the runtime from here; it must NEVER + # reach public npm (@github/copilot-sdk is a live public package). + FEED_URL: https://pkgs.dev.azure.com/devdiv/_packaging/copilot-canary/npm/registry/ + # Azure DevOps resource ID used to mint an ADO access token for the feed. + ADO_RESOURCE: 499b84ac-1321-427f-aa17-267ca6975798 + +on: + workflow_dispatch: + inputs: + runtime_version: + description: "Exact github/copilot-cli release (public) or @github/copilot package version (internal)" + required: true + type: string + runtime_source: + description: "Where to install the runtime from" + required: true + type: choice + options: + - public + - internal + default: public + mode: + description: "publish (tests must pass), publish-force (publish even if tests fail), or tests-only (run gate, never publish)" + required: false + type: choice + default: publish + options: + - publish + - publish-force + - tests-only + repository_dispatch: + types: [runtime-canary] + +permissions: + contents: read + id-token: write + +# Serialize runs per ref so two overlapping canary runs can't race the feed +# publish. cancel-in-progress: false — never kill an in-flight publish. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + resolve: + name: "Resolve runtime inputs" + if: github.event.repository.fork == false + runs-on: ubuntu-latest + permissions: {} + outputs: + RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} + RUNTIME_SOURCE: ${{ steps.normalize.outputs.RUNTIME_SOURCE }} + PUBLISH_MODE: ${{ steps.normalize.outputs.PUBLISH_MODE }} + steps: + # Normalize whichever trigger fired into a single (RUNTIME_VERSION, + # RUNTIME_SOURCE, PUBLISH_MODE) triple that every downstream step + # references. workflow_dispatch reads the human-supplied inputs; + # repository_dispatch reads client_payload and forces source=internal + # (a runtime canary only exists on the feed), defaulting mode to publish. + - name: Normalize inputs + id: normalize + env: + EVENT_NAME: ${{ github.event_name }} + INPUT_VERSION: ${{ inputs.runtime_version }} + INPUT_SOURCE: ${{ inputs.runtime_source }} + INPUT_MODE: ${{ inputs.mode }} + PAYLOAD_VERSION: ${{ github.event.client_payload.runtime_version }} + PAYLOAD_SOURCE: ${{ github.event.client_payload.runtime_source }} + PAYLOAD_MODE: ${{ github.event.client_payload.mode }} + run: | + set -euo pipefail + case "$EVENT_NAME" in + workflow_dispatch) + VERSION="$INPUT_VERSION" + SOURCE="$INPUT_SOURCE" + MODE="$INPUT_MODE" + ;; + repository_dispatch) + VERSION="$PAYLOAD_VERSION" + # A runtime canary only ever exists on the internal feed. + SOURCE="${PAYLOAD_SOURCE:-internal}" + MODE="${PAYLOAD_MODE:-publish}" + ;; + *) + echo "::error::Unsupported event '$EVENT_NAME'." + exit 1 + ;; + esac + if [ -z "$VERSION" ]; then echo "::error::Could not determine runtime version."; exit 1; fi + if [ -z "$SOURCE" ]; then SOURCE="public"; fi + case "$SOURCE" in + public|internal) ;; + *) echo "::error::Invalid runtime source '$SOURCE'. Expected one of: public, internal."; exit 1 ;; + esac + if [ -z "$MODE" ]; then MODE="publish"; fi + case "$MODE" in + publish|publish-force|tests-only) ;; + *) echo "::error::Invalid publish mode '$MODE'. Expected one of: publish, publish-force, tests-only."; exit 1 ;; + esac + echo "Resolved RUNTIME_VERSION=$VERSION RUNTIME_SOURCE=$SOURCE PUBLISH_MODE=$MODE" + echo "RUNTIME_VERSION=$VERSION" >> "$GITHUB_OUTPUT" + echo "RUNTIME_SOURCE=$SOURCE" >> "$GITHUB_OUTPUT" + echo "PUBLISH_MODE=$MODE" >> "$GITHUB_OUTPUT" + + - name: Validate runtime version (semver) + env: + RUNTIME_VERSION: ${{ steps.normalize.outputs.RUNTIME_VERSION }} + run: | + if [[ ! "$RUNTIME_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "::error::Invalid runtime version '$RUNTIME_VERSION'. Expected semver (e.g. 1.0.69 or 1.0.70-canary.abc123)." + exit 1 + fi + + test: + name: "E2E tests (${{ matrix.os }})" + needs: resolve + if: github.event.repository.fork == false + environment: cicd + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + env: + POWERSHELL_UPDATECHECK: Off + RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }} + RUNTIME_SOURCE: ${{ needs.resolve.outputs.RUNTIME_SOURCE }} + defaults: + run: + shell: bash + working-directory: ./nodejs + steps: + - uses: actions/checkout@v6.0.2 + + - uses: actions/setup-node@v6 + with: + cache: "npm" + cache-dependency-path: "./nodejs/package-lock.json" + node-version: 22 + + - name: Install SDK dependencies + run: npm ci --ignore-scripts + + - name: Install test harness dependencies + working-directory: ./test/harness + run: npm ci --ignore-scripts + + - name: Azure Login (OIDC -> id-cpd-ci) + if: env.RUNTIME_SOURCE == 'internal' + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci + tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" + allow-no-subscriptions: true + + # Route ONLY @github/* (the runtime + its platform packages) to the + # internal feed via a scoped registry. All other deps (e.g. detect-libc) + # still resolve from public npm. A global --registry would break because + # detect-libc is not on the feed. + - name: Configure canary feed (.npmrc) + if: env.RUNTIME_SOURCE == 'internal' + run: | + set -euo pipefail + TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" + echo "::add-mask::$TOKEN" + # Derive the protocol-relative auth scopes from FEED_URL so the feed + # name lives in exactly one place (the workflow-level env). + FEED_AUTH_REGISTRY="${FEED_URL#https:}" + FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" + NPMRC="$(printf '%s\n' \ + "@github:registry=${FEED_URL}" \ + "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ + "${FEED_AUTH_BASE}:_authToken=${TOKEN}")" + printf '%s\n' "$NPMRC" > .npmrc + echo "Wrote scoped @github registry .npmrc to ./nodejs" + + - name: Override runtime version + run: | + set -euo pipefail + if [ "$RUNTIME_SOURCE" = "internal" ]; then + echo "Installing internal @github/copilot@${RUNTIME_VERSION}" + npm install "@github/copilot@${RUNTIME_VERSION}" --save-exact --ignore-scripts + node scripts/set-cli-version.js "$RUNTIME_VERSION" --npm-package + else + echo "Pinning github/copilot-cli release ${RUNTIME_VERSION}" + node scripts/set-cli-version.js "$RUNTIME_VERSION" + npm install --ignore-scripts + fi + + - name: Verify release runtime + run: | + set -euo pipefail + runtime_path=$(npm run --silent prepare:runtime -- --print-path) + node -e " + const fs = require('node:fs'); + const path = require('node:path'); + const runtime = process.argv[1]; + const runtimeStat = fs.statSync(runtime); + if (!runtimeStat.isFile()) throw new Error('Runtime wrapper is not a file'); + if (process.platform !== 'win32' && (runtimeStat.mode & 0o111) === 0) { + throw new Error('Runtime wrapper is not executable'); + } + if (!fs.statSync(path.join(path.dirname(runtime), 'runtime.node')).isFile()) { + throw new Error('runtime.node is not adjacent to the runtime wrapper'); + } + " "$runtime_path" + legacy_path=$(npm run --silent prepare:runtime -- --print-legacy-path) + node "$legacy_path" --version | grep -F "$RUNTIME_VERSION" + echo "COPILOT_CLI_PATH=$runtime_path" >> "$GITHUB_ENV" + + - name: Build SDK + run: npm run build + + - name: Warm up PowerShell + if: runner.os == 'Windows' + run: pwsh.exe -Command "Write-Host 'PowerShell ready'" + + - name: Run Node.js SDK e2e tests + env: + COPILOT_HMAC_KEY: ${{ secrets.COPILOT_DEVELOPER_CLI_INTEGRATION_HMAC_KEY }} + run: npm test + + publish: + name: "Publish SDK canary (internal feed)" + needs: [resolve, test] + # Publish runs only when the gate permits it. Mode governs behavior: + # - tests-only: never publish (skips this job entirely). + # - publish: publish only when the e2e gate is green (the default for both + # the human and automated triggers). + # - publish-force: publish even on a non-green gate — a human-acknowledged + # flake override, audited via the ::warning:: step below and the run actor. + # publish-force only skips the e2e *signal* — the publish job still runs the + # build (so a broken build can't publish) and enforces the feed-only guards. + if: > + !cancelled() && + github.event.repository.fork == false && + needs.resolve.result == 'success' && + needs.resolve.outputs.PUBLISH_MODE != 'tests-only' && + (needs.test.result == 'success' || + needs.resolve.outputs.PUBLISH_MODE == 'publish-force') + environment: cicd + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + env: + RUNTIME_VERSION: ${{ needs.resolve.outputs.RUNTIME_VERSION }} + RUNTIME_SOURCE: ${{ needs.resolve.outputs.RUNTIME_SOURCE }} + defaults: + run: + shell: bash + working-directory: ./nodejs + steps: + - name: Warn — publishing despite failed e2e gate (publish-force) + # always() so this audit is never skipped by prior-step status; it fires + # specifically when publish proceeded on a non-green gate via publish-force. + # Runs at the workspace root because it executes before checkout, so the + # job's default working-directory (./nodejs) does not exist yet. + if: always() && needs.test.result != 'success' && needs.resolve.outputs.PUBLISH_MODE == 'publish-force' + working-directory: ${{ github.workspace }} + run: | + echo "::warning title=e2e gate bypassed::Publishing SDK canary despite a non-passing e2e gate (test job result: ${{ needs.test.result }}) via publish-force. Triggered by '${{ github.actor }}' through '${{ github.event_name }}'. The e2e signal was bypassed; build + feed-only guards still apply." + + - uses: actions/checkout@v6.0.2 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + + # Default public registry: installs build deps and the currently pinned + # runtime. Do NOT write any feed .npmrc or scoped @github:registry line + # here, or npm ci would try to fetch the runtime from the upstream-less + # feed and 404. + - name: Install SDK dependencies + run: npm ci --ignore-scripts + + - name: Compute SDK canary version + id: sdkver + env: + RUN_NUMBER: ${{ github.run_number }} + SHA: ${{ github.sha }} + run: | + set -euo pipefail + SHORT_SHA="${SHA:0:7}" + # Base the canary on the NEXT patch of the public SDK latest so canaries + # correlate with public releases: they sort ABOVE the current public + # latest and BELOW the eventual real release of that next patch (a + # prerelease of X.Y.Z always sorts below X.Y.Z), so a canary can never + # shadow the real release when it ships. + # Reuse the repo's own version helper (scripts/get-version.js) so this + # stays consistent with publish.yml: `current` returns the latest public + # dist-tag version, read-only from public npm (never the feed), then + # we bump the patch ourselves to keep strict patch+1 semantics. + PUBLIC_LATEST="$(node scripts/get-version.js current || true)" + BASE="${PUBLIC_LATEST%%-*}"; BASE="${BASE%%+*}" + if [[ "$BASE" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then + NEXT="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.$(( BASH_REMATCH[3] + 1 ))" + else + echo "::error::Could not resolve public SDK latest version (got '$PUBLIC_LATEST'); refusing to publish a canary with an unknown base." + exit 1 + fi + SDK_VERSION="${NEXT}-canary.${RUN_NUMBER}.g${SHORT_SHA}" + if [[ ! "$SDK_VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then + echo "::error::Computed SDK canary version '$SDK_VERSION' is not valid semver." + exit 1 + fi + echo "SDK canary version: $SDK_VERSION" + echo "SDK_VERSION=$SDK_VERSION" >> "$GITHUB_OUTPUT" + + - name: Set package and runtime versions + env: + SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }} + run: | + set -euo pipefail + npm version "$SDK_VERSION" --no-git-tag-version --allow-same-version + if [ "$RUNTIME_SOURCE" = "internal" ]; then + npm pkg set "dependencies.@github/copilot=$RUNTIME_VERSION" + node scripts/set-cli-version.js "$RUNTIME_VERSION" --npm-package + else + node scripts/set-cli-version.js "$RUNTIME_VERSION" + fi + echo "Pinned github/copilot-cli release to $(npm pkg get copilotCliVersion)" + + - name: Build SDK + run: npm run build + + - name: Package public release runtimes + if: env.RUNTIME_SOURCE == 'public' + run: npm run pack:release + + - name: Azure Login (OIDC -> id-cpd-ci) + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: "${{ vars.CPD_ID_CLIENT_ID }}" # id-cpd-ci + tenant-id: "${{ vars.CPD_ID_TENANT_ID }}" + allow-no-subscriptions: true + + # Auth-only .npmrc: just the two token lines, NO scoped registry line. + # The publish target is supplied explicitly via publishConfig + --registry. + - name: Configure feed auth (.npmrc) + run: | + set -euo pipefail + TOKEN="$(az account get-access-token --resource "$ADO_RESOURCE" --query accessToken -o tsv)" + echo "::add-mask::$TOKEN" + # Derive the protocol-relative auth scopes from FEED_URL (single source + # of truth). NO scoped @github:registry line here — publish target is + # supplied explicitly via publishConfig + --registry. + FEED_AUTH_REGISTRY="${FEED_URL#https:}" + FEED_AUTH_BASE="${FEED_AUTH_REGISTRY%registry/}" + printf '%s\n' \ + "${FEED_AUTH_REGISTRY}:_authToken=${TOKEN}" \ + "${FEED_AUTH_BASE}:_authToken=${TOKEN}" > .npmrc + echo "Wrote auth-only .npmrc to ./nodejs" + + # Belt and suspenders (2 of 3): pin the publish target in the package too. + - name: Set publishConfig registry + run: npm pkg set "publishConfig.registry=$FEED_URL" + + # Belt and suspenders (3 of 3): fail loudly unless the effective publish + # target is the internal feed. Guards against ever reaching public npm. + - name: Assert publish target is the internal feed + run: | + set -euo pipefail + EFFECTIVE="$(npm pkg get publishConfig.registry | tr -d '"')" + echo "Effective publishConfig.registry: $EFFECTIVE" + if [ "$EFFECTIVE" != "$FEED_URL" ]; then + echo "::error::publishConfig.registry ('$EFFECTIVE') is not the internal feed ('$FEED_URL'). Refusing to publish." + exit 1 + fi + + - name: Publish SDK canary to internal feed + run: | + set -euo pipefail + if [ "$RUNTIME_SOURCE" = "internal" ]; then + node scripts/npm-release.js publish . canary "$FEED_URL" azure + exit + fi + shopt -s nullglob + TARBALLS=(./github-copilot-sdk-*.tgz) + if [ "${#TARBALLS[@]}" -ne 9 ]; then + echo "::error::Expected nine Node.js package tarballs, found ${#TARBALLS[@]}." + exit 1 + fi + MAIN_TARBALL="" + for TARBALL in "${TARBALLS[@]}"; do + PACKAGE_NAME="$(tar -xOf "$TARBALL" package/package.json | jq -r .name)" + if [ "$PACKAGE_NAME" = "@github/copilot-sdk" ]; then + MAIN_TARBALL="$TARBALL" + else + node scripts/npm-release.js publish "$TARBALL" canary "$FEED_URL" azure + fi + done + if [ -z "$MAIN_TARBALL" ]; then + echo "::error::Main @github/copilot-sdk tarball not found." + exit 1 + fi + node scripts/npm-release.js publish "$MAIN_TARBALL" canary "$FEED_URL" azure + + - name: Summarize published canary + env: + SDK_VERSION: ${{ steps.sdkver.outputs.SDK_VERSION }} + run: | + set -euo pipefail + { + echo "## SDK canary published" + echo "" + echo "| | |" + echo "| --- | --- |" + if [ "$RUNTIME_SOURCE" = "public" ]; then + echo "| Runtime consumed | \`github/copilot-cli@${RUNTIME_VERSION}\` release assets |" + else + echo "| Runtime consumed | \`@github/copilot@${RUNTIME_VERSION}\` |" + fi + echo "| Canary SDK produced | \`@github/copilot-sdk@${SDK_VERSION}\` |" + echo "| Feed | ${FEED_URL} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/sdk-consistency-review.lock.yml b/.github/workflows/sdk-consistency-review.lock.yml index bc33be9ad1..cf977b5d53 100644 --- a/.github/workflows/sdk-consistency-review.lock.yml +++ b/.github/workflows/sdk-consistency-review.lock.yml @@ -1,6 +1,6 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"fb73d13f101fc375308576a64180f63934cc9e8306cb6ef6303f1b9788d9df28","body_hash":"cc60c817de34cdb662ae4c091203c67a5ef240ca0165b0cd26a842f03b22614f","compiler_version":"v0.83.1","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.73"}} -# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0","version":"v7"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"8bdba8075360648fe6802302a5b4e016361dc6ac","version":"v0.83.1"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38","digest":"sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38","digest":"sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38","digest":"sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.3","digest":"sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b","pinned_image":"ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b"},{"image":"ghcr.io/github/github-mcp-server:v1.6.0","digest":"sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3","pinned_image":"ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3"}],"has_pull_request":true} -# This file was automatically generated by gh-aw (v0.83.1). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"79b4a25722897db4bbdb7fa862f579bd3c37e12d0ce147e46d618ab79bce67c4","body_hash":"cc60c817de34cdb662ae4c091203c67a5ef240ca0165b0cd26a842f03b22614f","compiler_version":"v0.88.2","strict":true,"agent_id":"copilot","agent_model":"${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","detection_agent_id":"copilot","detection_agent_model":"${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}","engine_versions":{"copilot":"1.0.80"}} +# gh-aw-manifest: {"version":1,"secrets":["GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/cache/restore","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/cache/save","sha":"55cc8345863c7cc4c66a329aec7e433d2d1c52a9","version":"v6.1.0"},{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"373c709c69115d41ff229c7e5df9f8788daa9553","version":"v9"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"9271a1804551c0dc4fb0085a97979950aa2f8489","version":"v0.88.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12","digest":"sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12","digest":"sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12","digest":"sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.15","digest":"sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e","pinned_image":"ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e"},{"image":"ghcr.io/github/github-mcp-server:v1.11.0","digest":"sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699","pinned_image":"ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699"}],"has_pull_request":true,"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["add_comment","create_pull_request_review_comment","missing_data","missing_tool","noop"]}]} +# This file was automatically generated by gh-aw (v0.88.2). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # # ___ _ _ # / _ \ | | (_) @@ -26,7 +26,7 @@ # Reviews PRs to ensure features are implemented consistently across all SDK language implementations # # Secrets used: -# - COPILOT_GITHUB_TOKEN +# - GH_AW_DEFAULT_OTLP_HEADERS # - GH_AW_GITHUB_MCP_SERVER_TOKEN # - GH_AW_GITHUB_TOKEN # - GITHUB_TOKEN @@ -34,22 +34,20 @@ # Custom actions used: # - actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 # - actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 -# - actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 +# - actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # - actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 # - actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 # - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 -# - actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 # - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 -# - github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 +# - github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 # # Container images used: -# - ghcr.io/github/gh-aw-firewall/agent:0.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 -# - ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c -# - ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 -# - ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 -# - ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b -# - ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 +# - ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 +# - ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 +# - ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f +# - ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e +# - ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e +# - ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 name: "SDK Consistency Review Agent" on: @@ -88,9 +86,20 @@ concurrency: run-name: "SDK Consistency Review Agent" +env: + OTEL_EXPORTER_OTLP_ENDPOINT: ${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }} + OTEL_SERVICE_NAME: gh-aw.sdk-consistency-review + OTEL_RESOURCE_ATTRIBUTES: 'gh-aw.workflow.name=SDK%20Consistency%20Review%20Agent,gh-aw.repository=${{ github.repository }},gh-aw.run.id=${{ github.run_id }},github.run_id=${{ github.run_id }},gh-aw.engine.id=copilot' + OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }} + GH_AW_OTLP_ENDPOINTS: '[{"url":"${{ vars.GH_AW_DEFAULT_OTLP_ENDPOINT }}","headers":"${{ secrets.GH_AW_DEFAULT_OTLP_HEADERS }}"}]' + GH_AW_OTLP_IF_MISSING: ignore + jobs: activation: - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id + if: > + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.id == github.repository_id) && + ((github.event_name != 'pull_request' && github.event_name != 'pull_request_review') || github.event.pull_request.stack == null || + github.event.pull_request.stack.position == github.event.pull_request.stack.size) runs-on: ubuntu-slim permissions: actions: read @@ -103,6 +112,7 @@ jobs: comment_id: "" comment_repo: "" daily_ai_credits_exceeded: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_exceeded == 'true' }} + daily_ai_credits_guardrail_status: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_guardrail_status || '' }} daily_ai_credits_threshold: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_threshold || '' }} daily_ai_credits_total_effective_tokens: ${{ steps.daily-effective-workflow-guardrail.outputs.daily_ai_credits_total_effective_tokens || '' }} engine_id: ${{ steps.generate_aw_info.outputs.engine_id }} @@ -118,7 +128,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -126,34 +136,39 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - 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: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AGENT_VERSION: "1.0.73" - GH_AW_INFO_CLI_VERSION: "v0.83.1" + GH_AW_INFO_MODEL: "${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AGENT_VERSION: "1.0.80" + GH_AW_INFO_CLI_VERSION: "v0.88.2" GH_AW_INFO_WORKFLOW_NAME: "SDK Consistency Review Agent" 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.27.38" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_AWMG_VERSION: "" GH_AW_INFO_FIREWALL_TYPE: "squid" + GH_AW_INFO_AGENT_RUNTIME: "" 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_aw_info.cjs'); + const { main } = require(path.join(actionsDir, 'generate_aw_info.cjs')); await main(core, context); - name: Restore daily AIC usage cache id: restore-daily-aic-cache @@ -175,9 +190,11 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/restore_aic_usage_cache_fallback.cjs'); + const { main } = require(path.join(actionsDir, 'restore_aic_usage_cache_fallback.cjs')); await main(); - name: Check daily workflow token guardrail id: daily-effective-workflow-guardrail @@ -195,38 +212,37 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_daily_aic_workflow_guardrail.cjs'); + const { main } = require(path.join(actionsDir, 'check_daily_aic_workflow_guardrail.cjs')); await main(); - name: Check for OAuth tokens id: check-oauth-tokens run: bash "${RUNNER_TEMP}/gh-aw/actions/check_oauth_tokens.sh" env: - COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }} GH_AW_GITHUB_TOKEN: ${{ secrets.GH_AW_GITHUB_TOKEN }} GH_AW_GITHUB_MCP_SERVER_TOKEN: ${{ secrets.GH_AW_GITHUB_MCP_SERVER_TOKEN }} - name: Checkout .github and .agents folders - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false sparse-checkout: | .github .agents - .antigravity .claude .codex .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 .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "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" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" + 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 @@ -235,38 +251,47 @@ jobs: GH_AW_CONTEXT_WORKFLOW_REF: "${{ github.workflow_ref }}" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_workflow_timestamp_api.cjs'); + const { main } = require(path.join(actionsDir, 'check_workflow_timestamp_api.cjs')); await main(); - name: Check compile-agentic version uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_COMPILED_VERSION: "v0.83.1" + GH_AW_COMPILED_VERSION: "v0.88.2" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/check_version_updates.cjs'); + const { main } = require(path.join(actionsDir, 'check_version_updates.cjs')); await main(); - name: Compute current body text id: sanitized uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.com,ts-crl.ws.symantec.com,ts-ocsp.ws.symantec.com,www.googleapis.com" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/compute_text.cjs'); + const { main } = require(path.join(actionsDir, 'compute_text.cjs')); await main(); - name: Log runtime features if: ${{ contains(toJSON(vars), '"GH_AW_RUNTIME_FEATURES":') }} run: bash "${RUNNER_TEMP}/gh-aw/actions/log_runtime_features_summary.sh" - name: Create prompt with built-in context + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt + GH_AW_ACTIONS_DIR: ${{ runner.temp }}/gh-aw/actions + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_SAFE_OUTPUTS: ${{ runner.temp }}/gh-aw/safeoutputs/outputs.jsonl + GH_AW_PROMPT_CONFIG: "{\"items\":[{\"content_env\":\"GH_AW_PROMPT_CONTENT_0000\"},{\"file\":\"xpia.md\"},{\"file\":\"temp_folder_prompt.md\"},{\"file\":\"markdown.md\"},{\"file\":\"safe_outputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0001\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0002\"},{\"file\":\"mcp_cli_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0003\"},{\"file\":\"github_mcp_tools_with_safeoutputs_prompt.md\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0004\"},{\"content_env\":\"GH_AW_PROMPT_CONTENT_0005\"}]}" 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) }} @@ -277,77 +302,38 @@ jobs: GH_AW_GITHUB_RUN_ID: ${{ github.run_id }} GH_AW_GITHUB_WORKSPACE: ${{ github.workspace }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} - # poutine:ignore untrusted_checkout_exec - run: | - bash "${RUNNER_TEMP}/gh-aw/actions/create_prompt_first.sh" - { - cat << 'GH_AW_PROMPT_96d45caa4ffc7593_EOF' - - GH_AW_PROMPT_96d45caa4ffc7593_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_96d45caa4ffc7593_EOF' - - Tools: add_comment, create_pull_request_review_comment(max:10), missing_tool, missing_data, noop - - GH_AW_PROMPT_96d45caa4ffc7593_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/mcp_cli_tools_prompt.md" - cat << 'GH_AW_PROMPT_96d45caa4ffc7593_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_96d45caa4ffc7593_EOF - cat "${RUNNER_TEMP}/gh-aw/prompts/github_mcp_tools_with_safeoutputs_prompt.md" - cat << 'GH_AW_PROMPT_96d45caa4ffc7593_EOF' - - {{#runtime-import .github/workflows/sdk-consistency-review.md}} - GH_AW_PROMPT_96d45caa4ffc7593_EOF - } > "$GH_AW_PROMPT" + GH_AW_PROMPT_CONTENT_0000: "\n" + GH_AW_PROMPT_CONTENT_0001: "\nTools: add_comment, create_pull_request_review_comment(max:10), missing_tool, missing_data, noop\n" + GH_AW_PROMPT_CONTENT_0002: "\n" + GH_AW_PROMPT_CONTENT_0003: "\nThe following GitHub context information is available for this workflow:\n{{#if github.actor}}\n- **actor**: __GH_AW_GITHUB_ACTOR__\n{{/if}}\n{{#if github.repository}}\n- **repository**: __GH_AW_GITHUB_REPOSITORY__\n{{/if}}\n{{#if github.workspace}}\n- **workspace**: __GH_AW_GITHUB_WORKSPACE__\n{{/if}}\n{{#if github.event.issue.number || (github.aw.context.item_type == 'issue' && github.aw.context.item_number)}}\n- **issue-number**: #__GH_AW_EXPR_802A9F6A__\n{{/if}}\n{{#if github.event.discussion.number || (github.aw.context.item_type == 'discussion' && github.aw.context.item_number)}}\n- **discussion-number**: #__GH_AW_EXPR_1A3A194A__\n{{/if}}\n{{#if github.event.pull_request.number || (github.aw.context.item_type == 'pull_request' && github.aw.context.item_number)}}\n- **pull-request-number**: #__GH_AW_EXPR_463A214A__\n{{/if}}\n{{#if github.event.comment.id || github.aw.context.comment_id}}\n- **comment-id**: __GH_AW_EXPR_FF1D34CE__\n{{/if}}\n{{#if github.run_id}}\n- **workflow-run-id**: __GH_AW_GITHUB_RUN_ID__\n{{/if}}\n\n\n" + GH_AW_PROMPT_CONTENT_0004: "\n" + GH_AW_PROMPT_CONTENT_0005: "{{#runtime-import .github/workflows/sdk-consistency-review.md}}\n" + with: + script: | + const { setupGlobals } = require(process.env.GH_AW_ACTIONS_DIR + '/setup_globals.cjs'); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(process.env.GH_AW_ACTIONS_DIR + '/create_prompt.cjs'); + await main(core); - 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_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt GH_AW_ENGINE_ID: "copilot" GH_AW_EXPR_A0E5D436: ${{ github.event.pull_request.number || inputs.pr_number }} GH_AW_GITHUB_REPOSITORY: ${{ github.repository }} GH_AW_INPUTS_PR_NUMBER: ${{ inputs.pr_number }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/interpolate_prompt.cjs'); + const { main } = require(path.join(actionsDir, '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_PROMPT: ${{ runner.temp }}/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) }} @@ -361,10 +347,12 @@ jobs: GH_AW_MCP_CLI_SERVERS_LIST: "- `github` — run `github --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const substitutePlaceholders = require('${{ runner.temp }}/gh-aw/actions/substitute_placeholders.cjs'); + const substitutePlaceholders = require(path.join(actionsDir, 'substitute_placeholders.cjs')); // Call the substitution function return await substitutePlaceholders({ @@ -385,16 +373,20 @@ jobs: }); - 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + 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" + GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/print_prompt_summary.sh" + - name: Stage prompt files for artifact upload + run: | + mkdir -p /tmp/gh-aw/aw-prompts + cp -a "${RUNNER_TEMP}/gh-aw/aw-prompts/." /tmp/gh-aw/aw-prompts/ - name: Upload activation artifact - if: success() + if: success() || failure() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: activation @@ -421,12 +413,19 @@ jobs: copilot-requests: write issues: read pull-requests: read + timeout-minutes: 60 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_PR_HEAD_BASE_BRANCH: "" + GH_AW_PR_HEAD_BASE_PR_NUMBER: "" + GH_AW_PR_HEAD_BASE_REF: "" + GH_AW_PR_HEAD_BASE_REPO: "" + GH_AW_PR_HEAD_BASE_SHA: "" + GH_AW_PR_HEAD_REPO: "" GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_WORKFLOW_ID_SANITIZED: sdkconsistencyreview outputs: @@ -440,7 +439,10 @@ jobs: http_400_response_error: ${{ steps.detect-agent-errors.outputs.http_400_response_error || 'false' }} inference_access_error: ${{ steps.detect-agent-errors.outputs.inference_access_error || 'false' }} invocation_cap_exceeded: ${{ steps.detect-agent-errors.outputs.invocation_cap_exceeded || 'false' }} + max_cache_misses_exceeded: ${{ steps.detect-agent-errors.outputs.max_cache_misses_exceeded || 'false' }} mcp_policy_error: ${{ steps.detect-agent-errors.outputs.mcp_policy_error || 'false' }} + missing_model_pricing_error: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_error || 'false' }} + missing_model_pricing_model_name: ${{ steps.detect-agent-errors.outputs.missing_model_pricing_model_name || '' }} 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 }} @@ -448,11 +450,12 @@ jobs: 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 }} + shell_expansion_guard_rejected: ${{ steps.detect-agent-errors.outputs.shell_expansion_guard_rejected || 'false' }} unknown_model_ai_credits: ${{ steps.parse-mcp-gateway.outputs.unknown_model_ai_credits || 'false' }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -461,19 +464,28 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" - name: Set runtime paths id: set-runtime-paths + env: + GH_AW_RUNNER_TOOL_CACHE: ${{ runner.tool_cache }} run: | + if [ -z "${RUNNER_TOOL_CACHE:-}" ]; then + echo "RUNNER_TOOL_CACHE=${GH_AW_RUNNER_TOOL_CACHE}" >> "$GITHUB_ENV" + fi { 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: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" + - name: Check OTLP telemetry configuration + run: bash "${RUNNER_TEMP}/gh-aw/actions/check_otlp_default_credentials.sh" - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Create gh-aw temp directory @@ -503,16 +515,19 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/checkout_pr_branch.cjs'); + const { main } = require(path.join(actionsDir, 'checkout_pr_branch.cjs')); await main(); - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com + GH_AW_COMPILED_VERSION: v0.88.2 - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 --rootless + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Determine automatic lockdown mode for GitHub MCP Server id: determine-automatic-lockdown uses: actions/github-script@373c709c69115d41ff229c7e5df9f8788daa9553 # v9 @@ -521,13 +536,15 @@ jobs: 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const determineAutomaticLockdown = require(path.join(actionsDir, 'determine_automatic_lockdown.cjs')); await determineAutomaticLockdown(github, context, core); - name: Restore agent config folders from base branch if: steps.checkout-pr.outcome == 'success' env: - GH_AW_AGENT_FOLDERS: ".agents .antigravity .claude .codex .gemini .github .opencode .pi" - GH_AW_AGENT_FILES: "AGENTS.md ANTIGRAVITY.md CLAUDE.md GEMINI.md PI.md opencode.jsonc" + GH_AW_AGENT_FOLDERS: ".agents .github" + GH_AW_AGENT_FILES: "AGENTS.md" run: bash "${RUNNER_TEMP}/gh-aw/actions/restore_base_github_folders.sh" - name: Restore inline sub-agents from activation artifact env: @@ -539,15 +556,26 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 ghcr.io/github/gh-aw-mcpg:v0.4.3@sha256:3c744710ea275cd5ee65db92a1099e0d980754bd9fafda9ce67704c67004dc83 ghcr.io/github/gh-aw-node@sha256:529d02eb970b1161aa25c593a9c3df57fdfad5a8add328cb3b6eccef66f3183b ghcr.io/github/github-mcp-server:v1.6.0@sha256:2b0c48b070f61e9d3969269ead600f62d00fb237b60ac849ef3d166ee7de9ad3 - - name: Generate Safe Outputs Config + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f ghcr.io/github/gh-aw-mcpg:v0.4.15@sha256:60cd97533e93d8e7be36b979c0f08a70846189bda6190f28bbd6d427bc0d9b6e ghcr.io/github/gh-aw-node@sha256:bac2192f6374d6262116399b34fc5e143d576f82719e90a18261cae7480f4d4e ghcr.io/github/github-mcp-server:v1.11.0@sha256:fbec75de11c255213fa08d80fb166abe73d851fff631c51c0079872967720699 + - name: Prepare Safe Outputs Directories 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_05b64a640c1d5c26_EOF' - {"add_comment":{"hide_older_comments":true,"max":1},"create_pull_request_review_comment":{"max":10,"side":"RIGHT"},"create_report_incomplete_issue":{},"missing_data":{},"missing_tool":{},"noop":{"max":1,"report-as-issue":"true"},"report_incomplete":{}} - GH_AW_SAFE_OUTPUTS_CONFIG_05b64a640c1d5c26_EOF + - name: Generate Safe Outputs Config + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_FILE_ROOT: "${{ runner.temp }}/gh-aw" + GH_AW_FILE_CONFIG: "{\"files\":[{\"path\":\"safeoutputs/config.json\",\"content_env\":\"GH_AW_SAFE_OUTPUTS_CONFIG\"}]}" + GH_AW_SAFE_OUTPUTS_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"report_incomplete\":{}}" + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'create_files.cjs')); + await main(); - name: Generate Safe Outputs Tools env: GH_AW_TOOLS_META_JSON: | @@ -570,9 +598,18 @@ jobs: "sanitize": true, "maxLength": 65000 }, + "comment_id": { + "optionalPositiveInteger": true + }, "item_number": { "issueOrPRNumber": true }, + "pr": { + "issueOrPRNumber": true + }, + "pr_number": { + "issueOrPRNumber": true + }, "reply_to_id": { "type": "string", "maxLength": 256 @@ -580,6 +617,16 @@ jobs: "repo": { "type": "string", "maxLength": 256 + }, + "target": { + "type": "string", + "enum": [ + "status" + ] + }, + "temporary_id": { + "type": "string", + "pattern": "^#?aw_[A-Za-z0-9_]{3,12}$" } } }, @@ -697,9 +744,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/generate_safe_outputs_tools.cjs'); + const { main } = require(path.join(actionsDir, 'generate_safe_outputs_tools.cjs')); await main(); - name: Start MCP Gateway id: start-mcp-gateway @@ -708,6 +757,7 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} GH_AW_SAFE_OUTPUTS_CONFIG_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_CONFIG_PATH }} GH_AW_SAFE_OUTPUTS_TOOLS_PATH: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS_TOOLS_PATH }} + GH_AW_SINK_VISIBILITY: ${{ steps.determine-automatic-lockdown.outputs.visibility }} 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 }} @@ -715,33 +765,45 @@ jobs: run: | set -eo pipefail mkdir -p "${RUNNER_TEMP}/gh-aw/mcp-config" + if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -r "${GITHUB_EVENT_PATH}" ]; then + GH_AW_SAFEOUTPUTS_EVENT_PATH="${RUNNER_TEMP}/gh-aw/safeoutputs/github_event.json" + cp "${GITHUB_EVENT_PATH}" "${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + export GITHUB_EVENT_PATH="${GH_AW_SAFEOUTPUTS_EVENT_PATH}" + fi # Export gateway environment variables for MCP config and gateway script export MCP_GATEWAY_PORT="8080" export MCP_GATEWAY_DOMAIN="awmg-mcpg" 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 + MCP_GATEWAY_AGENT_ID=$(openssl rand -base64 45 | tr -d '/+=') + echo "::add-mask::${MCP_GATEWAY_AGENT_ID}" + export MCP_GATEWAY_AGENT_ID export MCP_GATEWAY_PAYLOAD_DIR="/tmp/gh-aw/mcp-payloads" mkdir -p "${MCP_GATEWAY_PAYLOAD_DIR}" export MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD="524288" + export MCP_GATEWAY_ALLOWED_MOUNT_ROOTS="${GITHUB_WORKSPACE}:rw,${RUNNER_TEMP}/gh-aw:ro,${RUNNER_TEMP}/gh-aw/safeoutputs:rw,/opt:ro,/tmp:rw,/usr/bin/gh:ro" + export GH_AW_PR_HEAD_BASE_BRANCH="${GH_AW_PR_HEAD_BASE_BRANCH:-}" + export GH_AW_PR_HEAD_BASE_SHA="${GH_AW_PR_HEAD_BASE_SHA:-}" + export GH_AW_PR_HEAD_BASE_REPO="${GH_AW_PR_HEAD_BASE_REPO:-}" + export GH_AW_PR_HEAD_BASE_PR_NUMBER="${GH_AW_PR_HEAD_BASE_PR_NUMBER:-}" + export GH_AW_PR_HEAD_BASE_REF="${GH_AW_PR_HEAD_BASE_REF:-}" + export GH_AW_PR_HEAD_REPO="${GH_AW_PR_HEAD_REPO:-}" 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') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" - export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_POLICY_ALLOW_CREATE_PULL_REQUEST -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 RUNNER_TEMP -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.3' + export MCP_GATEWAY_DOCKER_COMMAND='docker run -i --rm --network bridge -p 127.0.0.1:'"${MCP_GATEWAY_PORT}"':'"${MCP_GATEWAY_PORT}"' --name awmg-mcpg --add-host host.docker.internal:host-gateway --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_AGENT_ID -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_PR_HEAD_BASE_BRANCH -e GH_AW_PR_HEAD_BASE_SHA -e GH_AW_PR_HEAD_BASE_REPO -e GH_AW_PR_HEAD_BASE_PR_NUMBER -e GH_AW_PR_HEAD_BASE_REF -e GH_AW_PR_HEAD_REPO -e GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST -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 GH_AW_SINK_VISIBILITY -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 RUNNER_TEMP -e RUNNER_TOOL_CACHE -e MCP_GATEWAY_ALLOWED_MOUNT_ROOTS -e GITHUB_AW_OTEL_TRACE_ID -e GITHUB_AW_OTEL_PARENT_SPAN_ID -e OTEL_EXPORTER_OTLP_HEADERS -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 -v '"${RUNNER_TEMP}"'/gh-aw/safeoutputs:'"${RUNNER_TEMP}"'/gh-aw/safeoutputs:rw ghcr.io/github/gh-aw-mcpg:v0.4.15' mkdir -p "$HOME/.copilot" GH_AW_NODE=$(which node 2>/dev/null || command -v node 2>/dev/null || echo node) - cat << GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF | "$GH_AW_NODE" "${RUNNER_TEMP}/gh-aw/actions/start_mcp_gateway.cjs" + cat << GH_AW_MCP_CONFIG_46604863f3d8e286_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.6.0", + "container": "ghcr.io/github/github-mcp-server:v1.11.0", "env": { "GITHUB_FEATURES": "fields_param", "GITHUB_HOST": "${GITHUB_SERVER_URL}", @@ -774,6 +836,14 @@ jobs: "GH_AW_SAFE_OUTPUTS_CONFIG_PATH": "\${GH_AW_SAFE_OUTPUTS_CONFIG_PATH}", "GH_AW_SAFE_OUTPUTS_TOOLS_PATH": "\${GH_AW_SAFE_OUTPUTS_TOOLS_PATH}", "GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST": "\${GH_AW_POLICY_ALLOW_CREATE_PULL_REQUEST}", + "GH_AW_PR_HEAD_BASE_BRANCH": "\${GH_AW_PR_HEAD_BASE_BRANCH}", + "GH_AW_PR_HEAD_BASE_SHA": "\${GH_AW_PR_HEAD_BASE_SHA}", + "GH_AW_PR_HEAD_BASE_REPO": "\${GH_AW_PR_HEAD_BASE_REPO}", + "GH_AW_PR_HEAD_BASE_PR_NUMBER": "\${GH_AW_PR_HEAD_BASE_PR_NUMBER}", + "GH_AW_PR_HEAD_BASE_REF": "\${GH_AW_PR_HEAD_BASE_REF}", + "GH_AW_PR_HEAD_REPO": "\${GH_AW_PR_HEAD_REPO}", + "GITHUB_EVENT_NAME": "\${GITHUB_EVENT_NAME}", + "GITHUB_EVENT_PATH": "\${GITHUB_EVENT_PATH}", "GITHUB_REPOSITORY": "\${GITHUB_REPOSITORY}", "GITHUB_SHA": "\${GITHUB_SHA}", "GITHUB_TOKEN": "\${GITHUB_TOKEN}", @@ -785,7 +855,7 @@ jobs: "accept": [ "*" ], - "sink-visibility": ${{ toJSON(steps.determine-automatic-lockdown.outputs.visibility) }} + "sink-visibility": "${GH_AW_SINK_VISIBILITY}" } } } @@ -793,25 +863,32 @@ jobs: "gateway": { "port": $MCP_GATEWAY_PORT, "domain": "${MCP_GATEWAY_DOMAIN}", - "apiKey": "${MCP_GATEWAY_API_KEY}", + "agentId": "${MCP_GATEWAY_AGENT_ID}", "payloadDir": "${MCP_GATEWAY_PAYLOAD_DIR}", - "startupTimeout": 120 + "startupTimeout": 120, + "opentelemetry": { + "endpoint": "${OTEL_EXPORTER_OTLP_ENDPOINT}", + "traceId": "${GITHUB_AW_OTEL_TRACE_ID}", + "spanId": "${GITHUB_AW_OTEL_PARENT_SPAN_ID}" + } } } - GH_AW_MCP_CONFIG_4d6f106ca28cda70_EOF + GH_AW_MCP_CONFIG_46604863f3d8e286_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_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} 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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io); - const { main } = require('${{ runner.temp }}/gh-aw/actions/mount_mcp_as_cli.cjs'); + const { main } = require(path.join(actionsDir, 'mount_mcp_as_cli.cjs')); await main(); - name: Clean credentials continue-on-error: true @@ -827,18 +904,33 @@ jobs: run: | set -o pipefail printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT + trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"; if [ "$gh_aw_exit_code" -ne 0 ]; then echo "::error::Agent execution exited with code $gh_aw_exit_code"; fi' EXIT mkdir -p "$HOME/.copilot" printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" export XDG_CONFIG_HOME="$HOME" export GH_AW_MCP_CONFIG="$HOME/.copilot/mcp-config.json" + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + 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) GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-1000}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="1000" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"network\":{\"allowDomains\":[\"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\",\"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\",\"s.symcb.com\",\"s.symcd.com\",\"security.ubuntu.com\",\"ts-crl.ws.symantec.com\",\"ts-ocsp.ws.symantec.com\",\"www.googleapis.com\"],\"isolation\":true,\"topologyAttach\":[\"awmg-mcpg\"]},\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" GH_AW_DOCKER_HOST="" @@ -856,22 +948,28 @@ jobs: fi fi # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_API_KEY --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' 2>&1 | tee -a /tmp/gh-aw/agent-stdio.log + GH_AW_AWF_ENGINE_NAME=copilot \ + GH_AW_AWF_HARNESS_MARKER='[copilot-harness]' \ + GH_AW_AWF_LOG_FILE=/tmp/gh-aw/agent-stdio.log \ + GH_AW_AWF_ATTEMPT_LOG_NAME=copilot \ + bash "${RUNNER_TEMP}/gh-aw/actions/run_awf_with_startup_retries.sh" -- \ + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --exclude-env GITHUB_MCP_SERVER_TOKEN --exclude-env MCP_GATEWAY_AGENT_ID --mount /tmp/gh-aw:/tmp/gh-aw:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/mcp-cli/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; fi; "$GH_AW_NODE_EXEC" "${RUNNER_TEMP}/gh-aw/actions/copilot_harness.cjs" "${RUNNER_TEMP}/gh-aw/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 --allow-all-paths --add-dir "${GITHUB_WORKSPACE}" --prompt-file /tmp/gh-aw/aw-prompts/prompt.txt' env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_MAX_AI_CREDITS || '1000' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} + GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} 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_TIMEOUT_MINUTES: 15 - GH_AW_VERSION: v0.83.1 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -892,7 +990,18 @@ jobs: if: always() id: detect-agent-errors continue-on-error: true - run: node "${RUNNER_TEMP}/gh-aw/actions/detect_agent_errors.cjs" + env: + GH_AW_AGENTIC_EXECUTION_OUTCOME: ${{ steps.agentic_execution.outcome }} + GH_AW_ENGINE_STEP_TIMEOUT_MINUTES: 15 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'detect_agent_errors.cjs')); + await main(); - name: Configure Git credentials env: GITHUB_REPOSITORY: ${{ github.repository }} @@ -908,7 +1017,7 @@ jobs: 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 }} + MCP_GATEWAY_AGENT_ID: ${{ steps.start-mcp-gateway.outputs.gateway-agent-id }} GATEWAY_PID: ${{ steps.start-mcp-gateway.outputs.gateway-pid }} run: | bash "${RUNNER_TEMP}/gh-aw/actions/stop_mcp_gateway.sh" "$GATEWAY_PID" @@ -917,9 +1026,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/redact_secrets.cjs'); + const { main } = require(path.join(actionsDir, 'redact_secrets.cjs')); await main(); env: GH_AW_SECRET_NAMES: 'GH_AW_GITHUB_MCP_SERVER_TOKEN,GH_AW_GITHUB_TOKEN,GITHUB_TOKEN' @@ -942,14 +1053,16 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/collect_ndjson_output.cjs'); + const { main } = require(path.join(actionsDir, 'collect_ndjson_output.cjs')); await main(); - name: Parse agent logs for step summary if: always() @@ -959,9 +1072,11 @@ jobs: GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_copilot_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_copilot_log.cjs')); await main(); - name: Parse MCP Gateway logs for step summary if: always() @@ -969,9 +1084,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_mcp_gateway_log.cjs'); + const { main } = require(path.join(actionsDir, 'parse_mcp_gateway_log.cjs')); await main(); - name: Print firewall logs if: always() @@ -985,9 +1102,11 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); await main(); - name: Print AWF reflect summary if: always() @@ -995,16 +1114,41 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/awf_reflect_summary.cjs'); + const { main } = require(path.join(actionsDir, 'awf_reflect_summary.cjs')); await main(); + - name: Generate observability summary + if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'generate_observability_summary.cjs')); + await main(core); - 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 + # Small dedicated copy of the agent output so safe-output processing + # survives a failed or timed-out upload of the larger agent artifact + - name: Upload agent output fallback artifact + if: always() + continue-on-error: true + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: agent-output-fallback + path: | + /tmp/gh-aw/agent_output.json + /tmp/gh-aw/safeoutputs.jsonl + if-no-files-found: ignore - name: Upload agent artifacts if: always() continue-on-error: true @@ -1021,6 +1165,8 @@ jobs: /tmp/gh-aw/pre-agent-audit.txt /tmp/gh-aw/agent/ /tmp/gh-aw/github_rate_limits.jsonl + /tmp/gh-aw/otel.jsonl + /tmp/gh-aw/otlp-export-errors.jsonl /tmp/gh-aw/safeoutputs.jsonl /tmp/gh-aw/agent_output.json /tmp/gh-aw/aw-*.patch @@ -1040,10 +1186,10 @@ jobs: if: > always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' || needs.activation.outputs.oauth_token_check_failed == 'true' || needs.activation.outputs.stale_lock_file_failed == 'true' || - needs.activation.outputs.secret_verification_result == 'failed' || needs.activation.outputs.daily_ai_credits_exceeded == 'true') + needs.activation.outputs.daily_ai_credits_exceeded == 'true') runs-on: ubuntu-slim permissions: - contents: read + actions: read issues: write pull-requests: write concurrency: @@ -1060,7 +1206,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1069,15 +1215,16 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" 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 + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1085,42 +1232,29 @@ jobs: 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: Download safe outputs items manifest + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi + - name: Download detection artifact + id: download-detection-artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: detection + path: /tmp/gh-aw/threat-detection/ + - name: Download Safe Outputs Items Manifest id: download-safe-outputs-manifest if: always() continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: safe-outputs-items + pattern: safe-outputs-items + merge-multiple: true path: /tmp/gh-aw/ - name: Collect usage artifact files if: always() continue-on-error: true - run: | - mkdir -p /tmp/gh-aw/usage/agent /tmp/gh-aw/usage/detection - echo "Usage artifact source file status:" - for file in /tmp/gh-aw/aw_info.json /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/agent_usage.json /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl; do - [ -f "$file" ] && echo "FOUND: $file" || echo "MISSING: $file" - done - [ -f /tmp/gh-aw/aw_info.json ] && cp /tmp/gh-aw/aw_info.json /tmp/gh-aw/usage/aw_info.json || true - [ -f /tmp/gh-aw/aw-info.jsonl ] && cp /tmp/gh-aw/aw-info.jsonl /tmp/gh-aw/usage/aw-info.jsonl || true - [ -f /tmp/gh-aw/agent_usage.json ] && cp /tmp/gh-aw/agent_usage.json /tmp/gh-aw/usage/agent_usage.json || true - [ -f /tmp/gh-aw/agent_usage.jsonl ] && cp /tmp/gh-aw/agent_usage.jsonl /tmp/gh-aw/usage/agent_usage.jsonl || true - [ -f /tmp/gh-aw/detection_usage.jsonl ] && cp /tmp/gh-aw/detection_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl || true - [ -f /tmp/gh-aw/evals/evals.jsonl ] && cp /tmp/gh-aw/evals/evals.jsonl /tmp/gh-aw/usage/evals.jsonl || true - [ -f /tmp/gh-aw/github_rate_limits.jsonl ] && cp /tmp/gh-aw/github_rate_limits.jsonl /tmp/gh-aw/usage/github_rate_limits.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall-audit-logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/audit/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -s /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl ] && cp /tmp/gh-aw/threat-detection/sandbox/firewall/logs/api-proxy-logs/token-usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl || true - [ -f /tmp/gh-aw/usage/agent/token_usage.jsonl ] || : > /tmp/gh-aw/usage/agent/token_usage.jsonl - [ -f /tmp/gh-aw/usage/detection/token_usage.jsonl ] || : > /tmp/gh-aw/usage/detection/token_usage.jsonl - mkdir -p /tmp/gh-aw/usage/activity - node "${RUNNER_TEMP}/gh-aw/actions/generate_usage_activity_summary.cjs" - find /tmp/gh-aw/usage -type f -print | sort + run: bash "${RUNNER_TEMP}/gh-aw/actions/collect_usage_artifact_files.sh" - name: Upload usage artifact if: always() continue-on-error: true @@ -1134,6 +1268,8 @@ jobs: /tmp/gh-aw/usage/agent_usage.jsonl /tmp/gh-aw/usage/detection_usage.jsonl /tmp/gh-aw/usage/evals.jsonl + /tmp/gh-aw/usage/graders/grader_manifest.json + /tmp/gh-aw/usage/graders/grader_results.json /tmp/gh-aw/usage/github_rate_limits.jsonl /tmp/gh-aw/usage/agent/token_usage.jsonl /tmp/gh-aw/usage/detection/token_usage.jsonl @@ -1156,9 +1292,11 @@ jobs: with: github-token: ${{ github.token }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context); - const { main } = require('${{ runner.temp }}/gh-aw/actions/write_daily_aic_usage_cache.cjs'); + const { main } = require(path.join(actionsDir, 'write_daily_aic_usage_cache.cjs')); await main(); - name: Save daily AIC usage cache id: save-daily-aic-cache @@ -1189,7 +1327,7 @@ jobs: GH_AW_TRACKER_ID: "sdk-consistency-review" 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" + GH_AW_NOOP_REPORT_AS_ISSUE: "false" GH_AW_AIC: ${{ needs.agent.outputs.aic }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_AMBIENT_CONTEXT: ${{ needs.agent.outputs.ambient_context }} @@ -1197,9 +1335,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_noop_message.cjs'); + const { main } = require(path.join(actionsDir, 'handle_noop_message.cjs')); await main(); - name: Log detection run id: detection_runs @@ -1215,9 +1355,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_detection_runs.cjs'); + const { main } = require(path.join(actionsDir, 'handle_detection_runs.cjs')); await main(); - name: Record missing tool id: missing_tool @@ -1231,9 +1373,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/missing_tool.cjs'); + const { main } = require(path.join(actionsDir, 'missing_tool.cjs')); await main(); - name: Record incomplete id: report_incomplete @@ -1247,9 +1391,11 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/report_incomplete_handler.cjs'); + const { main } = require(path.join(actionsDir, 'report_incomplete_handler.cjs')); await main(); - name: Handle agent failure id: handle_agent_failure @@ -1263,7 +1409,7 @@ jobs: 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: "sdk-consistency-review" - GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "168" + GH_AW_ACTION_FAILURE_ISSUE_EXPIRES_HOURS: "0" GH_AW_ENGINE_ID: "copilot" GH_AW_CHECKOUT_PR_SUCCESS: ${{ needs.agent.outputs.checkout_pr_success }} GH_AW_EFFECTIVE_TOKENS: ${{ needs.agent.outputs.effective_tokens || '' }} @@ -1277,6 +1423,10 @@ jobs: 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_HTTP_400_RESPONSE_ERROR: ${{ needs.agent.outputs.http_400_response_error }} + GH_AW_MAX_CACHE_MISSES_EXCEEDED: ${{ needs.agent.outputs.max_cache_misses_exceeded }} + GH_AW_MISSING_MODEL_PRICING_ERROR: ${{ needs.agent.outputs.missing_model_pricing_error }} + GH_AW_MISSING_MODEL_PRICING_MODEL_NAME: ${{ needs.agent.outputs.missing_model_pricing_model_name }} + GH_AW_SHELL_EXPANSION_GUARD_REJECTED: ${{ needs.agent.outputs.shell_expansion_guard_rejected }} 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_OAUTH_TOKEN_CHECK_FAILED: ${{ needs.activation.outputs.oauth_token_check_failed }} @@ -1292,9 +1442,31 @@ jobs: with: github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/handle_agent_failure.cjs'); + const { main } = require(path.join(actionsDir, 'handle_agent_failure.cjs')); + await main(); + - name: Report failed jobs + id: report_failed_jobs + 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: "SDK Consistency Review Agent" + GH_AW_WORKFLOW_SOURCE_URL: "${{ github.server_url }}/${{ github.repository }}/blob/${{ github.ref_name }}/.github/workflows/sdk-consistency-review.md" + GH_AW_TRACKER_ID: "sdk-consistency-review" + GH_AW_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + GH_AW_REPORT_FAILED_JOBS: "true" + with: + github-token: ${{ secrets.GH_AW_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'report_failed_jobs.cjs')); await main(); detection: @@ -1306,6 +1478,7 @@ jobs: permissions: contents: read copilot-requests: write + timeout-minutes: 10 env: GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} outputs: @@ -1316,7 +1489,7 @@ jobs: steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1325,15 +1498,22 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Download activation artifact + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: activation + path: /tmp/gh-aw - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1341,10 +1521,12 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Checkout repository for patch context if: needs.agent.outputs.has_patch == 'true' - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false # --- Threat Detection --- @@ -1353,7 +1535,7 @@ jobs: 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.27.38@sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243 ghcr.io/github/gh-aw-firewall/api-proxy:0.27.38@sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c ghcr.io/github/gh-aw-firewall/squid:0.27.38@sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917 + run: bash "${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh" ghcr.io/github/gh-aw-firewall/agent:0.28.12@sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202 ghcr.io/github/gh-aw-firewall/api-proxy:0.28.12@sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32 ghcr.io/github/gh-aw-firewall/squid:0.28.12@sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f - name: Check if detection needed id: detection_guard if: always() @@ -1377,21 +1559,7 @@ jobs: - name: Prepare threat detection files if: always() && steps.detection_guard.outputs.run_detection == 'true' run: | - mkdir -p /tmp/gh-aw/threat-detection/aw-prompts - rm -f /tmp/gh-aw/agent_usage.json - 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 + bash "${RUNNER_TEMP}/gh-aw/actions/prepare_threat_detection_files.sh" - name: Setup threat detection if: always() && steps.detection_guard.outputs.run_detection == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 @@ -1399,82 +1567,53 @@ jobs: WORKFLOW_NAME: "SDK Consistency Review Agent" WORKFLOW_DESCRIPTION: "Reviews PRs to ensure features are implemented consistently across all SDK language implementations" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + GH_AW_DETECTION_SKIP_PROMPT_SUMMARY: "true" with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/setup_threat_detection.cjs'); + const { main } = require(path.join(actionsDir, '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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '24' - package-manager-cache: false + - name: Install AWF binary + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.28.12 --rootless - name: Install GitHub Copilot CLI - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" 1.0.73 + run: bash "${RUNNER_TEMP}/gh-aw/actions/install_copilot_cli.sh" env: GH_HOST: github.com - - name: Install AWF binary - run: bash "${RUNNER_TEMP}/gh-aw/actions/install_awf_binary.sh" v0.27.38 - - name: Execute GitHub Copilot CLI + GH_AW_COMPILED_VERSION: v0.88.2 + - name: Install threat-detect binary 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 - trap 'gh_aw_exit_code=$?; mkdir -p /tmp/gh-aw >/dev/null 2>&1 || true; printf "%s" "$gh_aw_exit_code" > /tmp/gh-aw/agent_execution_exit_code.txt || true; rm -f "$HOME/.copilot/settings.json"' EXIT - mkdir -p "$HOME/.copilot" - printf '%s' '{"builtInAgents":{"rubberDuck":false}}' > "$HOME/.copilot/settings.json" - export XDG_CONFIG_HOME="$HOME" - 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) - GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" - printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.27.38/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,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"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\",\"kimi\"],\"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*\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"fable\",\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.27.38,squid=sha256:6c19094d95aad5f9f128ad5e583f0f2b894b158aa66c3b86dd9bcc90970a2917,agent=sha256:cb928eb62d9139a013c2d278dab19af232d35a2d83dca71a3d98eb431f786243,api-proxy=sha256:cd6145620d96acee46e1ede25180a13aa36002467e663db0caa453a8bc8eb60c,cli-proxy=sha256:c30c5319da37505d42f95cb3faa2cfa55e794ccb5cc805dbd9201410d1ac2a3e\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json - export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" - GH_AW_DOCKER_HOST="" - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - GH_AW_DOCKER_HOST="${DOCKER_HOST}" - fi - if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then - _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" - fi - GH_AW_TOOL_CACHE_MOUNT="" - GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" - 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 - fi - # shellcheck disable=SC1003,SC2016,SC2086 - 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env COPILOT_GITHUB_TOKEN --log-level info --skip-pull \ - -- /bin/bash -c 'set +o histexpand; : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/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_NPM_GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"; if [ -n "$GH_AW_NPM_GLOBAL_ROOT" ]; then export NODE_PATH="${GH_AW_NPM_GLOBAL_ROOT}${NODE_PATH:+:${NODE_PATH}}"; 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 + bash "${RUNNER_TEMP}/gh-aw/actions/install_threat_detect_binary.sh" v0.5.1 + - name: Execute threat detection with AWF + id: detection_agentic_execution + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + timeout-minutes: 10 env: AWF_REFLECT_ENABLED: 1 COPILOT_AGENT_RUNNER_TYPE: STANDALONE COPILOT_DUMMY_BYOK: dummy-byok-key-for-offline-mode COPILOT_GITHUB_TOKEN: ${{ github.token }} - COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-4.6' }} + COPILOT_MODEL: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} + GH_AW_HARNESS_MAX_RETRIES: 0 GH_AW_LLM_PROVIDER: github GH_AW_MAX_AI_CREDITS: ${{ vars.GH_AW_DEFAULT_DETECTION_MAX_AI_CREDITS || '400' }} GH_AW_MAX_TURNS: ${{ vars.GH_AW_DEFAULT_MAX_TURNS || '' }} + GH_AW_MODEL_FALLBACK: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'auto' }} GH_AW_PHASE: detection GH_AW_PROMPT: /tmp/gh-aw/aw-prompts/prompt.txt - GH_AW_TIMEOUT_MINUTES: 20 - GH_AW_VERSION: v0.83.1 + GH_AW_TIMEOUT_MINUTES: 10 + GH_AW_VERSION: v0.88.2 GITHUB_API_URL: ${{ github.api_url }} GITHUB_AW: true GITHUB_COPILOT_INTEGRATION_ID: agentic-workflows @@ -1490,58 +1629,105 @@ jobs: RUNNER_TEMP: ${{ runner.temp }} S2STOKENS: true TRACEPARENT: ${{ env.GITHUB_AW_OTEL_TRACE_ID != '' && env.GITHUB_AW_OTEL_PARENT_SPAN_ID != '' && format('00-{0}-{1}-01', env.GITHUB_AW_OTEL_TRACE_ID, env.GITHUB_AW_OTEL_PARENT_SPAN_ID) || '' }} - - name: Parse threat detection token usage for step summary - id: parse_detection_token_usage - if: always() + WORKFLOW_NAME: "SDK Consistency Review Agent" + WORKFLOW_DESCRIPTION: "Reviews PRs to ensure features are implemented consistently across all SDK language implementations" + HAS_PATCH: ${{ needs.agent.outputs.has_patch }} + GH_AW_DETECTION_CONTINUE_ON_ERROR: "true" + run: | + set -o pipefail + printf '%s' "$(date +%s%3N)" > /tmp/gh-aw/agent_cli_start_ms.txt + GH_AW_COPILOT_SRC="$(command -v copilot 2>/dev/null || true)" + if [ -z "$GH_AW_COPILOT_SRC" ] || [ ! -x "$GH_AW_COPILOT_SRC" ]; then + echo "GitHub Copilot CLI executable not found on PATH after installation" >&2 + exit 127 + fi + GH_AW_COPILOT_BIN="${RUNNER_TEMP}/gh-aw/bin/copilot" + mkdir -p "${RUNNER_TEMP}/gh-aw/bin" + if [ "$GH_AW_COPILOT_SRC" != "$GH_AW_COPILOT_BIN" ]; then + cp "$GH_AW_COPILOT_SRC" "$GH_AW_COPILOT_BIN" + fi + chmod 755 "$GH_AW_COPILOT_BIN" + + (umask 177 && touch /tmp/gh-aw/threat-detection/detection.log) + GH_AW_MAX_AI_CREDITS="${GH_AW_MAX_AI_CREDITS:-400}" + if [[ ! "$GH_AW_MAX_AI_CREDITS" =~ ^[0-9]+$ ]]; then + GH_AW_MAX_AI_CREDITS="400" + fi + printf '%s\n' "{\"\$schema\":\"https://github.com/github/gh-aw-firewall/releases/download/v0.28.12/awf-config.schema.json\",\"apiProxy\":{\"enabled\":true,\"enableTokenSteering\":true,\"maxRuns\":500,\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS},\"maxCacheMisses\":5,\"models\":{\"agent\":[\"sonnet-6x\",\"gpt-5.4\",\"gpt-5.5\",\"gpt-5.6\",\"gpt-5.3\",\"gemini-pro\",\"any\"],\"antigravity\":[\"copilot/antigravity*\",\"google/antigravity*\",\"gemini/antigravity*\"],\"any\":[\"copilot/*\",\"anthropic/*\",\"openai/*\",\"google/*\",\"gemini/*\"],\"auto\":[\"copilot/auto\",\"large\"],\"claude\":[\"agent\"],\"codex\":[\"agent\"],\"coding\":[\"copilot/gpt-5*codex*\",\"openai/gpt-5*codex*\",\"gpt-5-codex\",\"kimi\"],\"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*\"],\"detection\":[\"small\"],\"evals\":[\"small\"],\"fable\":[\"copilot/*fable*\",\"anthropic/*fable*\"],\"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*\",\"google/nano-banana*\",\"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-3.6-flash\":[\"copilot/gemini-3.6*flash*\",\"google/gemini-3.6*flash*\",\"gemini/gemini-3.6*flash*\"],\"gemini-3.7-flash\":[\"copilot/gemini-3.7*flash*\",\"google/gemini-3.7*flash*\",\"gemini/gemini-3.7*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-omni\":[\"copilot/gemini-omni*\",\"google/gemini-omni*\",\"gemini/gemini-omni*\"],\"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.1\":[\"copilot/gpt-5.1*\",\"openai/gpt-5.1*\"],\"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*\"],\"gpt-5.6\":[\"copilot/gpt-5.6*\",\"openai/gpt-5.6*\"],\"grok\":[\"copilot/*grok*\",\"openai/*grok*\"],\"haiku\":[\"copilot/*haiku*\",\"anthropic/*haiku*\"],\"image-generation\":[\"copilot/gpt-image*\",\"openai/gpt-image*\",\"openai/chatgpt-image*\",\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"google/imagen*\"],\"kimi\":[\"copilot/kimi*\",\"openai/kimi*\"],\"kiwi\":[\"copilot/kiwi*\",\"openai/kiwi*\"],\"large\":[\"sonnet\",\"gpt-5-pro\",\"gpt-5\",\"gemini-pro\"],\"lyria\":[\"google/lyria*\",\"gemini/lyria*\",\"copilot/lyria*\"],\"mai-code\":[\"copilot/MAI-Code*\",\"copilot/mai-code*\",\"openai/MAI-Code*\"],\"mai-code-1-flash-picker\":[\"copilot/MAI-Code-1-Flash-picker*\",\"copilot/mai-code-1-flash-picker*\",\"openai/MAI-Code-1-Flash-picker*\"],\"mini\":[\"haiku\",\"gpt-5-mini\",\"gpt-5-nano\",\"gemini-flash-lite\"],\"nano-banana\":[\"copilot/nano-banana*\",\"google/nano-banana*\",\"gemini/nano-banana*\"],\"opus\":[\"copilot/*opus*\",\"anthropic/*opus*\"],\"opusplan\":[\"opus?effort=high\"],\"raptor-mini\":[\"copilot/raptor*\",\"openai/raptor*\"],\"reasoning\":[\"copilot/o1*\",\"copilot/o3*\",\"copilot/o4*\",\"openai/o1*\",\"openai/o3*\",\"openai/o4*\"],\"robotics\":[\"copilot/*robotics*\",\"google/*robotics*\",\"gemini/*robotics*\"],\"small\":[\"mini\"],\"small-agent\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash\"],\"sonnet\":[\"copilot/*sonnet*\",\"anthropic/*sonnet*\"],\"sonnet-6x\":[\"copilot/*sonnet-4.5*\",\"copilot/*sonnet-4.6*\",\"copilot/*sonnet-5*\",\"copilot/*sonnet-4-5-*\",\"anthropic/*sonnet-4-5-*\",\"copilot/*sonnet-4-6*\",\"anthropic/*sonnet-4-6*\",\"anthropic/*sonnet-5*\"],\"summarization\":[\"haiku\",\"gpt-5-mini\",\"gemini-flash-lite\",\"mini\"],\"veo\":[\"google/veo*\",\"gemini/veo*\"],\"vision\":[\"copilot/gemini-*image*\",\"google/gemini-*image*\",\"gemini/gemini-*image*\",\"copilot/gemini-*flash*\",\"google/gemini-*flash*\",\"gemini/gemini-*flash*\"]}},\"container\":{\"imageTag\":\"0.28.12,squid=sha256:52c34aca98d2a6833c329f1505912a6949c4fda16618c010c979bd59ea99254f,agent=sha256:390051be4ed1847f774fd8980b61d3a3523574c0175d00c3fc7cdf2002a88202,api-proxy=sha256:d7d533d87c80d87ff91ac0e21e9299055c3beedff1536262b97ed700fb065a32,cli-proxy=sha256:5250629d48eaedfedf2e948785228e8da29eec2a83cbab58ea0751c14a7b021d\"},\"logging\":{\"proxyLogsDir\":\"/tmp/gh-aw/sandbox/firewall/logs\",\"auditDir\":\"/tmp/gh-aw/sandbox/firewall/audit\"}}" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + cp "${RUNNER_TEMP}/gh-aw/awf-config.json" /tmp/gh-aw/awf-config.json + export GH_AW_MODELS_JSON_PATH="/tmp/gh-aw/models.json" + GH_AW_DOCKER_HOST="" + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + GH_AW_DOCKER_HOST="${DOCKER_HOST}" + fi + if [[ "${DOCKER_HOST:-}" =~ ^tcp:// ]]; then + _GH_AW_CHROOT_JSON=$(jq -c --arg src "${RUNNER_TEMP}/gh-aw" --arg user "$(id -un)" --argjson uid "$(id -u)" --argjson gid "$(id -g)" --arg home "${RUNNER_TEMP}/gh-aw/home" '.chroot={"binariesSourcePath":$src,"identity":{"user":$user,"uid":$uid,"gid":$gid,"home":$home}}' "${RUNNER_TEMP}/gh-aw/awf-config.json") || { echo "chroot config patch failed" >&2; exit 1; } + printf '%s\n' "$_GH_AW_CHROOT_JSON" > "${RUNNER_TEMP}/gh-aw/awf-config.json" + fi + GH_AW_TOOL_CACHE_MOUNT="" + GH_AW_TOOL_CACHE="${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}" + 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 + fi + # shellcheck disable=SC1003,SC2016,SC2086 + 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:+--docker-host "$GH_AW_DOCKER_HOST"} --env-all --exclude-env ACTIONS_ID_TOKEN_REQUEST_TOKEN --exclude-env ACTIONS_ID_TOKEN_REQUEST_URL --exclude-env COPILOT_GITHUB_TOKEN --mount /tmp/gh-aw:/tmp/gh-aw:rw --mount /tmp/gh-aw/threat-detection:/tmp/gh-aw/threat-detection:rw --log-level info --skip-pull \ + -- /bin/bash -c 'set +o histexpand; export PATH="${RUNNER_TEMP}/gh-aw/bin:$PATH" && : "${RUNNER_TOOL_CACHE:?RUNNER_TOOL_CACHE must be set}"; GH_AW_TOOL_CACHE="$RUNNER_TOOL_CACHE"; export PATH="$(find "$GH_AW_TOOL_CACHE" -maxdepth 5 -type d -name bin 2>/dev/null | tr '\''\n'\'' '\'':'\'')$PATH"; [ -n "$GOROOT" ] && export PATH="$GOROOT/bin:$PATH" || true; [ -n "$ERLANG_HOME" ] && export PATH="$ERLANG_HOME/bin:$PATH" || true && threat-detect --engine copilot --output /tmp/gh-aw/threat-detection/detection_result.json /tmp/gh-aw/threat-detection' 2>&1 | tee -a /tmp/gh-aw/threat-detection/detection.log + - name: Render detection log + if: always() && steps.detection_guard.outputs.run_detection == 'true' continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - env: - GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage with: script: | - const { setupGlobals } = require('${{ runner.temp }}/gh-aw/actions/setup_globals.cjs'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/parse_token_usage.cjs'); + const { main } = require(path.join(actionsDir, 'render_detection_log.cjs')); await main(); - - name: Upload threat detection log + - name: Copy detection firewall logs + if: always() && steps.detection_guard.outputs.run_detection == 'true' + continue-on-error: true + run: | + mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall + if [ -d /tmp/gh-aw/sandbox/firewall/logs ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/logs && cp -r /tmp/gh-aw/sandbox/firewall/logs/. /tmp/gh-aw/threat-detection/sandbox/firewall/logs/; fi + if [ -d /tmp/gh-aw/sandbox/firewall/audit ]; then mkdir -p /tmp/gh-aw/threat-detection/sandbox/firewall/audit && cp -r /tmp/gh-aw/sandbox/firewall/audit/. /tmp/gh-aw/threat-detection/sandbox/firewall/audit/; fi + - name: Upload threat detection artifact 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 + path: | + /tmp/gh-aw/threat-detection/detection_result.json + /tmp/gh-aw/threat-detection/sandbox/firewall/logs/ + /tmp/gh-aw/threat-detection/sandbox/firewall/audit/ if-no-files-found: ignore - - name: Parse and conclude threat detection - id: detection_conclusion + - name: Parse threat detection token usage for step summary + id: parse_detection_token_usage if: always() continue-on-error: true uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_AW_TOKEN_USAGE_SUMMARY_TITLE: Threat Detection Token Usage + with: + script: | + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); + setupGlobals(core, github, context, exec, io, getOctokit); + const { main } = require(path.join(actionsDir, 'parse_token_usage.cjs')); + await main(); + - name: Conclude threat detection + id: detection_conclusion + if: always() + continue-on-error: true 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); - } - } + run: | + bash "${RUNNER_TEMP}/gh-aw/actions/conclude_threat_detection.sh" /tmp/gh-aw/threat-detection/detection_result.json safe_outputs: needs: @@ -1551,7 +1737,6 @@ jobs: if: (!cancelled()) && needs.agent.result != 'skipped' && needs.detection.result == 'success' runs-on: ubuntu-slim permissions: - contents: read issues: write pull-requests: write timeout-minutes: 45 @@ -1564,8 +1749,8 @@ jobs: 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: ${{ needs.agent.outputs.model }} - GH_AW_ENGINE_VERSION: "1.0.73" + GH_AW_ENGINE_MODEL: "${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }}" + GH_AW_HEAD_SHA: ${{ github.event.pull_request.head.sha }} GH_AW_RUNTIME_FEATURES: ${{ vars.GH_AW_RUNTIME_FEATURES }} GH_AW_THREAT_DETECTION_AIC: ${{ needs.detection.outputs.aic }} GH_AW_TRACKER_ID: "sdk-consistency-review" @@ -1579,12 +1764,20 @@ jobs: comment_url: ${{ steps.process_safe_outputs.outputs.comment_url }} create_discussion_error_count: ${{ steps.process_safe_outputs.outputs.create_discussion_error_count }} create_discussion_errors: ${{ steps.process_safe_outputs.outputs.create_discussion_errors }} + process_safe_outputs_items_applied: ${{ steps.process_safe_outputs.outputs.items_applied }} + process_safe_outputs_items_cancelled: ${{ steps.process_safe_outputs.outputs.items_cancelled }} + process_safe_outputs_items_deferred: ${{ steps.process_safe_outputs.outputs.items_deferred }} + process_safe_outputs_items_failed: ${{ steps.process_safe_outputs.outputs.items_failed }} + process_safe_outputs_items_skipped: ${{ steps.process_safe_outputs.outputs.items_skipped }} + process_safe_outputs_items_succeeded: ${{ steps.process_safe_outputs.outputs.items_succeeded }} + process_safe_outputs_items_warnings: ${{ steps.process_safe_outputs.outputs.items_warnings }} process_safe_outputs_processed_count: ${{ steps.process_safe_outputs.outputs.processed_count }} + process_safe_outputs_status: ${{ steps.process_safe_outputs.outputs.status }} process_safe_outputs_temporary_id_map: ${{ steps.process_safe_outputs.outputs.temporary_id_map }} steps: - name: Setup Scripts id: setup - uses: github/gh-aw-actions/setup@8bdba8075360648fe6802302a5b4e016361dc6ac # v0.83.1 + uses: github/gh-aw-actions/setup@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: destination: ${{ runner.temp }}/gh-aw/actions job-name: ${{ github.job }} @@ -1593,15 +1786,18 @@ jobs: env: GH_AW_SETUP_WORKFLOW_NAME: "SDK Consistency Review Agent" GH_AW_CURRENT_WORKFLOW_REF: ${{ github.repository }}/.github/workflows/sdk-consistency-review.lock.yml@${{ github.ref }} - GH_AW_INFO_VERSION: "1.0.73" - GH_AW_INFO_AWF_VERSION: "v0.27.38" + GH_AW_INFO_VERSION: "1.0.80" + GH_AW_INFO_AWF_VERSION: "v0.28.12" GH_AW_INFO_ENGINE_ID: "copilot" + - name: Mask OTLP telemetry headers + run: bash "${RUNNER_TEMP}/gh-aw/actions/mask_otlp_headers.sh" - name: Download agent output artifact id: download-agent-output continue-on-error: true uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: agent + pattern: "{agent,agent-output-fallback}" + merge-multiple: true path: /tmp/gh-aw/ - name: Setup agent output environment variable id: setup-agent-output-env @@ -1609,7 +1805,9 @@ jobs: 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" + if [ -f "/tmp/gh-aw/agent_output.json" ]; then + echo "GH_AW_AGENT_OUTPUT=/tmp/gh-aw/agent_output.json" >> "$GITHUB_OUTPUT" + fi - name: Configure GH_HOST for enterprise compatibility id: ghes-host-config shell: bash @@ -1625,16 +1823,18 @@ jobs: 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" + GH_AW_ALLOWED_DOMAINS: "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,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,s.symcb.com,s.symcd.com,security.ubuntu.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: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"true\"},\"report_incomplete\":{}}" + GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG: "{\"add_comment\":{\"hide_older_comments\":true,\"max\":1},\"create_pull_request_review_comment\":{\"max\":10,\"side\":\"RIGHT\"},\"create_report_incomplete_issue\":{},\"missing_data\":{},\"missing_tool\":{},\"noop\":{\"max\":1,\"report-as-issue\":\"false\"},\"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'); + const path = require('path'); + const actionsDir = path.join(process.env.RUNNER_TEMP, 'gh-aw', 'actions'); + const { setupGlobals } = require(path.join(actionsDir, 'setup_globals.cjs')); setupGlobals(core, github, context, exec, io, getOctokit); - const { main } = require('${{ runner.temp }}/gh-aw/actions/safe_output_handler_manager.cjs'); + const { main } = require(path.join(actionsDir, 'process_safe_outputs.cjs')); await main(); - name: Upload Safe Outputs Items if: always() @@ -1644,4 +1844,5 @@ jobs: path: | /tmp/gh-aw/safe-output-items.jsonl /tmp/gh-aw/temporary-id-map.json + /tmp/gh-aw/safe-output-errors.json if-no-files-found: ignore diff --git a/.github/workflows/sdk-consistency-review.md b/.github/workflows/sdk-consistency-review.md index 550d9349d0..4d1c7e4511 100644 --- a/.github/workflows/sdk-consistency-review.md +++ b/.github/workflows/sdk-consistency-review.md @@ -1,6 +1,7 @@ --- description: Reviews PRs to ensure features are implemented consistently across all SDK language implementations tracker-id: sdk-consistency-review +model: ${{ vars.GH_AW_MODEL_AGENT_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} on: roles: all pull_request: @@ -35,6 +36,10 @@ safe-outputs: max: 1 hide-older-comments: true allowed-reasons: [outdated] + threat-detection: + engine: + id: copilot + model: ${{ vars.GH_AW_MODEL_DETECTION_COPILOT || vars.GH_AW_DEFAULT_MODEL_COPILOT || 'claude-sonnet-5' }} timeout-minutes: 15 --- diff --git a/.github/workflows/update-copilot-dependency.yml b/.github/workflows/update-copilot-dependency.yml index 9646366ad5..f1e4d9fca9 100644 --- a/.github/workflows/update-copilot-dependency.yml +++ b/.github/workflows/update-copilot-dependency.yml @@ -1,10 +1,10 @@ -name: "Update @github/copilot Dependency" +name: "Update Copilot CLI Version" on: workflow_dispatch: inputs: version: - description: "Target version of @github/copilot (e.g. 0.0.420)" + description: "Target github/copilot-cli release version (e.g. 1.0.83-0)" required: true type: string @@ -14,7 +14,7 @@ permissions: jobs: update: - name: "Update @github/copilot to ${{ inputs.version }}" + name: "Update Copilot CLI to ${{ inputs.version }}" runs-on: ubuntu-latest steps: - name: Validate version input @@ -56,17 +56,13 @@ jobs: toolchain: nightly-2026-04-14 components: rustfmt - - name: Update @github/copilot in nodejs + - name: Update the Node.js CLI release pin env: VERSION: ${{ inputs.version }} working-directory: ./nodejs - run: npm install "@github/copilot@$VERSION" - - - name: Update @github/copilot in test harness - env: - VERSION: ${{ inputs.version }} - working-directory: ./test/harness - run: npm install "@github/copilot@$VERSION" + run: | + node scripts/set-cli-version.js "$VERSION" + npm install --ignore-scripts - name: Refresh nodejs/samples lockfile working-directory: ./nodejs/samples @@ -91,25 +87,6 @@ jobs: java-version: "25" distribution: "microsoft" - - name: Update @github/copilot in Java codegen - env: - VERSION: ${{ inputs.version }} - working-directory: ./java/scripts/codegen - run: npm install "@github/copilot@$VERSION" - - - name: Update Java POM CLI version property - env: - VERSION: ${{ inputs.version }} - working-directory: ./java - run: | - PROP="readonly-copilot-sdk-ref-impl-version-from-lastmerge-file-updated-by-reference-impl-sync" - sed -i -E "s|(<${PROP}>)[^<]*()|\1^${VERSION}\2|" pom.xml - # Use fixed-string matching (-F) because npm versions contain regex - # metacharacters: '^' (caret ranges) and '.' (dots in semver) would - # otherwise be interpreted as start-of-line and any-char respectively, - # causing false negatives or spurious matches. - grep -qF "<${PROP}>^${VERSION}" pom.xml - - name: Run Java codegen working-directory: ./java run: mvn generate-sources -Pcodegen @@ -165,22 +142,23 @@ jobs: exit 0 fi - git commit -m "Update @github/copilot to $VERSION + git commit -m "Update Copilot CLI to $VERSION - - Updated nodejs and test harness dependencies + - Updated the shared CLI release pin - Re-ran code generators - Formatted generated code" git push origin "$BRANCH" --force-with-lease PR_BODY=$(cat <<'BODY_EOF' - Automated update of `@github/copilot` to version `PLACEHOLDER_VERSION`. + Automated update of the Copilot CLI release to version `PLACEHOLDER_VERSION`. ### Changes - - Updated `@github/copilot` in `nodejs/package.json` and `test/harness/package.json` + - Updated the shared release pin in `nodejs/package.json` + - Validated the release assets listed in `SHA256SUMS.txt` - Re-ran all code generators (`scripts/codegen`) - Formatted generated output - - Updated Java codegen dependency, POM property, and regenerated Java types + - Regenerated Java types from the pinned CLI release schemas ### Java Handwritten Code Adaptation Plan @@ -208,7 +186,7 @@ jobs: ### Next steps When ready, click **Ready for review** to trigger CI checks. - > Created by the **Update @github/copilot Dependency** workflow. + > Created by the **Update Copilot CLI Version** workflow. BODY_EOF ) PR_BODY="${PR_BODY//PLACEHOLDER_VERSION/$VERSION}" @@ -224,7 +202,7 @@ jobs: else gh pr create \ --draft \ - --title "Update @github/copilot to $VERSION" \ + --title "Update Copilot CLI to $VERSION" \ --body "$PR_BODY" \ --base main \ --head "$BRANCH" diff --git a/.github/workflows/verify-compiled.yml b/.github/workflows/verify-compiled.yml index 1a3dbb96fd..3cc5110225 100644 --- a/.github/workflows/verify-compiled.yml +++ b/.github/workflows/verify-compiled.yml @@ -17,9 +17,9 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install gh-aw CLI - uses: github/gh-aw-actions/setup-cli@05205436a78512d71a2d842e46586ed05f4fa058 # v0.82.10 + uses: github/gh-aw-actions/setup-cli@9271a1804551c0dc4fb0085a97979950aa2f8489 # v0.88.2 with: - version: v0.83.1 + version: v0.88.2 - name: Recompile workflows # Full-repository compile so the diff check below covers all workflows. run: gh aw compile diff --git a/CHANGELOG.md b/CHANGELOG.md index 0184a553db..788b983621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu ## [Unreleased] +### Feature: cancellation for host-owned external tools + +Host-owned external tool callbacks are now cancelled when their runtime request completes or their SDK session terminates. The cancellation primitive is idiomatic per SDK: .NET passes a request token to `AIFunction`, Node.js exposes `ToolInvocation.signal`, Go cancels `ToolInvocation.TraceContext`, Java cancels the returned `CompletableFuture`, Python cancels the handler task, and Rust drops the handler future. Go handlers that retain `TraceContext` for background work must derive a separate lifetime because the invocation context is cancelled when the request ends. + +### Feature: declare application identity with client info + +Client options now accept optional client info (application name and version, integration name and version) across all six SDKs, exposed idiomatically per language (`clientInfo` in Node.js, `client_info` in Python and Rust, `ClientInfo` in Go and .NET, `setClientInfo` in Java). When set, the SDK forwards it on the `server.connect` handshake so the telemetry the runtime emits on the connection is attributed to the application and its Copilot integration instead of the runtime's own build. All fields are optional, and leaving client info unset keeps the runtime's default attribution. See [Client info](./docs/features/client-info.md). + ### Feature: Node Agent Factories pagination and run notifications The experimental Node.js Agent Factories convenience API now supports paginated run history. Existing `session.factory.listRuns()` calls still return the runs array, while calls with `afterSeq`, `beforeSeq`, or `limit` return the full page with cursor and truncation metadata. @@ -40,6 +48,28 @@ const session = await joinSession({ const token = process.env.GITHUB_TOKEN; ``` +### Feature: early session-event subscription (Rust) + +The Rust SDK can now observe every event routed to a session, starting with that session's very first routed event. `Client::prepare_session` and `Client::prepare_resume_session` return an inert `PreparedSession` that owns the session's event channel, so a subscription can be installed *before* any protocol activity begins: + +```rust +let prepared = client.prepare_session( + SessionConfig::default().with_event_buffer_capacity(2048), +)?; +let mut events = prepared.subscribe(); +let session = prepared.start().await?; +``` + +Previously, `Session::subscribe` could only be called on the returned session, so events the runtime emitted while `session.create` / `session.resume` was still in flight were broadcast with no receiver installed and dropped. Ephemeral events such as `session.idle` are not persisted, so they could not be recovered with `getMessages` either. + +The guarantee is scoped to *routed* events. For cloud sessions where the server assigns the session ID, the SDK cannot route notifications until the `session.create` response arrives and the ID is known, so events emitted before that point are not routable to any session. Pin `session_id` on the config to get router registration before the RPC, and with it complete pre-response coverage. + +`prepare_*` is synchronous and inert: it validates the buffer capacity and allocates a local channel, and performs no router registration, task spawn, or wire activity until `start()` is first polled. `start(self)` consumes the handle and `PreparedSession` is not `Clone`, so a prepared session can never produce two event loops. Dropping an unstarted handle leaves no state behind; dropping a polled `start()` future cancels the startup and unregisters the session, so a retry with the same session ID succeeds. Session registrations now carry an ownership identity, so cleanup removes only the exact registration it owns and an abandoned startup can never evict a same-ID retry (or a session that replaced it). + +Both `SessionConfig` and `ResumeSessionConfig` gained a runtime-only `event_buffer_capacity` option (default 512, `Some(0)` rejected as an invalid config). The buffer is finite, so slow subscribers observe `Lagged` rather than applying backpressure; consumers that need a lossless view of a large startup burst must size the buffer accordingly or drain concurrently with `start()`. + +`create_session` and `resume_session` are unchanged wrappers over `prepare_*(...)?.start()` with identical RPC sequences and error kinds. + ### Feature: host-injected managed settings permissions Session create and resume accept a new optional `managedSettings` option that injects an enterprise permissions policy at session startup, alongside the existing `enableManagedSettings` self-fetch flag. The current contract is permissions-only: `disableBypassPermissionsMode` (the literal `"disable"`), plus `deny`, `ask`, and `allow` rule lists. The layer composes restrictively with any server- or device-level managed settings (deny/ask are unioned, every present allow list must admit a tool, and `disableBypassPermissionsMode` is deny-wins). diff --git a/docs/features/README.md b/docs/features/README.md index f97140b784..5ea070a5aa 100644 --- a/docs/features/README.md +++ b/docs/features/README.md @@ -20,6 +20,7 @@ These guides cover the capabilities you can add to your Copilot SDK application. | [Image Input](./image-input.md) | Send images to sessions as attachments | | [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) | | [Usage and Billing](./usage-and-billing.md) | Read token counts, context-window utilization, AI credit cost, and account quota | +| [Client info](./client-info.md) | Declare application and integration identity for runtime telemetry attribution | | [Steering & Queueing](./steering-and-queueing.md) | Control message delivery—immediate steering vs. sequential queueing | | [Context Clearing](./context-management.md) | Replace conversation context safely with terminal tools | | [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage | diff --git a/docs/features/client-info.md b/docs/features/client-info.md new file mode 100644 index 0000000000..f72780e41b --- /dev/null +++ b/docs/features/client-info.md @@ -0,0 +1,241 @@ +# Client info + +Client info identifies the application using the Copilot SDK and, when applicable, a specific integration within it. An integration is an identifiable sub-part of the application through which the SDK is used, such as an extension or plugin. Set the optional `clientInfo` client option to attribute runtime telemetry for that connection to your application instead of the runtime's own build. + +## When to set client info + +Set client info when your SDK application represents a distinct product, service, or integration whose runtime activity should be attributed consistently. + +Leave client info unset for scripts, one-off tools, and jobs that do not represent a distinct application. The runtime then keeps its default attribution. + +Client info has four optional string fields. Set the fields you know and omit the rest. The SDK includes client info in the `server.connect` handshake only when at least one field has a non-empty value. + +| Field | Example | Meaning | +|---|---|---| +| `applicationName` | `"vscode"` | Name of the application using the SDK | +| `applicationVersion` | `"1.124.2"` | Version of the application using the SDK | +| `integrationName` | `"copilot-chat"` | Name of the extension, plugin, or other application sub-part using the SDK | +| `integrationVersion` | `"0.54.0"` | Version of that extension, plugin, or application sub-part | + +For a standalone application without a distinct integration, set only the application fields. For example, a developer portal could set `applicationName` to `"acme-developer-portal"` and `applicationVersion` to `"2.4.0"`, leaving both integration fields unset. + +The SDK sends client info once when it establishes the connection. The identity applies for the lifetime of that connection. + +## Configure client info + +Pass client info when you create the client: + +
+TypeScript + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +async function main() { + const client = new CopilotClient({ + clientInfo: { + applicationName: "vscode", + applicationVersion: "1.124.2", + integrationName: "copilot-chat", + integrationVersion: "0.54.0", + }, + }); + + await client.start(); +} + +main(); +``` + + +```typescript +import { CopilotClient } from "@github/copilot-sdk"; + +const client = new CopilotClient({ + clientInfo: { + applicationName: "vscode", + applicationVersion: "1.124.2", + integrationName: "copilot-chat", + integrationVersion: "0.54.0", + }, +}); + +await client.start(); +``` + +
+ +
+Python + + +```python +from copilot import CopilotClient + +client = CopilotClient( + client_info={ + "application_name": "vscode", + "application_version": "1.124.2", + "integration_name": "copilot-chat", + "integration_version": "0.54.0", + }, +) +await client.start() +``` + +
+ +
+Go + + +```go +package main + +import ( + "context" + + copilot "github.com/github/copilot-sdk/go" +) + +func main() { + ctx := context.Background() + client := copilot.NewClient(&copilot.ClientOptions{ + ClientInfo: &copilot.ClientInfo{ + ApplicationName: "vscode", + ApplicationVersion: "1.124.2", + IntegrationName: "copilot-chat", + IntegrationVersion: "0.54.0", + }, + }) + if err := client.Start(ctx); err != nil { + return + } +} +``` + + +```go +client := copilot.NewClient(&copilot.ClientOptions{ + ClientInfo: &copilot.ClientInfo{ + ApplicationName: "vscode", + ApplicationVersion: "1.124.2", + IntegrationName: "copilot-chat", + IntegrationVersion: "0.54.0", + }, +}) +if err := client.Start(ctx); err != nil { + return err +} +``` + +
+ +
+.NET + +```csharp +using GitHub.Copilot; + +await using var client = new CopilotClient(new CopilotClientOptions +{ + ClientInfo = new CopilotClientInfo + { + ApplicationName = "vscode", + ApplicationVersion = "1.124.2", + IntegrationName = "copilot-chat", + IntegrationVersion = "0.54.0", + }, +}); + +await client.StartAsync(); +``` + +
+ +
+Java + + +```java +import com.github.copilot.CopilotClient; +import com.github.copilot.rpc.ClientInfo; +import com.github.copilot.rpc.CopilotClientOptions; + +public class ClientInfoExample { + public static void main(String[] args) throws Exception { + var options = new CopilotClientOptions() + .setClientInfo(new ClientInfo() + .setApplicationName("vscode") + .setApplicationVersion("1.124.2") + .setIntegrationName("copilot-chat") + .setIntegrationVersion("0.54.0")); + + var client = new CopilotClient(options); + client.start().get(); + } +} +``` + + +```java +var options = new CopilotClientOptions() + .setClientInfo(new ClientInfo() + .setApplicationName("vscode") + .setApplicationVersion("1.124.2") + .setIntegrationName("copilot-chat") + .setIntegrationVersion("0.54.0")); + +var client = new CopilotClient(options); +client.start().get(); +``` + +
+ +
+Rust + + +```rust +use github_copilot_sdk::{Client, ClientInfo, ClientOptions}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let _client = Client::start( + ClientOptions::new().with_client_info( + ClientInfo::new() + .with_application_name("vscode") + .with_application_version("1.124.2") + .with_integration_name("copilot-chat") + .with_integration_version("0.54.0"), + ), + ) + .await?; + Ok(()) +} +``` + + +```rust +use github_copilot_sdk::{Client, ClientInfo, ClientOptions}; + +let client = Client::start( + ClientOptions::new().with_client_info( + ClientInfo::new() + .with_application_name("vscode") + .with_application_version("1.124.2") + .with_integration_name("copilot-chat") + .with_integration_version("0.54.0"), + ), +) +.await?; +``` + +
+ +## Notes + +* Client info is advisory. The runtime can ignore values that do not match the expected format, such as an invalid version string. +* Setting client info changes how the runtime attributes its telemetry. It does not change what the runtime records. +* If every field is unset or empty, the SDK omits client info from the handshake and the runtime keeps its default attribution. diff --git a/docs/features/hooks.md b/docs/features/hooks.md index 6a78339901..a3cfc0f503 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -22,13 +22,13 @@ flowchart LR | Hook | When it fires | What you can do | | ------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------ | -| [`onSessionStart`](../hooks/session-lifecycle.md#session-start) | Session begins (new or resumed) | Inject context, load preferences | +| [`onSessionStart`](../hooks/session-lifecycle.md#session-start-hook) | Session begins (new or resumed) | Inject context, load preferences | | [`onUserPromptSubmitted`](../hooks/user-prompt-submitted.md) | User sends a message | Rewrite prompts, add context, filter input | | [`onUserPromptTransformed`](../hooks/user-prompt-transformed.md) | Runtime builds the model prompt | Inspect or replace model-facing content | | [`onPreToolUse`](../hooks/pre-tool-use.md) | Before a tool executes | Allow / deny / modify the call | | [`onPostToolUse`](../hooks/post-tool-use.md) | After a tool returns (success only) | Transform results, redact secrets, audit | | [`onPostToolUseFailure`](../hooks/post-tool-use.md#failure-variant) | After a tool returns a failure | Inject retry guidance, log failures | -| [`onSessionEnd`](../hooks/session-lifecycle.md#session-end) | Session ends | Clean up, record metrics | +| [`onSessionEnd`](../hooks/session-lifecycle.md#session-end-hook) | Session ends | Clean up, record metrics | | [`onErrorOccurred`](../hooks/error-handling.md) | An error is raised | Custom logging, retry logic, alerts | All hooks are **optional**—register only the ones you need. Returning `null` (or the language equivalent) from any hook tells the SDK to continue with default behavior. diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 3bfff10d0f..869af9b9a4 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -242,6 +242,7 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `availableTools` | Restrict which tools are available | | `excludedTools` | Disable specific tools | | `provider` | Re-provide BYOK credentials (required for BYOK sessions) | +| `capi.autoTier` | Override the persisted Auto routing preference | | `reasoningEffort` | Adjust reasoning effort level | | `streaming` | Enable/disable streaming responses | | `workingDirectory` | Change the working directory | @@ -253,6 +254,63 @@ When resuming a session, you can optionally reconfigure many settings. This is u | `disabledSkills` | Skills to disable | | `infiniteSessions` | Configure infinite session behavior | +### Auto tier persistence + +With `model: "auto"`, the optional `capi.autoTier` setting selects an Auto routing preference: `efficiency`, `balance`, or `intelligence`. In Python, use `capi={"auto_tier": "balance"}`. This requires Copilot CLI `1.0.82-1` or later with V2 Auto routing; V1 Auto requests are unchanged. + +The runtime persists the selected tier, so applications do not need to resend it on every resume: + +* Omitting the tier when creating a session uses the runtime's default routing behavior. +* A cold resume restores the persisted tier. Supplying an explicit tier overrides the restored value for the new activation. +* When resuming a session already resident in the runtime, omitting the tier preserves the current selection and supplying the same tier is a no-op. Supplying a different tier requests a safe switch that the runtime applies after the resume succeeds; it cannot change a turn that is already in flight. +* Older sessions without a persisted tier retain default routing behavior. + +Tier selection is not a live model-switch operation. The SDK forwards the preference; the runtime owns persistence and validation. + +The `session.start` and `session.resume` events expose the selected tier in their optional `data.autoTier` field (`data.auto_tier` in Python). When no tier is selected, the field is omitted. + +### Changing the Auto tier during a session + +Call `setAutoTier` to change the routing preference on a live session without changing the selected model. Pass `null` (Python `None`, Go `nil`) to return to the provider's default Auto routing. This requires Copilot CLI `1.0.83-4` or later, which is newer than the `1.0.82-1` needed to select a tier when creating or resuming a session. + +```typescript +const result = await session.setAutoTier("intelligence"); +if (result.status === "pending") { + // Accepted, but not yet in effect. +} +``` + +The runtime does not apply the preference immediately. It records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider. A `pending` status therefore confirms that the request was accepted, not that it took effect. Only the most recent request survives: a new request replaces any earlier one that no turn has claimed yet. + +Watch for the outcome through these events: + +* `session.model_change` when the preference commits. +* `session.auto_tier_switch_failed` when it does not. This event is ephemeral, so the runtime never persists or replays it on resume. Its `reason` field is one of `policy_rejected`, `request_failed`, `setup_failed`, or `unsupported`, and the previously effective preference stays active. + +You can also read the authoritative state at any time through the session's `model.getCurrent` RPC method, which reports the committed `autoTier`, any unclaimed `pendingAutoTier`, and the `activatingAutoTier` currently claimed by an in-progress activation. + +| SDK | Change the tier | Return to provider-default routing | +|-----|-----------------|------------------------------------| +| Node.js | `session.setAutoTier("balance")` | `session.setAutoTier(null)` | +| Python | `session.set_auto_tier("balance")` | `session.set_auto_tier(None)` | +| Go | `session.SetAutoTier(ctx, &tier)` | `session.SetAutoTier(ctx, nil)` | +| .NET | `session.SetAutoTierAsync(AutoTier.Balance)` | `session.SetAutoTierAsync(null)` | +| Rust | `session.set_auto_tier(Some(AutoTier::Balance))` | `session.set_auto_tier(None)` | +| Java | `session.setAutoTier(AutoTier.BALANCE)` | `session.setAutoTier(null)` | + +To select the `auto` model and its routing preference in a single call, stage the tier on the model switch instead. The runtime rejects this option when the model is anything other than `auto`. + +| SDK | Stage a tier with the switch | Reset to provider-default routing | +|-----|------------------------------|-----------------------------------| +| Node.js | `setModel("auto", { autoTier: "balance" })` | `setModel("auto", { autoTier: null })` | +| Python | `set_model("auto", auto_tier="balance")` | `set_model("auto", auto_tier=None)` | +| Go | `SetModelOptions{AutoTier: &tier}` | `SetModelOptions{ResetAutoTier: true}` | +| .NET | `new SetModelOptions { AutoTier = AutoTier.Balance }` | `new SetModelOptions { ResetAutoTier = true }` | +| Rust | `SetModelOptions::default().with_auto_tier(AutoTier::Balance)` | `SetModelOptions::default().with_reset_auto_tier()` | +| Java | `new SetModelOptions().setModel("auto").setAutoTier(AutoTier.BALANCE)` | `new SetModelOptions().setModel("auto").setResetAutoTier(true)` | + +Node.js, Python, and Rust express all three states in a single value: Node.js and Python because `null`/`None` is distinguishable from an omitted argument, and Rust because `AutoTierPreference::Reset` is a distinct variant of the same option. Go, .NET, and Java have no way to distinguish "reset" from "unset" in one value, so they carry a separate reset flag. Omitting both always means "leave the current preference alone." + ### Example: changing model on resume ```typescript diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index 10f111d9f9..0292f98e00 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -218,6 +218,48 @@ session.on(AssistantMessageDeltaEvent.class, event -> > [!TIP] > **(TypeScript)** The TypeScript SDK uses a discriminated union—when you match on `event.type`, the `data` payload is automatically narrowed to the correct shape. +## Subscribing before a session starts + +A session can emit events before its create or resume call returns. The agent may already be working—especially on resume with `continuePendingWork`—and ephemeral events such as `session.idle` are never written to the session log, so `getMessages` cannot recover them afterwards. A subscription installed after the session handle exists misses that startup window. + +> [!TIP] +> **(Rust)** `Client::prepare_session` and `Client::prepare_resume_session` return a `PreparedSession` that owns the session's event channel before any protocol activity happens. Subscribe first, then call `start()`. + +```rust +use github_copilot_sdk::{Client, SessionConfig}; + +async fn create_without_missing_startup_events( + client: &Client, +) -> Result<(), github_copilot_sdk::Error> { + let prepared = client.prepare_session( + SessionConfig::default().with_event_buffer_capacity(2048), + )?; + + // Installed before any wire activity: nothing is dropped for lack of a receiver. + let mut events = prepared.subscribe(); + tokio::spawn(async move { + while let Ok(event) = events.recv().await { + println!("{}", event.event_type); + } + }); + + let session = prepared.start().await?; + let _ = session; + Ok(()) +} +``` + +`prepare_*` is synchronous and inert: it validates the buffer capacity, allocates a local channel, and does nothing else. No session is registered and nothing reaches the CLI until `start()` is first polled. Dropping a prepared session that was never started leaves no state behind and closes its subscriptions; dropping the `start()` future cancels the in-flight startup and unregisters the session, so a retry with the same session ID succeeds. Cleanup is scoped to the exact registration the abandoned startup owned, so it cannot evict a retry that has already taken over the same session ID. + +Startup buffering is worth planning for: + +* The event buffer is finite—512 events unless `event_buffer_capacity` overrides it. A capacity of `0` is rejected with an invalid-config error rather than clamped. +* Slow subscribers observe a `Lagged` error reporting how many events were skipped. They never apply backpressure to the session's event loop. +* Consumers that need a lossless view of a large startup burst must either configure a capacity that covers it or drain the subscription concurrently with `start()`. + +> [!NOTE] +> For cloud sessions where the server assigns the session ID, the SDK cannot route notifications until the create response arrives and the ID is known. Events emitted before that point are not routable to any session. The guarantee is narrower: routed events are never dropped for lack of an installed receiver. Pin `session_id` on the config to get routing—and full pre-response coverage—from the first byte. + ## Render only the parent agent response Sub-agent events share the parent session stream and include envelope-level `agentId`. Root/main agent events and session-level events omit `agentId`, so main-chat renderers can ignore assistant events where `agentId` is set and route those events to traces or progress UI instead. diff --git a/docs/getting-started.md b/docs/getting-started.md index 53b6497fdb..5512da8531 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -19,8 +19,7 @@ Copilot: In Tokyo it's 75°F and sunny. Great day to be outside! Before you begin, make sure you have: * **GitHub Copilot CLI** installed and authenticated (the Node.js, Python, and .NET SDKs provide the CLI automatically—see [Bundled CLI](./setup/bundled-cli.md). Required for Go, Java, and Rust unless using their application-level CLI bundling features.) -* Your preferred language runtime: - * **Node.js** 20+ or **Python** 3.11+ or **Go** 1.24+ or **Rust** 1.94+ or **Java** 17+ or **.NET** 8.0+ +* Your preferred language runtime, at or above the version its SDK requires. Each SDK states its own floor in the Prerequisites section of its README: [Node.js](../nodejs/README.md#prerequisites), [Python](../python/README.md#prerequisites), [Go](../go/README.md#prerequisites), [Rust](../rust/README.md#prerequisites), [Java](../java/README.md#prerequisites), [.NET](../dotnet/README.md#prerequisites). Verify the CLI is working: diff --git a/docs/hooks/hooks-overview.md b/docs/hooks/hooks-overview.md index 8d5583e996..6177d1d2c9 100644 --- a/docs/hooks/hooks-overview.md +++ b/docs/hooks/hooks-overview.md @@ -17,10 +17,10 @@ Hooks allow you to intercept and customize the behavior of Copilot sessions at k | [`onPostToolUseFailure`](./post-tool-use.md#failure-variant) | After a tool execution whose result was a failure | Inject retry guidance, log failures | | [`onUserPromptSubmitted`](./user-prompt-submitted.md) | When user sends a message | Prompt modification, filtering | | [`onUserPromptTransformed`](./user-prompt-transformed.md) | After runtime prompt transformation | Inspect or replace model-facing content | -| [`onSessionStart`](./session-lifecycle.md#session-start) | Session begins | Add context, configure session | -| [`onSessionEnd`](./session-lifecycle.md#session-end) | Session ends | Cleanup, analytics | +| [`onSessionStart`](./session-lifecycle.md#session-start-hook) | Session begins | Add context, configure session | +| [`onSessionEnd`](./session-lifecycle.md#session-end-hook) | Session ends | Cleanup, analytics | | [`onErrorOccurred`](./error-handling.md) | Error happens | Custom error handling | -| [`onAgentStop`](./session-lifecycle.md#agent-stop) | Top-level agent naturally stops | Validate completion or request another turn | +| [`onAgentStop`](./session-lifecycle.md#agent-stop-hook) | Top-level agent naturally stops | Validate completion or request another turn | ## Quick start @@ -266,7 +266,7 @@ const session = await client.createSession({ * **[User Prompt Submitted Hook](./user-prompt-submitted.md)** - Modify user prompts * **[User Prompt Transformed Hook](./user-prompt-transformed.md)** - Replace model-facing prompts * **[Session Lifecycle Hooks](./session-lifecycle.md)** - Session start and end -* **[Agent Stop Hook](./session-lifecycle.md#agent-stop)** - Validate completion before the agent stops +* **[Agent Stop Hook](./session-lifecycle.md#agent-stop-hook)** - Validate completion before the agent stops * **[Error Handling Hook](./error-handling.md)** - Custom error handling ## See also diff --git a/docs/hooks/session-lifecycle.md b/docs/hooks/session-lifecycle.md index 485752601d..558a729ae7 100644 --- a/docs/hooks/session-lifecycle.md +++ b/docs/hooks/session-lifecycle.md @@ -7,7 +7,7 @@ Session lifecycle hooks let you respond to session start and end events. Use the * Track session metrics and analytics * Configure session behavior dynamically -## Session start hook {#session-start} +## Session start hook The `onSessionStart` hook is called when a session begins (new or resumed). @@ -250,7 +250,7 @@ const session = await client.createSession({ }); ``` -## Session end hook {#session-end} +## Session end hook The `onSessionEnd` hook is called when a session ends. @@ -540,7 +540,7 @@ Session Summary: }); ``` -## Agent stop hook {#agent-stop} +## Agent stop hook The agent stop hook runs when the top-level agent naturally reaches the end of a turn. It is separate from `onSessionEnd`: the session remains active, and the hook can request another agent turn. diff --git a/dotnet/README.md b/dotnet/README.md index 23a78030b4..9b9ca42c60 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -14,6 +14,13 @@ To use the SDK, you'll need: dotnet add package GitHub.Copilot.SDK ``` +The package downloads the pinned Copilot CLI runtime for the build RID from the +matching `github/copilot-cli` GitHub release and verifies the archive against +that release's `SHA256SUMS.txt`. Set `CopilotCliReleaseBaseUrl` in MSBuild (or +`COPILOT_CLI_DOWNLOAD_BASE_URL` in the environment) to use a release mirror. +Set `CopilotCliBinaryPath` to copy a preinstalled binary instead, or set +`CopilotSkipCliDownload=true` to omit runtime acquisition. + ## Run the Samples Try the interactive chat sample (from the repo root): @@ -284,6 +291,27 @@ await session2.DisposeAsync(); --- +## Auto routing tiers + +Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. + +```csharp +var result = await session.SetAutoTierAsync(AutoTier.Intelligence); +if (result.Status == ModelSwitchAutoTierStatus.Pending) +{ + // Accepted, but not yet in effect. +} + +// Return to the provider's default Auto routing. +await session.SetAutoTierAsync(null); +``` + +`SetModelAsync` accepts the same preference through `SetModelOptions.AutoTier`, which stages the tier atomically with selecting `auto`. Set `ResetAutoTier` instead to return to provider-default routing; the two options are mutually exclusive. + +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. + ## Event Types Sessions emit various events during processing. Each event type is a class that inherits from `SessionEvent`: diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 141473f535..cbc7d5fa2d 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -177,7 +177,7 @@ public CopilotClient(CopilotClientOptions? options = null) throw new ArgumentException("GitHubToken and UseLoggedInUser cannot be combined with RuntimeConnection.ForUri (the existing runtime manages its own auth).", nameof(options)); } var parsed = ParseRuntimeUrl(uri.Url); - _optionsHost = parsed.Host; + _optionsHost = parsed.Host.Trim('[', ']'); _optionsPort = parsed.Port; break; @@ -308,7 +308,7 @@ private static RuntimeConnection ResolveDefaultConnection(CopilotClientOptions o /// /// Parses a runtime URL into a URI with host and port. /// - /// The URL to parse. Supports formats: "port", "host:port", "http://host:port". + /// The URL to parse. Supports formats: "port", "host:port", "[ipv6]:port", "http://host:port". private static Uri ParseRuntimeUrl(string url) { // If it's just a port number, treat as localhost @@ -597,6 +597,10 @@ public async Task StopAsync() /// public async Task ForceStopAsync() { + foreach (var session in _sessions.Values) + { + session.CancelPendingExternalTools(); + } _sessions.Clear(); ClearGitHubTokenProviders(); @@ -762,7 +766,13 @@ private static async Task CleanupCliProcessAsync(Process childProcess, ProcessSt s_stderrPumpShutdownTimeout); } - AddCleanupError(errors, ex, logger); + // Once the owned process has exited, stderr is diagnostic-only. A descendant + // can briefly retain the inherited pipe on Windows, but that must not turn a + // successful process shutdown into a client cleanup failure. + if (!processExited) + { + AddCleanupError(errors, ex, logger); + } } catch (Exception ex) { @@ -1356,7 +1366,7 @@ public async Task CreateSessionAsync(SessionConfig config, Cance } catch (Exception ex) { - session?.RemoveFromClient(); + session?.Unregister(); if (ex is not OperationCanceledException) { @@ -1561,7 +1571,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes } catch (Exception ex) { - session?.RemoveFromClient(); + session?.Unregister(); if (ex is not OperationCanceledException) { LoggingHelpers.LogTiming(_logger, LogLevel.Warning, ex, @@ -2182,7 +2192,12 @@ [new ConnectHandshakeRequest( // handler is registered (mirrors the runtime, which reads this flag on the // `connect` handshake so the first session's un-replayable `session.start` // event is forwarded). Also sent on session.create/resume for older CLIs. - _options.OnGitHubTelemetry != null ? true : null)], + _options.OnGitHubTelemetry != null ? true : null, + // Declare the integrating application's identity so the runtime attributes the + // telemetry it emits on this connection to a consistent surface instead + // of its own build. Null when the app didn't supply it. + ConnectHandshakeClientInfo.From(_options.ClientInfo), + SupportedTaskKinds: [TaskKind.Agent, TaskKind.Client, TaskKind.Shell])], connection.StderrBuffer, cancellationToken); serverVersion = (int)connectResponse.ProtocolVersion; @@ -2540,17 +2555,23 @@ private static string ResolveRuntimePathForExplicitCli(string cliPath) var fullEntrypoint = Path.GetFullPath(cliPath); var directory = Path.GetDirectoryName(fullEntrypoint) ?? throw new InvalidOperationException($"Could not determine directory for '{cliPath}'."); - var flatLibraryPath = Path.Combine(directory, FfiRuntimeHost.GetRuntimeLibraryFileName()); + var flatLibraryPath = Path.GetFullPath( + $"{directory}{Path.DirectorySeparatorChar}{FfiRuntimeHost.GetRuntimeLibraryFileName()}"); if (File.Exists(flatLibraryPath)) { return flatLibraryPath; } + var adjacentPrebuildPath = Path.Combine(directory, "runtime.node"); + if (File.Exists(adjacentPrebuildPath)) + { + return adjacentPrebuildPath; + } var prebuildsLibraryPath = Path.Combine( directory, "prebuilds", GetNapiPrebuildsFolderOrThrow(), "runtime.node"); return File.Exists(prebuildsLibraryPath) ? prebuildsLibraryPath : throw new InvalidOperationException( - $"FFI runtime library not found. Looked for '{flatLibraryPath}' and '{prebuildsLibraryPath}'."); + $"FFI runtime library not found. Looked for '{flatLibraryPath}', '{adjacentPrebuildPath}', and '{prebuildsLibraryPath}'."); } /// @@ -2676,6 +2697,7 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? ClientGlobalApiRegistration.RegisterClientGlobalApiHandlers(rpc, _clientGlobalApis); } rpc.StartListening(); + _ = CancelExternalToolsWhenConnectionClosesAsync(rpc); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, "CopilotClient.ConnectToServerAsync transport setup complete. Elapsed={Elapsed}", setupTimestamp); @@ -2688,17 +2710,50 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? catch { try { rpc?.Dispose(); } - catch (Exception ex) { _logger.LogDebug(ex, "Failed to dispose JSON-RPC connection after startup failure"); } + catch (Exception ex) when (IsRecoverableConnectionCleanupFailure(ex)) + { + _logger.LogDebug(ex, "Failed to dispose JSON-RPC connection after startup failure"); + } if (networkStream is not null) { try { await networkStream.DisposeAsync(); } - catch (Exception ex) { _logger.LogDebug(ex, "Failed to dispose TCP stream after startup failure"); } + catch (Exception ex) when (IsRecoverableConnectionCleanupFailure(ex)) + { + _logger.LogDebug(ex, "Failed to dispose TCP stream after startup failure"); + } } throw; } } + private static bool IsRecoverableConnectionCleanupFailure(Exception exception) + => exception is not OutOfMemoryException + and not StackOverflowException + and not AccessViolationException + and not AppDomainUnloadedException; + + private async Task CancelExternalToolsWhenConnectionClosesAsync(JsonRpc rpc) + { + await Task.WhenAny(rpc.Completion).ConfigureAwait(false); + if (rpc.Completion.Exception is { } exception) + { + _logger.LogDebug(exception, "JSON-RPC connection completed with an error"); + } + + var connectionTask = _connectionTask; + if (connectionTask is null + || connectionTask.Status != System.Threading.Tasks.TaskStatus.RanToCompletion + || !ReferenceEquals(connectionTask.Result.Rpc, rpc)) + { + return; + } + foreach (var session in _sessions.Values) + { + session.CancelPendingExternalTools(); + } + } + private static JsonSerializerOptions SerializerOptionsForMessageFormatter { get; } = CreateSerializerOptions(); /// @@ -3187,7 +3242,43 @@ internal record GetSessionMetadataResponse( internal record ConnectHandshakeRequest( string? Token, - [property: JsonPropertyName("enableGitHubTelemetryForwarding")] bool? EnableGitHubTelemetryForwarding = null); + [property: JsonPropertyName("enableGitHubTelemetryForwarding")] bool? EnableGitHubTelemetryForwarding = null, + [property: JsonPropertyName("clientInfo")] ConnectHandshakeClientInfo? ClientInfo = null, + [property: JsonPropertyName("supportedTaskKinds")] IList? SupportedTaskKinds = null); + + internal record ConnectHandshakeClientInfo( + [property: JsonPropertyName("editorName")] string? EditorName = null, + [property: JsonPropertyName("editorVersion")] string? EditorVersion = null, + [property: JsonPropertyName("extensionName")] string? ExtensionName = null, + [property: JsonPropertyName("extensionVersion")] string? ExtensionVersion = null) + { + /// + /// Maps the public onto the connect wire + /// shape, dropping empty fields. Returns when no + /// identity was supplied so the handshake omits clientInfo and the + /// runtime keeps its default attribution. + /// + public static ConnectHandshakeClientInfo? From(CopilotClientInfo? info) + { + if (info is null) + { + return null; + } + + var editorName = NullIfEmpty(info.ApplicationName); + var editorVersion = NullIfEmpty(info.ApplicationVersion); + var extensionName = NullIfEmpty(info.IntegrationName); + var extensionVersion = NullIfEmpty(info.IntegrationVersion); + if (editorName is null && editorVersion is null && extensionName is null && extensionVersion is null) + { + return null; + } + + return new ConnectHandshakeClientInfo(editorName, editorVersion, extensionName, extensionVersion); + } + + private static string? NullIfEmpty(string? value) => string.IsNullOrEmpty(value) ? null : value; + } internal record BuiltinPluginDirectoriesRequest( string[] Paths); @@ -3227,6 +3318,7 @@ internal record HooksInvokeResponse( [JsonSerializable(typeof(GetSessionMetadataRequest))] [JsonSerializable(typeof(GetSessionMetadataResponse))] [JsonSerializable(typeof(ConnectHandshakeRequest))] + [JsonSerializable(typeof(ConnectHandshakeClientInfo))] [JsonSerializable(typeof(BuiltinPluginDirectoriesRequest))] [JsonSerializable(typeof(McpOAuthTokenStorageMode))] [JsonSerializable(typeof(EmbeddingCacheStorageMode))] diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index c7be742ee2..2c74ab4bde 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -56,6 +56,10 @@ internal sealed class ConnectResult [JsonPropertyName("protocolVersion")] public long ProtocolVersion { get; set; } + /// Task kinds the server may return to this connection. + [JsonPropertyName("taskKinds")] + public IList? TaskKinds { get; set; } + /// Server package version. [JsonPropertyName("version")] public string Version { get; set; } = string.Empty; @@ -94,11 +98,78 @@ internal sealed class ConnectRequest [JsonPropertyName("enableGitHubTelemetryForwarding")] public bool? EnableGitHubTelemetryForwarding { get; set; } + /// Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. + [JsonPropertyName("supportedTaskKinds")] + public IList? SupportedTaskKinds { get; set; } + /// Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN. [JsonPropertyName("token")] public string? Token { get; set; } } +/// One server-discovered hook action from user, repository, plugin, or managed-policy configuration. +[Experimental(Diagnostics.Experimental)] +public sealed class DiscoveredHook +{ + /// Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. + [JsonPropertyName("disableKey")] + public string? DisableKey { get; set; } + + /// Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Hook event that invokes this action. + [JsonPropertyName("hookType")] + public HookType HookType { get; set; } + + /// Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Configuration tier that contributed this hook action. + [JsonPropertyName("origin")] + public HookOrigin Origin { get; set; } + + /// Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. + [JsonPropertyName("projectPath")] + public string? ProjectPath { get; set; } + + /// Human-readable source label, such as a hook file path, settings source, or plugin name. + [JsonPropertyName("source")] + public string? Source { get; set; } +} + +/// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. +[Experimental(Diagnostics.Experimental)] +public sealed class HooksDiscoverResult +{ + /// Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + [JsonPropertyName("errors")] + public IList Errors { get => field ??= []; set; } + + /// All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + [JsonPropertyName("hooks")] + public IList Hooks { get => field ??= []; set; } + + /// Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + [JsonPropertyName("warnings")] + public IList Warnings { get => field ??= []; set; } +} + +/// Optional project paths and host-exclusion behavior for server-scoped hook discovery. +[Experimental(Diagnostics.Experimental)] +internal sealed class HooksDiscoverRequest +{ + /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + [JsonPropertyName("excludeHostHooks")] + public bool? ExcludeHostHooks { get; set; } + + /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + [JsonPropertyName("projectPaths")] + public IList? ProjectPaths { get; set; } +} + /// Active server-driven promotion for a model, including its discount and optional expiry. [Experimental(Diagnostics.Experimental)] public sealed class ModelBillingPromo @@ -118,6 +189,10 @@ public sealed class ModelBillingPromo /// Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. [JsonPropertyName("message")] public string? Message { get; set; } + + /// Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + [JsonPropertyName("showBanner")] + public bool? ShowBanner { get; set; } } /// Long context tier pricing (available for models with extended context windows). @@ -366,6 +441,10 @@ public sealed class Model [JsonPropertyName("infoMessages")] public IList? InfoMessages { get; set; } + /// Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. + [JsonPropertyName("metadata")] + public IDictionary? Metadata { get; set; } + /// Model capability category for grouping in the model picker. [JsonPropertyName("modelPickerCategory")] public ModelPickerCategory? ModelPickerCategory { get; set; } @@ -1841,6 +1920,11 @@ public partial class McpPlanInstallResultNetworkFailure : McpPlanInstallResult [JsonPropertyName("reason")] public required CatalogNetworkFailureReason Reason { get; set; } + /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("retryAfterSeconds")] + public int? RetryAfterSeconds { get; set; } + /// HTTP status code, when the failure was a rejected response. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("statusCode")] @@ -2159,6 +2243,10 @@ internal sealed class McpConfigUpdateRequest [Experimental(Diagnostics.Experimental)] internal sealed class McpConfigRemoveRequest { + /// OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. + [JsonPropertyName("authClientIdMetadataUrl")] + public string? AuthClientIdMetadataUrl { get; set; } + /// Name of the MCP server to remove. [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] @@ -2655,6 +2743,11 @@ public partial class CatalogSearchResultNetworkFailure : CatalogSearchResult [JsonPropertyName("reason")] public required CatalogNetworkFailureReason Reason { get; set; } + /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("retryAfterSeconds")] + public int? RetryAfterSeconds { get; set; } + /// HTTP status code, when the failure was a rejected response. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("statusCode")] @@ -2762,7 +2855,7 @@ internal sealed class CatalogSearchRequest [JsonPropertyName("limit")] public int? Limit { get; set; } - /// Free-text search query. Never written to logs or telemetry. + /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. [RegularExpression("\\S")] [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] [MinLength(1)] @@ -2828,6 +2921,10 @@ public sealed class PluginInstallResult /// Number of skills discovered and installed from the plugin. [JsonPropertyName("skillsInstalled")] public long SkillsInstalled { get; set; } + + /// Where the completed plugin tree was staged before atomic promotion. + [JsonPropertyName("stagingMode")] + public PluginInstallStagingMode? StagingMode { get; set; } } /// Plugin source and optional working directory for relative-path resolution. @@ -3227,7 +3324,7 @@ internal sealed class SkillsConfigSetSkillDisabledRequest public string Name { get; set; } = string.Empty; } -/// Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. +/// Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. [Experimental(Diagnostics.Experimental)] public sealed class AgentInfo { @@ -3252,6 +3349,14 @@ public sealed class AgentInfo [JsonPropertyName("model")] public string? Model { get; set; } + /// Whether authored models are preferences or required constraints. + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } + + /// Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + [JsonPropertyName("models")] + public IList? Models { get; set; } + /// Name of the agent. Use `id` as the stable selection identifier. [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; @@ -4216,6 +4321,48 @@ internal sealed class SessionsGetMetadataRequest public string SessionId { get; set; } = string.Empty; } +/// Batch of session events returned by a read, with cursor and continuation metadata. +[Experimental(Diagnostics.Experimental)] +public sealed class EventsReadResult +{ + /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). + [JsonPropertyName("cursor")] + public string Cursor { get; set; } = string.Empty; + + /// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. + [JsonPropertyName("cursorStatus")] + public EventsCursorStatus CursorStatus { get; set; } + + /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. + [JsonPropertyName("events")] + public IList Events { get => field ??= []; set; } + + /// True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. + [JsonPropertyName("hasMore")] + public bool HasMore { get; set; } +} + +/// Pagination options for reading an inactive or active local session's persisted event journal. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionsReadPersistedEventsRequest +{ + /// Opaque cursor returned by a previous persisted-event read. Omit on the first call. + [JsonPropertyName("cursor")] + public string? Cursor { get; set; } + + /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. + [JsonPropertyName("direction")] + public EventsReadDirection? Direction { get; set; } + + /// Maximum number of events to return in this batch (1–1000, default 200). + [JsonPropertyName("max")] + public long? Max { get; set; } + + /// Session ID whose persisted event journal should be read. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Recent local session IDs that contain user-visible history. [Experimental(Diagnostics.Experimental)] internal sealed class SessionsListNonEmptySessionIdsResult @@ -6244,6 +6391,7 @@ internal sealed class CanvasProviderUnregisterRequest [JsonDerivedType(typeof(FactoryRunFailureFactoryResumeDeclined), "factory_resume_declined")] [JsonDerivedType(typeof(FactoryRunFailureFactoryDurableFailure), "factory_durable_failure")] [JsonDerivedType(typeof(FactoryRunFailureFactoryAccountingIncomplete), "factory_accounting_incomplete")] +[JsonDerivedType(typeof(FactoryRunFailureFactoryProviderDisconnected), "factory_provider_disconnected")] public partial class FactoryRunFailure { /// The type discriminator. @@ -6329,15 +6477,33 @@ public partial class FactoryRunFailureFactoryAccountingIncomplete : FactoryRunFa public required string RunId { get; set; } } +/// The extension that owns the factory disconnected while the run was executing, so the host halted it. The run's journaled subagent results are preserved so a resume can reuse them. +/// The factory_provider_disconnected variant of . +[Experimental(Diagnostics.Experimental)] +public partial class FactoryRunFailureFactoryProviderDisconnected : FactoryRunFailure +{ + /// + [JsonIgnore] + public override string Type => "factory_provider_disconnected"; + + /// Factory run identifier. + [JsonPropertyName("runId")] + public required string RunId { get; set; } +} + /// Complete current or terminal factory run envelope. [Experimental(Diagnostics.Experimental)] public sealed class FactoryRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + [JsonPropertyName("attempt")] + public long? Attempt { get; set; } + /// Error message for an errored run. [JsonPropertyName("error")] public string? Error { get; set; } - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. [JsonPropertyName("failure")] public FactoryRunFailure? Failure { get; set; } @@ -7211,10 +7377,18 @@ internal sealed class FactoryJournalPutRequest public string SessionId { get; set; } = string.Empty; } -/// The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. +/// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. [Experimental(Diagnostics.Experimental)] public sealed class CurrentModel { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + [JsonPropertyName("activatingAutoTier")] + public AutoTier? ActivatingAutoTier { get; set; } + + /// Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Context tier for models that support multiple context-window sizes. [JsonPropertyName("contextTier")] public ContextTier? ContextTier { get; set; } @@ -7223,6 +7397,10 @@ public sealed class CurrentModel [JsonPropertyName("modelId")] public string? ModelId { get; set; } + /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + [JsonPropertyName("pendingAutoTier")] + public AutoTier? PendingAutoTier { get; set; } + /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. [JsonPropertyName("reasoningEffort")] public string? ReasoningEffort { get; set; } @@ -7278,6 +7456,10 @@ public sealed class ModelSwitchToResult [JsonPropertyName("modelId")] public string? ModelId { get; set; } + /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. + [JsonPropertyName("modelState")] + public CurrentModel? ModelState { get; set; } + /// Persistence failure encountered after applying the model switch. [JsonPropertyName("persistenceError")] public string? PersistenceError { get; set; } @@ -7402,6 +7584,10 @@ public sealed class ModelPickerPersistenceRequest [Experimental(Diagnostics.Experimental)] internal sealed class ModelSwitchToRequest { + /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. [JsonPropertyName("compactionDecision")] public string? CompactionDecision { get; set; } @@ -7454,7 +7640,7 @@ internal sealed class ModelSwitchToRequest [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. [JsonPropertyName("source")] public ModelChangeSource? Source { get; set; } @@ -7463,6 +7649,48 @@ internal sealed class ModelSwitchToRequest public Verbosity? Verbosity { get; set; } } +/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. +[Experimental(Diagnostics.Experimental)] +public sealed class ModelSwitchAutoTierResult +{ + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + [JsonPropertyName("activatingAutoTier")] + public AutoTier? ActivatingAutoTier { get; set; } + + /// Auto preference currently committed for the session. + [JsonPropertyName("effectiveAutoTier")] + public AutoTier? EffectiveAutoTier { get; set; } + + /// Latest unclaimed Auto preference waiting for a future user turn. + [JsonPropertyName("pendingAutoTier")] + public AutoTier? PendingAutoTier { get; set; } + + /// Immediate request status. `pending` means accepted but not committed. + [JsonPropertyName("status")] + public ModelSwitchAutoTierStatus Status { get; set; } + + /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + [JsonPropertyName("supersededAutoTier")] + public AutoTier? SupersededAutoTier { get; set; } +} + +/// An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. +[Experimental(Diagnostics.Experimental)] +internal sealed class ModelSwitchAutoTierRequest +{ + /// Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + [JsonPropertyName("source")] + public ModelChangeSource? Source { get; set; } +} + /// Managed, repository, and CLI model overrides to overlay onto the session at startup. [Experimental(Diagnostics.Experimental)] internal sealed class ModelApplyStartupOverlayRequest @@ -7479,6 +7707,10 @@ internal sealed class ModelApplyStartupOverlayRequest [JsonPropertyName("deviceManagedModel")] public string? DeviceManagedModel { get; set; } + /// Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. + [JsonPropertyName("policyHelperModel")] + public string? PolicyHelperModel { get; set; } + /// Context tier selected by repository settings, when configured. [JsonPropertyName("repoContextTier")] public string? RepoContextTier { get; set; } @@ -8333,6 +8565,80 @@ internal sealed class WorkspacesDiffRequest public string SessionId { get; set; } = string.Empty; } +/// Current per-window credit limit and consumption for an autopilot objective. +[Experimental(Diagnostics.Experimental)] +public sealed class AutopilotObjectiveCreditLimit +{ + /// Configured AI-credit cap, when one is set. + [JsonPropertyName("credits")] + public double? Credits { get; set; } + + /// Window consumption in fractional AI credits, for display. + [JsonPropertyName("creditsUsed")] + public double CreditsUsed { get; set; } + + /// Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. + [RegularExpression("^[0-9]+$")] + [JsonPropertyName("creditsUsedNanoAiu")] + public string CreditsUsedNanoAiu { get; set; } = string.Empty; +} + +/// Public, persistence-independent projection of an autopilot objective. +[Experimental(Diagnostics.Experimental)] +public sealed class AutopilotObjectiveState +{ + /// Optional summary recorded when the objective completed. + [JsonPropertyName("completionSummary")] + public string? CompletionSummary { get; set; } + + /// Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. + [RegularExpression("^[0-9]+$")] + [JsonPropertyName("creditCountNanoAiu")] + public string CreditCountNanoAiu { get; set; } = string.Empty; + + /// Current per-window consumption and optional cap, when a credit-tracking window is present. + [JsonPropertyName("creditLimit")] + public AutopilotObjectiveCreditLimit? CreditLimit { get; set; } + + /// Session-local objective identifier. + [JsonPropertyName("id")] + public long Id { get; set; } + + /// User-provided objective text. + [JsonPropertyName("objective")] + public string Objective { get; set; } = string.Empty; + + /// Optional reason the objective is paused. + [JsonPropertyName("pauseReason")] + public string? PauseReason { get; set; } + + /// Current normalized lifecycle status. + [JsonPropertyName("status")] + public AutopilotObjectiveStatus Status { get; set; } + + /// Number of objective turns started. + [JsonPropertyName("turnCount")] + public long TurnCount { get; set; } +} + +/// Canonical runtime state for the session's current autopilot objective. +[Experimental(Diagnostics.Experimental)] +public sealed class AutopilotObjectiveGetStateResult +{ + /// Current objective state, or `null` when the session has no objective. + [JsonPropertyName("state")] + public AutopilotObjectiveState? State { get; set; } +} + +/// Identifies the target session. +[Experimental(Diagnostics.Experimental)] +internal sealed class SessionAutopilotObjectiveGetStateRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Characters that, when typed in the composer, should trigger a `completions.request`. Empty when the session has no host-driven completions (e.g. local sessions, or a relay host that does not advertise `completionTriggerCharacters`). [Experimental(Diagnostics.Experimental)] public sealed class CompletionsGetTriggerCharactersResult @@ -8603,13 +8909,14 @@ internal sealed class TasksStartAgentRequest public string SessionId { get; set; } = string.Empty; } -/// Tracked task union returned by task APIs, containing either an agent task or a shell task. +/// Tracked task union returned by task APIs, containing an agent, client, or shell task. /// Polymorphic base type discriminated by type. [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(TaskInfoAgent), "agent")] +[JsonDerivedType(typeof(TaskInfoClient), "client")] [JsonDerivedType(typeof(TaskInfoShell), "shell")] public partial class TaskInfo { @@ -8718,6 +9025,143 @@ public partial class TaskInfoAgent : TaskInfo public required string ToolCallId { get; set; } } +/// Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. +[Experimental(Diagnostics.Experimental)] +public sealed class TaskClientOwner +{ + /// ISO 8601 timestamp when the bound join disconnected. + [JsonPropertyName("disconnectedAt")] + public DateTimeOffset? DisconnectedAt { get; set; } + + /// Display-only owner name. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Opaque identity of the currently or most recently bound session join. + [JsonPropertyName("joinId")] + public string JoinId { get; set; } = string.Empty; + + /// Class of the task owner. + [JsonPropertyName("kind")] + public TaskClientOwnerKind Kind { get; set; } + + /// Opaque session-scoped participant identity. + [JsonPropertyName("participantId")] + public string ParticipantId { get; set; } = string.Empty; + + /// Whether this task's bound join is currently connected. + [JsonPropertyName("presence")] + public TaskClientOwnerPresence Presence { get; set; } + + /// Display-only owner source. + [JsonPropertyName("source")] + public string? Source { get; set; } +} + +/// Tracked client-owned task metadata. +/// The client variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskInfoClient : TaskInfo +{ + /// + [JsonIgnore] + public override string Type => "client"; + + /// ISO 8601 timestamp when the current active segment started. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } + + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeTimeMs")] + public required long ActiveTimeMs { get; set; } + + /// Whether the currently bound owner can receive a cancellation request. + [JsonPropertyName("canCancel")] + public required bool CanCancel { get; set; } + + /// Human-readable reason for terminal cancellation. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cancellationReason")] + public string? CancellationReason { get; set; } + + /// Owner-scoped registration and reclaim key. + [JsonPropertyName("clientTaskId")] + public required string ClientTaskId { get; set; } + + /// ISO 8601 timestamp when the task reached a terminal status. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + /// Task description. + [JsonPropertyName("description")] + public required string Description { get; set; } + + /// Optional task display name. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Human-readable terminal failure message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Optional owner-supplied terminal failure code. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } + + /// Execution mode, which is always background for client-owned tasks. + [JsonPropertyName("executionMode")] + public required TaskClientExecutionMode ExecutionMode { get; set; } + + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public required string Id { get; set; } + + /// ISO 8601 timestamp when the connected owner entered idle status. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } + + /// ISO 8601 timestamp of the most recent orphan transition. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("orphanedAt")] + public DateTimeOffset? OrphanedAt { get; set; } + + /// Public attribution and presence for the task owner. + [JsonPropertyName("owner")] + public required TaskClientOwner Owner { get; set; } + + /// ISO 8601 timestamp of the most recent successful reclaim. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reclaimedAt")] + public DateTimeOffset? ReclaimedAt { get; set; } + + /// Opaque successful terminal result supplied by the task owner. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } + + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public required long Sequence { get; set; } + + /// ISO 8601 timestamp when the task started. + [JsonPropertyName("startedAt")] + public required DateTimeOffset StartedAt { get; set; } + + /// Client task lifecycle status. + [JsonPropertyName("status")] + public required TaskClientStatus Status { get; set; } + + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public required DateTimeOffset UpdatedAt { get; set; } +} + /// Tracked shell task metadata, including ID, command, status, timing, attachment/execution mode, log path, and PID. /// The shell variant of . [Experimental(Diagnostics.Experimental)] @@ -8795,6 +9239,299 @@ internal sealed class SessionTasksListRequest public string SessionId { get; set; } = string.Empty; } +/// Tracked client-owned task metadata. +[Experimental(Diagnostics.Experimental)] +public sealed class TaskClientInfo +{ + /// ISO 8601 timestamp when the current active segment started. + [JsonPropertyName("activeStartedAt")] + public DateTimeOffset? ActiveStartedAt { get; set; } + + /// Accumulated active execution time in milliseconds. + [JsonPropertyName("activeTimeMs")] + public long ActiveTimeMs { get; set; } + + /// Whether the currently bound owner can receive a cancellation request. + [JsonPropertyName("canCancel")] + public bool CanCancel { get; set; } + + /// Human-readable reason for terminal cancellation. + [JsonPropertyName("cancellationReason")] + public string? CancellationReason { get; set; } + + /// Owner-scoped registration and reclaim key. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; + + /// ISO 8601 timestamp when the task reached a terminal status. + [JsonPropertyName("completedAt")] + public DateTimeOffset? CompletedAt { get; set; } + + /// Task description. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Optional task display name. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Human-readable terminal failure message. + [JsonPropertyName("error")] + public string? Error { get; set; } + + /// Optional owner-supplied terminal failure code. + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } + + /// Execution mode, which is always background for client-owned tasks. + [JsonPropertyName("executionMode")] + public TaskClientExecutionMode ExecutionMode { get; set; } + + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// ISO 8601 timestamp when the connected owner entered idle status. + [JsonPropertyName("idleSince")] + public DateTimeOffset? IdleSince { get; set; } + + /// ISO 8601 timestamp of the most recent orphan transition. + [JsonPropertyName("orphanedAt")] + public DateTimeOffset? OrphanedAt { get; set; } + + /// Public attribution and presence for the task owner. + [JsonPropertyName("owner")] + public TaskClientOwner Owner { get => field ??= new(); set; } + + /// ISO 8601 timestamp of the most recent successful reclaim. + [JsonPropertyName("reclaimedAt")] + public DateTimeOffset? ReclaimedAt { get; set; } + + /// Opaque successful terminal result supplied by the task owner. + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } + + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public long Sequence { get; set; } + + /// ISO 8601 timestamp when the task started. + [JsonPropertyName("startedAt")] + public DateTimeOffset StartedAt { get; set; } + + /// Client task lifecycle status. + [JsonPropertyName("status")] + public TaskClientStatus Status { get; set; } + + /// Task kind. + [JsonPropertyName("type")] + public TaskClientType Type { get; set; } + + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public DateTimeOffset UpdatedAt { get; set; } +} + +/// Result of registering or reclaiming a client-owned task. +[Experimental(Diagnostics.Experimental)] +public sealed class TasksRegisterResult +{ + /// True only when this invocation created a new task. + [JsonPropertyName("created")] + public bool Created { get; set; } + + /// True only when this invocation reclaimed an orphaned task. + [JsonPropertyName("reclaimed")] + public bool Reclaimed { get; set; } + + /// Authoritative registered or reclaimed task. + [JsonPropertyName("task")] + public TaskClientInfo Task { get => field ??= new(); set; } +} + +/// Registers or reclaims a client-owned task. +[Experimental(Diagnostics.Experimental)] +internal sealed class TasksRegisterRequest +{ + /// Whether the owner supports runtime cancellation requests. + [JsonPropertyName("cancellable")] + public bool Cancellable { get; set; } + + /// Owner-scoped idempotency key used for registration and reclaim. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; + + /// Human-readable description of the external work. + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// Optional short display name for the external work. + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } + + /// Expected current sequence for idempotent registration or orphan reclaim. + [JsonPropertyName("expectedSequence")] + public long? ExpectedSequence { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Task kind. + [JsonPropertyName("type")] + public TaskClientType Type { get; set; } +} + +/// Result of publishing a client-owned task update. +[Experimental(Diagnostics.Experimental)] +public sealed class TasksUpdateResult +{ + /// Whether this invocation changed task state. + [JsonPropertyName("applied")] + public bool Applied { get; set; } + + /// Whether this invocation repeated the latest accepted update. + [JsonPropertyName("duplicate")] + public bool Duplicate { get; set; } + + /// Authoritative task after processing the update. + [JsonPropertyName("task")] + public TaskClientInfo Task { get => field ??= new(); set; } +} + +/// Progress or terminal update for a client-owned task. +/// Polymorphic base type discriminated by kind. +[Experimental(Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(TaskClientUpdateProgress), "progress")] +[JsonDerivedType(typeof(TaskClientUpdateCompleted), "completed")] +[JsonDerivedType(typeof(TaskClientUpdateFailed), "failed")] +[JsonDerivedType(typeof(TaskClientUpdateCancelled), "cancelled")] +public partial class TaskClientUpdate +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} + + +/// Publishes nonterminal progress for a running or idle client task. +/// The progress variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskClientUpdateProgress : TaskClientUpdate +{ + /// + [JsonIgnore] + public override string Kind => "progress"; + + /// Optional progress message appended to recent activity when nonempty. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// Optional completion percentage; null clears the current percentage. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("percentage")] + public double? Percentage { get; set; } + + /// Optional progress phase; null clears the current phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phase")] + public string? Phase { get; set; } + + /// Optional active status transition. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("status")] + public TaskClientActiveStatus? Status { get; set; } +} + +/// Reports successful terminal completion. +/// The completed variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskClientUpdateCompleted : TaskClientUpdate +{ + /// + [JsonIgnore] + public override string Kind => "completed"; + + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// Optional opaque successful terminal result. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("result")] + public JsonElement? Result { get; set; } +} + +/// Reports terminal failure. +/// The failed variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskClientUpdateFailed : TaskClientUpdate +{ + /// + [JsonIgnore] + public override string Kind => "failed"; + + /// Optional owner-supplied terminal failure code. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("code")] + public string? Code { get; set; } + + /// Human-readable terminal failure message. + [JsonPropertyName("error")] + public required string Error { get; set; } + + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } +} + +/// Reports terminal cancellation after external work stopped. +/// The cancelled variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskClientUpdateCancelled : TaskClientUpdate +{ + /// + [JsonIgnore] + public override string Kind => "cancelled"; + + /// Optional final progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("message")] + public string? Message { get; set; } + + /// Optional human-readable cancellation reason. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("reason")] + public string? Reason { get; set; } +} + +/// Updates a client-owned task. +[Experimental(Diagnostics.Experimental)] +internal sealed class TasksUpdateRequest +{ + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Owner update sequence to apply. + [JsonPropertyName("sequence")] + public long Sequence { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + /// Progress or terminal update payload. + [JsonPropertyName("update")] + public TaskClientUpdate Update { get => field ??= new(); set; } +} + /// Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop. [Experimental(Diagnostics.Experimental)] public sealed class TasksRefreshResult @@ -8825,13 +9562,16 @@ internal sealed class SessionTasksWaitForPendingRequest public string SessionId { get; set; } = string.Empty; } -/// Polymorphic base type discriminated by type. +/// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. +/// Polymorphic base type discriminated by type. +[Experimental(Diagnostics.Experimental)] [JsonPolymorphic( TypeDiscriminatorPropertyName = "type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] -[JsonDerivedType(typeof(TasksGetProgressResultProgressAgent), "agent")] -[JsonDerivedType(typeof(TasksGetProgressResultProgressShell), "shell")] -public partial class TasksGetProgressResultProgress +[JsonDerivedType(typeof(TaskProgressAgent), "agent")] +[JsonDerivedType(typeof(TaskProgressClient), "client")] +[JsonDerivedType(typeof(TaskProgressShell), "shell")] +public partial class TaskProgress { /// The type discriminator. [JsonPropertyName("type")] @@ -8853,8 +9593,9 @@ public sealed class TaskProgressLine } /// Progress snapshot for an agent task, with recent activity lines and optional latest intent. -/// The agent variant of . -public partial class TasksGetProgressResultProgressAgent : TasksGetProgressResultProgress +/// The agent variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskProgressAgent : TaskProgress { /// [JsonIgnore] @@ -8870,9 +9611,51 @@ public partial class TasksGetProgressResultProgressAgent : TasksGetProgressResul public required IList RecentActivity { get; set; } } +/// Generic progress for a client-owned task. +/// The client variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskProgressClient : TaskProgress +{ + /// + [JsonIgnore] + public override string Type => "client"; + + /// Most recent nonempty progress message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("lastMessage")] + public string? LastMessage { get; set; } + + /// Current completion percentage from zero through one hundred. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("percentage")] + public double? Percentage { get; set; } + + /// Current owner-defined progress phase. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phase")] + public string? Phase { get; set; } + + /// Recent server-timestamped progress messages. + [JsonPropertyName("recentActivity")] + public required IList RecentActivity { get; set; } + + /// Sequence number of the latest accepted owner update. + [JsonPropertyName("sequence")] + public required long Sequence { get; set; } + + /// Current client task lifecycle status. + [JsonPropertyName("status")] + public required TaskClientStatus Status { get; set; } + + /// ISO 8601 timestamp of the latest accepted lifecycle change. + [JsonPropertyName("updatedAt")] + public required DateTimeOffset UpdatedAt { get; set; } +} + /// Progress snapshot for a shell task, with recent stdout/stderr output and optional process ID. -/// The shell variant of . -public partial class TasksGetProgressResultProgressShell : TasksGetProgressResultProgress +/// The shell variant of . +[Experimental(Diagnostics.Experimental)] +public partial class TaskProgressShell : TaskProgress { /// [JsonIgnore] @@ -8894,7 +9677,7 @@ public sealed class TasksGetProgressResult { /// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. [JsonPropertyName("progress")] - public TasksGetProgressResultProgress? Progress { get; set; } + public TaskProgress? Progress { get; set; } } /// Identifier of the background task to fetch progress for. @@ -9117,6 +9900,10 @@ public sealed class SkillsInvokedSkill [JsonPropertyName("content")] public string Content { get; set; } = string.Empty; + /// Whether model invocation was disabled when this skill was invoked. + [JsonPropertyName("disableModelInvocation")] + public bool? DisableModelInvocation { get; set; } + /// Turn number when the skill was invoked. [JsonPropertyName("invokedAtTurn")] public long InvokedAtTurn { get; set; } @@ -9125,7 +9912,7 @@ public sealed class SkillsInvokedSkill [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; - /// Path to the SKILL.md file. + /// Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity. [JsonPropertyName("path")] public string Path { get; set; } = string.Empty; } @@ -9275,6 +10062,10 @@ public sealed class McpServer [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; + /// Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + [JsonPropertyName("serverMetadata")] + public McpServerMetadata? ServerMetadata { get; set; } + /// Configuration source: user, workspace, plugin, or builtin. [JsonPropertyName("source")] public McpServerSource? Source { get; set; } @@ -10959,7 +11750,7 @@ public sealed class OptionsUpdateAdditionalContentExclusionPolicy [Experimental(Diagnostics.Experimental)] public sealed class CapiSessionOptions { - /// Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. + /// Routing preference for sessions whose model is `auto`. On create or cold resume, this establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold resume, the runtime restores the last committed preference. On resident resume, a different value requests a safe switch after resume succeeds and cannot change an in-flight turn. Successful switches are persisted for later cold resume. When no preference is supplied or restored, CAPI default routing is used. [JsonPropertyName("autoTier")] public AutoTier? AutoTier { get; set; } @@ -11138,7 +11929,7 @@ public sealed class SandboxConfigUserPolicyNetworkProxy [JsonPropertyName("password")] public string? Password { get; set; } - /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. [JsonPropertyName("url")] public string Url { get; set; } = string.Empty; @@ -11159,7 +11950,7 @@ public sealed class SandboxConfigUserPolicyNetwork [JsonPropertyName("allowOutbound")] public bool? AllowOutbound { get; set; } - /// HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. + /// HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is accepted and routed through the IPv4 gateway), and does not support proxy credentials. macOS relies on applications honoring proxy environment variables. Windows also configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's networking stack. Configure supported credentials in the separate `username` and `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, while an https:// or authenticated loopback URL uses the URL form. [JsonPropertyName("proxy")] public SandboxConfigUserPolicyNetworkProxy? Proxy { get; set; } } @@ -11202,6 +11993,10 @@ public sealed class SandboxConfig [JsonPropertyName("addCurrentWorkingDirectory")] public bool? AddCurrentWorkingDirectory { get; set; } + /// Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). + [JsonPropertyName("allowBypass")] + public bool? AllowBypass { get; set; } + /// Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). [JsonPropertyName("allowDevToolAccess")] public bool? AllowDevToolAccess { get; set; } @@ -11214,6 +12009,24 @@ public sealed class SandboxConfig [JsonPropertyName("enabled")] public bool Enabled { get; set; } + /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + [JsonInclude] + [JsonPropertyName("managedLspRoutingLocked")] + internal bool? ManagedLspRoutingLocked { get; set; } + + /// Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. + [JsonInclude] + [JsonPropertyName("managedMcpRoutingLocked")] + internal bool? ManagedMcpRoutingLocked { get; set; } + + /// Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("sandboxLspServers")] + public bool? SandboxLspServers { get; set; } + + /// Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + [JsonPropertyName("sandboxMcpServers")] + public bool? SandboxMcpServers { get; set; } + /// User-managed sandbox policy fragment merged into the auto-discovered base policy. [JsonPropertyName("userPolicy")] public SandboxConfigUserPolicy? UserPolicy { get; set; } @@ -11399,7 +12212,7 @@ internal sealed class SessionUpdateOptionsParams [JsonPropertyName("enableSessionStore")] public bool? EnableSessionStore { get; set; } - /// Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. [JsonPropertyName("enableSkills")] public bool? EnableSkills { get; set; } @@ -12929,6 +13742,10 @@ public sealed class SubagentSettingsEntry /// Model override for matching subagents. [JsonPropertyName("model")] public string? Model { get; set; } + + /// Whether the configured model strategy is preferred or required. + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } } /// Configured per-agent subagent overrides. @@ -13100,6 +13917,11 @@ public partial class SlashCommandInvocationResultCompleted : SlashCommandInvocat [JsonPropertyName("message")] public string? Message { get; set; } + /// Optional target session mode applied without submitting an agent prompt. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("mode")] + public SessionMode? Mode { get; set; } + /// True when the invocation mutated user runtime settings; consumers caching settings should refresh. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("runtimeSettingsChanged")] @@ -13154,6 +13976,10 @@ public partial class SlashCommandInvocationResultSelectSubcommand : SlashCommand [Experimental(Diagnostics.Experimental)] public sealed class SlashCommandTimelineEntry { + /// What the user must do to recover, when the entry reports a failure the runtime knows an action for. The `text` never names a client affordance, so a client that offers one renders it from this value. + [JsonPropertyName("remediation")] + public RemediationAction? Remediation { get; set; } + /// Text displayed for the timeline entry. [JsonPropertyName("text")] public string Text { get; set; } = string.Empty; @@ -16641,6 +17467,10 @@ public sealed class QueuePendingItems /// Whether this item is a queued user message or a queued slash command / model change. [JsonPropertyName("kind")] public QueuePendingItemsKind Kind { get; set; } + + /// Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. + [JsonPropertyName("messageId")] + public string? MessageId { get; set; } } /// Snapshot of the session's pending queued items and immediate-steering messages. @@ -17063,27 +17893,6 @@ internal sealed class SessionQueueProcessRequest public string SessionId { get; set; } = string.Empty; } -/// Batch of session events returned by a read, with cursor and continuation metadata. -[Experimental(Diagnostics.Experimental)] -public sealed class EventsReadResult -{ - /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). - [JsonPropertyName("cursor")] - public string Cursor { get; set; } = string.Empty; - - /// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. - [JsonPropertyName("cursorStatus")] - public EventsCursorStatus CursorStatus { get; set; } - - /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. - [JsonPropertyName("events")] - public IList Events { get => field ??= []; set; } - - /// True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. - [JsonPropertyName("hasMore")] - public bool HasMore { get; set; } -} - /// Cursor, batch size, and optional long-poll/filter parameters for reading session events. [Experimental(Diagnostics.Experimental)] internal sealed class EventLogReadRequest @@ -17942,6 +18751,40 @@ public sealed class FactoryAbortRequest public string SessionId { get; set; } = string.Empty; } +/// Whether the client authoritatively confirmed its external work stopped. +[Experimental(Diagnostics.Experimental)] +public sealed class ClientTaskCancelResult +{ + /// True only when the owner confirms that external work stopped before responding. + [JsonPropertyName("cancelled")] + public bool Cancelled { get; set; } +} + +/// Runtime-to-owner cancellation request for a client-owned task. +[Experimental(Diagnostics.Experimental)] +public sealed class ClientTaskCancelRequest +{ + /// Opaque identifier shared by coalesced cancellation callers. + [JsonPropertyName("cancellationId")] + public string CancellationId { get; set; } = string.Empty; + + /// Owner-scoped task key included for correlation. + [JsonPropertyName("clientTaskId")] + public string ClientTaskId { get; set; } = string.Empty; + + /// Canonical runtime-generated task identifier. + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// Reason the runtime requests cancellation. + [JsonPropertyName("reason")] + public ClientTaskCancelReason Reason { get; set; } + + /// Session that owns the client task. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Describes a filesystem error. [Experimental(Diagnostics.Experimental)] public sealed class SessionFsError @@ -18795,6 +19638,249 @@ public sealed class GitHubTokenAcquireRequest public string? SessionId { get; set; } } +/// Closed set of public task kinds a connection can negotiate. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Runtime-owned background agent task. + public static TaskKind Agent { get; } = new("agent"); + + /// Runtime-owned shell task. + public static TaskKind Shell { get; } = new("shell"); + + /// Client-owned externally executed task. + public static TaskKind Client { get; } = new("client"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskKind left, TaskKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskKind left, TaskKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskKind other && Equals(other); + + /// + public bool Equals(TaskKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override TaskKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskKind)); + } + } +} + + +/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Runs before a tool is invoked. + public static HookType PreToolUse { get; } = new("preToolUse"); + + /// Runs before an MCP tool is invoked. + public static HookType PreMcpToolCall { get; } = new("preMcpToolCall"); + + /// Runs after a tool completes successfully. + public static HookType PostToolUse { get; } = new("postToolUse"); + + /// Runs after a tool fails. + public static HookType PostToolUseFailure { get; } = new("postToolUseFailure"); + + /// Runs after the user submits a prompt. + public static HookType UserPromptSubmitted { get; } = new("userPromptSubmitted"); + + /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. + public static HookType UserPromptTransformed { get; } = new("userPromptTransformed"); + + /// Runs when a session starts. + public static HookType SessionStart { get; } = new("sessionStart"); + + /// Runs when a session ends. + public static HookType SessionEnd { get; } = new("sessionEnd"); + + /// Runs after an agent result is produced. + public static HookType PostResult { get; } = new("postResult"); + + /// Runs before a pull request description is generated. + public static HookType PrePRDescription { get; } = new("prePRDescription"); + + /// Runs when the agent encounters an error. + public static HookType ErrorOccurred { get; } = new("errorOccurred"); + + /// Runs when the agent stops. + public static HookType AgentStop { get; } = new("agentStop"); + + /// Runs when a subagent starts. + public static HookType SubagentStart { get; } = new("subagentStart"); + + /// Runs when a subagent stops. + public static HookType SubagentStop { get; } = new("subagentStop"); + + /// Runs before conversation context is compacted. + public static HookType PreCompact { get; } = new("preCompact"); + + /// Runs when the agent requests permission. + public static HookType PermissionRequest { get; } = new("permissionRequest"); + + /// Runs when the agent emits a notification. + public static HookType Notification { get; } = new("notification"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookType left, HookType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookType left, HookType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HookType other && Equals(other); + + /// + public bool Equals(HookType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override HookType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HookType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookType)); + } + } +} + + +/// Configuration tier that contributed a discovered hook action. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct HookOrigin : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public HookOrigin(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Hook loaded from user settings or the user's hook directory. + public static HookOrigin User { get; } = new("user"); + + /// Hook loaded from repository settings or the repository hook directory. + public static HookOrigin Repository { get; } = new("repository"); + + /// Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. + public static HookOrigin Plugin { get; } = new("plugin"); + + /// Hook enforced by centrally managed policy. + public static HookOrigin Policy { get; } = new("policy"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(HookOrigin left, HookOrigin right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(HookOrigin left, HookOrigin right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is HookOrigin other && Equals(other); + + /// + public bool Equals(HookOrigin other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override HookOrigin Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, HookOrigin value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(HookOrigin)); + } + } +} + + /// Resolved Anthropic adaptive-thinking capability for a model. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -20285,7 +21371,16 @@ public CatalogNetworkFailureReason(string value) /// The connection was refused or reset. public static CatalogNetworkFailureReason ConnectionRefused { get; } = new("connection-refused"); - /// The authority returned a status the runtime treats as a failure. + /// The configured proxy returned 407 and requires authentication. + public static CatalogNetworkFailureReason ProxyAuthenticationRequired { get; } = new("proxy-authentication-required"); + + /// The authority rate-limited requests and supplied or implied a bounded cooldown. + public static CatalogNetworkFailureReason RateLimited { get; } = new("rate-limited"); + + /// The authority returned a transient 5xx response. + public static CatalogNetworkFailureReason ServiceUnavailable { get; } = new("service-unavailable"); + + /// The authority returned another status the runtime treats as a failure. public static CatalogNetworkFailureReason HttpStatus { get; } = new("http-status"); /// The response exceeded the permitted size. @@ -21069,6 +22164,69 @@ public override void Write(Utf8JsonWriter writer, CatalogCandidateKind value, Js } +/// Where completed plugin content was staged before atomic promotion. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct PluginInstallStagingMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public PluginInstallStagingMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A sibling of the installed-plugins root, outside the recursively watched tree. + public static PluginInstallStagingMode External { get; } = new("external"); + + /// A sibling of the destination plugin directory, used when external staging is unavailable. + public static PluginInstallStagingMode DestinationSibling { get; } = new("destination_sibling"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PluginInstallStagingMode left, PluginInstallStagingMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PluginInstallStagingMode left, PluginInstallStagingMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is PluginInstallStagingMode other && Equals(other); + + /// + public bool Equals(PluginInstallStagingMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override PluginInstallStagingMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, PluginInstallStagingMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PluginInstallStagingMode)); + } + } +} + + /// Which tier this directory belongs to. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -22278,6 +23436,132 @@ public override void Write(Utf8JsonWriter writer, SessionSource value, JsonSeria } +/// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history (the beginning for a forward read, the tail for a backward read). The fallback page is a fresh boundary snapshot, not a continuation of the requested cursor, so it may overlap already-rendered events; on 'expired' a consumer should reset/rebase its pagination state (or deduplicate by event id) before continuing from the returned cursor. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct EventsCursorStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public EventsCursorStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The cursor was applied successfully. + public static EventsCursorStatus Ok { get; } = new("ok"); + + /// The cursor referred to history that is no longer available. + public static EventsCursorStatus Expired { get; } = new("expired"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EventsCursorStatus left, EventsCursorStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EventsCursorStatus left, EventsCursorStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is EventsCursorStatus other && Equals(other); + + /// + public bool Equals(EventsCursorStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override EventsCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, EventsCursorStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); + } + } +} + + +/// Direction to page through the session's persisted event history. 'forward' pages from the cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct EventsReadDirection : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public EventsReadDirection(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Page from the cursor toward newer events (default). + public static EventsReadDirection Forward { get; } = new("forward"); + + /// Tail-first: return the newest events and page toward older events. + public static EventsReadDirection Backward { get; } = new("backward"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(EventsReadDirection left, EventsReadDirection right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(EventsReadDirection left, EventsReadDirection right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is EventsReadDirection other && Equals(other); + + /// + public bool Equals(EventsReadDirection other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override EventsReadDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, EventsReadDirection value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); + } + } +} + + /// Kind of attention required when status === "attention". Meaningful only when status === "attention". [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -23721,6 +25005,69 @@ public override void Write(Utf8JsonWriter writer, FactoryLogLineKind value, Json } +/// Whether the requested preference was already effective or was accepted for later transactional activation. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ModelSwitchAutoTierStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ModelSwitchAutoTierStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. + public static ModelSwitchAutoTierStatus Unchanged { get; } = new("unchanged"); + + /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. + public static ModelSwitchAutoTierStatus Pending { get; } = new("pending"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ModelSwitchAutoTierStatus left, ModelSwitchAutoTierStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ModelSwitchAutoTierStatus other && Equals(other); + + /// + public bool Equals(ModelSwitchAutoTierStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ModelSwitchAutoTierStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ModelSwitchAutoTierStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ModelSwitchAutoTierStatus)); + } + } +} + + /// Allowed values for the `WorkspacesWorkspaceDetailsHostType` enumeration. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -23985,6 +25332,72 @@ public override void Write(Utf8JsonWriter writer, HistoryRewindUnavailableReason } +/// Current normalized autopilot objective lifecycle status. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AutopilotObjectiveStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AutopilotObjectiveStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The objective is actively running. + public static AutopilotObjectiveStatus Active { get; } = new("active"); + + /// The objective is paused and may be resumed. + public static AutopilotObjectiveStatus Paused { get; } = new("paused"); + + /// The objective completed. + public static AutopilotObjectiveStatus Completed { get; } = new("completed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AutopilotObjectiveStatus left, AutopilotObjectiveStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AutopilotObjectiveStatus other && Equals(other); + + /// + public bool Equals(AutopilotObjectiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override AutopilotObjectiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AutopilotObjectiveStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutopilotObjectiveStatus)); + } + } +} + + /// Whether task execution is synchronously awaited or managed in the background. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -24120,6 +25533,267 @@ public override void Write(Utf8JsonWriter writer, TaskStatus value, JsonSerializ } +/// Client-owned tasks always execute outside the runtime in background mode. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientExecutionMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientExecutionMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the background value. + public static TaskClientExecutionMode Background { get; } = new("background"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientExecutionMode left, TaskClientExecutionMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientExecutionMode left, TaskClientExecutionMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientExecutionMode other && Equals(other); + + /// + public bool Equals(TaskClientExecutionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override TaskClientExecutionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientExecutionMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientExecutionMode)); + } + } +} + + +/// Connection class owning a client task. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientOwnerKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientOwnerKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A discovered extension connection owns the task. + public static TaskClientOwnerKind Extension { get; } = new("extension"); + + /// A generic SDK connection owns the task. + public static TaskClientOwnerKind Sdk { get; } = new("sdk"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientOwnerKind left, TaskClientOwnerKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientOwnerKind left, TaskClientOwnerKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientOwnerKind other && Equals(other); + + /// + public bool Equals(TaskClientOwnerKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override TaskClientOwnerKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientOwnerKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerKind)); + } + } +} + + +/// Presence of the task's bound join. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientOwnerPresence : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientOwnerPresence(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The bound session join is connected. + public static TaskClientOwnerPresence Connected { get; } = new("connected"); + + /// The bound session join is disconnected. + public static TaskClientOwnerPresence Disconnected { get; } = new("disconnected"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientOwnerPresence left, TaskClientOwnerPresence right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientOwnerPresence other && Equals(other); + + /// + public bool Equals(TaskClientOwnerPresence other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override TaskClientOwnerPresence Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientOwnerPresence value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientOwnerPresence)); + } + } +} + + +/// Lifecycle status of a client-owned task. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The external owner is actively working. + public static TaskClientStatus Running { get; } = new("running"); + + /// The external owner is connected but waiting. + public static TaskClientStatus Idle { get; } = new("idle"); + + /// The owner reported successful completion. + public static TaskClientStatus Completed { get; } = new("completed"); + + /// The owner reported failure. + public static TaskClientStatus Failed { get; } = new("failed"); + + /// The owner reported or confirmed cancellation. + public static TaskClientStatus Cancelled { get; } = new("cancelled"); + + /// The bound owner join disappeared; external executor state is unknown. + public static TaskClientStatus Orphaned { get; } = new("orphaned"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientStatus left, TaskClientStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientStatus left, TaskClientStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientStatus other && Equals(other); + + /// + public bool Equals(TaskClientStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override TaskClientStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientStatus)); + } + } +} + + /// Whether the shell runs inside a managed PTY session or as an independent background process. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -24183,6 +25857,129 @@ public override void Write(Utf8JsonWriter writer, TaskShellInfoAttachmentMode va } +/// Discriminator for a client-owned task. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientType : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientType(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Gets the client value. + public static TaskClientType Client { get; } = new("client"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientType left, TaskClientType right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientType left, TaskClientType right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientType other && Equals(other); + + /// + public bool Equals(TaskClientType other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override TaskClientType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientType value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientType)); + } + } +} + + +/// Active status a client owner may publish with a progress update. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct TaskClientActiveStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public TaskClientActiveStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The external owner is actively working. + public static TaskClientActiveStatus Running { get; } = new("running"); + + /// The external owner is connected but waiting. + public static TaskClientActiveStatus Idle { get; } = new("idle"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(TaskClientActiveStatus left, TaskClientActiveStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(TaskClientActiveStatus left, TaskClientActiveStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is TaskClientActiveStatus other && Equals(other); + + /// + public bool Equals(TaskClientActiveStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override TaskClientActiveStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, TaskClientActiveStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(TaskClientActiveStatus)); + } + } +} + + /// Consumer allowed to call an MCP tool. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -28568,69 +30365,6 @@ public override void Write(Utf8JsonWriter writer, QueuePendingItemsKind value, J } -/// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history (the beginning for a forward read, the tail for a backward read). The fallback page is a fresh boundary snapshot, not a continuation of the requested cursor, so it may overlap already-rendered events; on 'expired' a consumer should reset/rebase its pagination state (or deduplicate by event id) before continuing from the returned cursor. -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct EventsCursorStatus : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public EventsCursorStatus(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// The cursor was applied successfully. - public static EventsCursorStatus Ok { get; } = new("ok"); - - /// The cursor referred to history that is no longer available. - public static EventsCursorStatus Expired { get; } = new("expired"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EventsCursorStatus left, EventsCursorStatus right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EventsCursorStatus left, EventsCursorStatus right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is EventsCursorStatus other && Equals(other); - - /// - public bool Equals(EventsCursorStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - - /// - public override string ToString() => Value; - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override EventsCursorStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, EventsCursorStatus value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsCursorStatus)); - } - } -} - - /// Agent-scope filter: 'primary' returns only main-agent events plus events whose type starts with 'subagent.' (matching the typed-subscription default behavior); 'all' returns events from all agents (matching wildcard-subscription behavior). Default is 'all' to preserve wildcard semantics for catch-up callers. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -28694,69 +30428,6 @@ public override void Write(Utf8JsonWriter writer, EventsAgentScope value, JsonSe } -/// Direction to page through the session's persisted event history. 'forward' pages from the cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct EventsReadDirection : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public EventsReadDirection(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// Page from the cursor toward newer events (default). - public static EventsReadDirection Forward { get; } = new("forward"); - - /// Tail-first: return the newest events and page toward older events. - public static EventsReadDirection Backward { get; } = new("backward"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(EventsReadDirection left, EventsReadDirection right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(EventsReadDirection left, EventsReadDirection right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is EventsReadDirection other && Equals(other); - - /// - public bool Equals(EventsReadDirection other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - - /// - public override string ToString() => Value; - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override EventsReadDirection Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, EventsReadDirection value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(EventsReadDirection)); - } - } -} - - /// Client population used for the prediction baseline. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -29147,6 +30818,69 @@ public override void Write(Utf8JsonWriter writer, SessionVisibilityStatus value, } +/// Why the runtime requests client-task cancellation. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct ClientTaskCancelReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public ClientTaskCancelReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// A caller requested task cancellation. + public static ClientTaskCancelReason CancelRequested { get; } = new("cancel_requested"); + + /// The session is shutting down. + public static ClientTaskCancelReason SessionShutdown { get; } = new("session_shutdown"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(ClientTaskCancelReason left, ClientTaskCancelReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(ClientTaskCancelReason left, ClientTaskCancelReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is ClientTaskCancelReason other && Equals(other); + + /// + public bool Equals(ClientTaskCancelReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override ClientTaskCancelReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, ClientTaskCancelReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(ClientTaskCancelReason)); + } + } +} + + /// Error classification. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -29555,13 +31289,14 @@ public async Task PingAsync(string? message = null, CancellationToke /// Performs the SDK server connection handshake and validates the optional connection token. Marked internal because this is JSON-RPC transport plumbing invoked automatically by an SDK client's own `connect()` wrapper, not a user-facing method. Stays internal as long as the SDK client owns the handshake; would only become public if the SDK ever exposed the raw schema surface to consumers without a connection wrapper. /// Opt this connection in to GitHub telemetry forwarding for its lifetime. When set, the runtime forwards every internal telemetry event it emits — across all sessions, plus sessionless events — to this connection over the `gitHubTelemetry.event` notification. Regular events are also written to the runtime's normal GitHub/CTS path (dual-write); host-only compatibility events are forward-only and intentionally skip that path. Intended for first-party hosts that re-emit the events into their own telemetry stores. Both unrestricted and restricted events are forwarded, each tagged with a `restricted` discriminator; a backstop drops restricted events when restricted telemetry is disabled — using the process-global gate for ordinary events and an explicit session-scoped decision for host-only events. /// Identity of the integrating host. Optional; omit it to keep the default attribution. + /// Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. /// Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN. /// The to monitor for cancellation requests. The default is . /// Handshake result reporting the server's protocol version and package version on success. [Experimental(Diagnostics.Experimental)] - internal async Task ConnectAsync(bool? enableGitHubTelemetryForwarding = null, ConnectClientInfo? clientInfo = null, string? token = null, CancellationToken cancellationToken = default) + internal async Task ConnectAsync(bool? enableGitHubTelemetryForwarding = null, ConnectClientInfo? clientInfo = null, IList? supportedTaskKinds = null, string? token = null, CancellationToken cancellationToken = default) { - var request = new ConnectRequest { EnableGitHubTelemetryForwarding = enableGitHubTelemetryForwarding, ClientInfo = clientInfo, Token = token }; + var request = new ConnectRequest { EnableGitHubTelemetryForwarding = enableGitHubTelemetryForwarding, ClientInfo = clientInfo, SupportedTaskKinds = supportedTaskKinds, Token = token }; return await CopilotClient.InvokeRpcAsync(_rpc, "connect", [request], cancellationToken); } @@ -29573,6 +31308,12 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell await CopilotClient.InvokeRpcAsync(_rpc, "registerExtensionLaunchProvider", [], cancellationToken); } + /// Hooks APIs. + public ServerHooksApi Hooks => + field ?? + Interlocked.CompareExchange(ref field, new(_rpc), null) ?? + field; + /// Models APIs. public ServerModelsApi Models => field ?? @@ -29688,6 +31429,29 @@ public async Task RegisterExtensionLaunchProviderAsync(CancellationToken cancell field; } +/// Provides server-scoped Hooks APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class ServerHooksApi +{ + private readonly JsonRpc _rpc; + + internal ServerHooksApi(JsonRpc rpc) + { + _rpc = rpc; + } + + /// Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. + /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + /// The to monitor for cancellation requests. The default is . + /// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + public async Task DiscoverAsync(IList? projectPaths = null, bool? excludeHostHooks = null, CancellationToken cancellationToken = default) + { + var request = new HooksDiscoverRequest { ProjectPaths = projectPaths, ExcludeHostHooks = excludeHostHooks }; + return await CopilotClient.InvokeRpcAsync(_rpc, "hooks.discover", [request], cancellationToken); + } +} + /// Provides server-scoped Models APIs. [Experimental(Diagnostics.Experimental)] public sealed class ServerModelsApi @@ -29920,12 +31684,13 @@ public async Task UpdateAsync(string name, object config, CancellationToken canc /// Removes an MCP server from user configuration. /// Name of the MCP server to remove. + /// OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. /// The to monitor for cancellation requests. The default is . - public async Task RemoveAsync(string name, CancellationToken cancellationToken = default) + public async Task RemoveAsync(string name, string? authClientIdMetadataUrl = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(name); - var request = new McpConfigRemoveRequest { Name = name }; + var request = new McpConfigRemoveRequest { Name = name, AuthClientIdMetadataUrl = authClientIdMetadataUrl }; await CopilotClient.InvokeRpcAsync(_rpc, "mcp.config.remove", [request], cancellationToken); } @@ -30014,7 +31779,7 @@ internal ServerCatalogApi(JsonRpc rpc) /// Requests a bounded catalog search. This host-implemented server method is available through SDK/TUI hosts; standalone and C-ABI runtimes whose host does not implement server-method dispatch return JSON-RPC MethodNotFound. A runtime with search available returns inert candidate summaries, each with an opaque single-use handle scoped to this runtime instance; a runtime without it returns the typed search-unavailable result. Public authorities may be searched anonymously, while an authority that requires credentials yields the typed authentication-required result. All returned text, URLs, and package metadata are untrusted external data and can never trigger instructions, tools, or installation. Read-only: nothing is installed, configured, or persisted. /// Protocol version and capabilities the caller requires. - /// Free-text search query. Never written to logs or telemetry. + /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. /// Maximum number of candidates to return. Defaults to 10 when omitted. /// Restrict results to these candidate kinds. When omitted, every kind the runtime supports is searched. /// The to monitor for cancellation requests. The default is . @@ -30458,6 +32223,13 @@ public async Task ReadAsync(CancellationToken cancell { return await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.read", [], cancellationToken); } + + /// Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `<cacheHome>/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. + /// The to monitor for cancellation requests. The default is . + public async Task ClearCacheAsync(CancellationToken cancellationToken = default) + { + await CopilotClient.InvokeRpcAsync(_rpc, "managedSettings.clearCache", [], cancellationToken); + } } /// Provides server-scoped Runtime APIs. @@ -30631,6 +32403,21 @@ internal async Task GetMetadataAsync(string sessionId return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.getMetadata", [request], cancellationToken); } + /// Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session. + /// Session ID whose persisted event journal should be read. + /// Opaque cursor returned by a previous persisted-event read. Omit on the first call. + /// Maximum number of events to return in this batch (1–1000, default 200). + /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. + /// The to monitor for cancellation requests. The default is . + /// Batch of session events returned by a read, with cursor and continuation metadata. + public async Task ReadPersistedEventsAsync(string sessionId, string? cursor = null, long? max = null, EventsReadDirection? direction = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(sessionId); + + var request = new SessionsReadPersistedEventsRequest { SessionId = sessionId, Cursor = cursor, Max = max, Direction = direction }; + return await CopilotClient.InvokeRpcAsync(_rpc, "sessions.readPersistedEvents", [request], cancellationToken); + } + /// Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. /// Maximum number of session IDs to return. /// The to monitor for cancellation requests. The default is . @@ -31035,6 +32822,12 @@ internal SessionRpc(CopilotSession session) Interlocked.CompareExchange(ref field, new(_session), null) ?? field; + /// AutopilotObjective APIs. + public AutopilotObjectiveApi AutopilotObjective => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// Completions APIs. public CompletionsApi Completions => field ?? @@ -31927,9 +33720,9 @@ internal ModelApi(CopilotSession session) _session = session; } - /// Gets the currently selected model for the session. + /// Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. /// The to monitor for cancellation requests. The default is . - /// The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + /// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. public async Task GetCurrentAsync(CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); @@ -31940,12 +33733,13 @@ public async Task GetCurrentAsync(CancellationToken cancellationTo /// Switches the session to a model and optional reasoning configuration. /// Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. + /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. /// Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. When omitted, no effort override is applied. /// Reasoning summary mode to request for supported model clients. /// Output verbosity level to request for supported models. /// Override individual model capabilities resolved by the runtime. /// Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. /// When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. /// When true, evaluate context-window compaction policy before applying the switch. @@ -31955,18 +33749,32 @@ public async Task GetCurrentAsync(CancellationToken cancellationTo /// Optional settings context and explicit-override flags used to persist a picker selection. /// The to monitor for cancellation requests. The default is . /// The model identifier active on the session after the switch. - public async Task SwitchToAsync(string modelId, string? reasoningEffort = null, ReasoningSummary? reasoningSummary = null, Verbosity? verbosity = null, ModelCapabilitiesOverride? modelCapabilities = null, ContextTier? contextTier = null, ModelChangeSource? source = null, bool? deferIfModelChangeQueued = null, string? compactionDecision = null, bool? runCompactionPreflight = null, string? repoScope = null, string? modelChangeScope = null, bool? requireAvailable = null, ModelPickerPersistenceRequest? pickerPersistence = null, CancellationToken cancellationToken = default) + public async Task SwitchToAsync(string modelId, AutoTier? autoTier = null, string? reasoningEffort = null, ReasoningSummary? reasoningSummary = null, Verbosity? verbosity = null, ModelCapabilitiesOverride? modelCapabilities = null, ContextTier? contextTier = null, ModelChangeSource? source = null, bool? deferIfModelChangeQueued = null, string? compactionDecision = null, bool? runCompactionPreflight = null, string? repoScope = null, string? modelChangeScope = null, bool? requireAvailable = null, ModelPickerPersistenceRequest? pickerPersistence = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(modelId); _session.ThrowIfDisposed(); - var request = new ModelSwitchToRequest { SessionId = _session.SessionId, ModelId = modelId, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, Verbosity = verbosity, ModelCapabilities = modelCapabilities, ContextTier = contextTier, Source = source, DeferIfModelChangeQueued = deferIfModelChangeQueued, CompactionDecision = compactionDecision, RunCompactionPreflight = runCompactionPreflight, RepoScope = repoScope, ModelChangeScope = modelChangeScope, RequireAvailable = requireAvailable, PickerPersistence = pickerPersistence }; + var request = new ModelSwitchToRequest { SessionId = _session.SessionId, ModelId = modelId, AutoTier = autoTier, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, Verbosity = verbosity, ModelCapabilities = modelCapabilities, ContextTier = contextTier, Source = source, DeferIfModelChangeQueued = deferIfModelChangeQueued, CompactionDecision = compactionDecision, RunCompactionPreflight = runCompactionPreflight, RepoScope = repoScope, ModelChangeScope = modelChangeScope, RequireAvailable = requireAvailable, PickerPersistence = pickerPersistence }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.switchTo", [request], cancellationToken); } + /// Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. + /// Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. + /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// The to monitor for cancellation requests. The default is . + /// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + public async Task SwitchAutoTierAsync(AutoTier? autoTier, ModelChangeSource? source = null, CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new ModelSwitchAutoTierRequest { SessionId = _session.SessionId, AutoTier = autoTier, Source = source }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.switchAutoTier", [request], cancellationToken); + } + /// Resolves and applies organization-managed and repository model overlays. /// Model required by device-managed policy, when configured. /// Model required by server-managed policy, when configured. + /// Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. /// Model selected by repository settings, when configured. /// Reasoning effort selected by repository settings, when configured. /// Context tier selected by repository settings, when configured. @@ -31974,11 +33782,11 @@ public async Task SwitchToAsync(string modelId, string? rea /// Whether the overlay is being applied while resuming a deferred session. /// The to monitor for cancellation requests. The default is . /// The model identifier active on the session after the switch. - internal async Task ApplyStartupOverlayAsync(string? deviceManagedModel = null, string? serverManagedModel = null, string? repoModel = null, string? repoReasoningEffort = null, string? repoContextTier = null, string? cliModel = null, bool? deferredResume = null, CancellationToken cancellationToken = default) + internal async Task ApplyStartupOverlayAsync(string? deviceManagedModel = null, string? serverManagedModel = null, string? policyHelperModel = null, string? repoModel = null, string? repoReasoningEffort = null, string? repoContextTier = null, string? cliModel = null, bool? deferredResume = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new ModelApplyStartupOverlayRequest { SessionId = _session.SessionId, DeviceManagedModel = deviceManagedModel, ServerManagedModel = serverManagedModel, RepoModel = repoModel, RepoReasoningEffort = repoReasoningEffort, RepoContextTier = repoContextTier, CliModel = cliModel, DeferredResume = deferredResume }; + var request = new ModelApplyStartupOverlayRequest { SessionId = _session.SessionId, DeviceManagedModel = deviceManagedModel, ServerManagedModel = serverManagedModel, PolicyHelperModel = policyHelperModel, RepoModel = repoModel, RepoReasoningEffort = repoReasoningEffort, RepoContextTier = repoContextTier, CliModel = cliModel, DeferredResume = deferredResume }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.model.applyStartupOverlay", [request], cancellationToken); } @@ -32376,6 +34184,29 @@ public async Task DiffAsync(WorkspaceDiffMode mode, bool? i } } +/// Provides session-scoped AutopilotObjective APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class AutopilotObjectiveApi +{ + private readonly CopilotSession _session; + + internal AutopilotObjectiveApi(CopilotSession session) + { + _session = session; + } + + /// Reads the current canonical autopilot objective state for this session. + /// The to monitor for cancellation requests. The default is . + /// Canonical runtime state for the session's current autopilot objective. + public async Task GetStateAsync(CancellationToken cancellationToken = default) + { + _session.ThrowIfDisposed(); + + var request = new SessionAutopilotObjectiveGetStateRequest { SessionId = _session.SessionId }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.autopilotObjective.getState", [request], cancellationToken); + } +} + /// Provides session-scoped Completions APIs. [Experimental(Diagnostics.Experimental)] public sealed class CompletionsApi @@ -32584,6 +34415,41 @@ public async Task ListAsync(CancellationToken cancellationToken = defa return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tasks.list", [request], cancellationToken); } + /// Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. + /// Task kind. + /// Owner-scoped idempotency key used for registration and reclaim. + /// Human-readable description of the external work. + /// Whether the owner supports runtime cancellation requests. + /// Optional short display name for the external work. + /// Expected current sequence for idempotent registration or orphan reclaim. + /// The to monitor for cancellation requests. The default is . + /// Result of registering or reclaiming a client-owned task. + public async Task RegisterAsync(TaskClientType type, string clientTaskId, string description, bool cancellable, string? displayName = null, long? expectedSequence = null, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(clientTaskId); + ArgumentNullException.ThrowIfNull(description); + _session.ThrowIfDisposed(); + + var request = new TasksRegisterRequest { SessionId = _session.SessionId, Type = type, ClientTaskId = clientTaskId, Description = description, Cancellable = cancellable, DisplayName = displayName, ExpectedSequence = expectedSequence }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tasks.register", [request], cancellationToken); + } + + /// Publishes generic progress or a terminal outcome for a client-owned task. + /// Canonical runtime-generated task identifier. + /// Owner update sequence to apply. + /// Progress or terminal update payload. + /// The to monitor for cancellation requests. The default is . + /// Result of publishing a client-owned task update. + public async Task UpdateAsync(string id, long sequence, TaskClientUpdate update, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(id); + ArgumentNullException.ThrowIfNull(update); + _session.ThrowIfDisposed(); + + var request = new TasksUpdateRequest { SessionId = _session.SessionId, Id = id, Sequence = sequence, Update = update }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.tasks.update", [request], cancellationToken); + } + /// Refreshes metadata for any detached background shells the runtime knows about. /// The to monitor for cancellation requests. The default is . /// Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop. @@ -33435,7 +35301,7 @@ internal OptionsApi(CopilotSession session) /// Whether to enable loading of `.github/hooks/` filesystem hooks. Separate from the SDK callback hook mechanism. /// Whether to enable host git operations (context resolution, child repo scanning, git info in system prompt). /// Whether to enable cross-session store writes and reads. - /// Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. /// Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. /// Optional session limits. Pass null to clear the session limits. /// The to monitor for cancellation requests. The default is . @@ -35287,6 +37153,17 @@ public interface IFactoryHandler Task AbortAsync(FactoryAbortRequest request, CancellationToken cancellationToken = default); } +/// Handles `tasks` client session API methods. +[Experimental(Diagnostics.Experimental)] +public interface ITasksHandler +{ + /// Asks the client currently bound to a client-owned session task to confirm that its external work stopped. + /// Runtime-to-owner cancellation request for a client-owned task. + /// The to monitor for cancellation requests. The default is . + /// Whether the client authoritatively confirmed its external work stopped. + Task CancelAsync(ClientTaskCancelRequest request, CancellationToken cancellationToken = default); +} + /// Handles `sessionFs` client session API methods. [Experimental(Diagnostics.Experimental)] public interface ISessionFsHandler @@ -35387,6 +37264,9 @@ public sealed class ClientSessionApiHandlers /// Optional handler for Factory client session API methods. public IFactoryHandler? Factory { get; set; } + /// Optional handler for Tasks client session API methods. + public ITasksHandler? Tasks { get; set; } + /// Optional handler for SessionFs client session API methods. public ISessionFsHandler? SessionFs { get; set; } @@ -35422,6 +37302,12 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, Func>)(async (request, cancellationToken) => + { + var handler = getHandlers(request.SessionId).Tasks; + if (handler is null) throw new InvalidOperationException($"No tasks handler registered for session: {request.SessionId}"); + return await handler.CancelAsync(request, cancellationToken); + }), singleObjectParam: true); rpc.SetLocalRpcMethod("sessionFs.readFile", (Func>)(async (request, cancellationToken) => { var handler = getHandlers(request.SessionId).SessionFs; @@ -35640,6 +37526,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedCancelPhase), TypeInfoPropertyName = "SessionEventsAgentInterruptedCancelPhase")] [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedData), TypeInfoPropertyName = "SessionEventsAgentInterruptedData")] [JsonSerializable(typeof(GitHub.Copilot.AgentInterruptedEvent), TypeInfoPropertyName = "SessionEventsAgentInterruptedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.AgentModelPolicy), TypeInfoPropertyName = "SessionEventsAgentModelPolicy")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseActivityEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseActivityEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseCompletedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseFailedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseFailedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantFusionPhaseStartedEvent), TypeInfoPropertyName = "SessionEventsAssistantFusionPhaseStartedEvent")] @@ -35713,6 +37601,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchRequestedEvent), TypeInfoPropertyName = "SessionEventsAutoModeSwitchRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.AutoModeSwitchResponse), TypeInfoPropertyName = "SessionEventsAutoModeSwitchResponse")] [JsonSerializable(typeof(GitHub.Copilot.AutoTier), TypeInfoPropertyName = "SessionEventsAutoTier")] +[JsonSerializable(typeof(GitHub.Copilot.AutoTierSwitchFailureReason), TypeInfoPropertyName = "SessionEventsAutoTierSwitchFailureReason")] [JsonSerializable(typeof(GitHub.Copilot.AutopilotObjectiveChangedOperation), TypeInfoPropertyName = "SessionEventsAutopilotObjectiveChangedOperation")] [JsonSerializable(typeof(GitHub.Copilot.AutopilotObjectiveChangedStatus), TypeInfoPropertyName = "SessionEventsAutopilotObjectiveChangedStatus")] [JsonSerializable(typeof(GitHub.Copilot.BinaryAssetReference), TypeInfoPropertyName = "SessionEventsBinaryAssetReference")] @@ -35745,6 +37634,10 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.CompactionCompleteCompactionTokensUsed), TypeInfoPropertyName = "SessionEventsCompactionCompleteCompactionTokensUsed")] [JsonSerializable(typeof(GitHub.Copilot.CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail), TypeInfoPropertyName = "SessionEventsCompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail")] [JsonSerializable(typeof(GitHub.Copilot.CompactionTrigger), TypeInfoPropertyName = "SessionEventsCompactionTrigger")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptEventRange), TypeInfoPropertyName = "SessionEventsCompletionReceiptEventRange")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptFinalTool), TypeInfoPropertyName = "SessionEventsCompletionReceiptFinalTool")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptStopReason), TypeInfoPropertyName = "SessionEventsCompletionReceiptStopReason")] +[JsonSerializable(typeof(GitHub.Copilot.CompletionReceiptToolStatus), TypeInfoPropertyName = "SessionEventsCompletionReceiptToolStatus")] [JsonSerializable(typeof(GitHub.Copilot.ContextTier), TypeInfoPropertyName = "SessionEventsContextTier")] [JsonSerializable(typeof(GitHub.Copilot.CustomAgentsUpdatedAgent), TypeInfoPropertyName = "SessionEventsCustomAgentsUpdatedAgent")] [JsonSerializable(typeof(GitHub.Copilot.ElicitationCompletedAction), TypeInfoPropertyName = "SessionEventsElicitationCompletedAction")] @@ -35782,7 +37675,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.FusionFollowUpAction), TypeInfoPropertyName = "SessionEventsFusionFollowUpAction")] [JsonSerializable(typeof(GitHub.Copilot.FusionFollowUpRecommendation), TypeInfoPropertyName = "SessionEventsFusionFollowUpRecommendation")] [JsonSerializable(typeof(GitHub.Copilot.FusionPattern), TypeInfoPropertyName = "SessionEventsFusionPattern")] +[JsonSerializable(typeof(GitHub.Copilot.FusionPhaseActivityKind), TypeInfoPropertyName = "SessionEventsFusionPhaseActivityKind")] [JsonSerializable(typeof(GitHub.Copilot.FusionPhaseKind), TypeInfoPropertyName = "SessionEventsFusionPhaseKind")] +[JsonSerializable(typeof(GitHub.Copilot.FusionPhasePlanStep), TypeInfoPropertyName = "SessionEventsFusionPhasePlanStep")] [JsonSerializable(typeof(GitHub.Copilot.FusionPhaseStatus), TypeInfoPropertyName = "SessionEventsFusionPhaseStatus")] [JsonSerializable(typeof(GitHub.Copilot.FusionPhaseUsage), TypeInfoPropertyName = "SessionEventsFusionPhaseUsage")] [JsonSerializable(typeof(GitHub.Copilot.FusionProjectionMode), TypeInfoPropertyName = "SessionEventsFusionProjectionMode")] @@ -35825,6 +37720,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.McpOauthWWWAuthenticateParams), TypeInfoPropertyName = "SessionEventsMcpOauthWWWAuthenticateParams")] [JsonSerializable(typeof(GitHub.Copilot.McpPromptsListChangedEvent), TypeInfoPropertyName = "SessionEventsMcpPromptsListChangedEvent")] [JsonSerializable(typeof(GitHub.Copilot.McpResourcesListChangedEvent), TypeInfoPropertyName = "SessionEventsMcpResourcesListChangedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.McpServerMetadata), TypeInfoPropertyName = "SessionEventsMcpServerMetadata")] [JsonSerializable(typeof(GitHub.Copilot.McpServerSource), TypeInfoPropertyName = "SessionEventsMcpServerSource")] [JsonSerializable(typeof(GitHub.Copilot.McpServerStatus), TypeInfoPropertyName = "SessionEventsMcpServerStatus")] [JsonSerializable(typeof(GitHub.Copilot.McpServerTransport), TypeInfoPropertyName = "SessionEventsMcpServerTransport")] @@ -35898,6 +37794,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.PromptCacheBreakData), TypeInfoPropertyName = "SessionEventsPromptCacheBreakData")] [JsonSerializable(typeof(GitHub.Copilot.PromptCacheBreakEvent), TypeInfoPropertyName = "SessionEventsPromptCacheBreakEvent")] [JsonSerializable(typeof(GitHub.Copilot.ReasoningSummary), TypeInfoPropertyName = "SessionEventsReasoningSummary")] +[JsonSerializable(typeof(GitHub.Copilot.RemediationAction), TypeInfoPropertyName = "SessionEventsRemediationAction")] [JsonSerializable(typeof(GitHub.Copilot.SamplingCompletedData), TypeInfoPropertyName = "SessionEventsSamplingCompletedData")] [JsonSerializable(typeof(GitHub.Copilot.SamplingCompletedEvent), TypeInfoPropertyName = "SessionEventsSamplingCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.SamplingRequestedData), TypeInfoPropertyName = "SessionEventsSamplingRequestedData")] @@ -36052,6 +37949,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(AuthIdentity))] [JsonSerializable(typeof(AuthInfo))] [JsonSerializable(typeof(AuthValidationError))] +[JsonSerializable(typeof(AutopilotObjectiveCreditLimit))] +[JsonSerializable(typeof(AutopilotObjectiveGetStateResult))] +[JsonSerializable(typeof(AutopilotObjectiveState))] [JsonSerializable(typeof(BuiltInModelCatalog))] [JsonSerializable(typeof(BuiltInModelCatalogEntry))] [JsonSerializable(typeof(BuiltinToolDescriptor))] @@ -36084,6 +37984,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(CatalogNegotiatedContract))] [JsonSerializable(typeof(CatalogSearchRequest))] [JsonSerializable(typeof(CatalogSearchResult))] +[JsonSerializable(typeof(ClientTaskCancelRequest))] +[JsonSerializable(typeof(ClientTaskCancelResult))] [JsonSerializable(typeof(CommandList))] [JsonSerializable(typeof(CommandsFinalizeInvocationEffectRequest))] [JsonSerializable(typeof(CommandsFinalizeInvocationEffectRequestEffect))] @@ -36129,6 +38031,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(DiscoveredExtensions))] [JsonSerializable(typeof(DiscoveredExtensionsDisableRequest))] [JsonSerializable(typeof(DiscoveredExtensionsEnableRequest))] +[JsonSerializable(typeof(DiscoveredHook))] [JsonSerializable(typeof(DiscoveredMcpServer))] [JsonSerializable(typeof(EnqueueCommandParams))] [JsonSerializable(typeof(EnqueueCommandResult))] @@ -36213,6 +38116,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(HistorySummarizeForHandoffResult))] [JsonSerializable(typeof(HistoryTruncateRequest))] [JsonSerializable(typeof(HistoryTruncateResult))] +[JsonSerializable(typeof(HooksDiscoverRequest))] +[JsonSerializable(typeof(HooksDiscoverResult))] [JsonSerializable(typeof(IDictionary))] [JsonSerializable(typeof(IList))] [JsonSerializable(typeof(IList))] @@ -36387,6 +38292,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(ModelPolicy))] [JsonSerializable(typeof(ModelSetReasoningEffortRequest))] [JsonSerializable(typeof(ModelSetReasoningEffortResult))] +[JsonSerializable(typeof(ModelSwitchAutoTierRequest))] +[JsonSerializable(typeof(ModelSwitchAutoTierResult))] [JsonSerializable(typeof(ModelSwitchConfirmation))] [JsonSerializable(typeof(ModelSwitchToRequest))] [JsonSerializable(typeof(ModelSwitchToResult))] @@ -36586,6 +38493,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionAuthLogoutUserRequest))] [JsonSerializable(typeof(SessionAuthStatus))] [JsonSerializable(typeof(SessionAuthSwitchRequest))] +[JsonSerializable(typeof(SessionAutopilotObjectiveGetStateRequest))] [JsonSerializable(typeof(SessionBulkDeleteResult))] [JsonSerializable(typeof(SessionCancelAllBackgroundAgentsRequest))] [JsonSerializable(typeof(SessionCanvasListOpenRequest))] @@ -36760,6 +38668,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SessionsLoadDeferredRepoHooksRequest))] [JsonSerializable(typeof(SessionsOpenProgress))] [JsonSerializable(typeof(SessionsPruneOldRequest))] +[JsonSerializable(typeof(SessionsReadPersistedEventsRequest))] [JsonSerializable(typeof(SessionsRegisterExtensionToolsOnSessionOptions))] [JsonSerializable(typeof(SessionsReleaseLockRequest))] [JsonSerializable(typeof(SessionsReleaseLockResult))] @@ -36806,27 +38715,34 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(SlashCommandSelectSubcommandOption))] [JsonSerializable(typeof(SlashCommandTimelineEntry))] [JsonSerializable(typeof(SubagentSettingsEntry))] +[JsonSerializable(typeof(TaskClientInfo))] +[JsonSerializable(typeof(TaskClientOwner))] +[JsonSerializable(typeof(TaskClientUpdate))] [JsonSerializable(typeof(TaskCompleteData))] [JsonSerializable(typeof(TaskCompletionDecision))] [JsonSerializable(typeof(TaskInfo))] [JsonSerializable(typeof(TaskList))] +[JsonSerializable(typeof(TaskProgress))] [JsonSerializable(typeof(TaskProgressLine))] [JsonSerializable(typeof(TasksCancelRequest))] [JsonSerializable(typeof(TasksCancelResult))] [JsonSerializable(typeof(TasksGetCurrentPromotableResult))] [JsonSerializable(typeof(TasksGetProgressRequest))] [JsonSerializable(typeof(TasksGetProgressResult))] -[JsonSerializable(typeof(TasksGetProgressResultProgress))] [JsonSerializable(typeof(TasksPromoteCurrentToBackgroundResult))] [JsonSerializable(typeof(TasksPromoteToBackgroundRequest))] [JsonSerializable(typeof(TasksPromoteToBackgroundResult))] [JsonSerializable(typeof(TasksRefreshResult))] +[JsonSerializable(typeof(TasksRegisterRequest))] +[JsonSerializable(typeof(TasksRegisterResult))] [JsonSerializable(typeof(TasksRemoveRequest))] [JsonSerializable(typeof(TasksRemoveResult))] [JsonSerializable(typeof(TasksSendMessageRequest))] [JsonSerializable(typeof(TasksSendMessageResult))] [JsonSerializable(typeof(TasksStartAgentRequest))] [JsonSerializable(typeof(TasksStartAgentResult))] +[JsonSerializable(typeof(TasksUpdateRequest))] +[JsonSerializable(typeof(TasksUpdateResult))] [JsonSerializable(typeof(TasksWaitForPendingResult))] [JsonSerializable(typeof(TelemetrySetFeatureOverridesRequest))] [JsonSerializable(typeof(Tool))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 6bfe202bff..58c8a2c9cc 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -26,6 +26,7 @@ namespace GitHub.Copilot; IgnoreUnrecognizedTypeDiscriminators = true)] [JsonDerivedType(typeof(AbortEvent), "abort")] [JsonDerivedType(typeof(AgentInterruptedEvent), "agent.interrupted")] +[JsonDerivedType(typeof(AssistantFusionPhaseActivityEvent), "assistant.fusion_phase_activity")] [JsonDerivedType(typeof(AssistantFusionPhaseCompletedEvent), "assistant.fusion_phase_completed")] [JsonDerivedType(typeof(AssistantFusionPhaseFailedEvent), "assistant.fusion_phase_failed")] [JsonDerivedType(typeof(AssistantFusionPhaseStartedEvent), "assistant.fusion_phase_started")] @@ -83,6 +84,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionLimitsExhaustedCompletedEvent), "session_limits_exhausted.completed")] [JsonDerivedType(typeof(SessionLimitsExhaustedRequestedEvent), "session_limits_exhausted.requested")] [JsonDerivedType(typeof(SessionAutoModeResolvedEvent), "session.auto_mode_resolved")] +[JsonDerivedType(typeof(SessionAutoTierSwitchFailedEvent), "session.auto_tier_switch_failed")] [JsonDerivedType(typeof(SessionAutopilotObjectiveChangedEvent), "session.autopilot_objective_changed")] [JsonDerivedType(typeof(SessionBackgroundTasksChangedEvent), "session.background_tasks_changed")] [JsonDerivedType(typeof(SessionBinaryAssetEvent), "session.binary_asset")] @@ -94,6 +96,7 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionCanvasUnavailableEvent), "session.canvas.unavailable")] [JsonDerivedType(typeof(SessionCompactionCompleteEvent), "session.compaction_complete")] [JsonDerivedType(typeof(SessionCompactionStartEvent), "session.compaction_start")] +[JsonDerivedType(typeof(SessionCompletionReceiptEvent), "session.completion_receipt")] [JsonDerivedType(typeof(SessionContextChangedEvent), "session.context_changed")] [JsonDerivedType(typeof(SessionContextClearedEvent), "session.context_cleared")] [JsonDerivedType(typeof(SessionCustomAgentsUpdatedEvent), "session.custom_agents_updated")] @@ -110,9 +113,12 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(SessionInfoEvent), "session.info")] [JsonDerivedType(typeof(SessionManagedSettingsEnforcedEvent), "session.managed_settings_enforced")] [JsonDerivedType(typeof(SessionManagedSettingsResolvedEvent), "session.managed_settings_resolved")] +[JsonDerivedType(typeof(SessionMcpServerNeedsReconnectEvent), "session.mcp_server_needs_reconnect")] +[JsonDerivedType(typeof(SessionMcpServerRemovedEvent), "session.mcp_server_removed")] [JsonDerivedType(typeof(SessionMcpServerStatusChangedEvent), "session.mcp_server_status_changed")] [JsonDerivedType(typeof(SessionMcpServersLoadedEvent), "session.mcp_servers_loaded")] [JsonDerivedType(typeof(SessionModeChangedEvent), "session.mode_changed")] +[JsonDerivedType(typeof(SessionModeNoticeDeliveredEvent), "session.mode_notice_delivered")] [JsonDerivedType(typeof(SessionModelChangeEvent), "session.model_change")] [JsonDerivedType(typeof(SessionPermissionsChangedEvent), "session.permissions_changed")] [JsonDerivedType(typeof(SessionPlanChangedEvent), "session.plan_changed")] @@ -365,6 +371,19 @@ public sealed partial class SessionModelChangeEvent : SessionEvent public required SessionModelChangeData Data { get; set; } } +/// A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. +/// Represents the session.auto_tier_switch_failed event. +public sealed partial class SessionAutoTierSwitchFailedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.auto_tier_switch_failed"; + + /// The session.auto_tier_switch_failed event payload. + [JsonPropertyName("data")] + public required SessionAutoTierSwitchFailedData Data { get; set; } +} + /// Agent mode change details including previous and new modes. /// Represents the session.mode_changed event. public sealed partial class SessionModeChangedEvent : SessionEvent @@ -378,6 +397,19 @@ public sealed partial class SessionModeChangedEvent : SessionEvent public required SessionModeChangedData Data { get; set; } } +/// Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +/// Represents the session.mode_notice_delivered event. +public sealed partial class SessionModeNoticeDeliveredEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.mode_notice_delivered"; + + /// The session.mode_notice_delivered event payload. + [JsonPropertyName("data")] + public required SessionModeNoticeDeliveredData Data { get; set; } +} + /// Session limits update details. Null clears the limits. /// Represents the session.session_limits_changed event. public sealed partial class SessionSessionLimitsChangedEvent : SessionEvent @@ -587,6 +619,20 @@ public sealed partial class SessionTaskCompleteEvent : SessionEvent public required SessionTaskCompleteData Data { get; set; } } +/// Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +/// Represents the session.completion_receipt event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionCompletionReceiptEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.completion_receipt"; + + /// The session.completion_receipt event payload. + [JsonPropertyName("data")] + public required SessionCompletionReceiptData Data { get; set; } +} + /// Experimental transient signal that HydraFusion routing has started for an eligible turn. /// Represents the session.fusion_route_started event. [Experimental(Diagnostics.Experimental)] @@ -735,6 +781,20 @@ public sealed partial class AssistantFusionPhaseStartedEvent : SessionEvent public required AssistantFusionPhaseStartedData Data { get; set; } } +/// Experimental content-safe activity signal for a running HydraFusion phase. +/// Represents the assistant.fusion_phase_activity event. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseActivityEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "assistant.fusion_phase_activity"; + + /// The assistant.fusion_phase_activity event payload. + [JsonPropertyName("data")] + public required AssistantFusionPhaseActivityData Data { get; set; } +} + /// Experimental durable HydraFusion phase output and lossless replay checkpoint. /// Represents the assistant.fusion_phase_completed event. [Experimental(Diagnostics.Experimental)] @@ -1546,7 +1606,7 @@ public sealed partial class SessionAutoModeResolvedEvent : SessionEvent public required SessionAutoModeResolvedData Data { get; set; } } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. /// Represents the session.managed_settings_resolved event. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionManagedSettingsResolvedEvent : SessionEvent @@ -1746,6 +1806,32 @@ public sealed partial class SessionMcpServerStatusChangedEvent : SessionEvent public required SessionMcpServerStatusChangedData Data { get; set; } } +/// Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. +/// Represents the session.mcp_server_removed event. +public sealed partial class SessionMcpServerRemovedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.mcp_server_removed"; + + /// The session.mcp_server_removed event payload. + [JsonPropertyName("data")] + public required SessionMcpServerRemovedData Data { get; set; } +} + +/// Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. +/// Represents the session.mcp_server_needs_reconnect event. +public sealed partial class SessionMcpServerNeedsReconnectEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.mcp_server_needs_reconnect"; + + /// The session.mcp_server_needs_reconnect event payload. + [JsonPropertyName("data")] + public required SessionMcpServerNeedsReconnectData Data { get; set; } +} + /// Payload identifying the MCP server associated with a list change. /// Represents the mcp.tools.list_changed event. public sealed partial class McpToolsListChangedEvent : SessionEvent @@ -2103,6 +2189,11 @@ public sealed partial class SessionErrorData [JsonPropertyName("providerCallId")] public string? ProviderCallId { get; set; } + /// What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("remediation")] + public RemediationAction? Remediation { get; set; } + /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("serviceRequestId")] @@ -2266,6 +2357,11 @@ public sealed partial class SessionWarningData [JsonPropertyName("message")] public required string Message { get; set; } + /// What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("remediation")] + public RemediationAction? Remediation { get; set; } + /// Optional URL associated with this warning that the user can open in a browser. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("url")] @@ -2279,6 +2375,11 @@ public sealed partial class SessionWarningData /// Model change details including previous and new model identifiers. public sealed partial class SessionModelChangeData { + /// Committed Auto preference after the model configuration change, when applicable. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } + /// Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("cause")] @@ -2293,6 +2394,11 @@ public sealed partial class SessionModelChangeData [JsonPropertyName("newModel")] public required string NewModel { get; set; } + /// Previously committed Auto preference, when one was explicitly selected. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("previousAutoTier")] + public AutoTier? PreviousAutoTier { get; set; } + /// Model that was previously selected, if any. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("previousModel")] @@ -2334,6 +2440,23 @@ public sealed partial class SessionModelChangeData public Verbosity? Verbosity { get; set; } } +/// A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. +public sealed partial class SessionAutoTierSwitchFailedData +{ + /// Auto preference that remains effective after the failed request. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("effectiveAutoTier")] + public AutoTier? EffectiveAutoTier { get; set; } + + /// Low-cardinality failure outcome reported by Auto resolution. + [JsonPropertyName("reason")] + public required AutoTierSwitchFailureReason Reason { get; set; } + + /// Auto preference that failed to activate, or null when returning to provider-default routing failed. + [JsonPropertyName("requestedAutoTier")] + public AutoTier? RequestedAutoTier { get; set; } +} + /// Agent mode change details including previous and new modes. public sealed partial class SessionModeChangedData { @@ -2346,6 +2469,19 @@ public sealed partial class SessionModeChangedData public required SessionMode PreviousMode { get; set; } } +/// Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +public sealed partial class SessionModeNoticeDeliveredData +{ + /// Model-visible transition notice persisted for a mid-turn delivery. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("content")] + public string? Content { get; set; } + + /// Mode established by the delivered transition notice. + [JsonPropertyName("mode")] + public required SessionMode Mode { get; set; } +} + /// Session limits update details. Null clears the limits. public sealed partial class SessionSessionLimitsChangedData { @@ -2866,6 +3002,44 @@ public sealed partial class SessionTaskCompleteData public string? Summary { get; set; } } +/// Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +[Experimental(Diagnostics.Experimental)] +public sealed partial class SessionCompletionReceiptData +{ + /// One-based accepted completion receipt ordinal in the durable session history. + [JsonPropertyName("attempt")] + public required long Attempt { get; set; } + + /// Inclusive durable event range summarized by this receipt. + [JsonPropertyName("eventRange")] + public required CompletionReceiptEventRange EventRange { get; set; } + + /// Number of failed structured tool completions in the covered range. + [JsonPropertyName("failedToolCount")] + public required long FailedToolCount { get; set; } + + /// Final structured tool completion in the covered range, when one exists. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("finalTool")] + public CompletionReceiptFinalTool? FinalTool { get; set; } + + /// Version of the completion receipt payload. + [JsonPropertyName("schemaVersion")] + public required long SchemaVersion { get; set; } + + /// Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + [JsonPropertyName("sourceEventId")] + public required string SourceEventId { get; set; } + + /// Runtime reason the completion decision was accepted. + [JsonPropertyName("stopReason")] + public required CompletionReceiptStopReason StopReason { get; set; } + + /// Number of successful structured tool completions in the covered range. + [JsonPropertyName("successfulToolCount")] + public required long SuccessfulToolCount { get; set; } +} + /// Experimental transient signal that HydraFusion routing has started for an eligible turn. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionFusionRouteStartedData @@ -2958,6 +3132,12 @@ public sealed partial class SessionFusionResolvedData [JsonPropertyName("pattern")] public required FusionPattern Pattern { get; set; } + /// Presentation-neutral phase plan for clients that render workflow progress. + [Experimental(Diagnostics.Experimental)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("phasePlan")] + public FusionPhasePlanStep[]? PhasePlan { get; set; } + /// Version of the validated execution-plan format. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("planVersion")] @@ -3129,6 +3309,11 @@ public sealed partial class UserMessageData [JsonPropertyName("isAutopilotContinuation")] public bool? IsAutopilotContinuation { get; set; } + /// Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("messageId")] + public string? MessageId { get; set; } + /// Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("nativeDocumentPathFallbackPaths")] @@ -3310,6 +3495,49 @@ public sealed partial class AssistantFusionPhaseStartedData public required string Role { get; set; } } +/// Experimental content-safe activity signal for a running HydraFusion phase. +[Experimental(Diagnostics.Experimental)] +public sealed partial class AssistantFusionPhaseActivityData +{ + /// Kind of real activity observed. + [JsonPropertyName("activity")] + public required FusionPhaseActivityKind Activity { get; set; } + + /// Conversation scope in which the phase executes. + [JsonPropertyName("conversationScope")] + public required FusionConversationScope ConversationScope { get; set; } + + /// Identifier of the HydraFusion turn containing the phase. + [JsonPropertyName("fusionId")] + public required string FusionId { get; set; } + + /// HydraFusion orchestration pattern containing the phase. + [JsonPropertyName("pattern")] + public required FusionPattern Pattern { get; set; } + + /// Stable identifier for the concrete phase. + [JsonPropertyName("phaseId")] + public required string PhaseId { get; set; } + + /// Kind of phase currently executing. + [JsonPropertyName("phaseKind")] + public required FusionPhaseKind PhaseKind { get; set; } + + /// Semantic role assigned to the phase. + [JsonPropertyName("role")] + public required string Role { get; set; } + + /// Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolCallId")] + public string? ToolCallId { get; set; } + + /// Cumulative private response bytes observed for this model call. The event never includes response text. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("totalResponseSizeBytes")] + public long? TotalResponseSizeBytes { get; set; } +} + /// Experimental durable HydraFusion phase output and lossless replay checkpoint. [Experimental(Diagnostics.Experimental)] public sealed partial class AssistantFusionPhaseCompletedData @@ -4467,6 +4695,11 @@ public sealed partial class SkillInvokedData [JsonPropertyName("description")] public string? Description { get; set; } + /// Whether model invocation is disabled for this skill. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("disableModelInvocation")] + public bool? DisableModelInvocation { get; set; } + /// Model identifier active when the skill was invoked, when known. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("model")] @@ -4476,7 +4709,7 @@ public sealed partial class SkillInvokedData [JsonPropertyName("name")] public required string Name { get; set; } - /// File path to the SKILL.md definition. + /// File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity. [JsonPropertyName("path")] public required string Path { get; set; } @@ -4490,7 +4723,7 @@ public sealed partial class SkillInvokedData [JsonPropertyName("pluginVersion")] public string? PluginVersion { get; set; } - /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill). + /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] public string? Source { get; set; } @@ -4628,6 +4861,11 @@ public sealed partial class SubagentCompletedData [JsonPropertyName("model")] public string? Model { get; set; } + /// Why an explicit task-call model did not become the effective model. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelOverrideReason")] + public string? ModelOverrideReason { get; set; } + /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -4694,6 +4932,11 @@ public sealed partial class SubagentFailedData [JsonPropertyName("model")] public string? Model { get; set; } + /// Why an explicit task-call model did not become the effective model. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelOverrideReason")] + public string? ModelOverrideReason { get; set; } + /// Tool call ID of the parent tool invocation that spawned this sub-agent. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -4873,6 +5116,11 @@ public sealed partial class SystemNotificationData /// Permission request notification requiring client approval with request details. public sealed partial class PermissionRequestedData { + /// Agent mode captured from the owning turn when permission evaluation began. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("agentMode")] + public SessionMode? AgentMode { get; set; } + /// Details of the permission being requested. [JsonPropertyName("permissionRequest")] public required PermissionRequest PermissionRequest { get; set; } @@ -5410,7 +5658,7 @@ public sealed partial class SessionAutoModeResolvedData public bool? StickyOverride { get; set; } } -/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. [Experimental(Diagnostics.Experimental)] public sealed partial class SessionManagedSettingsResolvedData { @@ -5440,6 +5688,11 @@ public sealed partial class SessionManagedSettingsResolvedData [JsonPropertyName("permissionsAllowIntersected")] public bool? PermissionsAllowIntersected { get; set; } + /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("policyHelperManaged")] + public bool? PolicyHelperManaged { get; set; } + /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("sandboxEnabledByUndeterminedPolicy")] @@ -5454,7 +5707,7 @@ public sealed partial class SessionManagedSettingsResolvedData [JsonPropertyName("settings")] public JsonElement? Settings { get; set; } - /// Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. [JsonPropertyName("source")] public required ManagedSettingsResolvedSource Source { get; set; } } @@ -5681,6 +5934,22 @@ public sealed partial class SessionMcpServerStatusChangedData public required McpServerStatus Status { get; set; } } +/// Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. +public sealed partial class SessionMcpServerRemovedData +{ + /// Name of the MCP server that was removed from the graph. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + +/// Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. +public sealed partial class SessionMcpServerNeedsReconnectData +{ + /// Name of the MCP server that needs to reconnect. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + /// Payload identifying the MCP server associated with a list change. public sealed partial class McpToolsListChangedData { @@ -6206,6 +6475,42 @@ public sealed partial class CompactionCompleteCompactionTokensUsed public long? OutputTokens { get; set; } } +/// Inclusive durable event range summarized by a completion receipt. +/// Nested data type for CompletionReceiptEventRange. +public sealed partial class CompletionReceiptEventRange +{ + /// Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + [JsonPropertyName("endEventId")] + public required string EndEventId { get; set; } + + /// Identifier of the user message that starts the covered exchange. + [JsonPropertyName("startEventId")] + public required string StartEventId { get; set; } +} + +/// Final structured tool completion in the covered event range. +/// Nested data type for CompletionReceiptFinalTool. +public sealed partial class CompletionReceiptFinalTool +{ + /// Process exit code from a structured shell result, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("exitCode")] + public long? ExitCode { get; set; } + + /// Structured success or failure status from the tool completion event. + [JsonPropertyName("status")] + public required CompletionReceiptToolStatus Status { get; set; } + + /// Unique identifier of the completed tool call. + [JsonPropertyName("toolCallId")] + public required string ToolCallId { get; set; } + + /// Tool name from the matching tool execution start event, when available. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } +} + /// Durable server recommendation for subsequent HydraFusion turns. /// Nested data type for FusionFollowUpRecommendation. [Experimental(Diagnostics.Experimental)] @@ -6220,6 +6525,28 @@ public sealed partial class FusionFollowUpRecommendation public required FusionFollowUpAction UserTurn { get; set; } } +/// Presentation-neutral phase planned for a HydraFusion turn. +/// Nested data type for FusionPhasePlanStep. +[Experimental(Diagnostics.Experimental)] +public sealed partial class FusionPhasePlanStep +{ + /// Whether the phase executes only when an earlier phase requests it. + [JsonPropertyName("conditional")] + public required bool Conditional { get; set; } + + /// Kind of phase that may execute. + [JsonPropertyName("kind")] + public required FusionPhaseKind Kind { get; set; } + + /// Semantic role assigned to the phase. + [JsonPropertyName("role")] + public required string Role { get; set; } + + /// Conversation scope in which the phase executes. + [JsonPropertyName("scope")] + public required FusionConversationScope Scope { get; set; } +} + /// Validated HydraFusion routing capability scores. /// Nested data type for FusionScores. [Experimental(Diagnostics.Experimental)] @@ -7068,7 +7395,7 @@ public sealed partial class FusionAttribution [Experimental(Diagnostics.Experimental)] public sealed partial class AssistantMessageReasoningBlocks { - /// Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + /// Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("blocks")] public JsonElement[]? Blocks { get; set; } @@ -7378,6 +7705,11 @@ public sealed partial class ToolExecutionCompleteError /// Human-readable error message. [JsonPropertyName("message")] public required string Message { get; set; } + + /// What the user must do to recover, when the runtime knows of an action. Set on sandbox policy denials, where `message` names the rule that blocked the call but never the client affordance that relaxes it. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("remediation")] + public RemediationAction? Remediation { get; set; } } /// Binary result returned by a tool for the model. @@ -8520,12 +8852,12 @@ public override bool? ManagedApprovalRequired [JsonPropertyName("possibleUrls")] public required PermissionRequestShellPossibleUrl[] PossibleUrls { get; set; } - /// True when the model has requested to run this command outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this command outside the sandbox, either because the command detaches and cannot be sandboxed at all, or because a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypass")] public bool? RequestSandboxBypass { get; set; } - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypassReason")] public string? RequestSandboxBypassReason { get; set; } @@ -8620,12 +8952,12 @@ public override bool? ManagedApprovalRequired [JsonPropertyName("path")] public required string Path { get; set; } - /// True when the model has requested to run this search outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to re-run this search outside the sandbox, after a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypass")] public bool? RequestSandboxBypass { get; set; } - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypassReason")] public string? RequestSandboxBypassReason { get; set; } @@ -8703,12 +9035,12 @@ public override bool? ManagedApprovalRequired [JsonPropertyName("redirectedFrom")] public string? RedirectedFrom { get; set; } - /// True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypass")] public bool? RequestSandboxBypass { get; set; } - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypassReason")] public string? RequestSandboxBypassReason { get; set; } @@ -9257,12 +9589,12 @@ public sealed partial class PermissionPromptRequestUrl : PermissionPromptRequest [JsonPropertyName("redirectedFrom")] public string? RedirectedFrom { get; set; } - /// True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypass")] public bool? RequestSandboxBypass { get; set; } - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("requestSandboxBypassReason")] public string? RequestSandboxBypassReason { get; set; } @@ -10129,7 +10461,7 @@ public sealed partial class SkillsLoadedSkill [JsonPropertyName("path")] public string? Path { get; set; } - /// Source location type (e.g., project, personal-copilot, plugin, builtin). + /// Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk). [JsonPropertyName("source")] public required SkillSource Source { get; set; } @@ -10138,7 +10470,7 @@ public sealed partial class SkillsLoadedSkill public required bool UserInvocable { get; set; } } -/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. +/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. /// Nested data type for CustomAgentsUpdatedAgent. public sealed partial class CustomAgentsUpdatedAgent { @@ -10159,6 +10491,16 @@ public sealed partial class CustomAgentsUpdatedAgent [JsonPropertyName("model")] public string? Model { get; set; } + /// Whether authored models are preferences or required constraints. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("modelPolicy")] + public AgentModelPolicy? ModelPolicy { get; set; } + + /// Authored model ids in priority order, if configured. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("models")] + public string[]? Models { get; set; } + /// Internal name of the agent. [JsonPropertyName("name")] public required string Name { get; set; } @@ -10176,6 +10518,15 @@ public sealed partial class CustomAgentsUpdatedAgent public required bool UserInvocable { get; set; } } +/// Server-advertised metadata learned through modern discovery or legacy initialization. +/// Nested data type for McpServerMetadata. +public sealed partial class McpServerMetadata +{ + /// Non-empty natural-language guidance for using the server, or null when the server omitted instructions or advertised an empty string. + [JsonPropertyName("instructions")] + public string? Instructions { get; set; } +} + /// A single MCP server status summary in `session.mcp_servers_loaded`, including name, status, source, transport, and plugin metadata. /// Nested data type for McpServersLoadedServer. public sealed partial class McpServersLoadedServer @@ -10199,6 +10550,11 @@ public sealed partial class McpServersLoadedServer [JsonPropertyName("pluginVersion")] public string? PluginVersion { get; set; } + /// Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("serverMetadata")] + public McpServerMetadata? ServerMetadata { get; set; } + /// Configuration source: user, workspace, plugin, or builtin. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("source")] @@ -10647,6 +11003,76 @@ public override void Write(Utf8JsonWriter writer, Verbosity value, JsonSerialize } } +/// What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct RemediationAction : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public RemediationAction(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Authenticate again with the Copilot backend. The current credential is absent, expired, or rejected. + public static RemediationAction SignIn { get; } = new("sign_in"); + + /// Authenticate as a different account. The current account exists but lacks access to the requested resource. + public static RemediationAction SwitchAccount { get; } = new("switch_account"); + + /// Inspect which account is currently authenticated before deciding what to change. + public static RemediationAction ShowAccount { get; } = new("show_account"); + + /// Review or widen the sandbox policy. The blocked path or host is named by the accompanying message or by the tool result the action arrived with. + public static RemediationAction ReviewSandboxPolicy { get; } = new("review_sandbox_policy"); + + /// Permit outbound network access in the sandbox policy. + public static RemediationAction AllowSandboxOutbound { get; } = new("allow_sandbox_outbound"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(RemediationAction left, RemediationAction right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(RemediationAction left, RemediationAction right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is RemediationAction other && Equals(other); + + /// + public bool Equals(RemediationAction other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override RemediationAction Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, RemediationAction value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(RemediationAction)); + } + } +} + /// The session mode the agent is operating in. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -10991,46 +11417,48 @@ public override void Write(Utf8JsonWriter writer, ModelChangeSource value, JsonS } } -/// Permission mode for the session. -[Experimental(Diagnostics.Experimental)] +/// Terminal reason an Auto preference activation failed. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct PermissionMode : IEquatable +public readonly struct AutoTierSwitchFailureReason : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public PermissionMode(string value) + public AutoTierSwitchFailureReason(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Permission requests follow the normal approval flow. - public static PermissionMode Manual { get; } = new("manual"); + /// The candidate model was rejected by model policy. + public static AutoTierSwitchFailureReason PolicyRejected { get; } = new("policy_rejected"); - /// Permission requests include an LLM safety recommendation; clients may automatically approve requests judged acceptable. - public static PermissionMode Assisted { get; } = new("assisted"); + /// The Auto routing request failed or returned an unusable response. + public static AutoTierSwitchFailureReason RequestFailed { get; } = new("request_failed"); - /// Tool, path, and URL permission requests are automatically approved. - public static PermissionMode AllowAll { get; } = new("allow-all"); + /// The runtime could not prepare the Auto routing request. + public static AutoTierSwitchFailureReason SetupFailed { get; } = new("setup_failed"); - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(PermissionMode left, PermissionMode right) => left.Equals(right); + /// The provider does not support Auto routing. + public static AutoTierSwitchFailureReason Unsupported { get; } = new("unsupported"); - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(PermissionMode left, PermissionMode right) => !(left == right); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AutoTierSwitchFailureReason left, AutoTierSwitchFailureReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AutoTierSwitchFailureReason left, AutoTierSwitchFailureReason right) => !(left == right); /// - public override bool Equals(object? obj) => obj is PermissionMode other && Equals(other); + public override bool Equals(object? obj) => obj is AutoTierSwitchFailureReason other && Equals(other); /// - public bool Equals(PermissionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(AutoTierSwitchFailureReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -11038,25 +11466,90 @@ public PermissionMode(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override PermissionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override AutoTierSwitchFailureReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, PermissionMode value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, AutoTierSwitchFailureReason value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionMode)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AutoTierSwitchFailureReason)); } } } -/// The type of operation performed on the plan file. +/// Permission mode for the session. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct PermissionMode : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public PermissionMode(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Permission requests follow the normal approval flow. + public static PermissionMode Manual { get; } = new("manual"); + + /// Permission requests include an LLM safety recommendation; clients may automatically approve requests judged acceptable. + public static PermissionMode Assisted { get; } = new("assisted"); + + /// Tool, path, and URL permission requests are automatically approved. + public static PermissionMode AllowAll { get; } = new("allow-all"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(PermissionMode left, PermissionMode right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(PermissionMode left, PermissionMode right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is PermissionMode other && Equals(other); + + /// + public bool Equals(PermissionMode other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override PermissionMode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, PermissionMode value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(PermissionMode)); + } + } +} + +/// The type of operation performed on the plan file. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct PlanChangedOperation : IEquatable @@ -11437,6 +11930,140 @@ public override void Write(Utf8JsonWriter writer, TaskCompletionOutcome value, J } } +/// Structured terminal status from a tool completion event. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CompletionReceiptToolStatus : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CompletionReceiptToolStatus(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The tool completed successfully. + public static CompletionReceiptToolStatus Success { get; } = new("success"); + + /// The tool failed without a more specific structured status. + public static CompletionReceiptToolStatus Failure { get; } = new("failure"); + + /// The tool exceeded its time budget. + public static CompletionReceiptToolStatus Timeout { get; } = new("timeout"); + + /// The user rejected the tool call. + public static CompletionReceiptToolStatus Rejected { get; } = new("rejected"); + + /// The permissions service denied the tool call. + public static CompletionReceiptToolStatus Denied { get; } = new("denied"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CompletionReceiptToolStatus left, CompletionReceiptToolStatus right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CompletionReceiptToolStatus left, CompletionReceiptToolStatus right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is CompletionReceiptToolStatus other && Equals(other); + + /// + public bool Equals(CompletionReceiptToolStatus other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override CompletionReceiptToolStatus Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, CompletionReceiptToolStatus value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CompletionReceiptToolStatus)); + } + } +} + +/// Runtime reason the completion decision was accepted. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct CompletionReceiptStopReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public CompletionReceiptStopReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The model reached a natural terminal response. + public static CompletionReceiptStopReason Natural { get; } = new("natural"); + + /// A terminal tool ended the interaction. + public static CompletionReceiptStopReason TerminalTool { get; } = new("terminal_tool"); + + /// The configured agentStop continuation limit was reached. + public static CompletionReceiptStopReason AgentStopBlockLimit { get; } = new("agent_stop_block_limit"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(CompletionReceiptStopReason left, CompletionReceiptStopReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(CompletionReceiptStopReason left, CompletionReceiptStopReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is CompletionReceiptStopReason other && Equals(other); + + /// + public bool Equals(CompletionReceiptStopReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override CompletionReceiptStopReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, CompletionReceiptStopReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(CompletionReceiptStopReason)); + } + } +} + /// Kind of turn for which HydraFusion routing is running. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -11626,6 +12253,145 @@ public override void Write(Utf8JsonWriter writer, FusionPattern value, JsonSeria } } +/// HydraFusion phase kind. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionPhaseKind : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionPhaseKind(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Primary solver phase. + public static FusionPhaseKind Primary { get; } = new("primary"); + + /// Read-only cascade judge phase. + public static FusionPhaseKind Judge { get; } = new("judge"); + + /// Cascade repair phase. + public static FusionPhaseKind Repair { get; } = new("repair"); + + /// Initial critique-pattern draft phase. + public static FusionPhaseKind Draft { get; } = new("draft"); + + /// Read-only critique phase. + public static FusionPhaseKind Critic { get; } = new("critic"); + + /// Critique-pattern revision phase. + public static FusionPhaseKind Revision { get; } = new("revision"); + + /// Follow-up phase continuing from the resolved model. + public static FusionPhaseKind FollowUp { get; } = new("follow_up"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionPhaseKind left, FusionPhaseKind right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionPhaseKind left, FusionPhaseKind right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionPhaseKind other && Equals(other); + + /// + public bool Equals(FusionPhaseKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override FusionPhaseKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionPhaseKind value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseKind)); + } + } +} + +/// Conversation scope in which a HydraFusion phase executes. +[Experimental(Diagnostics.Experimental)] +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct FusionConversationScope : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public FusionConversationScope(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Canonical root conversation history. + public static FusionConversationScope Root { get; } = new("root"); + + /// Isolated read-only review history that does not enter the root conversation. + public static FusionConversationScope Review { get; } = new("review"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionConversationScope left, FusionConversationScope right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionConversationScope left, FusionConversationScope right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is FusionConversationScope other && Equals(other); + + /// + public bool Equals(FusionConversationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override FusionConversationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, FusionConversationScope value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionConversationScope)); + } + } +} + /// The agent mode that was active when this message was sent. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -12071,120 +12837,46 @@ public override void Write(Utf8JsonWriter writer, ModelCallFailureTransport valu } } -/// Conversation scope in which a HydraFusion phase executes. -[Experimental(Diagnostics.Experimental)] -[JsonConverter(typeof(Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct FusionConversationScope : IEquatable -{ - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The value to associate with this . - [JsonConstructor] - public FusionConversationScope(string value) - { - ArgumentException.ThrowIfNullOrWhiteSpace(value); - _value = value; - } - - /// Gets the value associated with this . - public string Value => _value ?? string.Empty; - - /// Canonical root conversation history. - public static FusionConversationScope Root { get; } = new("root"); - - /// Isolated read-only review history that does not enter the root conversation. - public static FusionConversationScope Review { get; } = new("review"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FusionConversationScope left, FusionConversationScope right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FusionConversationScope left, FusionConversationScope right) => !(left == right); - - /// - public override bool Equals(object? obj) => obj is FusionConversationScope other && Equals(other); - - /// - public bool Equals(FusionConversationScope other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - - /// - public override string ToString() => Value; - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override FusionConversationScope Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); - } - - /// - public override void Write(Utf8JsonWriter writer, FusionConversationScope value, JsonSerializerOptions options) - { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionConversationScope)); - } - } -} - -/// HydraFusion phase kind. +/// Content-safe activity observed while a HydraFusion phase is running. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] -public readonly struct FusionPhaseKind : IEquatable +public readonly struct FusionPhaseActivityKind : IEquatable { private readonly string? _value; - /// Initializes a new instance of the struct. - /// The value to associate with this . + /// Initializes a new instance of the struct. + /// The value to associate with this . [JsonConstructor] - public FusionPhaseKind(string value) + public FusionPhaseActivityKind(string value) { ArgumentException.ThrowIfNullOrWhiteSpace(value); _value = value; } - /// Gets the value associated with this . + /// Gets the value associated with this . public string Value => _value ?? string.Empty; - /// Primary solver phase. - public static FusionPhaseKind Primary { get; } = new("primary"); + /// The provider produced additional private output bytes. + public static FusionPhaseActivityKind ModelOutput { get; } = new("model_output"); - /// Read-only cascade judge phase. - public static FusionPhaseKind Judge { get; } = new("judge"); + /// A tool began executing inside the phase. + public static FusionPhaseActivityKind ToolStarted { get; } = new("tool_started"); - /// Cascade repair phase. - public static FusionPhaseKind Repair { get; } = new("repair"); + /// A tool finished executing inside the phase. + public static FusionPhaseActivityKind ToolCompleted { get; } = new("tool_completed"); - /// Initial critique-pattern draft phase. - public static FusionPhaseKind Draft { get; } = new("draft"); + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(FusionPhaseActivityKind left, FusionPhaseActivityKind right) => left.Equals(right); - /// Read-only critique phase. - public static FusionPhaseKind Critic { get; } = new("critic"); - - /// Critique-pattern revision phase. - public static FusionPhaseKind Revision { get; } = new("revision"); - - /// Follow-up phase continuing from the resolved model. - public static FusionPhaseKind FollowUp { get; } = new("follow_up"); - - /// Returns a value indicating whether two instances are equivalent. - public static bool operator ==(FusionPhaseKind left, FusionPhaseKind right) => left.Equals(right); - - /// Returns a value indicating whether two instances are not equivalent. - public static bool operator !=(FusionPhaseKind left, FusionPhaseKind right) => !(left == right); + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(FusionPhaseActivityKind left, FusionPhaseActivityKind right) => !(left == right); /// - public override bool Equals(object? obj) => obj is FusionPhaseKind other && Equals(other); + public override bool Equals(object? obj) => obj is FusionPhaseActivityKind other && Equals(other); /// - public bool Equals(FusionPhaseKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + public bool Equals(FusionPhaseActivityKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); /// public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); @@ -12192,20 +12884,20 @@ public FusionPhaseKind(string value) /// public override string ToString() => Value; - /// Provides a for serializing instances. + /// Provides a for serializing instances. [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter + public sealed class Converter : JsonConverter { /// - public override FusionPhaseKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override FusionPhaseActivityKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); } /// - public override void Write(Utf8JsonWriter writer, FusionPhaseKind value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, FusionPhaseActivityKind value, JsonSerializerOptions options) { - GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseKind)); + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(FusionPhaseActivityKind)); } } } @@ -14833,7 +15525,10 @@ public ManagedSettingsResolvedSource(string value) /// Only session-local SDK-host injection contributed. public static ManagedSettingsResolvedSource Client { get; } = new("client"); - /// More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + /// A policy helper registered by device or server policy contributed. Device registration takes priority when present. + public static ManagedSettingsResolvedSource PolicyHelper { get; } = new("policyHelper"); + + /// More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. public static ManagedSettingsResolvedSource Mixed { get; } = new("mixed"); /// No managed policy is in force (no channel contributed). @@ -15140,7 +15835,7 @@ public override void Write(Utf8JsonWriter writer, FactoryRunSettledStatus value, } } -/// Source location type (e.g., project, personal-copilot, plugin, builtin). +/// Source location type (e.g., project, personal-copilot, plugin, builtin, sdk). [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] public readonly struct SkillSource : IEquatable @@ -15180,6 +15875,9 @@ public SkillSource(string value) /// Skill bundled with the runtime. public static SkillSource Builtin { get; } = new("builtin"); + /// Pathless skill supplied lazily by an SDK skill provider. + public static SkillSource Sdk { get; } = new("sdk"); + /// Returns a value indicating whether two instances are equivalent. public static bool operator ==(SkillSource left, SkillSource right) => left.Equals(right); @@ -15216,6 +15914,67 @@ public override void Write(Utf8JsonWriter writer, SkillSource value, JsonSeriali } } +/// Whether configured models are advisory preferences or required constraints. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct AgentModelPolicy : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public AgentModelPolicy(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Treat the authored models as advisory preferences that callers may override. + public static AgentModelPolicy Preferred { get; } = new("preferred"); + + /// Require subagent execution to use one of the authored models. + public static AgentModelPolicy Required { get; } = new("required"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(AgentModelPolicy left, AgentModelPolicy right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(AgentModelPolicy left, AgentModelPolicy right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is AgentModelPolicy other && Equals(other); + + /// + public bool Equals(AgentModelPolicy other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override AgentModelPolicy Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, AgentModelPolicy value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(AgentModelPolicy)); + } + } +} + /// Configuration source: user, workspace, plugin, or builtin. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -15569,6 +16328,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(AbortEvent))] [JsonSerializable(typeof(AgentInterruptedData))] [JsonSerializable(typeof(AgentInterruptedEvent))] +[JsonSerializable(typeof(AssistantFusionPhaseActivityData))] +[JsonSerializable(typeof(AssistantFusionPhaseActivityEvent))] [JsonSerializable(typeof(AssistantFusionPhaseCompletedData))] [JsonSerializable(typeof(AssistantFusionPhaseCompletedEvent))] [JsonSerializable(typeof(AssistantFusionPhaseFailedData))] @@ -15663,6 +16424,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(CompactionCompleteCompactionTokensUsed))] [JsonSerializable(typeof(CompactionCompleteCompactionTokensUsedCopilotUsage))] [JsonSerializable(typeof(CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail))] +[JsonSerializable(typeof(CompletionReceiptEventRange))] +[JsonSerializable(typeof(CompletionReceiptFinalTool))] [JsonSerializable(typeof(CustomAgentsUpdatedAgent))] [JsonSerializable(typeof(ElicitationCompletedData))] [JsonSerializable(typeof(ElicitationCompletedEvent))] @@ -15689,6 +16452,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(FactoryRunUpdatedEvent))] [JsonSerializable(typeof(FusionAttribution))] [JsonSerializable(typeof(FusionFollowUpRecommendation))] +[JsonSerializable(typeof(FusionPhasePlanStep))] [JsonSerializable(typeof(FusionPhaseUsage))] [JsonSerializable(typeof(FusionScores))] [JsonSerializable(typeof(FusionStagedTerminal))] @@ -15723,6 +16487,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(McpPromptsListChangedEvent))] [JsonSerializable(typeof(McpResourcesListChangedData))] [JsonSerializable(typeof(McpResourcesListChangedEvent))] +[JsonSerializable(typeof(McpServerMetadata))] [JsonSerializable(typeof(McpServersLoadedServer))] [JsonSerializable(typeof(McpToolsListChangedData))] [JsonSerializable(typeof(McpToolsListChangedEvent))] @@ -15794,6 +16559,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SandboxDecisionEvent))] [JsonSerializable(typeof(SessionAutoModeResolvedData))] [JsonSerializable(typeof(SessionAutoModeResolvedEvent))] +[JsonSerializable(typeof(SessionAutoTierSwitchFailedData))] +[JsonSerializable(typeof(SessionAutoTierSwitchFailedEvent))] [JsonSerializable(typeof(SessionAutopilotObjectiveChangedData))] [JsonSerializable(typeof(SessionAutopilotObjectiveChangedEvent))] [JsonSerializable(typeof(SessionBackgroundTasksChangedData))] @@ -15816,6 +16583,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionCompactionCompleteEvent))] [JsonSerializable(typeof(SessionCompactionStartData))] [JsonSerializable(typeof(SessionCompactionStartEvent))] +[JsonSerializable(typeof(SessionCompletionReceiptData))] +[JsonSerializable(typeof(SessionCompletionReceiptEvent))] [JsonSerializable(typeof(SessionContextChangedData))] [JsonSerializable(typeof(SessionContextChangedEvent))] [JsonSerializable(typeof(SessionContextClearedData))] @@ -15855,12 +16624,18 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(SessionManagedSettingsEnforcedEvent))] [JsonSerializable(typeof(SessionManagedSettingsResolvedData))] [JsonSerializable(typeof(SessionManagedSettingsResolvedEvent))] +[JsonSerializable(typeof(SessionMcpServerNeedsReconnectData))] +[JsonSerializable(typeof(SessionMcpServerNeedsReconnectEvent))] +[JsonSerializable(typeof(SessionMcpServerRemovedData))] +[JsonSerializable(typeof(SessionMcpServerRemovedEvent))] [JsonSerializable(typeof(SessionMcpServerStatusChangedData))] [JsonSerializable(typeof(SessionMcpServerStatusChangedEvent))] [JsonSerializable(typeof(SessionMcpServersLoadedData))] [JsonSerializable(typeof(SessionMcpServersLoadedEvent))] [JsonSerializable(typeof(SessionModeChangedData))] [JsonSerializable(typeof(SessionModeChangedEvent))] +[JsonSerializable(typeof(SessionModeNoticeDeliveredData))] +[JsonSerializable(typeof(SessionModeNoticeDeliveredEvent))] [JsonSerializable(typeof(SessionModelChangeData))] [JsonSerializable(typeof(SessionModelChangeEvent))] [JsonSerializable(typeof(SessionPermissionsChangedData))] diff --git a/dotnet/src/GitHub.Copilot.SDK.csproj b/dotnet/src/GitHub.Copilot.SDK.csproj index f48fb802d7..e5a2d9fb98 100644 --- a/dotnet/src/GitHub.Copilot.SDK.csproj +++ b/dotnet/src/GitHub.Copilot.SDK.csproj @@ -63,10 +63,10 @@ - + - + diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 5995abaaff..8637da948c 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -5,12 +5,14 @@ using GitHub.Copilot.Rpc; using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Threading.Channels; namespace GitHub.Copilot; @@ -59,6 +61,8 @@ public sealed partial class CopilotSession : IAsyncDisposable private readonly Dictionary _toolHandlers = []; private readonly Dictionary> _commandHandlers = []; private readonly Dictionary>> _bearerTokenProviders = new(StringComparer.Ordinal); + private readonly ConcurrentDictionary _pendingExternalTools = new(StringComparer.Ordinal); + private readonly CancellationTokenSource _externalToolLifetime = new(); private readonly ILogger _logger; private readonly CopilotClient _parentClient; @@ -204,6 +208,32 @@ internal void RemoveFromClient() ((ICollection>)_parentClient._sessions).Remove(new(SessionId, this)); } + /// + /// Stops the session's event consumer () without + /// making an RPC. and + /// use this on error paths where a + /// locally registered session fails before it can be returned to the caller: + /// starts the consumer eagerly, and no caller + /// ever receives the failed session to dispose it. Safe to call more than once — + /// is idempotent. + /// + internal void CloseEventChannel() + { + _eventChannel.Writer.TryComplete(); + } + + /// + /// Removes the session from its parent client and stops its event consumer. + /// Used on session-creation/resume error paths where the session was registered + /// (and its event loop started) but never returned to the caller. + /// + internal void Unregister() + { + CancelPendingExternalTools(); + CloseEventChannel(); + RemoveFromClient(); + } + internal void SetGitHubTokenProviderRegistration(string registrationId) { _gitHubTokenProviderRegistrationId = registrationId; @@ -649,6 +679,10 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent) break; } + case ExternalToolCompletedEvent completedEvent: + CancelExternalTool(completedEvent.Data.RequestId); + break; + case PermissionRequestedEvent permEvent: { var data = permEvent.Data; @@ -858,8 +892,24 @@ private async Task TryCancelMcpAuthRequestAsync(string requestId) /// private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, string toolCallId, JsonElement? arguments, AIFunction tool) { + if (_externalToolLifetime.IsCancellationRequested) + { + return; + } + + using var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(_externalToolLifetime.Token); + if (!_pendingExternalTools.TryAdd(requestId, cancellationSource)) + { + return; + } + try { + if (cancellationSource.IsCancellationRequested) + { + return; + } + var invocation = new ToolInvocation { SessionId = SessionId, @@ -877,7 +927,7 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, { try { - var metadata = await Rpc.Tools.GetCurrentMetadataAsync(); + var metadata = await Rpc.Tools.GetCurrentMetadataAsync(cancellationSource.Token); invocation.AvailableTools = metadata.Tools; } catch (Exception ex) when (ex is RemoteRpcException or IOException or ObjectDisposedException or JsonException) @@ -898,14 +948,21 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, if (arguments is JsonElement incomingJsonArgs) { - foreach (var prop in incomingJsonArgs.EnumerateObject()) + if (incomingJsonArgs.ValueKind == JsonValueKind.Object) { - aiFunctionArgs[prop.Name] = prop.Value; + foreach (var prop in incomingJsonArgs.EnumerateObject()) + { + aiFunctionArgs[prop.Name] = prop.Value; + } + } + else + { + aiFunctionArgs[GetSingleParameterName(tool)] = incomingJsonArgs; } } var toolTimestamp = Stopwatch.GetTimestamp(); - var result = await tool.InvokeAsync(aiFunctionArgs); + var result = await tool.InvokeAsync(aiFunctionArgs, cancellationSource.Token); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, "CopilotSession.ExecuteToolAndRespondAsync tool dispatch. Elapsed={Elapsed}, SessionId={SessionId}, RequestId={RequestId}, ToolCallId={ToolCallId}, Tool={ToolName}", toolTimestamp, @@ -915,9 +972,14 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, toolName); var toolResultObject = ToolResultObject.ConvertFromInvocationResult(result, tool.JsonSerializerOptions); + if (!TryClaimExternalTool(requestId, cancellationSource)) + { + return; + } var responseRpcTimestamp = Stopwatch.GetTimestamp(); - await Rpc.Tools.HandlePendingToolCallAsync(requestId, toolResultObject, error: null); + await Rpc.Tools.HandlePendingToolCallAsync( + requestId, toolResultObject, error: null, cancellationSource.Token); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, "CopilotSession.ExecuteToolAndRespondAsync response sent successfully. Elapsed={Elapsed}, SessionId={SessionId}, RequestId={RequestId}, ToolCallId={ToolCallId}, Tool={ToolName}", responseRpcTimestamp, @@ -926,11 +988,33 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, toolCallId, toolName); } - catch (Exception ex) + catch (OperationCanceledException) when (cancellationSource.IsCancellationRequested) + { + // The runtime has already completed the request or the session is shutting down. + } + catch (RemoteRpcException) when (cancellationSource.IsCancellationRequested) + { + // Another client answered after this invocation completed locally. + } + catch (Exception) when (cancellationSource.IsCancellationRequested) { + // Cancellation won the request; no response or error should escape. + } + catch (Exception ex) when (!cancellationSource.IsCancellationRequested) + { + if (!TryClaimExternalTool(requestId, cancellationSource)) + { + return; + } + try { - await Rpc.Tools.HandlePendingToolCallAsync(requestId, result: null, error: ex.Message); + await Rpc.Tools.HandlePendingToolCallAsync( + requestId, result: null, error: ex.Message, cancellationSource.Token); + } + catch (OperationCanceledException) + { + // Teardown canceled the in-flight error response. } catch (IOException) { @@ -940,7 +1024,108 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, { // Connection already disposed — nothing we can do } + catch (RemoteRpcException) + { + // Another client may have answered the broadcast request first. + } + } + finally + { + ((ICollection>)_pendingExternalTools) + .Remove(new(requestId, cancellationSource)); + } + + static string GetSingleParameterName(AIFunction tool) + { + if (tool.JsonSchema.TryGetProperty("properties", out var properties) && + properties.ValueKind == JsonValueKind.Object) + { + string? parameterName = null; + foreach (var property in properties.EnumerateObject()) + { + if (parameterName is not null) + { + parameterName = null; + break; + } + + parameterName = property.Name; + } + + if (parameterName is not null) + { + return parameterName; + } + + if (tool.JsonSchema.TryGetProperty("required", out var required) && + required.ValueKind == JsonValueKind.Array) + { + string? requiredParameterName = null; + foreach (var requiredParameter in required.EnumerateArray()) + { + if (requiredParameterName is not null) + { + requiredParameterName = null; + break; + } + + requiredParameterName = requiredParameter.GetString(); + } + + if (requiredParameterName is not null && + properties.TryGetProperty(requiredParameterName, out _)) + { + return requiredParameterName; + } + } + } + + throw new ArgumentException( + $"Tool '{tool.Name}' received non-object arguments, but its schema does not define exactly one parameter or one required parameter."); + } + } + + private bool TryClaimExternalTool(string requestId, CancellationTokenSource cancellationSource) + => ((ICollection>)_pendingExternalTools) + .Remove(new(requestId, cancellationSource)); + + private void CancelExternalTool(string requestId) + { + if (!string.IsNullOrEmpty(requestId) && _pendingExternalTools.TryRemove(requestId, out var cancellationSource)) + { + _ = Task.Run(() => + { + try + { + cancellationSource.Cancel(); + } + catch (AggregateException) + { + // Cancellation callbacks are consumer code and must not disrupt event dispatch. + } + catch (ObjectDisposedException) + { + // The invocation completed while cancellation was being delivered. + } + }); + } + } + + internal void CancelPendingExternalTools() + { + try + { + _externalToolLifetime.Cancel(); + } + catch (AggregateException) + { + // User cancellation callbacks must not prevent session teardown. } + catch (ObjectDisposedException) + { + // Session teardown already completed. + } + _pendingExternalTools.Clear(); } /// @@ -1846,8 +2031,35 @@ public async Task SetModelAsync(string model, SetModelOptions options, Cancellat ArgumentNullException.ThrowIfNull(model); ThrowIfDisposed(); + if (options.AutoTier is not null && options.ResetAutoTier) + { + throw new ArgumentException( + $"{nameof(SetModelOptions.AutoTier)} and {nameof(SetModelOptions.ResetAutoTier)} are mutually exclusive.", + nameof(options)); + } + + if (options.ResetAutoTier) + { + var request = new ModelSwitchToRequest + { + SessionId = SessionId, + ModelId = model, + ReasoningEffort = options.ReasoningEffort, + ReasoningSummary = options.ReasoningSummary, + ModelCapabilities = options.ModelCapabilities, + ContextTier = options.ContextTier, + }; + await CopilotClient.InvokeRpcAsync( + Rpc, + "session.model.switchTo", + [WithExplicitNullAutoTier(request, RpcJsonContext.Default.ModelSwitchToRequest)], + cancellationToken); + return; + } + await Rpc.Model.SwitchToAsync( modelId: model, + autoTier: options.AutoTier, reasoningEffort: options.ReasoningEffort, reasoningSummary: options.ReasoningSummary, verbosity: null, @@ -1857,6 +2069,69 @@ await Rpc.Model.SwitchToAsync( cancellationToken: cancellationToken); } + /// + /// Changes the Auto routing preference without changing the selected model. + /// + /// + /// + /// The runtime does not apply the preference immediately. It records the request and + /// commits it only when a later user turn using the auto model successfully + /// obtains a usable model from the provider. A pending status therefore confirms + /// that the request was accepted, not that it took effect. + /// + /// + /// Watch for the outcome through the session.model_change event on success, or the + /// ephemeral session.auto_tier_switch_failed event on failure. You can also read + /// the current committed and in-flight state at any time with + /// session.Rpc.Model.GetCurrentAsync. + /// + /// + /// Only the most recent request survives: issuing a new request replaces any earlier one + /// that has not yet been claimed by a turn. + /// + /// + /// Routing preference to activate, or to return to the provider's default Auto routing. + /// Optional cancellation token. + /// The runtime's immediate acknowledgement and Auto preference snapshot. + /// + /// + /// var result = await session.SetAutoTierAsync(AutoTier.Intelligence); + /// + /// + [Experimental(Diagnostics.Experimental)] + public async Task SetAutoTierAsync(AutoTier? autoTier, CancellationToken cancellationToken = default) + { + ThrowIfDisposed(); + + if (autoTier is not null) + { + return await Rpc.Model.SwitchAutoTierAsync(autoTier, cancellationToken: cancellationToken); + } + + var request = new ModelSwitchAutoTierRequest { SessionId = SessionId }; + return await CopilotClient.InvokeRpcAsync( + Rpc, + "session.model.switchAutoTier", + [WithExplicitNullAutoTier(request, RpcJsonContext.Default.ModelSwitchAutoTierRequest)], + cancellationToken); + } + + /// + /// Serializes a generated request and restores the autoTier property as an explicit null. + /// + /// + /// The generated request types omit autoTier when it is null. The runtime reads an + /// omitted tier as "leave the current preference alone" and an explicit null as "return to + /// provider-default Auto routing", so the null has to survive serialization. Serializing the + /// generated type keeps every other field on the request in sync with the schema. + /// + private static JsonObject WithExplicitNullAutoTier(T request, JsonTypeInfo typeInfo) + { + var payload = JsonSerializer.SerializeToNode(request, typeInfo)!.AsObject(); + payload["autoTier"] = null; + return payload; + } + /// /// Changes the model for this session. /// @@ -1933,12 +2208,17 @@ public async ValueTask DisposeAsync() return; } - _eventChannel.Writer.TryComplete(); + CancelPendingExternalTools(); + CloseEventChannel(); try { - await InvokeRpcAsync( - "session.destroy", [new SessionDestroyRequest() { SessionId = SessionId }], CancellationToken.None); + var response = await InvokeRpcAsync( + "session.detach", [new SessionDetachRequest() { SessionId = SessionId }], CancellationToken.None); + if (!response.Success) + { + LogSessionDetachFailed(SessionId, response.Error ?? "unknown error"); + } } catch (ObjectDisposedException) { @@ -1975,6 +2255,9 @@ await InvokeRpcAsync( [LoggerMessage(Level = LogLevel.Debug, Message = "Failed to fetch tool metadata for {toolName}")] private partial void LogToolMetadataFetchFailed(Exception exception, string toolName); + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to detach session {sessionId}: {error}")] + private partial void LogSessionDetachFailed(string sessionId, string error); + [LoggerMessage(Level = LogLevel.Error, Message = "Permission handler or response delivery failed. SessionId={SessionId}, RequestId={RequestId}")] private partial void LogPermissionHandlerOrDeliveryFailed(Exception exception, string sessionId, string requestId); @@ -2012,11 +2295,17 @@ internal record SessionAbortRequest public string SessionId { get; init; } = string.Empty; } - internal record SessionDestroyRequest + internal record SessionDetachRequest { public string SessionId { get; init; } = string.Empty; } + internal record SessionDetachResponse + { + public bool Success { get; init; } + public string? Error { get; init; } + } + internal void ThrowIfDisposed() { ObjectDisposedException.ThrowIf(Volatile.Read(ref _isDisposed) != 0, this); @@ -2049,7 +2338,8 @@ internal void ThrowIfDisposed() [JsonSerializable(typeof(SendMessageRequest))] [JsonSerializable(typeof(SendMessageResponse))] [JsonSerializable(typeof(SessionAbortRequest))] - [JsonSerializable(typeof(SessionDestroyRequest))] + [JsonSerializable(typeof(SessionDetachRequest))] + [JsonSerializable(typeof(SessionDetachResponse))] [JsonSerializable(typeof(SessionEndHookInput))] [JsonSerializable(typeof(SessionEndHookOutput))] [JsonSerializable(typeof(SessionStartHookInput))] diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index a7de7d85e4..9129b118e8 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -322,6 +322,7 @@ private CopilotClientOptions(CopilotClientOptions? other) OnGitHubTelemetry = other.OnGitHubTelemetry; SessionIdleTimeoutSeconds = other.SessionIdleTimeoutSeconds; EnableRemoteSessions = other.EnableRemoteSessions; + ClientInfo = other.ClientInfo; Mode = other.Mode; } @@ -465,6 +466,16 @@ private CopilotClientOptions(CopilotClientOptions? other) /// public bool EnableRemoteSessions { get; set; } + /// + /// Declares the integrating application's identity, forwarded to the runtime on the + /// server.connect handshake. Declaring it lets the telemetry the + /// runtime emits on this connection be attributed to a consistent surface + /// (the application and its Copilot integration) instead of the runtime's own + /// build. All fields are optional; leave it to keep + /// the runtime's default attribution. + /// + public CopilotClientInfo? ClientInfo { get; set; } + /// /// Creates a shallow clone of this instance. /// @@ -531,6 +542,38 @@ public sealed class TelemetryConfig public bool? CaptureContent { get; set; } } +/// +/// Identifies the integrating application on the server.connect handshake. +/// +/// +/// Declaring it lets the telemetry the runtime emits on the connection be +/// attributed to a single, consistent surface instead of the runtime's own +/// build. All properties are optional; an unset property is omitted from the +/// handshake. +/// +public sealed class CopilotClientInfo +{ + /// + /// Name of the application using the SDK. + /// + public string? ApplicationName { get; set; } + + /// + /// Version of the application using the SDK. + /// + public string? ApplicationVersion { get; set; } + + /// + /// Optionally specifies a named integration within the application, such as an extension or plugin. + /// + public string? IntegrationName { get; set; } + + /// + /// Optionally specifies the version of that integration. + /// + public string? IntegrationVersion { get; set; } +} + /// /// Configuration for a custom session filesystem provider. /// @@ -2398,6 +2441,20 @@ public sealed class CapiSessionOptions /// [JsonPropertyName("enableWebSocketResponses")] public bool? EnableWebSocketResponses { get; set; } + + /// + /// Routing tier for model auto with V2 Auto. + /// + /// + /// Requires a runtime that supports Auto tiers; it has no effect outside V2 Auto. + /// When omitted, the runtime uses its default on create and restores the last committed + /// tier on cold resume. On resident resume, a different tier requests a safe switch that + /// takes effect after resume succeeds and never disturbs a turn that is already running. + /// To change the preference on a live session, use + /// . + /// + [JsonPropertyName("autoTier")] + public AutoTier? AutoTier { get; set; } } /// @@ -2962,6 +3019,26 @@ public struct SetModelOptions /// Per-property overrides for model capabilities, deep-merged over runtime defaults. public ModelCapabilitiesOverride? ModelCapabilities { get; set; } + + /// + /// Routing preference to stage atomically with selecting the auto model. + /// + /// + /// Leave unset to leave the current preference alone. Set + /// instead to return to the provider's default Auto + /// routing. The runtime rejects this option when the model is anything other than + /// auto; use to change the + /// preference without changing the selected model. + /// + [Experimental(Diagnostics.Experimental)] + public AutoTier? AutoTier { get; set; } + + /// + /// Returns to the provider's default Auto routing as part of this switch. + /// Mutually exclusive with . + /// + [Experimental(Diagnostics.Experimental)] + public bool ResetAutoTier { get; set; } } /// diff --git a/dotnet/src/build/GitHub.Copilot.SDK.targets b/dotnet/src/build/GitHub.Copilot.SDK.targets index 95770dba8e..a5aba612ab 100644 --- a/dotnet/src/build/GitHub.Copilot.SDK.targets +++ b/dotnet/src/build/GitHub.Copilot.SDK.targets @@ -26,7 +26,7 @@ - + <_CopilotPlatform Condition="'$(_CopilotRid)' == 'win-x64'">win32-x64 <_CopilotPlatform Condition="'$(_CopilotRid)' == 'win-arm64'">win32-arm64 @@ -49,15 +49,16 @@ <_CopilotRuntimeLib Condition="'$(_CopilotRuntimeLib)' == ''">libcopilot_runtime.so - + COPILOT_CLI_DOWNLOAD_BASE_URL is also honored. --> - https://registry.npmjs.org + $(COPILOT_CLI_DOWNLOAD_BASE_URL) + https://github.com/github/copilot-cli/releases/download @@ -93,23 +94,58 @@ <_CopilotCacheDir>$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform) - <_CopilotCliBinaryPath Condition="'$(_CopilotCliBinaryPath)' == ''">$(_CopilotCacheDir)\$(_CopilotBinary) + + <_CopilotCliBinaryPath Condition="'$(_CopilotCliBinaryPath)' == ''">$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\$(_CopilotRuntimeWrapper) + <_CopilotRuntimeNodePath>$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\runtime.node + <_CopilotRuntimeBundleCompleteMarker>$(_CopilotCacheDir)\.copilot-runtime-complete <_CopilotArchivePath>$(_CopilotCacheDir)\copilot.tgz - <_CopilotNormalizedRegistryUrl>$([System.String]::Copy('$(CopilotNpmRegistryUrl)').TrimEnd('/')) - <_CopilotDownloadUrl>$(_CopilotNormalizedRegistryUrl)/@github/copilot-$(_CopilotPlatform)/-/copilot-$(_CopilotPlatform)-$(CopilotCliVersion).tgz + <_CopilotChecksumPath>$(_CopilotCacheDir)\SHA256SUMS.txt + <_CopilotAssetName>github-copilot-$(CopilotCliVersion)-$(_CopilotPlatform).tgz + <_CopilotAssetNameRegex>$([System.Text.RegularExpressions.Regex]::Escape('$(_CopilotAssetName)')) + <_CopilotNormalizedReleaseBaseUrl>$([System.String]::Copy('$(CopilotCliReleaseBaseUrl)').TrimEnd('/')) + <_CopilotReleaseUrl>$(_CopilotNormalizedReleaseBaseUrl)/v$(CopilotCliVersion) + <_CopilotDownloadUrl>$(_CopilotReleaseUrl)/$(_CopilotAssetName) + <_CopilotChecksumsUrl>$(_CopilotReleaseUrl)/SHA256SUMS.txt + <_CopilotRuntimeBundleMissing Condition="!Exists('$(_CopilotCliBinaryPath)') Or !Exists('$(_CopilotRuntimeNodePath)') Or !Exists('$(_CopilotRuntimeBundleCompleteMarker)')">true <_CopilotCliDownloadTimeoutMs>$([System.Convert]::ToInt32($([MSBuild]::Multiply($(CopilotCliDownloadTimeout), 1000)))) - - + + - - + + + + + + + + <_CopilotChecksumMatch Include="@(_CopilotChecksumLine)" + Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(Identity)', '^[0-9a-fA-F]{64}[\t ]+\*?$(_CopilotAssetNameRegex)[\t ]*$'))" /> + + + Condition="'$(_CopilotRuntimeBundleMissing)' == 'true'" /> + + + + + <_CopilotChecksumLineValue>@(_CopilotChecksumMatch) + <_CopilotArchiveHashValue>@(_CopilotArchiveHash->'%(FileHash)') + <_CopilotExpectedChecksum>$([System.String]::Copy('$(_CopilotChecksumLineValue)').Substring(0, 64).ToUpperInvariant()) + <_CopilotActualChecksum>$([System.String]::Copy('$(_CopilotArchiveHashValue)').ToUpperInvariant()) + <_CopilotChecksumMismatch Condition="'$(_CopilotExpectedChecksum)' != '$(_CopilotActualChecksum)'">true + + + @@ -117,23 +153,26 @@ <_TarCommand Condition="'$(_TarCommand)' == ''">tar + Condition="'$(_CopilotRuntimeBundleMissing)' == 'true'" /> - + + + - + <_CopilotCacheDir Condition="'$(_CopilotCacheDir)' == ''">$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform) - <_CopilotCliBinaryPath Condition="'$(_CopilotCliBinaryPath)' == ''">$(_CopilotCacheDir)\$(_CopilotBinary) + <_CopilotCliBinaryPath Condition="'$(_CopilotCliBinaryPath)' == ''">$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\$(_CopilotRuntimeWrapper) <_CopilotOutputDir>$(OutDir)runtimes\$(_CopilotRid)\native <_CopilotRuntimeNodePath>$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\runtime.node <_CopilotRuntimeWrapperPath Condition="'$(_CopilotRuntimeWrapperPath)' == ''">$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\$(_CopilotRuntimeWrapper) @@ -153,7 +192,7 @@ <_CopilotRuntimeRootAsset Include="$(_CopilotCacheDir)\**\*" - Exclude="$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot-sdk\**\*;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\preloads\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sdk\**\*;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" /> + Exclude="$(_CopilotCacheDir)\.copilot-runtime-complete;$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\SHA256SUMS.txt;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" /> <_CopilotRuntimePrebuildAsset Include="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\**\*" Exclude="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\cli-native.node;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\mediaremote-adapter\**\*;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\copilot-runtime-bin*" /> @@ -176,12 +215,12 @@ - <_CopilotCacheDir Condition="'$(_CopilotCacheDir)' == ''">$(IntermediateOutputPath)copilot-cli\$(CopilotCliVersion)\$(_CopilotPlatform) - <_CopilotCliBinaryPath Condition="'$(_CopilotCliBinaryPath)' == ''">$(_CopilotCacheDir)\$(_CopilotBinary) + <_CopilotCliBinaryPath Condition="'$(_CopilotCliBinaryPath)' == ''">$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\$(_CopilotRuntimeWrapper) <_CopilotRuntimeNodePath>$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\runtime.node <_CopilotRuntimeWrapperPath Condition="'$(_CopilotRuntimeWrapperPath)' == ''">$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\$(_CopilotRuntimeWrapper) <_CopilotExplicitCliMarker>$(_CopilotCacheDir)\.copilot-explicit-cli @@ -193,7 +232,7 @@ Condition="'$(CopilotCliBinaryPath)' != ''" /> <_CopilotRuntimeRootAsset Include="$(_CopilotCacheDir)\**\*" - Exclude="$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot-sdk\**\*;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\preloads\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sdk\**\*;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" + Exclude="$(_CopilotCacheDir)\.copilot-runtime-complete;$(_CopilotCacheDir)\app.js;$(_CopilotCacheDir)\assets\**\*;$(_CopilotCacheDir)\changelog.json;$(_CopilotCacheDir)\copilot;$(_CopilotCacheDir)\copilot.exe;$(_CopilotCacheDir)\copilot.tgz;$(_CopilotCacheDir)\foundry-local-sdk\**\*;$(_CopilotCacheDir)\index.js;$(_CopilotCacheDir)\LICENSE.md;$(_CopilotCacheDir)\napi-oop-runtime\**\*;$(_CopilotCacheDir)\npm-loader.js;$(_CopilotCacheDir)\package.json;$(_CopilotCacheDir)\prebuilds\**\*;$(_CopilotCacheDir)\pvrecorder\**\*;$(_CopilotCacheDir)\queries\**\*;$(_CopilotCacheDir)\README.md;$(_CopilotCacheDir)\sea-loader.js;$(_CopilotCacheDir)\SHA256SUMS.txt;$(_CopilotCacheDir)\tree-sitter*.wasm;$(_CopilotCacheDir)\voice-*.js;$(_CopilotCacheDir)\webview\**\*" Condition="Exists('$(_CopilotRuntimeWrapperPath)') And Exists('$(_CopilotRuntimeNodePath)')" /> <_CopilotRuntimePrebuildAsset Include="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\**\*" Exclude="$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\cli-native.node;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\mediaremote-adapter\**\*;$(_CopilotCacheDir)\prebuilds\$(_CopilotPlatform)\copilot-runtime-bin*" diff --git a/dotnet/test/E2E/AutoTierE2ETests.cs b/dotnet/test/E2E/AutoTierE2ETests.cs new file mode 100644 index 0000000000..3758eee0f5 --- /dev/null +++ b/dotnet/test/E2E/AutoTierE2ETests.cs @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.Rpc; +using GitHub.Copilot.Test.Harness; +using Xunit; +using Xunit.Abstractions; + +namespace GitHub.Copilot.Test.E2E; + +/// +/// Mirrors nodejs/test/e2e/auto_tier.e2e.test.ts (snapshot category "auto_tier"). +/// +/// +/// The runtime stages an Auto routing preference instead of applying it immediately: a +/// request stays unclaimed until a later turn using the auto model mints a usable +/// model and token pair. These tests observe that staged state through +/// Model.GetCurrentAsync, so they assert what the runtime actually recorded rather +/// than what the SDK serialized. +/// +public class AutoTierE2ETests(E2ETestFixture fixture, ITestOutputHelper output) + : E2ETestBase(fixture, "auto_tier", output) +{ + private static async Task AssertPendingAutoTierAsync(CopilotSession session, AutoTier? expected) + { + var current = await session.Rpc.Model.GetCurrentAsync(); + Assert.Equal(expected, current.PendingAutoTier); + } + + [Fact] + public async Task Should_Stage_And_Reset_Auto_Tier_Preference() + { + await using var session = await CreateSessionAsync(new SessionConfig + { + Model = "auto", + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + await AssertPendingAutoTierAsync(session, null); + + var staged = await session.SetAutoTierAsync(AutoTier.Efficiency); + Assert.Equal(ModelSwitchAutoTierStatus.Pending, staged.Status); + Assert.Equal(AutoTier.Efficiency, staged.PendingAutoTier); + await AssertPendingAutoTierAsync(session, AutoTier.Efficiency); + + // A second request replaces the first and reports the one it displaced. + var superseded = await session.SetAutoTierAsync(AutoTier.Intelligence); + Assert.Equal(ModelSwitchAutoTierStatus.Pending, superseded.Status); + Assert.Equal(AutoTier.Intelligence, superseded.PendingAutoTier); + Assert.Equal(AutoTier.Efficiency, superseded.SupersededAutoTier); + await AssertPendingAutoTierAsync(session, AutoTier.Intelligence); + + // A null tier returns the session to provider-default routing. The status is + // Unchanged because provider-default was already the committed preference; the + // request's effect is cancelling the staged one. + var reset = await session.SetAutoTierAsync(null); + Assert.Equal(ModelSwitchAutoTierStatus.Unchanged, reset.Status); + Assert.Equal(AutoTier.Intelligence, reset.SupersededAutoTier); + await AssertPendingAutoTierAsync(session, null); + } + + [Fact] + public async Task Should_Preserve_Auto_Tier_When_Set_Model_Omits_It() + { + await using var session = await CreateSessionAsync(new SessionConfig + { + Model = "auto", + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + await session.SetAutoTierAsync(AutoTier.Balance); + await AssertPendingAutoTierAsync(session, AutoTier.Balance); + + // Leaving AutoTier unset without asking for a reset leaves the staged preference alone. + await session.SetModelAsync("auto", new SetModelOptions()); + await AssertPendingAutoTierAsync(session, AutoTier.Balance); + + // Supplying a tier replaces it. + await session.SetModelAsync("auto", new SetModelOptions { AutoTier = AutoTier.Intelligence }); + await AssertPendingAutoTierAsync(session, AutoTier.Intelligence); + + // ResetAutoTier clears it. Omission, a value, and a reset are three distinct + // outcomes, which is why a single nullable property cannot express the request. + await session.SetModelAsync("auto", new SetModelOptions { ResetAutoTier = true }); + await AssertPendingAutoTierAsync(session, null); + } +} diff --git a/dotnet/test/E2E/ClientLifecycleE2ETests.cs b/dotnet/test/E2E/ClientLifecycleE2ETests.cs index 4b09c695d4..82b2d2badf 100644 --- a/dotnet/test/E2E/ClientLifecycleE2ETests.cs +++ b/dotnet/test/E2E/ClientLifecycleE2ETests.cs @@ -121,7 +121,7 @@ public async Task Should_Receive_Session_Deleted_Lifecycle_Event_When_Deleted() } }); - // Do NOT DisposeAsync the session before deleting: dispose sends session.destroy + // Do NOT DisposeAsync the session before deleting: dispose sends session.detach // which closes in-memory state but does not remove the disk file; calling // delete afterwards still succeeds, but skipping dispose keeps the test minimal. await Client.DeleteSessionAsync(sessionId); diff --git a/dotnet/test/E2E/ClientOptionsE2ETests.cs b/dotnet/test/E2E/ClientOptionsE2ETests.cs index 5391e4bdbc..1ff473bd01 100644 --- a/dotnet/test/E2E/ClientOptionsE2ETests.cs +++ b/dotnet/test/E2E/ClientOptionsE2ETests.cs @@ -296,7 +296,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request var session = await Ctx.CreateSessionAsync(client, new SessionConfig { ClientName = "advanced-create-client", - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", ReasoningEffort = "medium", ReasoningSummary = ReasoningSummary.Detailed, ContextTier = ContextTier.LongContext, @@ -379,7 +379,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request Provider = "create-provider", Id = "create-model", Name = "Create Model", - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", WireModel = "create-wire-model", MaxContextWindowTokens = 12_000, MaxPromptTokens = 10_000, @@ -392,7 +392,7 @@ public async Task Should_Forward_Advanced_Session_Options_In_Create_Wire_Request using var capture = JsonDocument.Parse(await File.ReadAllTextAsync(capturePath)); var createRequest = GetCapturedRequestParams(capture.RootElement, "session.create"); Assert.Equal("advanced-create-client", createRequest.GetProperty("clientName").GetString()); - Assert.Equal("claude-sonnet-4.5", createRequest.GetProperty("model").GetString()); + Assert.Equal("claude-sonnet-5", createRequest.GetProperty("model").GetString()); Assert.Equal("medium", createRequest.GetProperty("reasoningEffort").GetString()); Assert.Equal("detailed", createRequest.GetProperty("reasoningSummary").GetString()); Assert.Equal("long_context", createRequest.GetProperty("contextTier").GetString()); @@ -442,7 +442,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques var session = await Ctx.CreateSessionAsync(client, new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = new ProviderConfig { Type = "azure", @@ -453,7 +453,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques BearerToken = "provider-bearer-token", Azure = new AzureOptions { ApiVersion = "2024-02-15-preview" }, Headers = new Dictionary { ["X-Provider-Wire"] = "yes" }, - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", WireModel = "azure-deployment", MaxPromptTokens = 8192, MaxOutputTokens = 1024, @@ -471,7 +471,7 @@ public async Task Should_Forward_Singular_Provider_Options_In_Create_Wire_Reques Assert.Equal("provider-bearer-token", provider.GetProperty("bearerToken").GetString()); Assert.Equal("2024-02-15-preview", provider.GetProperty("azure").GetProperty("apiVersion").GetString()); Assert.Equal("yes", provider.GetProperty("headers").GetProperty("X-Provider-Wire").GetString()); - Assert.Equal("claude-sonnet-4.5", provider.GetProperty("modelId").GetString()); + Assert.Equal("claude-sonnet-5", provider.GetProperty("modelId").GetString()); Assert.Equal("azure-deployment", provider.GetProperty("wireModel").GetString()); Assert.Equal(8192, provider.GetProperty("maxPromptTokens").GetInt32()); Assert.Equal(1024, provider.GetProperty("maxOutputTokens").GetInt32()); diff --git a/dotnet/test/E2E/CopilotRequestE2EProvider.cs b/dotnet/test/E2E/CopilotRequestE2EProvider.cs index 89826b4f84..f4c250752a 100644 --- a/dotnet/test/E2E/CopilotRequestE2EProvider.cs +++ b/dotnet/test/E2E/CopilotRequestE2EProvider.cs @@ -160,9 +160,9 @@ internal static HttpResponseMessage BuildNonInferenceResponse(string url) private static readonly string[] ChatCompletionStreamEvents = [ - "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\n", - "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"" + SyntheticText + "\"},\"finish_reason\":null}]}\n\n", - "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}\n\n", + "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"finish_reason\":null}]}\n\n", + "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"delta\":{\"content\":\"" + SyntheticText + "\"},\"finish_reason\":null}]}\n\n", + "data: {\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}\n\n", "data: [DONE]\n\n", ]; @@ -172,7 +172,7 @@ internal static HttpResponseMessage BuildNonInferenceResponse(string url) // runtime's Anthropic client fail with "stream ended without producing a Message". private static readonly string[] AnthropicStreamEvents = [ - "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4.5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":1}}}\n\n", + "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":1}}}\n\n", "event: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\n", "event: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"" + SyntheticText + "\"}}\n\n", "event: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\n", @@ -184,13 +184,13 @@ internal static HttpResponseMessage BuildNonInferenceResponse(string url) "{\"id\":\"resp_stub_1\",\"object\":\"response\",\"status\":\"completed\",\"output\":[{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"" + SyntheticText + "\"}]}],\"usage\":{\"input_tokens\":5,\"output_tokens\":7,\"total_tokens\":12}}"; private static readonly string BufferedChatCompletionJson = - "{\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion\",\"created\":1,\"model\":\"claude-sonnet-4.5\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"" + SyntheticText + "\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}"; + "{\"id\":\"chatcmpl-stub-1\",\"object\":\"chat.completion\",\"created\":1,\"model\":\"claude-sonnet-5\",\"choices\":[{\"index\":0,\"message\":{\"role\":\"assistant\",\"content\":\"" + SyntheticText + "\"},\"finish_reason\":\"stop\"}],\"usage\":{\"prompt_tokens\":5,\"completion_tokens\":7,\"total_tokens\":12}}"; private static readonly string BufferedAnthropicMessageJson = - "{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4.5\",\"content\":[{\"type\":\"text\",\"text\":\"" + SyntheticText + "\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":7}}"; + "{\"id\":\"msg_stub_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-5\",\"content\":[{\"type\":\"text\",\"text\":\"" + SyntheticText + "\"}],\"stop_reason\":\"end_turn\",\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":7}}"; private const string ModelCatalogJson = - "{\"data\":[{\"id\":\"claude-sonnet-4.5\",\"name\":\"Claude Sonnet 4.5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-4.5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; + "{\"data\":[{\"id\":\"claude-sonnet-5\",\"name\":\"Claude Sonnet 5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; } /// A single request the callback intercepted. diff --git a/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs b/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs index fd00cc9b99..2fca912592 100644 --- a/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs +++ b/dotnet/test/E2E/CopilotRequestSessionIdE2ETests.cs @@ -76,15 +76,15 @@ public async Task Threads_The_Session_Id_Into_A_Byok_Session_Inference_Request() { OnPermissionRequest = PermissionHandler.ApproveAll, // BYOK providers require an explicit model id. - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = new ProviderConfig { Type = "openai", WireApi = "responses", BaseUrl = "https://byok.invalid/v1", ApiKey = "byok-secret", - ModelId = "claude-sonnet-4.5", - WireModel = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", + WireModel = "claude-sonnet-5", }, }); var byokSessionId = session.SessionId; diff --git a/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs b/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs index 80ccdb8c90..e6890d6992 100644 --- a/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs +++ b/dotnet/test/E2E/CopilotRequestWebSocketE2ETests.cs @@ -349,7 +349,7 @@ private static (string Type, string Json)[] ResponseEvents(string text, string i ]; private const string ModelCatalogJson = - "{\"data\":[{\"id\":\"claude-sonnet-4.5\",\"name\":\"Claude Sonnet 4.5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"supported_endpoints\":[\"/responses\",\"ws:/responses\"],\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-4.5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; + "{\"data\":[{\"id\":\"claude-sonnet-5\",\"name\":\"Claude Sonnet 5\",\"object\":\"model\",\"vendor\":\"Anthropic\",\"version\":\"1\",\"preview\":false,\"model_picker_enabled\":true,\"supported_endpoints\":[\"/responses\",\"ws:/responses\"],\"capabilities\":{\"type\":\"chat\",\"family\":\"claude-sonnet-5\",\"tokenizer\":\"o200k_base\",\"limits\":{\"max_context_window_tokens\":200000,\"max_output_tokens\":8192},\"supports\":{\"streaming\":true,\"tool_calls\":true,\"parallel_tool_calls\":true,\"vision\":true}}}]}"; private static int GetFreePort() { diff --git a/dotnet/test/E2E/EventFidelityE2ETests.cs b/dotnet/test/E2E/EventFidelityE2ETests.cs index 8882f972d5..da9869757f 100644 --- a/dotnet/test/E2E/EventFidelityE2ETests.cs +++ b/dotnet/test/E2E/EventFidelityE2ETests.cs @@ -12,7 +12,7 @@ namespace GitHub.Copilot.Test.E2E; /// Verifies the shape and ordering of s emitted from the /// runtime: every event has an id and timestamp, user/assistant messages carry /// content, tool execution events carry a toolCallId, and -/// session.idle is the last event of a turn. Mirrors +/// session.idle follows the final assistant message of a turn. Mirrors /// nodejs/test/e2e/event_fidelity.e2e.test.ts. /// public class EventFidelityE2ETests(E2ETestFixture fixture, ITestOutputHelper output) @@ -43,9 +43,12 @@ await session.SendAndWaitAsync(new MessageOptions var assistantIdx = types.LastIndexOf("assistant.message"); Assert.True(userIdx < assistantIdx, $"Expected user.message ({userIdx}) before last assistant.message ({assistantIdx})"); - // session.idle should be the last event we observed + // session.idle should complete the conversational turn. Post-turn + // metadata events may arrive after it on slower target frameworks. var idleIdx = types.LastIndexOf("session.idle"); - Assert.Equal(types.Count - 1, idleIdx); + Assert.True( + assistantIdx < idleIdx, + $"Expected last assistant.message ({assistantIdx}) before session.idle ({idleIdx}): {string.Join(", ", types)}"); await session.DisposeAsync(); } diff --git a/dotnet/test/E2E/ExternalToolCancellationE2ETests.cs b/dotnet/test/E2E/ExternalToolCancellationE2ETests.cs new file mode 100644 index 0000000000..b7f34fd037 --- /dev/null +++ b/dotnet/test/E2E/ExternalToolCancellationE2ETests.cs @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using Microsoft.Extensions.AI; +using System.ComponentModel; +using Xunit; +using Xunit.Abstractions; + +namespace GitHub.Copilot.Test.E2E; + +public class ExternalToolCancellationE2ETests(E2ETestFixture fixture, ITestOutputHelper output) + : E2ETestBase(fixture, "external_tool_cancellation", output) +{ + [Fact] + public async Task Should_Cancel_Tool_Handler_When_Session_Disposes() + { + var toolStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var toolCancelled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseTool = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var session = await CreateSessionAsync(new SessionConfig + { + Tools = [AIFunctionFactory.Create(SlowTool, "slow_analysis")], + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + _ = session.SendAsync(new MessageOptions + { + Prompt = "Use slow_analysis with value 'test_abort'. Wait for the result.", + }); + + var startedValue = await toolStarted.Task.WaitAsync(TimeSpan.FromSeconds(60)); + Assert.Equal("test_abort", startedValue); + + await session.DisposeAsync(); + await toolCancelled.Task.WaitAsync(TimeSpan.FromSeconds(60)); + + releaseTool.TrySetResult("RELEASED"); + + [Description("A slow analysis tool that blocks until released")] + async Task SlowTool([Description("Value to analyze")] string value, CancellationToken cancellationToken) + { + toolStarted.TrySetResult(value); + try + { + var completed = await Task.WhenAny(releaseTool.Task, Task.Delay(Timeout.Infinite, cancellationToken)); + if (completed == releaseTool.Task) + { + return await releaseTool.Task; + } + + throw new OperationCanceledException(cancellationToken); + } + catch (OperationCanceledException) + { + toolCancelled.TrySetResult(true); + throw; + } + } + } +} diff --git a/dotnet/test/E2E/McpOAuthE2ETests.cs b/dotnet/test/E2E/McpOAuthE2ETests.cs index 1085aba040..e947ea7640 100644 --- a/dotnet/test/E2E/McpOAuthE2ETests.cs +++ b/dotnet/test/E2E/McpOAuthE2ETests.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Net.Http; using System.Text.Json; +using System.Threading.Channels; using Xunit; using Xunit.Abstractions; @@ -48,6 +49,7 @@ public async Task Should_Satisfy_MCP_OAuth_Using_Host_Provided_Token() } }); + await session.Rpc.Mcp.ReloadAsync(); await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected); var tools = await session.Rpc.Mcp.ListToolsAsync(serverName); Assert.Contains(tools.Tools, tool => tool.Name == "whoami"); @@ -75,14 +77,14 @@ public async Task Should_Resolve_Pending_MCP_OAuth_Request_With_Direct_Rpc() { await using var oauthServer = await OAuthMcpServer.StartAsync(ExpectedToken); var serverName = "oauth-direct-rpc-mcp"; - var authRequest = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var authRequests = Channel.CreateUnbounded(); var releaseHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); await using var session = await CreateSessionAsync(new SessionConfig { OnMcpAuthRequest = request => { - authRequest.TrySetResult(request); + authRequests.Writer.TryWrite(request); return releaseHandler.Task; }, McpServers = new Dictionary @@ -95,8 +97,27 @@ public async Task Should_Resolve_Pending_MCP_OAuth_Request_With_Direct_Rpc() }, }); + var reload = session.Rpc.Mcp.ReloadAsync(); var connected = WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected); - var request = await authRequest.Task.WaitAsync(TimeSpan.FromSeconds(30)); + var request = await authRequests.Reader.ReadAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(30)); + + while (true) + { + var handled = await session.Rpc.Mcp.Oauth.HandlePendingRequestAsync( + request.RequestId, + new McpOauthPendingRequestResponseToken + { + AccessToken = ExpectedToken, + TokenType = "Bearer", + ExpiresIn = 3600, + }); + if (handled.Success) + { + break; + } + request = await authRequests.Reader.ReadAsync().AsTask().WaitAsync(TimeSpan.FromSeconds(30)); + } + Assert.NotEmpty(request.RequestId); Assert.Equal(serverName, request.ServerName); Assert.Equal($"{oauthServer.Url}/mcp", request.ServerUrl); @@ -104,21 +125,11 @@ public async Task Should_Resolve_Pending_MCP_OAuth_Request_With_Direct_Rpc() Assert.NotNull(request.WwwAuthenticateParams); Assert.Equal("mcp.read", request.WwwAuthenticateParams!.Scope); - var handled = await session.Rpc.Mcp.Oauth.HandlePendingRequestAsync( - request.RequestId, - new McpOauthPendingRequestResponseToken - { - AccessToken = ExpectedToken, - TokenType = "Bearer", - ExpiresIn = 3600, - }); - Assert.True(handled.Success); - + releaseHandler.SetResult(McpAuthResult.FromToken(new McpAuthToken { AccessToken = ExpectedToken })); + await reload; await connected; var tools = await session.Rpc.Mcp.ListToolsAsync(serverName); Assert.Contains(tools.Tools, tool => tool.Name == "whoami"); - - releaseHandler.SetResult(McpAuthResult.FromToken(new McpAuthToken { AccessToken = ExpectedToken })); } [Fact] @@ -178,14 +189,16 @@ public async Task Should_Request_Replacement_Tokens_Across_MCP_OAuth_Lifecycle() } }); + await session.Rpc.Mcp.ReloadAsync(); await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected); + refreshCount = 0; await CallWhoamiAsync(session, serverName, "refresh"); await CallWhoamiAsync(session, serverName, "upscope"); await CallWhoamiAsync(session, serverName, "reauth"); + observedReasons.RemoveAll(reason => reason == McpOauthRequestReason.Initial); Assert.Equal( [ - McpOauthRequestReason.Initial, McpOauthRequestReason.Refresh, McpOauthRequestReason.Upscope, McpOauthRequestReason.Refresh, @@ -223,6 +236,7 @@ public async Task Should_Cancel_Pending_MCP_OAuth_Request() } }); + await session.Rpc.Mcp.ReloadAsync(); await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.NeedsAuth); // The MCP connection is kicked off by session.create, but the SDK only registers its diff --git a/dotnet/test/E2E/MultiClientE2ETests.cs b/dotnet/test/E2E/MultiClientE2ETests.cs index 4dbe7190ad..7597aa580b 100644 --- a/dotnet/test/E2E/MultiClientE2ETests.cs +++ b/dotnet/test/E2E/MultiClientE2ETests.cs @@ -324,6 +324,9 @@ public async Task Disconnecting_Client_Removes_Its_Tools() // Disconnect client 2 await Client2.ForceStopAsync(); + // Give the server time to process the connection close and remove tools. + await Task.Delay(500); + // Recreate client2 for cleanup var port = Client1.RuntimePort!.Value; _client2 = Ctx.CreateClient(options: new CopilotClientOptions diff --git a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs index bf1ed687c1..bc3f4bd672 100644 --- a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs +++ b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs @@ -140,10 +140,12 @@ await session1.SendAsync(new MessageOptions } [Description("Looks up a value after resumption")] - async Task BlockingExternalTool([Description("Value to look up")] string value) + async Task BlockingExternalTool( + [Description("Value to look up")] string value, + CancellationToken cancellationToken) { originalToolStarted.TrySetResult(value); - return await releaseOriginalTool.Task; + return await releaseOriginalTool.Task.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); } } @@ -274,11 +276,13 @@ await TestHelper.WaitForConditionAsync( } [Description("Looks up a value after resumption")] - async Task BlockingExternalTool([Description("Value to look up")] string value) + async Task BlockingExternalTool( + [Description("Value to look up")] string value, + CancellationToken cancellationToken) { Interlocked.Increment(ref invocationCount); originalToolStarted.TrySetResult(value); - return await releaseOriginalTool.Task; + return await releaseOriginalTool.Task.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); } [Description("Looks up a value after resumption")] @@ -327,6 +331,8 @@ await Task.WhenAll( Assert.Equal("beta", await originalToolBStarted.Task); await suspendedClient.ForceStopAsync(); + releaseOriginalToolA.TrySetResult("ORIGINAL_A_SHOULD_NOT_WIN"); + releaseOriginalToolB.TrySetResult("ORIGINAL_B_SHOULD_NOT_WIN"); await using var resumedClient = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForUri(cliUrl, connectionToken: SharedToken) }); var session2 = await Ctx.ResumeSessionAsync(resumedClient, sessionId, new ResumeSessionConfig @@ -356,17 +362,21 @@ await Task.WhenAll( } [Description("Looks up the first value after resumption")] - async Task BlockingToolA([Description("Value to look up")] string value) + async Task BlockingToolA( + [Description("Value to look up")] string value, + CancellationToken cancellationToken) { originalToolAStarted.TrySetResult(value); - return await releaseOriginalToolA.Task; + return await releaseOriginalToolA.Task.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); } [Description("Looks up the second value after resumption")] - async Task BlockingToolB([Description("Value to look up")] string value) + async Task BlockingToolB( + [Description("Value to look up")] string value, + CancellationToken cancellationToken) { originalToolBStarted.TrySetResult(value); - return await releaseOriginalToolB.Task; + return await releaseOriginalToolB.Task.WaitAsync(Timeout.InfiniteTimeSpan, cancellationToken); } } diff --git a/dotnet/test/E2E/RewindE2ETests.cs b/dotnet/test/E2E/RewindE2ETests.cs index 3152b2e908..e8379f988d 100644 --- a/dotnet/test/E2E/RewindE2ETests.cs +++ b/dotnet/test/E2E/RewindE2ETests.cs @@ -24,7 +24,7 @@ public async Task Should_Restore_Tracked_File_And_Conversation() await File.WriteAllTextAsync(filePath, OriginalFileContent); await using var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EnableFileChangeTracking = true, }); diff --git a/dotnet/test/E2E/RpcAdditionalEdgeCasesE2ETests.cs b/dotnet/test/E2E/RpcAdditionalEdgeCasesE2ETests.cs index 241a978a99..5157eb6336 100644 --- a/dotnet/test/E2E/RpcAdditionalEdgeCasesE2ETests.cs +++ b/dotnet/test/E2E/RpcAdditionalEdgeCasesE2ETests.cs @@ -26,7 +26,7 @@ public async Task Shell_Exec_With_Zero_Timeout_Does_Not_Kill_Long_Running_Comman var session = await CreateSessionAsync(); var markerPath = Path.Join(Ctx.WorkDir, $"shell-zero-timeout-{Guid.NewGuid():N}.txt"); var command = OperatingSystem.IsWindows() - ? $"powershell -NoLogo -NoProfile -Command \"Start-Sleep -Milliseconds 500; Set-Content -LiteralPath '{markerPath}' -Value 'alive'; Start-Sleep -Seconds 60\"" + ? $"ping 127.0.0.1 -n 2 >nul & echo alive>\"{markerPath}\" & ping 127.0.0.1 -n 61 >nul" : $"sh -c \"sleep 0.5; printf alive > '{markerPath}'; sleep 60\""; var execResult = await session.Rpc.Shell.ExecAsync(command, cwd: Path.GetTempPath(), timeout: TimeSpan.Zero); diff --git a/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs b/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs index 0a3513e4b5..3d017d8165 100644 --- a/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs +++ b/dotnet/test/E2E/RpcExtensionsLoadedE2ETests.cs @@ -57,10 +57,22 @@ private CopilotClient CreateExtensionsClient() { return Ctx.CreateClient(options: new CopilotClientOptions { - Connection = RuntimeConnection.ForStdio(args: ["--yolo"]), + Connection = RuntimeConnection.ForStdio( + path: Ctx.GetLegacyCliPath(), + args: ["--yolo"]), }, environment: ExtensionsEnabledEnvironment()); } + private static SessionConfig CreateExtensionsSessionConfig(string? workingDirectory = null) + { + return new SessionConfig + { + EnableConfigDiscovery = true, + WorkingDirectory = workingDirectory, + OnPermissionRequest = PermissionHandler.ApproveAll, + }; + } + /// /// Writes a minimal user extension into {HomeDir}/extensions/{name}/extension.mjs. /// The body imports @github/copilot-sdk/extension, calls joinSession @@ -189,12 +201,9 @@ public async Task Discovers_Loads_And_Reports_Running_Extension(string sourceVal await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - WorkingDirectory = workingDirectory, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig(workingDirectory)); var ext = await WaitForExtensionAsync(session, extId, ExtensionStatus.Running); @@ -214,11 +223,9 @@ public async Task Disable_Then_Enable_Cycles_Extension_Status() await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig()); // Wait until the initial running state is observed before mutating. await WaitForExtensionAsync(session, extId, ExtensionStatus.Running); @@ -240,11 +247,9 @@ public async Task Reload_Picks_Up_Extension_Added_After_Session_Create() // Start the session BEFORE writing the extension so the initial discovery sees nothing. await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig()); // setupExtensionsForSession runs asynchronously; until it completes the // controller isn't installed and ReloadAsync throws "Extensions not @@ -285,11 +290,9 @@ public async Task Failed_Extension_Reports_Failed_Status() await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig()); var ext = await WaitForExtensionAsync(session, extId, ExtensionStatus.Failed); Assert.Equal(extId, ext.Id); @@ -306,11 +309,9 @@ public async Task Multiple_Extensions_Are_Discovered_Independently() await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig()); await WaitForExtensionAsync(session, ext1Id, ExtensionStatus.Running); await WaitForExtensionAsync(session, ext2Id, ExtensionStatus.Running); @@ -328,11 +329,9 @@ public async Task Reload_Preserves_Disabled_State_Across_Calls() await using var client = CreateExtensionsClient(); - await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig - { - EnableConfigDiscovery = true, - OnPermissionRequest = PermissionHandler.ApproveAll, - }); + await using var session = await Ctx.CreateSessionAsync( + client, + CreateExtensionsSessionConfig()); await WaitForExtensionAsync(session, extId, ExtensionStatus.Running); diff --git a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs index 0d2942d4bf..ec923b31a1 100644 --- a/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs +++ b/dotnet/test/E2E/RpcMcpAndSkillsE2ETests.cs @@ -33,7 +33,7 @@ public async Task Should_List_And_Toggle_Session_Skills() { var skillName = $"session-rpc-skill-{Guid.NewGuid():N}"; var skillsDir = CreateSkillDirectory(skillName, "Session skill controlled by RPC."); - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir], DisabledSkills = [skillName], @@ -56,7 +56,7 @@ public async Task Should_Ensure_Skills_Are_Loaded_And_List_Invoked_Skills() { var skillName = $"ensure-rpc-skill-{Guid.NewGuid():N}"; var skillsDir = CreateSkillDirectory(skillName, "Skill loaded explicitly by RPC."); - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir], }); @@ -79,7 +79,7 @@ public async Task Should_Reload_Session_Skills() Directory.CreateDirectory(skillsDir); var skillName = $"reload-rpc-skill-{Guid.NewGuid():N}"; - var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir] }); + await using var session = await CreateSessionAsync(new SessionConfig { SkillDirectories = [skillsDir] }); var before = await session.Rpc.Skills.ListAsync(); Assert.DoesNotContain(before.Skills, skill => string.Equals(skill.Name, skillName, StringComparison.Ordinal)); @@ -95,7 +95,7 @@ public async Task Should_Reload_Session_Skills() public async Task Should_List_Mcp_Servers_With_Configured_Server() { const string serverName = "rpc-list-mcp-server"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); @@ -111,7 +111,7 @@ public async Task Should_List_Mcp_Servers_With_Configured_Server() public async Task Should_Set_Mcp_Env_Value_Mode_And_Remove_GitHub_Server() { const string serverName = "github"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); @@ -137,7 +137,7 @@ public async Task Should_Set_Mcp_Env_Value_Mode_And_Remove_GitHub_Server() public async Task Should_Report_Mcp_Sampling_Failure_And_Cancel_Missing_Sampling() { const string serverName = "rpc-sampling-server"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); @@ -172,7 +172,7 @@ public async Task Should_Report_Mcp_Sampling_Failure_And_Cancel_Missing_Sampling [Fact] public async Task Should_List_Plugins() { - var session = await CreateSessionAsync(); + await using var session = await CreateSessionAsync(); var result = await session.Rpc.Plugins.ListAsync(); @@ -314,7 +314,7 @@ public async Task Should_Report_Error_When_Mcp_App_Resource_Is_Not_Available() [Fact] public async Task Should_Report_Error_When_Mcp_Host_Is_Not_Initialized() { - var session = await CreateSessionAsync(); + await using var session = await CreateSessionAsync(); await AssertFailureAsync( () => session.Rpc.Mcp.EnableAsync("missing-server"), @@ -333,7 +333,7 @@ await AssertFailureAsync( [Fact] public async Task Should_Report_Error_When_Mcp_Oauth_Server_Is_Not_Configured() { - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers("configured-stdio-server"), }); @@ -348,7 +348,7 @@ await AssertFailureAsync( public async Task Should_Report_Error_When_Mcp_Oauth_Server_Is_Not_Remote() { const string serverName = "configured-stdio-server"; - var session = await CreateSessionAsync(new SessionConfig + await using var session = await CreateSessionAsync(new SessionConfig { McpServers = CreateTestMcpServers(serverName), }); diff --git a/dotnet/test/E2E/RpcServerE2ETests.cs b/dotnet/test/E2E/RpcServerE2ETests.cs index 2df8593cc4..5e39c5ca5a 100644 --- a/dotnet/test/E2E/RpcServerE2ETests.cs +++ b/dotnet/test/E2E/RpcServerE2ETests.cs @@ -171,7 +171,7 @@ public async Task Should_Call_Rpc_Models_List_With_Typed_Result() var result = await client.Rpc.Models.ListAsync(); Assert.NotNull(result.Models); - Assert.Contains(result.Models, model => model.Id == "claude-sonnet-4.5"); + Assert.Contains(result.Models, model => model.Id == "claude-sonnet-5"); Assert.All(result.Models, model => Assert.False(string.IsNullOrWhiteSpace(model.Name))); } diff --git a/dotnet/test/E2E/RpcSessionStateE2ETests.cs b/dotnet/test/E2E/RpcSessionStateE2ETests.cs index 803c1c602b..196861cd9b 100644 --- a/dotnet/test/E2E/RpcSessionStateE2ETests.cs +++ b/dotnet/test/E2E/RpcSessionStateE2ETests.cs @@ -22,14 +22,14 @@ private static async Task AssertImplementedFailureAsync(Func ac [Fact] public async Task Should_Call_Session_Rpc_Model_GetCurrent() { - await using var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); + await using var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-5" }); var result = await session.Rpc.Model.GetCurrentAsync(); Assert.NotNull(result.ModelId); Assert.NotEmpty(result.ModelId); // Strengthen: verify the configured model is actually in effect, not just any model - Assert.Equal("claude-sonnet-4.5", result.ModelId); + Assert.Equal("claude-sonnet-5", result.ModelId); } [Fact] @@ -48,12 +48,12 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo() await using var session = await isolatedCtx.CreateSessionAsync(isolatedClient, new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", OnPermissionRequest = PermissionHandler.ApproveAll, }); var before = await session.Rpc.Model.GetCurrentAsync(); - Assert.Equal("claude-sonnet-4.5", before.ModelId); + Assert.Equal("claude-sonnet-5", before.ModelId); var result = await session.Rpc.Model.SwitchToAsync(modelId: "gpt-5.4", reasoningEffort: "high"); Assert.Equal("gpt-5.4", result.ModelId); @@ -281,14 +281,14 @@ public async Task Should_Call_Metadata_Snapshot_SetWorkingDirectory_And_RecordCo var branch = $"rpc-context-{Guid.NewGuid():N}"; await using var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", WorkingDirectory = firstDirectory, }); var initialSnapshot = await session.Rpc.Metadata.SnapshotAsync(); Assert.Equal(session.SessionId, initialSnapshot.SessionId); Assert.Equal(MetadataSnapshotCurrentMode.Interactive, initialSnapshot.CurrentMode); - Assert.Equal("claude-sonnet-4.5", initialSnapshot.SelectedModel); + Assert.Equal("claude-sonnet-5", initialSnapshot.SelectedModel); Assert.False(initialSnapshot.IsRemote); Assert.False(initialSnapshot.AlreadyInUse); Assert.NotEqual(default, initialSnapshot.StartTime); @@ -405,14 +405,14 @@ public async Task Should_Set_ReasoningEffort_And_Auto_Name() { await using var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", }); var reasoning = await session.Rpc.Model.SetReasoningEffortAsync("high"); Assert.Equal("high", reasoning.ReasoningEffort); var currentModel = await session.Rpc.Model.GetCurrentAsync(); - Assert.Equal("claude-sonnet-4.5", currentModel.ModelId); + Assert.Equal("claude-sonnet-5", currentModel.ModelId); Assert.Equal("high", currentModel.ReasoningEffort); var autoName = $"Auto Session {Guid.NewGuid():N}"; @@ -653,9 +653,9 @@ public async Task Should_Compact_Session_History_After_Messages() var contextInfo = await session.Rpc.Metadata.ContextInfoAsync( promptTokenLimit: 128_000, outputTokenLimit: 4_096, - selectedModel: "claude-sonnet-4.5"); + selectedModel: "claude-sonnet-5"); var context = Assert.IsType(contextInfo.ContextInfo); - Assert.Equal("claude-sonnet-4.5", context.ModelName); + Assert.Equal("claude-sonnet-5", context.ModelName); Assert.Equal(128_000, context.PromptTokenLimit); Assert.True(context.Limit >= context.PromptTokenLimit); Assert.True(context.TotalTokens > 0); @@ -666,7 +666,7 @@ public async Task Should_Compact_Session_History_After_Messages() context.SystemTokens + context.ConversationTokens + context.ToolDefinitionsTokens, context.TotalTokens); - var recomputed = await session.Rpc.Metadata.RecomputeContextTokensAsync("claude-sonnet-4.5"); + var recomputed = await session.Rpc.Metadata.RecomputeContextTokensAsync("claude-sonnet-5"); Assert.True(recomputed.SystemTokenCount > 0); Assert.True(recomputed.MessagesTokenCount > 0); Assert.Equal(recomputed.SystemTokenCount + recomputed.MessagesTokenCount, recomputed.TotalTokens); diff --git a/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs b/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs index 72663c35a4..234a5d8694 100644 --- a/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs +++ b/dotnet/test/E2E/RpcSessionStateExtrasE2ETests.cs @@ -32,7 +32,7 @@ public async Task Should_List_Models_For_Session() await using var client = CreateAuthenticatedClient(token); await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", OnPermissionRequest = PermissionHandler.ApproveAll, }); @@ -41,7 +41,7 @@ public async Task Should_List_Models_For_Session() Assert.NotNull(result.List); Assert.NotEmpty(result.List); // The configured model must be present in the returned catalog. - Assert.Contains(result.List, model => model.GetRawText().Contains("claude-sonnet-4.5", StringComparison.Ordinal)); + Assert.Contains(result.List, model => model.GetRawText().Contains("claude-sonnet-5", StringComparison.Ordinal)); } [Fact] @@ -73,7 +73,7 @@ public async Task Should_Add_Byok_Provider_And_Model_At_Runtime() Provider = providerName, Id = modelId, Name = "SDK Runtime Model", - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", WireModel = "wire-sdk-runtime-model", MaxContextWindowTokens = 4_096, MaxPromptTokens = 3_072, @@ -277,7 +277,7 @@ public async Task Should_Update_And_Clear_Live_Subagent_Settings() { ["general-purpose"] = new() { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EffortLevel = "high", ContextTier = SubagentSettingsEntryContextTier.Default, }, diff --git a/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs b/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs index b6036b7b8d..25055a671a 100644 --- a/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs +++ b/dotnet/test/E2E/RpcShellEdgeCaseE2ETests.cs @@ -28,16 +28,21 @@ public async Task Shell_Exec_With_Timeout_Kills_Long_Running_Command() var markerPath = Path.Join(Ctx.WorkDir, $"shell-timeout-{Guid.NewGuid():N}.txt"); var startedPath = Path.Join(Ctx.WorkDir, $"shell-timeout-started-{Guid.NewGuid():N}.txt"); - // Sleep 30s but timeout at 200ms — runtime should SIGTERM the child before the + // Sleep 30s but use a much shorter timeout — runtime should SIGTERM the child before the // sleep completes, which means the marker file must NEVER appear within a wait // window comfortably greater than the timeout but well under the sleep duration. + // Process startup on Windows can exceed 200ms on loaded runners, so match the + // platform-specific allowance used by the Rust coverage for this RPC. + var timeout = OperatingSystem.IsWindows() + ? TimeSpan.FromSeconds(2) + : TimeSpan.FromMilliseconds(200); var command = OperatingSystem.IsWindows() - ? $"echo started>\"{startedPath}\" & for /L %i in (1,1,2147483647) do @rem & echo should-not-exist>\"{markerPath}\"" + ? $"powershell -NoLogo -NoProfile -Command \"Set-Content -LiteralPath '{startedPath}' -Value started; Start-Sleep -Seconds 30; Set-Content -LiteralPath '{markerPath}' -Value should-not-exist\"" : $"printf 'started' > '{startedPath}'; sleep 30; printf 'should-not-exist' > '{markerPath}'"; // On Windows, terminating the shell wrapper can briefly leave children alive. // Keep this long-running command outside the fixture workspace so cleanup is not blocked by cwd handles. - var result = await session.Rpc.Shell.ExecAsync(command, cwd: Path.GetTempPath(), timeout: TimeSpan.FromMilliseconds(200)); + var result = await session.Rpc.Shell.ExecAsync(command, cwd: Path.GetTempPath(), timeout: timeout); Assert.False(string.IsNullOrWhiteSpace(result.ProcessId)); await TestHelper.WaitForConditionAsync( @@ -164,12 +169,10 @@ public async Task Shell_Exec_With_Large_Stdout_Cleans_Up() var session = await CreateSessionAsync(); var markerPath = Path.Join(Ctx.WorkDir, $"shell-stdout-{Guid.NewGuid():N}.txt"); - // Print a payload large enough to exceed the runtime's 64KB chunk threshold so the - // chunked-output path is executed. We use a single 200KB write so the runtime has to - // emit at least 3 chunks (200KB / 64KB ≈ 4). + // Exceed the runtime's 64KB chunk threshold without flooding slower CI runners. var command = OperatingSystem.IsWindows() - ? $"powershell -NoLogo -NoProfile -Command \"Write-Host ('x' * 204800); Set-Content -LiteralPath '{markerPath}' -Value 'done'\"" - : $"printf '%0.s=' $(seq 1 204800); printf 'done' > '{markerPath}'"; + ? $"powershell -NoLogo -NoProfile -Command \"Write-Host ('x' * 71680); Set-Content -LiteralPath '{markerPath}' -Value 'done'\"" + : $"printf '%0.s=' $(seq 1 71680); printf 'done' > '{markerPath}'"; var result = await session.Rpc.Shell.ExecAsync(command); Assert.False(string.IsNullOrWhiteSpace(result.ProcessId)); @@ -185,10 +188,9 @@ await TestHelper.WaitForConditionAsync( private static async Task AssertProcessMapCleanedUpAsync(CopilotSession session, string processId, string scenario) { // The shell RPC surface exposes kill but not a non-mutating status API. - // Give the runtime's close/exit handler a bounded grace period, then - // probe exactly once; if this returns true, the assertion fails instead - // of letting a polling kill make the test pass by cleaning up itself. - await Task.Delay(TimeSpan.FromSeconds(1)); + // Give slower CI runners enough time to flush output and process the exit + // before the single mutating cleanup probe. + await Task.Delay(TimeSpan.FromSeconds(5)); var killResult = await session.Rpc.Shell.KillAsync(processId); Assert.False(killResult.Killed, $"{scenario} should have already exited and been removed from the runtime's process map."); } diff --git a/dotnet/test/E2E/SessionConfigE2ETests.cs b/dotnet/test/E2E/SessionConfigE2ETests.cs index 1bc4c52eb9..314ba2f0cf 100644 --- a/dotnet/test/E2E/SessionConfigE2ETests.cs +++ b/dotnet/test/E2E/SessionConfigE2ETests.cs @@ -31,7 +31,7 @@ public async Task Vision_Disabled_Then_Enabled_Via_SetModel() var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", ModelCapabilities = new ModelCapabilitiesOverride { Supports = new ModelCapabilitiesOverrideSupports { Vision = false }, @@ -46,7 +46,7 @@ public async Task Vision_Disabled_Then_Enabled_Via_SetModel() // Switch vision on await session.SetModelAsync( - "claude-sonnet-4.5", + "claude-sonnet-5", reasoningEffort: null, modelCapabilities: new ModelCapabilitiesOverride { @@ -74,7 +74,7 @@ public async Task Vision_Enabled_Then_Disabled_Via_SetModel() var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", ModelCapabilities = new ModelCapabilitiesOverride { Supports = new ModelCapabilitiesOverrideSupports { Vision = true }, @@ -89,7 +89,7 @@ public async Task Vision_Enabled_Then_Disabled_Via_SetModel() // Switch vision off await session.SetModelAsync( - "claude-sonnet-4.5", + "claude-sonnet-5", reasoningEffort: null, modelCapabilities: new ModelCapabilitiesOverride { @@ -216,7 +216,7 @@ public async Task Should_Forward_Custom_Provider_Headers_On_Create() { var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = CreateProxyProvider("create-provider-header"), }); @@ -240,7 +240,7 @@ public async Task Should_Forward_Custom_Provider_Headers_On_Resume() var session2 = await ResumeSessionAsync(sessionId, new ResumeSessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = CreateProxyProvider("resume-provider-header"), }); @@ -267,7 +267,7 @@ public async Task Should_Forward_Provider_Wire_Model() // tests for serialization coverage). var session = await CreateSessionAsync(new SessionConfig { - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", Provider = new ProviderConfig { Type = "openai", @@ -300,14 +300,14 @@ public async Task Should_Use_Provider_Model_Id_As_Wire_Model() Type = "openai", BaseUrl = Ctx.ProxyUrl, ApiKey = "test-provider-key", - ModelId = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", }, }); await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 1+1?" }); var exchange = Assert.Single(await Ctx.GetExchangesAsync()); - Assert.Equal("claude-sonnet-4.5", exchange.Request.Model); + Assert.Equal("claude-sonnet-5", exchange.Request.Model); await session.DisposeAsync(); } @@ -598,7 +598,7 @@ public async Task Should_Enable_Citations_For_Anthropic_File_Attachments_On_Crea var session = await Ctx.CreateSessionAsync(client, new SessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EnableCitations = true, Provider = CreateAnthropicProvider(), }); @@ -645,7 +645,7 @@ public async Task Should_Enable_Citations_For_Anthropic_File_Attachments_On_Resu var session2 = await Ctx.ResumeSessionAsync(resumeClient, sessionId, new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, - Model = "claude-sonnet-4.5", + Model = "claude-sonnet-5", EnableCitations = true, Provider = CreateAnthropicProvider(), }); @@ -834,8 +834,8 @@ private static ProviderConfig CreateAnthropicProvider() Type = "anthropic", BaseUrl = "https://anthropic-citations.invalid/v1", ApiKey = "test-provider-key", - ModelId = "claude-sonnet-4.5", - WireModel = "claude-sonnet-4.5", + ModelId = "claude-sonnet-5", + WireModel = "claude-sonnet-5", }; } diff --git a/dotnet/test/E2E/SessionE2ETests.cs b/dotnet/test/E2E/SessionE2ETests.cs index 27ef7437f7..fab84bc439 100644 --- a/dotnet/test/E2E/SessionE2ETests.cs +++ b/dotnet/test/E2E/SessionE2ETests.cs @@ -17,7 +17,7 @@ public class SessionE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : [Fact] public async Task ShouldCreateAndDisconnectSessions() { - var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-4.5" }); + var session = await CreateSessionAsync(new SessionConfig { Model = "claude-sonnet-5" }); Assert.Matches(@"^[a-f0-9-]+$", session.SessionId); @@ -295,6 +295,50 @@ public async Task Resumes_A_Persisted_Session_From_A_New_Client_When_An_Mcp_OAut Assert.Equal(sessionId, session2.SessionId); } + [Fact] + public async Task Should_Recover_Marker_After_Cold_Resume_With_Explicit_Session_Id() + { + await using var isolatedCtx = await E2ETestContext.CreateAsync(); + await isolatedCtx.ConfigureForTestAsync("session", nameof(Should_Recover_Marker_After_Cold_Resume_With_Explicit_Session_Id)); + + var sessionId = $"e2e-cold-resume-{Guid.NewGuid()}"; + + var client1 = isolatedCtx.CreateClient(); + var session1 = await isolatedCtx.CreateSessionAsync(client1, new SessionConfig + { + SessionId = sessionId, + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + Assert.Equal(sessionId, session1.SessionId); + + var answer = await session1.SendAndWaitAsync(new MessageOptions + { + Prompt = "Please remember this exact secret marker for later - MARKER-7f3ac21e. Reply with only the single word \"Acknowledged\".", + }); + Assert.NotNull(answer); + Assert.Contains("Acknowledged", answer!.Data.Content ?? string.Empty); + + await session1.DisposeAsync(); + await client1.ForceStopAsync(); + + var client2 = isolatedCtx.CreateClient(); + var session2 = await isolatedCtx.ResumeSessionAsync(client2, sessionId, new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + Assert.Equal(sessionId, session2.SessionId); + + var answer2 = await session2.SendAndWaitAsync(new MessageOptions + { + Prompt = "What was the exact secret marker I asked you to remember earlier? Reply with only that marker value and nothing else.", + }); + Assert.NotNull(answer2); + Assert.Contains("MARKER-7f3ac21e", answer2!.Data.Content ?? string.Empty); + + await session2.DisposeAsync(); + await client2.ForceStopAsync(); + } + [Fact] public async Task Should_Throw_Error_When_Resuming_Non_Existent_Session() { diff --git a/dotnet/test/E2E/SessionEventLoopLeakE2ETests.cs b/dotnet/test/E2E/SessionEventLoopLeakE2ETests.cs new file mode 100644 index 0000000000..40470109c5 --- /dev/null +++ b/dotnet/test/E2E/SessionEventLoopLeakE2ETests.cs @@ -0,0 +1,198 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.Test.Harness; +using System.Collections; +using System.Reflection; +using Xunit; +using Xunit.Abstractions; + +namespace GitHub.Copilot.Test.E2E; + +/// +/// Regression coverage for the goroutine/task leak fixed alongside +/// github/copilot-sdk#2360: and +/// construct a +/// and start its event-dispatch consumer (ProcessEventsAsync) eagerly, before the +/// CLI confirms the session, so the CLI can route session-scoped requests to it while +/// session.create (or session.resume) is still being processed. Every failure path must +/// stop that consumer — otherwise each failed call leaks a background task forever, since +/// no caller ever receives the failed session to dispose it. Mirrors +/// go/internal/e2e/session_event_loop_leak_e2e_test.go. +/// +[Trait(E2ETestTraits.Backend, E2ETestTraits.CapiOnly)] +public class SessionEventLoopLeakE2ETests(E2ETestFixture fixture, ITestOutputHelper output) + : E2ETestBase(fixture, "session-event-loop-leak", output) +{ + private static readonly FieldInfo SessionsField = + typeof(CopilotClient).GetField("_sessions", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("CopilotClient._sessions was not found."); + + private static readonly FieldInfo EventChannelField = + typeof(CopilotSession).GetField("_eventChannel", BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("CopilotSession._eventChannel was not found."); + + private static IDictionary GetSessionsMap(CopilotClient client) => + (IDictionary)SessionsField.GetValue(client)!; + + private static bool IsEventChannelClosed(CopilotSession session) + { + var channel = EventChannelField.GetValue(session)!; + var readerProperty = channel.GetType().GetProperty("Reader") + ?? throw new InvalidOperationException("Channel.Reader was not found."); + var reader = readerProperty.GetValue(channel)!; + var completionProperty = reader.GetType().GetProperty("Completion") + ?? throw new InvalidOperationException("ChannelReader.Completion was not found."); + var completion = (Task)completionProperty.GetValue(reader)!; + return completion.IsCompleted; + } + + /// + /// Continuously scans 's session dictionary on a dedicated, + /// tightly-spinning thread (not the thread pool, and no await-based yielding) so + /// that even a sub-millisecond in-flight registration window — as seen with a fast local + /// RPC failure like a nonexistent-session resume — is reliably observed. + /// + private sealed class SessionSniffer : IDisposable + { + private readonly List _seen = []; + private readonly Thread _thread; + private volatile bool _stop; + + public SessionSniffer(IDictionary sessions) + { + _thread = new Thread(() => + { + var iterations = 0L; + while (!_stop) + { + iterations++; + foreach (CopilotSession s in sessions.Values) + { + lock (_seen) + { + if (!_seen.Contains(s)) _seen.Add(s); + } + } + } + Iterations = iterations; + }) + { IsBackground = true }; + _thread.Start(); + } + + public long Iterations { get; private set; } + + public IReadOnlyList Stop() + { + _stop = true; + _thread.Join(); + lock (_seen) return [.. _seen]; + } + + public void Dispose() => _stop = true; + } + + [Fact] + public async Task CreateSessionAsync_Failure_Does_Not_Leak_The_Session_Or_Its_Event_Loop() + { + // An invalid per-session GitHub token, redirected at the replaying proxy, makes + // the real CLI reject session.create with a genuine RPC error (401 Unauthorized) + // — the same failure path a real user would hit, not a mocked transport. + var env = new Dictionary(Ctx.GetEnvironment()) + { + ["COPILOT_DEBUG_GITHUB_API_URL"] = Ctx.ProxyUrl, + }; + var client = Ctx.CreateClient(environment: env, autoInjectGitHubToken: false); + + async Task CreateFailingAsync() + { + var ex = await Assert.ThrowsAnyAsync(() => Ctx.CreateSessionAsync(client, new SessionConfig + { + GitHubToken = "invalid-token", + OnPermissionRequest = PermissionHandler.ApproveAll, + })); + Assert.Contains("401", ex.ToString(), StringComparison.OrdinalIgnoreCase); + } + + // Warm up: the first call establishes the CLI connection. + await CreateFailingAsync(); + + var sessions = GetSessionsMap(client); + + // The session is registered (and its event-loop consumer started) before the RPC + // completes, and is only ever removed inside CreateSessionAsync's own catch block — + // by the time a failed call *returns* to us, RemoveFromClient() has already run, so + // polling the dictionary after each await observes nothing. We must instead observe + // it concurrently, while each call is still in flight, to capture the real session + // object and verify its event channel actually got closed. + using var sniffer = new SessionSniffer(sessions); + + for (var i = 0; i < 20; i++) + { + await CreateFailingAsync(); + } + + var seen = sniffer.Stop(); + + Assert.True(seen.Count > 0, "Test did not observe any in-flight session registrations; cannot validate the fix."); + Assert.Empty(sessions); + + foreach (var s in seen) + { + Assert.True(IsEventChannelClosed(s), "A failed CreateSessionAsync's session had its event channel left open, leaking its background event-processing task."); + } + } + + [Fact] + public async Task ResumeSessionAsync_Failure_Does_Not_Leak_The_Session_Or_Its_Event_Loop() + { + // Use our own dedicated client rather than the shared fixture's ResumeSessionAsync + // helper: that helper spins up a brand-new CopilotClient for every call it makes + // (to support the multi-client resume scenarios it's designed for), so each call's + // pre-registered session would land in a different, short-lived client's dictionary + // that we'd never get to observe. A single, reused client lets us watch one + // dictionary across all 20 failed calls. + var client = Ctx.CreateClient(); + var sessions = GetSessionsMap(client); + + async Task ResumeNonExistentAsync() + { + await Assert.ThrowsAnyAsync(() => + Ctx.ResumeSessionAsync(client, "non-existent-leak-check-session", new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + })); + } + + // Warm up: the first call establishes the CLI connection. + await ResumeNonExistentAsync(); + + // Same rationale as the CreateSessionAsync test above: the pre-registered session is + // already removed from the dictionary by the time a failed call returns, so we must + // observe it concurrently, while the call is still in flight, to actually validate + // that its event channel got closed rather than just that it got unregistered. + using var sniffer = new SessionSniffer(sessions); + + var baseline = sessions.Count; + for (var i = 0; i < 20; i++) + { + await ResumeNonExistentAsync(); + } + + var seen = sniffer.Stop(); + + Assert.True(seen.Count > 0, $"Test did not observe any in-flight session registrations (sniffer ran {sniffer.Iterations} iterations); cannot validate the fix."); + + Assert.True( + sessions.Count == baseline, + $"Expected no sessions left registered after 20 failed ResumeSessionAsync calls (baseline={baseline}), " + + $"but found {sessions.Count}. Failed ResumeSessionAsync calls must not leak the local session registration."); + + foreach (var s in seen) + { + Assert.True(IsEventChannelClosed(s), "A failed ResumeSessionAsync's session had its event channel left open, leaking its background event-processing task."); + } + } +} diff --git a/dotnet/test/E2E/ToolsE2ETests.cs b/dotnet/test/E2E/ToolsE2ETests.cs index ea615fbc4a..8a786f3927 100644 --- a/dotnet/test/E2E/ToolsE2ETests.cs +++ b/dotnet/test/E2E/ToolsE2ETests.cs @@ -7,7 +7,11 @@ using Microsoft.Extensions.AI; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Net; +using System.Net.Http; +using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using Xunit; using Xunit.Abstractions; @@ -16,6 +20,9 @@ namespace GitHub.Copilot.Test.E2E; public partial class ToolsE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "tools", output) { + private const string ApplyPatchInput = "*** Begin Patch\n*** End Patch"; + private const string ApplyPatchResult = "patched by the host"; + [Fact] public async Task Invokes_Built_In_Tools() { @@ -234,6 +241,91 @@ static string CustomGrep([Description("Search query")] string query) => $"CUSTOM_GREP_RESULT: {query}"; } + [Theory] + [InlineData("string")] + [InlineData("object")] + [InlineData("JsonElement")] + [InlineData("JsonNode")] + [Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)] + public async Task ApplyPatch_Override_Receives_Freeform_Input_Shapes(string parameterType) + { + foreach (var useCustomToolCall in new[] { true, false }) + { + object? receivedInput = null; + var invocationCount = 0; + var handler = new ApplyPatchOverrideRequestHandler(useCustomToolCall); + await using var client = Ctx.CreateClient(options: new CopilotClientOptions + { + Connection = RuntimeConnection.ForStdio(), + RequestHandler = handler, + }); + await client.StartAsync(); + + string CaptureInput(object input) + { + receivedInput = input; + invocationCount++; + return ApplyPatchResult; + } + + Delegate applyPatch = parameterType switch + { + "string" => (string input) => CaptureInput(input), + "object" => (object input) => CaptureInput(input), + "JsonElement" => (JsonElement input) => CaptureInput(input), + "JsonNode" => (JsonNode input) => CaptureInput(input), + _ => throw new ArgumentOutOfRangeException(nameof(parameterType)), + }; + var tool = CopilotTool.DefineTool( + applyPatch, + new CopilotToolOptions + { + OverridesBuiltInTool = true, + SkipPermission = true, + }, + new AIFunctionFactoryOptions + { + Name = "apply_patch", + Description = "Host-implemented apply_patch", + }); + + await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig + { + Model = "gpt-4o-mini", + Provider = new ProviderConfig + { + Type = "openai", + WireApi = "completions", + BaseUrl = "https://apply-patch.invalid/v1", + ApiKey = "test-key", + ModelId = "gpt-4o-mini", + WireModel = "gpt-4o-mini", + }, + Streaming = true, + Tools = [tool], + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + var message = await session.SendAndWaitAsync(new MessageOptions { Prompt = "Use apply_patch" }); + + var input = receivedInput switch + { + string value => value, + JsonElement value => value.GetString(), + JsonNode value => value.GetValue(), + _ => null, + }; + Assert.Equal(ApplyPatchInput, input); + Assert.Equal(1, invocationCount); + Assert.Equal("override complete", message?.Data.Content); + + var requests = handler.InferenceRequests; + Assert.Equal(2, requests.Count); + AssertApplyPatchOverrideAdvertised(requests[0], parameterType); + AssertApplyPatchResultReachedModel(requests[1]); + } + } + [Fact] public async Task SkipPermission_Sent_In_Tool_Definition() { @@ -448,4 +540,114 @@ string ExcludedTool([Description("Input value")] string input) return $"EXCLUDED_{input.ToUpperInvariant()}"; } } + + private static void AssertApplyPatchOverrideAdvertised(string requestBody, string parameterType) + { + using var request = JsonDocument.Parse(requestBody); + var applyPatchTools = request.RootElement.GetProperty("tools") + .EnumerateArray() + .Where(tool => + tool.TryGetProperty("function", out var function) + && function.TryGetProperty("name", out var functionName) + && functionName.GetString() == "apply_patch" + || tool.TryGetProperty("custom", out var custom) + && custom.TryGetProperty("name", out var customName) + && customName.GetString() == "apply_patch") + .ToArray(); + + var applyPatch = Assert.Single(applyPatchTools); + Assert.Equal("function", applyPatch.GetProperty("type").GetString()); + var definition = applyPatch.GetProperty("function"); + Assert.Equal("apply_patch", definition.GetProperty("name").GetString()); + + var parameters = definition.GetProperty("parameters"); + Assert.Equal("object", parameters.GetProperty("type").GetString()); + var inputSchema = parameters.GetProperty("properties").GetProperty("input"); + if (parameterType == "string") + { + Assert.Equal("string", inputSchema.GetProperty("type").GetString()); + } + else + { + Assert.Equal(JsonValueKind.True, inputSchema.ValueKind); + } + Assert.Contains(parameters.GetProperty("required").EnumerateArray(), item => item.GetString() == "input"); + } + + private static void AssertApplyPatchResultReachedModel(string requestBody) + { + using var request = JsonDocument.Parse(requestBody); + var toolResult = Assert.Single( + request.RootElement.GetProperty("messages").EnumerateArray(), + message => + message.GetProperty("role").GetString() == "tool" + && message.GetProperty("tool_call_id").GetString() == "call-1"); + Assert.Equal(ApplyPatchResult, toolResult.GetProperty("content").GetString()); + } + + private sealed class ApplyPatchOverrideRequestHandler(bool useCustomToolCall) : CopilotRequestHandler + { + private const string CustomToolCallResponse = + "data: {\"id\":\"chatcmpl-tool\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-4o-mini\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"call-1\",\"type\":\"custom\",\"custom\":{\"name\":\"apply_patch\",\"input\":\"*** Begin Patch\\n*** End Patch\"}}]},\"finish_reason\":\"tool_calls\"}]}\n\n" + + "data: [DONE]\n\n"; + + private const string FunctionToolCallResponse = + "data: {\"id\":\"chatcmpl-tool\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-4o-mini\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"tool_calls\":[{\"index\":0,\"id\":\"call-1\",\"type\":\"function\",\"function\":{\"name\":\"apply_patch\",\"arguments\":\"{\\\"input\\\":\\\"*** Begin Patch\\\\n*** End Patch\\\"}\"}}]},\"finish_reason\":\"tool_calls\"}]}\n\n" + + "data: [DONE]\n\n"; + + private const string FinalResponse = + "data: {\"id\":\"chatcmpl-final\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-4o-mini\",\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":\"override complete\"},\"finish_reason\":null}]}\n\n" + + "data: {\"id\":\"chatcmpl-final\",\"object\":\"chat.completion.chunk\",\"created\":1,\"model\":\"gpt-4o-mini\",\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n" + + "data: [DONE]\n\n"; + + private readonly object _lock = new(); + private readonly List _inferenceRequests = []; + + internal IReadOnlyList InferenceRequests + { + get + { + lock (_lock) + { + return [.. _inferenceRequests]; + } + } + } + + protected override async Task SendRequestAsync(HttpRequestMessage request, CopilotRequestContext ctx) + { + var url = request.RequestUri!.ToString(); + if (!RecordingRequestHandler.IsInferenceUrl(url)) + { + return RecordingRequestHandler.BuildNonInferenceResponse(url); + } + + var requestBody = request.Content is null + ? string.Empty +#if NET8_0_OR_GREATER + : await request.Content.ReadAsStringAsync(ctx.CancellationToken).ConfigureAwait(false); +#else + : await request.Content.ReadAsStringAsync().ConfigureAwait(false); +#endif + + int requestNumber; + lock (_lock) + { + _inferenceRequests.Add(requestBody); + requestNumber = _inferenceRequests.Count; + } + + return requestNumber switch + { + 1 => Sse(useCustomToolCall ? CustomToolCallResponse : FunctionToolCallResponse), + 2 => Sse(FinalResponse), + _ => throw new InvalidOperationException($"Unexpected inference request #{requestNumber}."), + }; + } + + private static HttpResponseMessage Sse(string body) => new(HttpStatusCode.OK) + { + Content = new StringContent(body, Encoding.UTF8, "text/event-stream"), + }; + } } diff --git a/dotnet/test/Harness/E2ETestBackend.cs b/dotnet/test/Harness/E2ETestBackend.cs index 04808ed7a9..71c8f85761 100644 --- a/dotnet/test/Harness/E2ETestBackend.cs +++ b/dotnet/test/Harness/E2ETestBackend.cs @@ -15,7 +15,7 @@ internal enum E2ETestBackend internal static class E2ETestBackendConfiguration { internal const string EnvironmentVariable = "COPILOT_SDK_E2E_BACKEND"; - private const string AnthropicDefaultModel = "claude-sonnet-4.5"; + private const string AnthropicDefaultModel = "claude-sonnet-5"; private const string OpenAIDefaultModel = "gpt-4.1"; private const string FakeCredential = "fake-byok-credential-for-e2e-tests"; diff --git a/dotnet/test/Harness/E2ETestBase.cs b/dotnet/test/Harness/E2ETestBase.cs index 3eb0f0e97a..f664812d58 100644 --- a/dotnet/test/Harness/E2ETestBase.cs +++ b/dotnet/test/Harness/E2ETestBase.cs @@ -113,7 +113,7 @@ protected static async Task SuspendAndUntrackSessionForResumeAsync(CopilotSessio { await session.Rpc.SuspendAsync(); - // In-process clients host separate runtimes, while session.destroy removes the + // In-process clients host separate runtimes, while session.detach releases the // session from the current runtime. Untrack locally to exercise resume without // either replacing an active wrapper or destroying the session first. var removeFromClient = typeof(CopilotSession).GetMethod( diff --git a/dotnet/test/Harness/E2ETestContext.cs b/dotnet/test/Harness/E2ETestContext.cs index 0080cbc609..8d474bfe7e 100644 --- a/dotnet/test/Harness/E2ETestContext.cs +++ b/dotnet/test/Harness/E2ETestContext.cs @@ -3,9 +3,9 @@ *--------------------------------------------------------------------------------------------*/ using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Text.RegularExpressions; namespace GitHub.Copilot.Test.Harness; @@ -14,6 +14,7 @@ public sealed class E2ETestContext : IAsyncDisposable { private const string DefaultGitHubToken = "fake-token-for-e2e-tests"; private static readonly TimeSpan s_gracefulClientStopTimeout = TimeSpan.FromSeconds(30); + private static readonly ConcurrentDictionary> s_preparedCliPaths = new(StringComparer.Ordinal); public string HomeDir { get; } public string WorkDir { get; } @@ -25,6 +26,8 @@ public sealed class E2ETestContext : IAsyncDisposable private readonly ReplayProxy _proxy; private readonly string _repoRoot; + private readonly Lazy _cliPath; + private readonly Lazy _legacyCliPath; private readonly object _clientsLock = new(); private readonly List _persistentClients = []; private readonly List _transientClients = []; @@ -36,6 +39,8 @@ private E2ETestContext(string homeDir, string workDir, string proxyUrl, ReplayPr ProxyUrl = proxyUrl; _proxy = proxy; _repoRoot = repoRoot; + _cliPath = GetCachedCliPath(repoRoot, "--print-path"); + _legacyCliPath = GetCachedCliPath(repoRoot, "--print-legacy-path"); } public static async Task CreateAsync() @@ -144,46 +149,54 @@ private static string FindRepoRoot() throw new InvalidOperationException("Could not find repository root"); } - private static string GetCliPath(string repoRoot) + private string GetCliPath() + => _cliPath.Value; + + public string GetLegacyCliPath() + => _legacyCliPath.Value; + + private static Lazy GetCachedCliPath(string repoRoot, string option) + { + var envPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); + var cacheKey = $"{repoRoot}\0{option}\0{envPath}"; + return s_preparedCliPaths.GetOrAdd( + cacheKey, + _ => new Lazy( + () => PrepareCliPath(repoRoot, option), + LazyThreadSafetyMode.ExecutionAndPublication)); + } + + private static string PrepareCliPath(string repoRoot, string option) { var envPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); if (!string.IsNullOrEmpty(envPath)) return envPath; - // As of CLI 1.0.64-1 the @github/copilot package is a thin loader; the - // runnable index.js ships in the installed platform package. - var githubModules = Path.Join(repoRoot, "nodejs", "node_modules", "@github"); - var packagePrefix = GetCliPackagePrefix(); - var candidates = Directory.Exists(githubModules) - ? Directory.EnumerateDirectories(githubModules, $"{packagePrefix}-*", SearchOption.TopDirectoryOnly) - .Select(directory => Path.Join(directory, "index.js")) - .Where(File.Exists) - .ToArray() - : []; - - return candidates.Length switch + var startInfo = new ProcessStartInfo { - 1 => candidates[0], - 0 => throw new InvalidOperationException( - $"CLI package matching '{packagePrefix}-*' not found under {githubModules}. " + - "Run 'npm install' in the nodejs directory first."), - _ => throw new InvalidOperationException( - $"Multiple CLI packages matching '{packagePrefix}-*' found under {githubModules}: " + - string.Join(", ", candidates.Select(Path.GetDirectoryName))), + FileName = "node", + WorkingDirectory = Path.Join(repoRoot, "nodejs"), + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + Arguments = $"node_modules/tsx/dist/cli.mjs scripts/prepare-runtime.ts {option}", }; - } - private static string GetCliPackagePrefix() - { - var platform = OperatingSystem.IsWindows() - ? "win32" - : OperatingSystem.IsMacOS() - ? "darwin" - : OperatingSystem.IsLinux() - ? RuntimeInformation.RuntimeIdentifier.StartsWith("linux-musl-", StringComparison.Ordinal) - ? "linuxmusl" - : "linux" - : throw new PlatformNotSupportedException("Unsupported operating system for Copilot CLI E2E tests."); - return $"copilot-{platform}"; + using var process = Process.Start(startInfo) + ?? throw new InvalidOperationException("Failed to start Node.js runtime preparation."); + var stdout = process.StandardOutput.ReadToEndAsync(); + var stderr = process.StandardError.ReadToEndAsync(); + process.WaitForExit(); + var output = stdout.GetAwaiter().GetResult(); + var lines = output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var cliPath = lines.Length == 0 ? string.Empty : lines[^1].Trim(); + var error = stderr.GetAwaiter().GetResult().Trim(); + if (process.ExitCode != 0 || string.IsNullOrEmpty(cliPath)) + throw new InvalidOperationException( + $"Failed to prepare the pinned Copilot CLI: {error}"); + if (!File.Exists(cliPath)) + throw new InvalidOperationException( + $"Pinned Copilot CLI was not created at {cliPath}."); + return cliPath; } public async Task ConfigureForTestAsync(string testFile, [CallerMemberName] string? testName = null) @@ -310,21 +323,20 @@ public CopilotClient CreateClient( // CopilotClient honors COPILOT_SDK_DEFAULT_CONNECTION (stdio by default, // or in-process); the CI matrix uses this to run the suite under both. // Tests that need a specific transport set options.Connection directly. - var cliPath = GetCliPath(_repoRoot); switch (options.Connection) { case null when !IsInProcess(null): // No explicit connection and not the in-process default: the // default resolves to stdio, so materialize it here so the // environment can be attached to the connection below. - options.Connection = RuntimeConnection.ForStdio(path: cliPath); + options.Connection = RuntimeConnection.ForStdio(path: GetCliPath()); break; case null: // In-process default: leave Connection unset so CopilotClient's // ResolveDefaultConnection honors COPILOT_SDK_DEFAULT_CONNECTION. break; case ChildProcessRuntimeConnection child when child.Path is null: - child.Path = cliPath; + child.Path = GetCliPath(); break; } @@ -583,7 +595,7 @@ private static async Task StopClientForCleanupAsync(CopilotClient client) $"Graceful in-process client cleanup exceeded {s_gracefulClientStopTimeout}; forcing shutdown."); await client.ForceStopAsync(); - // Disposing the connection completes any session.destroy RPC that + // Disposing the connection completes any session.detach RPC that // blocked graceful cleanup. Observe that task before continuing. await gracefulStop.WaitAsync(s_gracefulClientStopTimeout); } diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index bb0042efdb..04ef6e6405 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -12,6 +12,7 @@ using System.Text; using System.Text.Json; using GitHub.Copilot.Rpc; +using Microsoft.Extensions.AI; using Xunit; namespace GitHub.Copilot.Test.Unit; @@ -464,6 +465,199 @@ public async Task CreateSessionAsync_Omits_CustomAgent_ReasoningEffort_When_Unse Assert.False(agent.TryGetProperty("reasoningEffort", out _)); } + public static TheoryData CapiAutoTiers => new() + { + { AutoTier.Efficiency, "efficiency", null }, + { AutoTier.Balance, "balance", null }, + { AutoTier.Intelligence, "intelligence", null }, + { AutoTier.Efficiency, "efficiency", false }, + { AutoTier.Balance, "balance", false }, + { AutoTier.Intelligence, "intelligence", false }, + }; + + [Theory] + [MemberData(nameof(CapiAutoTiers))] + public async Task SessionRequests_Serialize_CapiAutoTier(AutoTier tier, string expectedTier, bool? enableWebSocketResponses) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var capi = new CapiSessionOptions { AutoTier = tier, EnableWebSocketResponses = enableWebSocketResponses }; + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + Model = "auto", + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + await using var resumed = await client.ResumeSessionAsync("resume-with-auto-tier", new ResumeSessionConfig + { + Model = "auto", + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + foreach (var method in new[] { "session.create", "session.resume" }) + { + var request = Assert.Single(server.Requests, request => request.Method == method); + var serializedCapi = request.Params.GetProperty("capi"); + Assert.Equal(expectedTier, serializedCapi.GetProperty("autoTier").GetString()); + if (enableWebSocketResponses.HasValue) + { + Assert.Equal(enableWebSocketResponses.Value, serializedCapi.GetProperty("enableWebSocketResponses").GetBoolean()); + } + else + { + Assert.False(serializedCapi.TryGetProperty("enableWebSocketResponses", out _)); + } + } + } + + [Theory] + [InlineData("efficiency")] + [InlineData("balance")] + [InlineData("intelligence")] + public async Task SetModelAsync_Serializes_AutoTier(string expectedTier) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await session.SetModelAsync("auto", new SetModelOptions { AutoTier = new AutoTier(expectedTier) }); + + var request = Assert.Single(server.Requests, request => request.Method == "session.model.switchTo"); + Assert.Equal("auto", request.Params.GetProperty("modelId").GetString()); + Assert.Equal(expectedTier, request.Params.GetProperty("autoTier").GetString()); + } + + [Fact] + public async Task SetModelAsync_Omits_AutoTier_WhenUnset() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await session.SetModelAsync("gpt-5.4"); + + var request = Assert.Single(server.Requests, request => request.Method == "session.model.switchTo"); + Assert.False(request.Params.TryGetProperty("autoTier", out _)); + } + + [Fact] + public async Task SetModelAsync_Writes_Null_AutoTier_WhenCleared() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await session.SetModelAsync("auto", new SetModelOptions { ResetAutoTier = true }); + + // An explicit null must survive to the wire. Omitting it would mean "leave the + // preference alone" rather than "use provider-default routing". + var request = Assert.Single(server.Requests, request => request.Method == "session.model.switchTo"); + Assert.True(request.Params.TryGetProperty("autoTier", out var autoTier)); + Assert.Equal(JsonValueKind.Null, autoTier.ValueKind); + } + + [Fact] + public async Task SetModelAsync_Rejects_Conflicting_AutoTier_Options() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await Assert.ThrowsAsync(() => session.SetModelAsync( + "auto", + new SetModelOptions { AutoTier = AutoTier.Balance, ResetAutoTier = true })); + } + + [Fact] + public async Task SetAutoTierAsync_Serializes_Tier_And_Returns_Snapshot() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + var result = await session.SetAutoTierAsync(AutoTier.Intelligence); + + var request = Assert.Single(server.Requests, request => request.Method == "session.model.switchAutoTier"); + Assert.Equal("intelligence", request.Params.GetProperty("autoTier").GetString()); + Assert.Equal(ModelSwitchAutoTierStatus.Pending, result.Status); + Assert.Equal(AutoTier.Balance, result.EffectiveAutoTier); + } + + [Fact] + public async Task SetAutoTierAsync_Writes_Null_Tier_ForDefaultRouting() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await session.SetAutoTierAsync(null); + + var request = Assert.Single(server.Requests, request => request.Method == "session.model.switchAutoTier"); + Assert.True(request.Params.TryGetProperty("autoTier", out var autoTier)); + Assert.Equal(JsonValueKind.Null, autoTier.ValueKind); + } + + [Theory] + [InlineData(false, null)] + [InlineData(true, null)] + [InlineData(true, false)] + public async Task SessionRequests_Omit_CapiAutoTier_WhenUnset(bool includeCapi, bool? enableWebSocketResponses) + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var capi = includeCapi ? new CapiSessionOptions { EnableWebSocketResponses = enableWebSocketResponses } : null; + + await using var created = await client.CreateSessionAsync(new SessionConfig + { + Model = "auto", + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + await using var resumed = await client.ResumeSessionAsync("resume-without-auto-tier", new ResumeSessionConfig + { + Capi = capi, + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + foreach (var method in new[] { "session.create", "session.resume" }) + { + var request = Assert.Single(server.Requests, request => request.Method == method); + Assert.Equal(includeCapi, request.Params.TryGetProperty("capi", out var serializedCapi)); + if (includeCapi) + { + Assert.False(serializedCapi.TryGetProperty("autoTier", out _)); + if (enableWebSocketResponses.HasValue) + { + Assert.Equal(enableWebSocketResponses.Value, serializedCapi.GetProperty("enableWebSocketResponses").GetBoolean()); + } + else + { + Assert.Empty(serializedCapi.EnumerateObject()); + } + } + } + } + [Fact] public async Task CreateSessionAsync_Forwards_AskUserVariant() { @@ -577,6 +771,166 @@ public async Task SessionRequests_Serialize_Terminal_Tools() Assert.True(resumeRequest.Params.GetProperty("tools")[0].GetProperty("isTerminal").GetBoolean()); } + [Fact] + public async Task ExternalTool_String_Arguments_Bind_To_Single_Function_Parameter() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + string? receivedPatch = null; + var tool = CopilotTool.DefineTool( + (string patch) => + { + receivedPatch = patch; + return "applied"; + }, + new CopilotToolOptions { OverridesBuiltInTool = true }, + new AIFunctionFactoryOptions { Name = "apply_patch" }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [tool], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + server.ClearRequests(); + using var arguments = JsonDocument.Parse("\"*** Begin Patch\\n*** End Patch\""); + + DispatchEvent(session, new ExternalToolRequestedEvent + { + Data = new ExternalToolRequestedData + { + Arguments = arguments.RootElement.Clone(), + RequestId = "apply-patch-request", + SessionId = session.SessionId, + ToolCallId = "apply-patch-call", + ToolName = "apply_patch" + } + }); + + var request = await WaitForRequestAsync(server, "session.tools.handlePendingToolCall"); + Assert.Equal("*** Begin Patch\n*** End Patch", receivedPatch); + Assert.False(request.Params.TryGetProperty("error", out _)); + Assert.Equal("applied", request.Params.GetProperty("result").GetProperty("textResultForLlm").GetString()); + } + + [Fact] + public async Task ExternalTool_String_Arguments_Reject_Ambiguous_Function_Parameters() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var invoked = false; + var tool = CopilotTool.DefineTool( + (string patch, string explanation) => + { + invoked = true; + return "applied"; + }, + new CopilotToolOptions { OverridesBuiltInTool = true }, + new AIFunctionFactoryOptions { Name = "apply_patch" }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [tool], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + server.ClearRequests(); + using var arguments = JsonDocument.Parse("\"*** Begin Patch\\n*** End Patch\""); + + DispatchEvent(session, new ExternalToolRequestedEvent + { + Data = new ExternalToolRequestedData + { + Arguments = arguments.RootElement.Clone(), + RequestId = "ambiguous-apply-patch-request", + SessionId = session.SessionId, + ToolCallId = "ambiguous-apply-patch-call", + ToolName = "apply_patch" + } + }); + + var request = await WaitForRequestAsync(server, "session.tools.handlePendingToolCall"); + Assert.False(invoked); + Assert.Contains("received non-object arguments", request.Params.GetProperty("error").GetString()); + Assert.False(request.Params.TryGetProperty("result", out _)); + } + + [Fact] + public async Task ExternalTool_Number_Arguments_Bind_To_Single_Function_Parameter() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + int? receivedLine = null; + var tool = CopilotTool.DefineTool( + (int line) => + { + receivedLine = line; + return "selected"; + }, + factoryOptions: new AIFunctionFactoryOptions { Name = "select_line" }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [tool], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + server.ClearRequests(); + using var arguments = JsonDocument.Parse("42"); + + DispatchEvent(session, new ExternalToolRequestedEvent + { + Data = new ExternalToolRequestedData + { + Arguments = arguments.RootElement.Clone(), + RequestId = "select-line-request", + SessionId = session.SessionId, + ToolCallId = "select-line-call", + ToolName = "select_line" + } + }); + + var request = await WaitForRequestAsync(server, "session.tools.handlePendingToolCall"); + Assert.Equal(42, receivedLine); + Assert.False(request.Params.TryGetProperty("error", out _)); + } + + [Fact] + public async Task ExternalTool_String_Arguments_Bind_To_Sole_Required_Function_Parameter() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + string? receivedPatch = null; + string? receivedExplanation = null; + var tool = CopilotTool.DefineTool( + (string patch, string? explanation = null) => + { + receivedPatch = patch; + receivedExplanation = explanation; + return "applied"; + }, + new CopilotToolOptions { OverridesBuiltInTool = true }, + new AIFunctionFactoryOptions { Name = "apply_patch" }); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [tool], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + server.ClearRequests(); + using var arguments = JsonDocument.Parse("\"*** Begin Patch\\n*** End Patch\""); + + DispatchEvent(session, new ExternalToolRequestedEvent + { + Data = new ExternalToolRequestedData + { + Arguments = arguments.RootElement.Clone(), + RequestId = "optional-apply-patch-request", + SessionId = session.SessionId, + ToolCallId = "optional-apply-patch-call", + ToolName = "apply_patch" + } + }); + + var request = await WaitForRequestAsync(server, "session.tools.handlePendingToolCall"); + Assert.Equal("*** Begin Patch\n*** End Patch", receivedPatch); + Assert.Null(receivedExplanation); + Assert.False(request.Params.TryGetProperty("error", out _)); + } + [Fact] public async Task EmptyMode_Create_Sends_Empty_IncludedBuiltinSkills() { @@ -862,6 +1216,252 @@ public async Task McpAuth_Handler_Exception_Cancels_Pending_Request() Assert.Equal("cancelled", request.Params.GetProperty("result").GetProperty("kind").GetString()); } + [Fact] + public async Task ExternalToolCompleted_Cancels_Blocked_Tool_When_Cancellation_Callback_Throws() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var toolStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var toolCancelled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [AIFunctionFactory.Create(BlockedTool, "blocked_tool")], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + DispatchEvent(session, ExternalToolRequested("request-1")); + await toolStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + DispatchEvent(session, new ExternalToolCompletedEvent + { + Data = new ExternalToolCompletedData { RequestId = "request-1" } + }); + + await toolCancelled.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await Task.Delay(50); + Assert.DoesNotContain(server.Requests, + request => request.Method == "session.tools.handlePendingToolCall" + && request.Params.GetProperty("requestId").GetString() == "request-1"); + + async Task BlockedTool(CancellationToken cancellationToken) + { + toolStarted.TrySetResult(); + using var registration = cancellationToken.Register( + () => throw new InvalidOperationException("cancellation callback failed")); + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + return "unreachable"; + } + catch (OperationCanceledException) + { + toolCancelled.TrySetResult(); + throw; + } + } + } + + [Fact] + public async Task ExternalToolCompleted_Does_Not_Block_Event_Dispatch_On_Cancellation_Callback() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var toolStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var callbackStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseCallback = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var toolCancelled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + await using var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [AIFunctionFactory.Create(BlockedTool, "blocked_tool")], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + DispatchEvent(session, ExternalToolRequested("request-blocking-callback")); + await toolStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + var dispatchTask = Task.Run(() => DispatchEvent(session, new ExternalToolCompletedEvent + { + Data = new ExternalToolCompletedData { RequestId = "request-blocking-callback" } + })); + await callbackStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); + try + { + await dispatchTask.WaitAsync(TimeSpan.FromSeconds(5)); + } + finally + { + releaseCallback.TrySetResult(); + } + await toolCancelled.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + async Task BlockedTool(CancellationToken cancellationToken) + { + toolStarted.TrySetResult(); + using var registration = cancellationToken.Register(() => + { + callbackStarted.TrySetResult(); + releaseCallback.Task.GetAwaiter().GetResult(); + }); + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + return "unreachable"; + } + catch (OperationCanceledException) + { + toolCancelled.TrySetResult(); + throw; + } + } + } + + [Fact] + public async Task ForceStopAsync_Cancels_Blocked_Tool_When_Cancellation_Callback_Throws() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var toolStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var toolCancelled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [AIFunctionFactory.Create(BlockedTool, "blocked_tool")], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + DispatchEvent(session, ExternalToolRequested("request-force-stop")); + await toolStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + await client.ForceStopAsync(); + + await toolCancelled.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + async Task BlockedTool(CancellationToken cancellationToken) + { + toolStarted.TrySetResult(); + using var registration = cancellationToken.Register( + () => throw new InvalidOperationException("cancellation callback failed")); + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + return "unreachable"; + } + catch (OperationCanceledException) + { + toolCancelled.TrySetResult(); + throw; + } + } + } + + [Fact] + public async Task ForceStopAsync_Does_Not_Start_Late_External_Tool() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var toolStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [AIFunctionFactory.Create(Tool, "late_tool")], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + await client.ForceStopAsync(); + DispatchEvent(session, ExternalToolRequested("request-after-force-stop", "late_tool")); + + Assert.False(toolStarted.Task.IsCompleted); + + string Tool() + { + toolStarted.TrySetResult(); + return "unexpected"; + } + } + + [Fact] + public async Task ConnectionClose_Cancels_Blocked_Tool_Delegate() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var toolStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var toolCancelled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [AIFunctionFactory.Create(BlockedTool, "blocked_tool")], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + DispatchEvent(session, ExternalToolRequested("request-connection-close")); + await toolStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + server.CloseConnection(); + + await toolCancelled.Task.WaitAsync(TimeSpan.FromSeconds(5)); + await client.ForceStopAsync(); + + async Task BlockedTool(CancellationToken cancellationToken) + { + toolStarted.TrySetResult(); + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + return "unreachable"; + } + catch (OperationCanceledException) + { + toolCancelled.TrySetResult(); + throw; + } + } + } + + [Fact] + public async Task DisposeAsync_Cancels_Blocked_Tool_Delegate() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var toolStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var toolCancelled = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var session = await client.CreateSessionAsync(new SessionConfig + { + Tools = [AIFunctionFactory.Create(BlockedTool, "blocked_tool")], + OnPermissionRequest = PermissionHandler.ApproveAll + }); + + DispatchEvent(session, ExternalToolRequested("request-2")); + await toolStarted.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + await session.DisposeAsync(); + + await toolCancelled.Task.WaitAsync(TimeSpan.FromSeconds(5)); + + async Task BlockedTool(CancellationToken cancellationToken) + { + toolStarted.TrySetResult(); + try + { + await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); + return "unreachable"; + } + catch (OperationCanceledException) + { + toolCancelled.TrySetResult(); + throw; + } + } + } + + private static ExternalToolRequestedEvent ExternalToolRequested(string requestId, string toolName = "blocked_tool") => + new() + { + Data = new ExternalToolRequestedData + { + RequestId = requestId, + SessionId = "session-1", + ToolCallId = "tool-call-1", + ToolName = toolName + } + }; + [Fact] public async Task Generated_Session_Rpc_Throws_When_Session_Disposed() { @@ -1426,6 +2026,11 @@ public void FailSessionCreate() _failSessionCreate = true; } + public void CloseConnection() + { + _stream?.Dispose(); + } + public async Task SendRequestAsync(string method, Dictionary parameters) { var stream = _stream ?? throw new InvalidOperationException("Client is not connected."); @@ -1579,11 +2184,24 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel { ["success"] = true }, + "session.tools.handlePendingToolCall" => new Dictionary + { + ["success"] = true + }, + "session.model.switchTo" => new Dictionary + { + ["modelId"] = "auto" + }, + "session.model.switchAutoTier" => new Dictionary + { + ["status"] = "pending", + ["effectiveAutoTier"] = "balance" + }, "session.delete" => new Dictionary { ["success"] = true }, - "session.destroy" => await DestroySessionAsync(cancellationToken), + "session.detach" => await DetachSessionAsync(cancellationToken), "runtime.shutdown" => HandleRuntimeShutdown(), _ => throw new InvalidOperationException($"Unexpected RPC method '{method}'.") }; @@ -1620,7 +2238,7 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel }; } - private async Task> DestroySessionAsync(CancellationToken cancellationToken) + private async Task> DetachSessionAsync(CancellationToken cancellationToken) { if (_delayDestroy) { @@ -1628,7 +2246,7 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel await _allowDestroy.Task.WaitAsync(cancellationToken); } - return []; + return new Dictionary { ["success"] = true }; } private Dictionary HandleRuntimeShutdown() diff --git a/dotnet/test/Unit/CloneTests.cs b/dotnet/test/Unit/CloneTests.cs index 2948ac2129..2f213525f6 100644 --- a/dotnet/test/Unit/CloneTests.cs +++ b/dotnet/test/Unit/CloneTests.cs @@ -23,6 +23,13 @@ public void CopilotClientOptions_Clone_CopiesAllProperties() BuiltinPluginDirectories = ["/plugins/core", "/plugins/github"], EnableRemoteSessions = true, SessionIdleTimeoutSeconds = 600, + ClientInfo = new CopilotClientInfo + { + ApplicationName = "example-app", + ApplicationVersion = "1.0.0", + IntegrationName = "example-integration", + IntegrationVersion = "2.0.0", + }, }; var clone = original.Clone(); @@ -38,6 +45,7 @@ public void CopilotClientOptions_Clone_CopiesAllProperties() Assert.NotSame(original.BuiltinPluginDirectories, clone.BuiltinPluginDirectories); Assert.Equal(original.EnableRemoteSessions, clone.EnableRemoteSessions); Assert.Equal(original.SessionIdleTimeoutSeconds, clone.SessionIdleTimeoutSeconds); + Assert.Same(original.ClientInfo, clone.ClientInfo); } [Fact] diff --git a/dotnet/test/Unit/E2ETestBackendTests.cs b/dotnet/test/Unit/E2ETestBackendTests.cs index f7c39a5083..43b23c1a48 100644 --- a/dotnet/test/Unit/E2ETestBackendTests.cs +++ b/dotnet/test/Unit/E2ETestBackendTests.cs @@ -25,7 +25,7 @@ public void RejectsUnknownBackend() () => E2ETestBackendConfiguration.Parse("unknown")); [Theory] - [InlineData("anthropic-messages", "anthropic", null, "claude-sonnet-4.5")] + [InlineData("anthropic-messages", "anthropic", null, "claude-sonnet-5")] [InlineData("openai-responses", "openai", "responses", "gpt-4.1")] [InlineData("openai-completions", "openai", "completions", "gpt-4.1")] public void AppliesProvider( diff --git a/dotnet/test/Unit/GitHubTelemetryTests.cs b/dotnet/test/Unit/GitHubTelemetryTests.cs index a4a241e38d..6213e0b1d0 100644 --- a/dotnet/test/Unit/GitHubTelemetryTests.cs +++ b/dotnet/test/Unit/GitHubTelemetryTests.cs @@ -138,6 +138,92 @@ public async Task Connect_Does_Not_Opt_In_Without_Handler() Assert.True( !present || flag.ValueKind == JsonValueKind.Null, "connect request should omit enableGitHubTelemetryForwarding (or send null) when no handler is registered"); + Assert.Equal( + ["agent", "client", "shell"], + connectParams.GetProperty("supportedTaskKinds").EnumerateArray().Select(kind => kind.GetString())); + } + + [Fact] + public async Task Connect_Forwards_Declared_ClientInfo() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + ClientInfo = new CopilotClientInfo + { + ApplicationName = "acme-developer-portal", + ApplicationVersion = "2.4.0", + IntegrationName = "copilot-assistant", + IntegrationVersion = "1.5.0", + }, + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.True(connectParams.TryGetProperty("clientInfo", out var clientInfo)); + Assert.Equal("acme-developer-portal", clientInfo.GetProperty("editorName").GetString()); + Assert.Equal("2.4.0", clientInfo.GetProperty("editorVersion").GetString()); + Assert.Equal("copilot-assistant", clientInfo.GetProperty("extensionName").GetString()); + Assert.Equal("1.5.0", clientInfo.GetProperty("extensionVersion").GetString()); + } + + [Fact] + public async Task Connect_Omits_ClientInfo_When_Unset() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.False( + connectParams.TryGetProperty("clientInfo", out _), + "connect request should omit clientInfo when none was declared"); + } + + [Fact] + public async Task Connect_Omits_Empty_ClientInfo_Fields() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + ClientInfo = new CopilotClientInfo { ApplicationName = "example-app", ApplicationVersion = "" }, + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.True(connectParams.TryGetProperty("clientInfo", out var clientInfo)); + Assert.Equal("example-app", clientInfo.GetProperty("editorName").GetString()); + Assert.False(clientInfo.TryGetProperty("editorVersion", out _)); + Assert.False(clientInfo.TryGetProperty("extensionName", out _)); + Assert.False(clientInfo.TryGetProperty("extensionVersion", out _)); + } + + [Fact] + public async Task Connect_Omits_All_Empty_ClientInfo() + { + await using var server = await FakeTelemetryServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri(server.Url), + ClientInfo = new CopilotClientInfo + { + ApplicationName = "", + ApplicationVersion = "", + IntegrationName = "", + IntegrationVersion = "", + }, + }); + await client.StartAsync(); + + var connectParams = server.LastConnectParams ?? throw new InvalidOperationException("connect was not captured."); + Assert.False( + connectParams.TryGetProperty("clientInfo", out _), + "connect request should omit an all-empty clientInfo"); } [Fact] @@ -409,7 +495,7 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel "session.create" => CaptureCreate(request), "session.resume" => CaptureResume(request), "session.send" => new Dictionary { ["messageId"] = "message-1" }, - "session.destroy" => new Dictionary(), + "session.detach" => new Dictionary { ["success"] = true }, "session.options.update" => new Dictionary { ["success"] = true }, "runtime.shutdown" => new Dictionary(), _ => throw new InvalidOperationException($"Unexpected RPC method '{method}'."), diff --git a/dotnet/test/Unit/MSBuildTargetsTests.cs b/dotnet/test/Unit/MSBuildTargetsTests.cs index cab91e568f..a6e3bb5660 100644 --- a/dotnet/test/Unit/MSBuildTargetsTests.cs +++ b/dotnet/test/Unit/MSBuildTargetsTests.cs @@ -2,8 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using System.Collections.Concurrent; using System.Diagnostics; +using System.Net; +using System.Net.Sockets; using System.Runtime.CompilerServices; +using System.Security.Cryptography; using System.Text; using Xunit; @@ -16,11 +20,7 @@ namespace GitHub.Copilot.Test.Unit; /// a subprocess so we exercise real MSBuild evaluation. /// /// -/// These tests deliberately do not exercise the network-bound default download path; they -/// pin a fake CopilotCliVersion and supply a fake CLI binary via -/// CopilotCliBinaryPath. That is sufficient to cover the regression in issue -/// #921 ("preinstalled CLI is ignored and copy/register are skipped when -/// CopilotSkipCliDownload=true"). +/// Download tests use a loopback release server; they never access the default GitHub URL. /// public class MSBuildTargetsTests { @@ -28,6 +28,9 @@ public class MSBuildTargetsTests private static readonly string BinaryName = OperatingSystem.IsWindows() ? "copilot.exe" : "copilot"; + private static readonly string RuntimeWrapperName = + OperatingSystem.IsWindows() ? "copilot-runtime.exe" : "copilot-runtime"; + [Fact] public async Task PreinstalledCliBinaryPath_IsHonored_DownloadSkipped_AndCopiedToOutput() { @@ -106,16 +109,111 @@ public async Task PreinstalledCliBinaryPath_WithSkipCliDownload_StillCopiesToOut Assert.True(File.Exists(sandbox.ExpectedOutputBinary()), result.FailureMessage()); } + [Fact] + public async Task ReleaseAsset_IsDownloadedVerifiedExtractedAndCached() + { + using var sandbox = MSBuildSandbox.Create(); + var archive = sandbox.CreateReleaseArchive("release-runtime-wrapper"); + var assetName = $"github-copilot-0.0.0-test-{GetReleasePlatform()}.tgz"; + var assetPath = $"/v0.0.0-test/{assetName}"; + var checksumsPath = "/v0.0.0-test/SHA256SUMS.txt"; + var checksum = ComputeSha256(archive); + using var server = new ReleaseServer(new Dictionary + { + [checksumsPath] = Encoding.UTF8.GetBytes($"{checksum} {assetName}\n"), + [assetPath] = archive, + }); + + var properties = new Dictionary + { + ["CopilotCliReleaseBaseUrl"] = server.BaseUrl, + }; + var firstBuild = await sandbox.BuildAsync(properties); + + Assert.True(firstBuild.Succeeded, firstBuild.FailureMessage()); + Assert.Equal("release-runtime-wrapper", File.ReadAllText(sandbox.ExpectedOutputBinary())); + Assert.Equal("release-runtime-wrapper", File.ReadAllText(sandbox.ExpectedRuntimeAsset(RuntimeWrapperName))); + Assert.Equal("runtime", File.ReadAllText(sandbox.ExpectedRuntimeAsset("runtime.node"))); + Assert.True(File.Exists(sandbox.ExpectedCacheAsset(".copilot-runtime-complete"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset(".copilot-runtime-complete"))); + Assert.Equal(1, server.RequestPaths.Count(path => path == checksumsPath)); + Assert.Equal(1, server.RequestPaths.Count(path => path == assetPath)); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("SHA256SUMS.txt"))); + + var secondBuild = await sandbox.BuildAsync(properties); + + Assert.True(secondBuild.Succeeded, secondBuild.FailureMessage()); + Assert.Equal(2, server.RequestPaths.Count); + } + + [Fact] + public async Task IncompleteCache_WithRuntimePairButNoMarker_IsReacquired() + { + using var sandbox = MSBuildSandbox.Create(); + sandbox.WriteRuntimeCacheAsset("prebuilds", GetReleasePlatform(), RuntimeWrapperName, "partial-wrapper"); + sandbox.WriteRuntimeCacheAsset("prebuilds", GetReleasePlatform(), "runtime.node", "partial-runtime"); + sandbox.WriteRuntimeCacheAsset("definitions", "stale.json", "stale"); + var archive = sandbox.CreateReleaseArchive("complete-wrapper"); + var assetName = $"github-copilot-0.0.0-test-{GetReleasePlatform()}.tgz"; + var assetPath = $"/v0.0.0-test/{assetName}"; + var checksumsPath = "/v0.0.0-test/SHA256SUMS.txt"; + using var server = new ReleaseServer(new Dictionary + { + [checksumsPath] = Encoding.UTF8.GetBytes($"{ComputeSha256(archive)} {assetName}\n"), + [assetPath] = archive, + }); + + var result = await sandbox.BuildAsync(new Dictionary + { + ["CopilotCliReleaseBaseUrl"] = server.BaseUrl, + }); + + Assert.True(result.Succeeded, result.FailureMessage()); + Assert.Equal(1, server.RequestPaths.Count(path => path == checksumsPath)); + Assert.Equal(1, server.RequestPaths.Count(path => path == assetPath)); + Assert.Equal("complete-wrapper", File.ReadAllText(sandbox.ExpectedRuntimeAsset(RuntimeWrapperName))); + Assert.Equal("runtime", File.ReadAllText(sandbox.ExpectedRuntimeAsset("runtime.node"))); + Assert.True(File.Exists(sandbox.ExpectedCacheAsset(".copilot-runtime-complete"))); + Assert.False(File.Exists(sandbox.ExpectedCacheAsset("definitions", "stale.json"))); + Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("definitions", "stale.json"))); + } + + [Fact] + public async Task ReleaseAsset_WithChecksumMismatch_FailsBeforeExtraction() + { + using var sandbox = MSBuildSandbox.Create(); + var archive = Encoding.UTF8.GetBytes("not the expected archive"); + var assetName = $"github-copilot-0.0.0-test-{GetReleasePlatform()}.tgz"; + using var server = new ReleaseServer(new Dictionary + { + ["/v0.0.0-test/SHA256SUMS.txt"] = + Encoding.UTF8.GetBytes($"{new string('0', 64)} *{assetName}\n"), + [$"/v0.0.0-test/{assetName}"] = archive, + }); + + var result = await sandbox.BuildAsync(new Dictionary + { + ["CopilotCliReleaseBaseUrl"] = server.BaseUrl, + }); + + Assert.False(result.Succeeded, "Build should fail when the release checksum does not match."); + Assert.Contains($"Checksum mismatch for {assetName}", result.StandardOutput, StringComparison.Ordinal); + Assert.False(File.Exists(sandbox.ExpectedOutputBinary())); + } + [Fact] public async Task RuntimePackageAssets_AreFilteredAndCopiedToOutput() { using var sandbox = MSBuildSandbox.Create(); var preinstalled = sandbox.WritePreinstalledBinary("fake-cli-contents"); - sandbox.WriteRuntimeCacheAsset("prebuilds", GetNpmPlatform(), "runtime.node", "runtime"); - sandbox.WriteRuntimeCacheAsset("prebuilds", GetNpmPlatform(), - OperatingSystem.IsWindows() ? "copilot-runtime.exe" : "copilot-runtime", "wrapper"); - sandbox.WriteRuntimeCacheAsset("ripgrep", "bin", GetNpmPlatform(), "rg", "ripgrep"); + sandbox.WriteRuntimeCacheAsset("prebuilds", GetReleasePlatform(), "runtime.node", "runtime"); + sandbox.WriteRuntimeCacheAsset("prebuilds", GetReleasePlatform(), + RuntimeWrapperName, "wrapper"); + sandbox.WriteRuntimeCacheAsset("ripgrep", "bin", GetReleasePlatform(), "rg", "ripgrep"); sandbox.WriteRuntimeCacheAsset("definitions", "future.json", "{}"); + sandbox.WriteRuntimeCacheAsset("copilot-sdk", "extension.js", "extension"); + sandbox.WriteRuntimeCacheAsset("preloads", "extension_bootstrap.mjs", "preload"); + sandbox.WriteRuntimeCacheAsset("sdk", "factory.js", "factory"); sandbox.WriteRuntimeCacheAsset("app.js", "excluded"); sandbox.WriteRuntimeCacheAsset("LICENSE.md", "excluded"); sandbox.WriteRuntimeCacheAsset("README.md", "excluded"); @@ -127,8 +225,11 @@ public async Task RuntimePackageAssets_AreFilteredAndCopiedToOutput() }); Assert.True(result.Succeeded, result.FailureMessage()); - Assert.Equal("ripgrep", File.ReadAllText(sandbox.ExpectedRuntimeAsset("ripgrep", "bin", GetNpmPlatform(), "rg"))); + Assert.Equal("ripgrep", File.ReadAllText(sandbox.ExpectedRuntimeAsset("ripgrep", "bin", GetReleasePlatform(), "rg"))); Assert.Equal("{}", File.ReadAllText(sandbox.ExpectedRuntimeAsset("definitions", "future.json"))); + Assert.Equal("extension", File.ReadAllText(sandbox.ExpectedRuntimeAsset("copilot-sdk", "extension.js"))); + Assert.Equal("preload", File.ReadAllText(sandbox.ExpectedRuntimeAsset("preloads", "extension_bootstrap.mjs"))); + Assert.Equal("factory", File.ReadAllText(sandbox.ExpectedRuntimeAsset("sdk", "factory.js"))); Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("app.js"))); Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("LICENSE.md"))); Assert.False(File.Exists(sandbox.ExpectedRuntimeAsset("README.md"))); @@ -180,7 +281,7 @@ private static string FindTargetsFile([CallerFilePath] string? thisFile = null) "Could not locate GitHub.Copilot.SDK.targets relative to test assembly or source file."); } - private static string GetNpmPlatform() + private static string GetReleasePlatform() { var arch = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.Arm64 @@ -191,6 +292,16 @@ private static string GetNpmPlatform() return $"linux-{arch}"; } + private static string ComputeSha256(byte[] contents) + { +#if NETFRAMEWORK + using var sha256 = SHA256.Create(); + return BitConverter.ToString(sha256.ComputeHash(contents)).Replace("-", "").ToLowerInvariant(); +#else + return Convert.ToHexString(SHA256.HashData(contents)).ToLowerInvariant(); +#endif + } + /// /// A throwaway directory containing a minimal csproj that imports the SDK targets /// file. Disposing removes the directory tree. @@ -238,6 +349,36 @@ public string WritePreinstalledBinary(string contents, string? fileName = null) return path; } + public byte[] CreateReleaseArchive(string runtimeWrapperContents) + { + var sourceDir = Path.Combine(ProjectDir, "release-source"); + var packageDir = Path.Combine(sourceDir, "package"); + var prebuildDir = Path.Combine(packageDir, "prebuilds", GetReleasePlatform()); + Directory.CreateDirectory(prebuildDir); + File.WriteAllText(Path.Combine(prebuildDir, "runtime.node"), "runtime"); + File.WriteAllText(Path.Combine(prebuildDir, RuntimeWrapperName), runtimeWrapperContents); + + var archivePath = Path.Combine(ProjectDir, "release-asset.tgz"); + var tarPath = OperatingSystem.IsWindows() + ? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32", "tar.exe") + : "tar"; + var startInfo = new ProcessStartInfo(tarPath) + { + Arguments = $"-czf \"{archivePath}\" -C \"{sourceDir}\" package", + RedirectStandardError = true, + UseShellExecute = false, + }; + using var process = Process.Start(startInfo) ?? + throw new InvalidOperationException("Failed to start tar while creating a release test asset."); + var standardError = process.StandardError.ReadToEnd(); + process.WaitForExit(); + if (process.ExitCode != 0) + { + throw new InvalidOperationException($"tar failed while creating a release test asset: {standardError}"); + } + return File.ReadAllBytes(archivePath); + } + public string ExpectedOutputBinary() { var rid = GetPortableRid(); @@ -247,14 +388,20 @@ public string ExpectedOutputBinary() public void WriteRuntimeCacheAsset(params string[] pathAndContents) { var pathParts = pathAndContents.Take(pathAndContents.Length - 1).ToArray(); + var path = ExpectedCacheAsset(pathParts); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + File.WriteAllText(path, pathAndContents[^1]); + } + + public string ExpectedCacheAsset(params string[] pathParts) + { var path = Path.Combine(ProjectDir, "obj", "Debug", "net8.0", "copilot-cli", "0.0.0-test", - GetNpmPlatform()); + GetReleasePlatform()); foreach (var part in pathParts) { path = Path.Combine(path, part); } - Directory.CreateDirectory(Path.GetDirectoryName(path)!); - File.WriteAllText(path, pathAndContents[^1]); + return path; } public string ExpectedRuntimeAsset(params string[] pathParts) @@ -366,6 +513,92 @@ private static string GetPortableRid() } } + private sealed class ReleaseServer : IDisposable + { + private readonly IReadOnlyDictionary _responses; + private readonly TcpListener _listener = new(IPAddress.Loopback, 0); + private readonly CancellationTokenSource _cancellation = new(); + private readonly Task _serverTask; + + public ReleaseServer(IReadOnlyDictionary responses) + { + _responses = responses; + _listener.Start(); + var endpoint = (IPEndPoint)_listener.LocalEndpoint; + BaseUrl = $"http://127.0.0.1:{endpoint.Port}"; + _serverTask = ServeAsync(); + } + + public string BaseUrl { get; } + + public ConcurrentQueue RequestPaths { get; } = new(); + + public void Dispose() + { + _cancellation.Cancel(); + _listener.Stop(); + try { _serverTask.GetAwaiter().GetResult(); } + catch (OperationCanceledException) { } + _cancellation.Dispose(); + } + + private async Task ServeAsync() + { + while (!_cancellation.IsCancellationRequested) + { + TcpClient client; + try + { + client = await _listener.AcceptTcpClientAsync(); + } + catch (ObjectDisposedException) when (_cancellation.IsCancellationRequested) + { + break; + } + catch (SocketException) when (_cancellation.IsCancellationRequested) + { + break; + } + await RespondAsync(client); + } + } + + private async Task RespondAsync(TcpClient client) + { + using (client) + { + var stream = client.GetStream(); + using var reader = new StreamReader(stream, Encoding.ASCII, false, 1024, leaveOpen: true); + var requestLine = await reader.ReadLineAsync(); + string? header; + do + { + header = await reader.ReadLineAsync(); + } + while (!string.IsNullOrEmpty(header)); + + var path = requestLine?.Split(' ', StringSplitOptions.RemoveEmptyEntries).ElementAtOrDefault(1) ?? ""; + RequestPaths.Enqueue(path); + var found = _responses.TryGetValue(path, out var body); + body ??= Encoding.UTF8.GetBytes("Not found"); + var status = found ? "200 OK" : "404 Not Found"; + var responseHeaders = Encoding.ASCII.GetBytes( + $"HTTP/1.1 {status}\r\nContent-Length: {body.Length}\r\nConnection: close\r\n\r\n"); + await WriteBytesAsync(stream, responseHeaders); + await WriteBytesAsync(stream, body); + } + } + + private static Task WriteBytesAsync(Stream stream, byte[] contents) + { +#if NETFRAMEWORK + return stream.WriteAsync(contents, 0, contents.Length); +#else + return stream.WriteAsync(contents).AsTask(); +#endif + } + } + private sealed record BuildResult(int ExitCode, string StandardOutput, string StandardError, string CommandLine) { public bool Succeeded => ExitCode == 0; diff --git a/dotnet/test/Unit/RuntimeConnectionUrlParsingTests.cs b/dotnet/test/Unit/RuntimeConnectionUrlParsingTests.cs new file mode 100644 index 0000000000..1b16456b0d --- /dev/null +++ b/dotnet/test/Unit/RuntimeConnectionUrlParsingTests.cs @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using Xunit; +using System.Reflection; + +namespace GitHub.Copilot.Test.Unit; + +public class RuntimeConnectionUrlParsingTests +{ + [Fact] + public void ForUri_ParsesBracketedIpv6HostPort() + { + var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri("[::1]:9000") + }); + + Assert.Equal("::1", GetPrivateField(client, "_optionsHost")); + Assert.Equal(9000, GetPrivateField(client, "_optionsPort")); + } + + [Fact] + public void ForUri_ParsesHttpIpv6HostPort() + { + var client = new CopilotClient(new CopilotClientOptions + { + Connection = RuntimeConnection.ForUri("http://[::1]:7000") + }); + + Assert.Equal("::1", GetPrivateField(client, "_optionsHost")); + Assert.Equal(7000, GetPrivateField(client, "_optionsPort")); + } + + private static T? GetPrivateField(object instance, string name) + { + var field = instance.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(field); + return (T?)field.GetValue(instance); + } +} diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 2414093797..7d0f535160 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -17,6 +17,25 @@ namespace GitHub.Copilot.Test.Unit; /// public class SerializationTests { + [Fact] + public void SandboxConfig_RoundtripsAllowBypass_AndOmitsWhenAbsent() + { + var options = GetSerializerOptions(); + var configured = new SandboxConfig { Enabled = true, AllowBypass = true }; + + var json = JsonSerializer.Serialize(configured, options); + using var document = JsonDocument.Parse(json); + Assert.True(document.RootElement.GetProperty("allowBypass").GetBoolean()); + + var roundTripped = JsonSerializer.Deserialize(json, options); + Assert.NotNull(roundTripped); + Assert.True(roundTripped.AllowBypass); + + var omitted = JsonSerializer.Serialize(new SandboxConfig { Enabled = true }, options); + using var omittedDocument = JsonDocument.Parse(omitted); + Assert.False(omittedDocument.RootElement.TryGetProperty("allowBypass", out _)); + } + [Fact] public void ProviderConfig_CanSerializeHeaders_WithSdkOptions() { @@ -1122,6 +1141,40 @@ public void ToolResultObject_OmitsToolReferences_WhenNull_WithSdkOptions() Assert.False(document.RootElement.TryGetProperty("toolReferences", out _)); } +#pragma warning disable GHCP001 // The queue management surface is intentionally experimental. + [Theory] + [InlineData("message-1")] + [InlineData(null)] + public void QueuePendingItems_MessageId_UsesCamelCaseAndIsOptional(string? messageId) + { + var options = GetSerializerOptions(); + var messageIdProperty = messageId is null ? "" : $""","messageId":"{messageId}" """; + var json = $$""" + { + "id": "queue-1", + "kind": "message", + "displayText": "hello", + "agentMode": "interactive" + {{messageIdProperty}} + } + """; + + var item = JsonSerializer.Deserialize(json, options); + Assert.NotNull(item); + Assert.Equal(messageId, item.MessageId); + + using var document = JsonDocument.Parse(JsonSerializer.Serialize(item, options)); + if (messageId is null) + { + Assert.False(document.RootElement.TryGetProperty("messageId", out _)); + } + else + { + Assert.Equal(messageId, document.RootElement.GetProperty("messageId").GetString()); + } + } +#pragma warning restore GHCP001 + private static JsonSerializerOptions GetSerializerOptions() { var prop = typeof(CopilotClient) diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs index 326ac3f3c7..405ffb379f 100644 --- a/dotnet/test/Unit/SessionEventSerializationTests.cs +++ b/dotnet/test/Unit/SessionEventSerializationTests.cs @@ -9,6 +9,140 @@ namespace GitHub.Copilot.Test.Unit; public class SessionEventSerializationTests { + public static TheoryData AutoTiers => new() + { + { AutoTier.Efficiency, "efficiency" }, + { AutoTier.Balance, "balance" }, + { AutoTier.Intelligence, "intelligence" }, + { null, null }, + }; + + [Theory] + [MemberData(nameof(AutoTiers))] + public void SessionEvent_Deserializes_AutoTier(AutoTier? expectedTier, string? wireTier) + { + foreach (var eventType in new[] { "session.start", "session.resume" }) + { + var autoTierProperty = wireTier is null ? "" : $""", "autoTier": "{wireTier}" """; + var json = $$""" + { + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": "{{eventType}}", + "data": { + "sessionId": "test-session", "version": 1, + "producer": "copilot", "copilotVersion": "1.0.82-1", + "startTime": "2026-08-28T00:00:00Z", + "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1 + {{autoTierProperty}} + } + } + """; + + var sessionEvent = SessionEvent.FromJson(json); + var actualTier = eventType == "session.start" + ? Assert.IsType(sessionEvent).Data.AutoTier + : Assert.IsType(sessionEvent).Data.AutoTier; + Assert.Equal(expectedTier, actualTier); + } + } + + [Theory] + [InlineData("message-1")] + [InlineData(null)] + public void UserMessageEvent_MessageId_UsesCamelCaseAndIsOptional(string? messageId) + { + var messageIdProperty = messageId is null ? "" : $""", "messageId": "{messageId}" """; + var json = $$""" + { + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": "user.message", + "data": { + "content": "hello" + {{messageIdProperty}} + } + } + """; + + var sessionEvent = Assert.IsType(SessionEvent.FromJson(json)); + Assert.Equal(messageId, sessionEvent.Data.MessageId); + + using var document = JsonDocument.Parse(sessionEvent.ToJson()); + var data = document.RootElement.GetProperty("data"); + if (messageId is null) + { + Assert.False(data.TryGetProperty("messageId", out _)); + } + else + { + Assert.Equal(messageId, data.GetProperty("messageId").GetString()); + } + } + + public static TheoryData AutoTierSwitchFailureReasons => + [ + "policy_rejected", + "request_failed", + "setup_failed", + "unsupported", + ]; + + [Theory] + [MemberData(nameof(AutoTierSwitchFailureReasons))] + public void SessionEvent_Deserializes_AutoTierSwitchFailed(string wireReason) + { + var json = $$""" + { + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": "session.auto_tier_switch_failed", + "data": { + "effectiveAutoTier": "balance", + "requestedAutoTier": "intelligence", + "reason": "{{wireReason}}" + } + } + """; + + var sessionEvent = SessionEvent.FromJson(json); + + var data = Assert.IsType(sessionEvent).Data; + Assert.Equal(new AutoTierSwitchFailureReason(wireReason), data.Reason); + Assert.Equal(AutoTier.Balance, data.EffectiveAutoTier); + Assert.Equal(AutoTier.Intelligence, data.RequestedAutoTier); + } + + [Fact] + public void SessionEvent_Deserializes_AutoTierSwitchFailed_WithNullRequestedTier() + { + // A null requested tier means the attempt to return to provider-default + // Auto routing is what failed. + var json = """ + { + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": "session.auto_tier_switch_failed", + "data": { + "effectiveAutoTier": "efficiency", + "requestedAutoTier": null, + "reason": "unsupported" + } + } + """; + + var sessionEvent = SessionEvent.FromJson(json); + + var data = Assert.IsType(sessionEvent).Data; + Assert.Null(data.RequestedAutoTier); + Assert.Equal(AutoTier.Efficiency, data.EffectiveAutoTier); + Assert.Equal(AutoTierSwitchFailureReason.Unsupported, data.Reason); + } + public static TheoryData JsonElementBackedEvents => new() { { diff --git a/go/README.md b/go/README.md index 6c85f93ae1..1fa8202419 100644 --- a/go/README.md +++ b/go/README.md @@ -20,7 +20,10 @@ go get github.com/github/copilot-sdk/go Try the interactive chat sample (from the repo root): ```bash -cd go/samples +cd nodejs +npm ci +export COPILOT_CLI_PATH="$(npm run --silent prepare:runtime -- --print-path)" +cd ../go/samples go run chat.go ``` @@ -98,6 +101,9 @@ tool name is `-`. For `AvailableTools` and The SDK supports bundling, using Go's `embed` package, the Copilot CLI binary within your application's distribution. This allows you to bundle a specific CLI version and avoid external dependencies on the user's system. +The bundler downloads the matching `github-copilot--.tgz` +asset from the `github/copilot-cli` release and verifies it against that +release's `SHA256SUMS.txt`. Follow these steps to embed the CLI: @@ -329,6 +335,30 @@ Each section override supports five actions: Unknown section IDs are handled gracefully: content from `replace`/`append`/`prepend` overrides is appended to additional instructions, and `remove` overrides are silently ignored. +## Auto routing tiers + +Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. + +```go +tier := copilot.AutoTierIntelligence +result, err := session.SetAutoTier(ctx, &tier) +if err != nil { + return err +} +if result.Status == rpc.ModelSwitchAutoTierStatusPending { + // Accepted, but not yet in effect. +} + +// Return to the provider's default Auto routing. +_, err = session.SetAutoTier(ctx, nil) +``` + +`SetModel` accepts the same preference through `SetModelOptions.AutoTier`, which stages the tier atomically with selecting `auto`. Set `ResetAutoTier` instead to return to provider-default routing; the two options are mutually exclusive. + +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. + ## Image Support The SDK supports image attachments via the `Attachments` field in `MessageOptions`. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: diff --git a/go/client.go b/go/client.go index 8f6b41a983..6cac724f0d 100644 --- a/go/client.go +++ b/go/client.go @@ -36,6 +36,7 @@ import ( "fmt" "log" "net" + "net/netip" "os" "os/exec" "path/filepath" @@ -401,13 +402,31 @@ func setEnvValue(env []string, key string, value string) []string { // parseCLIURL parses a CLI URL into host and port components. // -// Supports formats: "host:port", "http://host:port", "https://host:port", or just "port". +// Supports formats: "host:port", "[ipv6]:port", "http://host:port", "https://host:port", or just "port". // Panics if the URL format is invalid or the port is out of range. func parseCLIURL(url string) (string, int) { // Remove protocol if present cleanURL, _ := strings.CutPrefix(url, "https://") cleanURL, _ = strings.CutPrefix(cleanURL, "http://") + // Use the standard parser only for the bracketed IPv6 form. Keep the + // existing host:port parsing behavior for all other inputs. + if strings.HasPrefix(cleanURL, "[") { + host, portStr, err := net.SplitHostPort(cleanURL) + if err != nil { + panic(fmt.Sprintf("Invalid port in URIConnection: %s", url)) + } + addr, err := netip.ParseAddr(host) + if err != nil || !addr.Is6() { + panic(fmt.Sprintf("Invalid URIConnection format: %s", url)) + } + port, err := strconv.Atoi(portStr) + if err != nil || port <= 0 || port > 65535 { + panic(fmt.Sprintf("Invalid port in URIConnection: %s", url)) + } + return host, port + } + // Parse host:port or port format var host string var portStr string @@ -415,15 +434,13 @@ func parseCLIURL(url string) (string, int) { host = before portStr = after } else { - // Only port provided - portStr = before + portStr = cleanURL } if host == "" { host = "localhost" } - // Validate port port, err := strconv.Atoi(portStr) if err != nil || port <= 0 || port > 65535 { panic(fmt.Sprintf("Invalid port in URIConnection: %s", url)) @@ -684,8 +701,15 @@ func (c *Client) ForceStop() { // Clear sessions immediately without trying to destroy them c.sessionsMux.Lock() + sessions := make([]*Session, 0, len(c.sessions)) + for _, session := range c.sessions { + sessions = append(sessions, session) + } c.sessions = make(map[string]*Session) c.sessionsMux.Unlock() + for _, session := range sessions { + session.cancelPendingExternalTools() + } c.clearGitHubTokenProviders() c.startStopMux.Lock() @@ -985,6 +1009,20 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses } req.SessionID = localSessionID + // unregisterSession removes only the session created by this call and stops + // its event consumer. The latter is essential on CreateSession error paths: + // newSession starts processEvents eagerly, and no caller receives the failed + // session to disconnect it. + unregisterSession := func(sessionID string, s *Session) { + c.sessionsMux.Lock() + if c.sessions[sessionID] == s { + delete(c.sessions, sessionID) + } + c.sessionsMux.Unlock() + s.cancelPendingExternalTools() + s.stopEventProcessing() + } + // initializeSession creates the session, wires up handlers, and registers // it in the sessions map. Invoked from the read loop the instant the // session.create response arrives (synchronously, before the next @@ -1038,17 +1076,13 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses if c.options.SessionFS != nil { if config.CreateSessionFSProvider == nil { - c.sessionsMux.Lock() - delete(c.sessions, sessionID) - c.sessionsMux.Unlock() + unregisterSession(sessionID, s) return nil, fmt.Errorf("CreateSessionFSProvider is required in session config when SessionFS is enabled in client options") } provider := config.CreateSessionFSProvider(s) if c.options.SessionFS.Capabilities != nil && c.options.SessionFS.Capabilities.Sqlite { if _, ok := provider.(SessionFSSqliteProvider); !ok { - c.sessionsMux.Lock() - delete(c.sessions, sessionID) - c.sessionsMux.Unlock() + unregisterSession(sessionID, s) return nil, fmt.Errorf("SessionFS capabilities declare SQLite support but the provider does not implement SessionFSSqliteProvider") } } @@ -1105,9 +1139,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses result, err := c.client.RequestWithInlineResponse(ctx, "session.create", req, inlineCb) if err != nil { if registeredSessionID != "" { - c.sessionsMux.Lock() - delete(c.sessions, registeredSessionID) - c.sessionsMux.Unlock() + unregisterSession(registeredSessionID, session) } return nil, fmt.Errorf("failed to create session: %w", err) } @@ -1115,9 +1147,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses var response createSessionResponse if err := json.Unmarshal(result, &response); err != nil { if registeredSessionID != "" { - c.sessionsMux.Lock() - delete(c.sessions, registeredSessionID) - c.sessionsMux.Unlock() + unregisterSession(registeredSessionID, session) } return nil, fmt.Errorf("failed to unmarshal response: %w", err) } @@ -1127,9 +1157,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses } if localSessionID != "" && response.SessionID != "" && response.SessionID != localSessionID { - c.sessionsMux.Lock() - delete(c.sessions, registeredSessionID) - c.sessionsMux.Unlock() + unregisterSession(registeredSessionID, session) return nil, fmt.Errorf("session.create returned sessionId %s but the caller requested %s", response.SessionID, localSessionID) } if config.OnMCPAuthRequest != nil { @@ -1137,6 +1165,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses "sessionId": session.SessionID, "eventType": "mcp.oauth_required", }); err != nil { + unregisterSession(registeredSessionID, session) return nil, err } } @@ -1151,6 +1180,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses ManageScheduleEnabled: config.ManageScheduleEnabled, IncludedBuiltinSkills: config.IncludedBuiltinSkills, }); err != nil { + unregisterSession(registeredSessionID, session) return nil, err } @@ -1411,6 +1441,12 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, } } c.sessionsMux.Unlock() + session.cancelPendingExternalTools() + // newSession starts processEvents eagerly, before the RPC confirms the + // resume; every failure path here restores the previously-registered + // session (if any) but never returns this failed one to the caller, so + // its event consumer must be stopped here or it leaks forever. + session.stopEventProcessing() } if c.options.SessionFS != nil { @@ -1943,7 +1979,14 @@ func (c *Client) verifyProtocolVersion(ctx context.Context) error { t := c.effectiveConnectionToken tokenPtr = &t } - connectReq := &connectHandshakeRequest{Token: tokenPtr} + connectReq := &connectHandshakeRequest{ + Token: tokenPtr, + SupportedTaskKinds: []rpc.TaskKind{ + rpc.TaskKindAgent, + rpc.TaskKindClient, + rpc.TaskKindShell, + }, + } // Opt in to GitHub telemetry forwarding at the connection level when a handler is // registered (mirrors the runtime, which reads this flag on the `connect` handshake // so the first session's un-replayable `session.start` event is forwarded). Also @@ -1951,6 +1994,10 @@ func (c *Client) verifyProtocolVersion(ctx context.Context) error { if c.options.OnGitHubTelemetry != nil { connectReq.EnableGitHubTelemetryForwarding = Bool(true) } + // Declare the integrating host's identity so the runtime attributes the + // telemetry it emits on this connection to a consistent surface instead of + // its own build. Nil when the app didn't supply it. + connectReq.ClientInfo = c.options.ClientInfo.toWire() rawConnectResult, err := c.client.Request(ctx, "connect", connectReq) if err != nil { var rpcErr *jsonrpc2.Error @@ -1987,8 +2034,10 @@ func (c *Client) verifyProtocolVersion(ctx context.Context) error { } type connectHandshakeRequest struct { - Token *string `json:"token,omitempty"` - EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` + Token *string `json:"token,omitempty"` + EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` + ClientInfo *rpc.ConnectClientInfo `json:"clientInfo,omitempty"` + SupportedTaskKinds []rpc.TaskKind `json:"supportedTaskKinds,omitempty"` } // stderrBufferSize is the maximum number of bytes kept from the CLI process's @@ -2440,11 +2489,9 @@ func (c *Client) setupNotificationHandler() { } if c.options.RequestHandler != nil { + llmInference := c.RPC.LlmInference handlers.LlmInference = newCopilotRequestAdapter(c.options.RequestHandler, func() *rpc.ServerLlmInferenceAPI { - if c.RPC == nil { - return nil - } - return c.RPC.LlmInference + return llmInference }) } if c.options.OnGitHubTelemetry != nil { @@ -2484,6 +2531,15 @@ func (c *Client) clearGitHubTokenProviders() { func (c *Client) handleConnectionClose() { c.clearGitHubTokenProviders() + c.sessionsMux.Lock() + sessions := make([]*Session, 0, len(c.sessions)) + for _, session := range c.sessions { + sessions = append(sessions, session) + } + c.sessionsMux.Unlock() + for _, session := range sessions { + session.cancelPendingExternalTools() + } // Avoid deadlocking with Stop/ForceStop, which hold startStopMux while // waiting for the JSON-RPC read loop to finish. go func() { diff --git a/go/client_test.go b/go/client_test.go index ec3924f0a9..52587c8462 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -78,6 +78,15 @@ func TestClient_URLParsing(t *testing.T) { } }) + t.Run("should parse bracketed IPv6 host:port URL format", func(t *testing.T) { + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: "[::1]:9000"}, + }) + if client.actualPort != 9000 || client.actualHost != "::1" { + t.Errorf("Expected [::1]:9000, got %s:%d", client.actualHost, client.actualPort) + } + }) + t.Run("should parse http://host:port URL format", func(t *testing.T) { client := NewClient(&ClientOptions{ Connection: URIConnection{URL: "http://localhost:7000"}, @@ -87,6 +96,24 @@ func TestClient_URLParsing(t *testing.T) { } }) + t.Run("should parse http://[ipv6]:port URL format", func(t *testing.T) { + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: "http://[::1]:7000"}, + }) + if client.actualPort != 7000 || client.actualHost != "::1" { + t.Errorf("Expected [::1]:7000, got %s:%d", client.actualHost, client.actualPort) + } + }) + + t.Run("should panic for bracketed non-IPv6 host", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic for invalid bracketed host") + } + }() + NewClient(&ClientOptions{Connection: URIConnection{URL: "[not-ipv6]:1234"}}) + }) + t.Run("should parse https://host:port URL format", func(t *testing.T) { client := NewClient(&ClientOptions{ Connection: URIConnection{URL: "https://example.com:443"}, @@ -276,6 +303,106 @@ func TestClient_BuiltinPluginDirectories(t *testing.T) { }) } +func TestClient_ClientInfo(t *testing.T) { + findConnect := func(requests []startupRPCRequest) map[string]any { + t.Helper() + for _, request := range requests { + if request.Method != "connect" { + continue + } + var params map[string]any + if err := json.Unmarshal(request.Params, ¶ms); err != nil { + t.Fatalf("decode connect params: %v", err) + } + return params + } + t.Fatal("connect was not called") + return nil + } + + t.Run("forwards a declared identity on the connect handshake", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: url}, + ClientInfo: &ClientInfo{ + ApplicationName: "acme-developer-portal", + ApplicationVersion: "2.4.0", + IntegrationName: "copilot-assistant", + IntegrationVersion: "1.5.0", + }, + }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + params := findConnect(requests()) + want := map[string]any{ + "editorName": "acme-developer-portal", + "editorVersion": "2.4.0", + "extensionName": "copilot-assistant", + "extensionVersion": "1.5.0", + } + if !reflect.DeepEqual(params["clientInfo"], want) { + t.Fatalf("clientInfo = %v, want %v", params["clientInfo"], want) + } + }) + + t.Run("omits clientInfo when unset", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{Connection: URIConnection{URL: url}}) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + if _, ok := findConnect(requests())["clientInfo"]; ok { + t.Fatal("clientInfo should be omitted when unset") + } + }) + + t.Run("omits empty fields", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: url}, + ClientInfo: &ClientInfo{ApplicationName: "example-app"}, + }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + want := map[string]any{"editorName": "example-app"} + if got := findConnect(requests())["clientInfo"]; !reflect.DeepEqual(got, want) { + t.Fatalf("clientInfo = %v, want %v", got, want) + } + }) + + t.Run("omits clientInfo when all fields empty", func(t *testing.T) { + url, requests, cleanup := newStartupRPCServer(t) + defer cleanup() + + client := NewClient(&ClientOptions{ + Connection: URIConnection{URL: url}, + ClientInfo: &ClientInfo{}, + }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Start failed: %v", err) + } + defer client.ForceStop() + + if _, ok := findConnect(requests())["clientInfo"]; ok { + t.Fatal("clientInfo should be omitted when all fields are empty") + } + }) +} + type startupRPCRequest struct { Method string Params json.RawMessage @@ -414,6 +541,67 @@ func TestClient_ForceStopAndExternalStopDoNotRequestRuntimeShutdown(t *testing.T externalServer.Stop() } +func TestClient_ForceStopCancelsPendingExternalTools(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + session := &Session{ + pendingExternalTools: map[string]*pendingExternalTool{ + "request-1": {ctx: ctx, cancel: cancel}, + }, + } + + client := &Client{sessions: map[string]*Session{"session-1": session}} + + client.ForceStop() + + if len(client.sessions) != 0 { + t.Fatal("ForceStop did not clear sessions") + } + select { + case <-ctx.Done(): + case <-time.After(time.Second): + t.Fatal("ForceStop did not cancel the pending external tool") + } +} + +func TestClient_ConnectionCloseCancelsPendingExternalTools(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + server.SetRequestHandler("session.detach", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil + }) + ctx, cancel := context.WithCancel(context.Background()) + session := newSession("session-1", rpcClient, "", false) + session.pendingExternalTools = map[string]*pendingExternalTool{ + "request-1": {ctx: ctx, cancel: cancel}, + } + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: map[string]*Session{"session-1": session}, + isExternalServer: true, + } + + client.handleConnectionClose() + + if len(client.sessions) != 1 { + t.Fatal("connection close removed sessions before Stop could clean them up") + } + select { + case <-ctx.Done(): + case <-time.After(time.Second): + t.Fatal("connection close did not cancel the pending external tool") + } + + if err := client.Stop(); err != nil { + t.Fatalf("Stop failed after connection close: %v", err) + } + session.toolHandlersM.RLock() + defer session.toolHandlersM.RUnlock() + if session.toolHandlers != nil { + t.Fatal("Stop did not clean up the retained session") + } + server.Stop() +} + func newRuntimeShutdownRpcPair(t *testing.T) (*jsonrpc2.Client, *jsonrpc2.Client, chan struct{}) { t.Helper() @@ -436,42 +624,63 @@ func newRuntimeShutdownRpcPair(t *testing.T) (*jsonrpc2.Client, *jsonrpc2.Client } func TestClient_ForwardsCapiOptionsToSessionRequests(t *testing.T) { - rpcClient, server, _ := newRuntimeShutdownRpcPair(t) - t.Cleanup(server.Stop) - client := &Client{ - client: rpcClient, - RPC: rpc.NewServerRPC(rpcClient), - sessions: make(map[string]*Session), - } + tests := []struct { + name string + capi *CapiSessionOptions + want map[string]any + }{ + {"omitted", nil, nil}, + {"empty", &CapiSessionOptions{}, map[string]any{}}, + {"websocket only", &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, map[string]any{"enableWebSocketResponses": false}}, + {"efficiency", &CapiSessionOptions{AutoTier: AutoTierEfficiency}, map[string]any{"autoTier": "efficiency"}}, + {"balance", &CapiSessionOptions{AutoTier: AutoTierBalance}, map[string]any{"autoTier": "balance"}}, + {"intelligence", &CapiSessionOptions{AutoTier: AutoTierIntelligence}, map[string]any{"autoTier": "intelligence"}}, + {"efficiency with websocket", &CapiSessionOptions{AutoTier: AutoTierEfficiency, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "efficiency", "enableWebSocketResponses": false}}, + {"balance with websocket", &CapiSessionOptions{AutoTier: AutoTierBalance, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "balance", "enableWebSocketResponses": false}}, + {"intelligence with websocket", &CapiSessionOptions{AutoTier: AutoTierIntelligence, EnableWebSocketResponses: Bool(false)}, map[string]any{"autoTier": "intelligence", "enableWebSocketResponses": false}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } - createParams := make(chan json.RawMessage, 1) - server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - createParams <- append(json.RawMessage(nil), params...) - sessionID := sessionIDFromParams(t, params) - return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil - }) + createParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + createParams <- append(json.RawMessage(nil), params...) + sessionID := sessionIDFromParams(t, params) + return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil + }) - _, err := client.CreateSession(t.Context(), &SessionConfig{ - Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, - }) - if err != nil { - t.Fatalf("CreateSession failed: %v", err) - } - assertCapiEnableWebSocketResponses(t, <-createParams) + _, err := client.CreateSession(t.Context(), &SessionConfig{ + Model: "auto", + Capi: tt.capi, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + assertCapiOptions(t, <-createParams, tt.want) - resumeParams := make(chan json.RawMessage, 1) - server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - resumeParams <- append(json.RawMessage(nil), params...) - return []byte(`{"sessionId":"resumed-capi","workspacePath":"/workspace"}`), nil - }) + resumeParams := make(chan json.RawMessage, 1) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + resumeParams <- append(json.RawMessage(nil), params...) + return []byte(`{"sessionId":"resumed-capi","workspacePath":"/workspace"}`), nil + }) - _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-capi", &ResumeSessionConfig{ - Capi: &CapiSessionOptions{EnableWebSocketResponses: Bool(false)}, - }) - if err != nil { - t.Fatalf("ResumeSessionWithOptions failed: %v", err) + _, err = client.ResumeSessionWithOptions(t.Context(), "resumed-capi", &ResumeSessionConfig{ + Model: "auto", + Capi: tt.capi, + }) + if err != nil { + t.Fatalf("ResumeSessionWithOptions failed: %v", err) + } + assertCapiOptions(t, <-resumeParams, tt.want) + }) } - assertCapiEnableWebSocketResponses(t, <-resumeParams) } func TestClient_ForwardsAskUserVariantToSessionRequests(t *testing.T) { @@ -728,7 +937,7 @@ func TestClient_ForwardsNewSessionOptionsToSessionRequests(t *testing.T) { assertNewSessionOptions(t, <-resumeParams, false, false, "task", 15) } -func assertCapiEnableWebSocketResponses(t *testing.T, params json.RawMessage) { +func assertCapiOptions(t *testing.T, params json.RawMessage, want map[string]any) { t.Helper() var decoded map[string]any @@ -736,12 +945,18 @@ func assertCapiEnableWebSocketResponses(t *testing.T, params json.RawMessage) { t.Fatalf("failed to unmarshal request params: %v", err) } + if want == nil { + if _, present := decoded["capi"]; present { + t.Fatalf("expected capi to be omitted, got %v", decoded["capi"]) + } + return + } capi, ok := decoded["capi"].(map[string]any) if !ok { t.Fatalf("expected capi object in request params, got %T", decoded["capi"]) } - if capi["enableWebSocketResponses"] != false { - t.Fatalf("expected capi.enableWebSocketResponses=false, got %v", capi["enableWebSocketResponses"]) + if !reflect.DeepEqual(capi, want) { + t.Fatalf("expected capi %v, got %v", want, capi) } } @@ -797,6 +1012,225 @@ func sessionIDFromParams(t *testing.T, params json.RawMessage) string { return decoded.SessionID } +func TestClient_CreateSessionFailureClosesRegisteredSession(t *testing.T) { + tests := []struct { + name string + response func(string) (json.RawMessage, *jsonrpc2.Error) + wantErrSub string + }{ + { + name: "RPC failure", + response: func(string) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "session creation failed"} + }, + wantErrSub: "failed to create session", + }, + { + name: "invalid response", + response: func(string) (json.RawMessage, *jsonrpc2.Error) { + return json.RawMessage(`"invalid"`), nil + }, + wantErrSub: "failed to unmarshal response", + }, + { + name: "session ID mismatch", + response: func(string) (json.RawMessage, *jsonrpc2.Error) { + return json.RawMessage(`{"sessionId":"different-session"}`), nil + }, + wantErrSub: "but the caller requested failed-session", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + + captured := make(chan *Session, 1) + server.SetRequestHandler("session.create", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + client.sessionsMux.Lock() + session := client.sessions[sessionID] + client.sessionsMux.Unlock() + captured <- session + return tt.response(sessionID) + }) + + _, err := client.CreateSession(t.Context(), &SessionConfig{SessionID: "failed-session"}) + if err == nil || !strings.Contains(err.Error(), tt.wantErrSub) { + t.Fatalf("CreateSession error = %v, want substring %q", err, tt.wantErrSub) + } + + session := <-captured + if session == nil { + t.Fatal("session was not registered before session.create") + } + assertSessionEventChannelClosed(t, session) + assertSessionNotRegistered(t, client, "failed-session") + }) + } +} + +func TestClient_CreateSessionInitializationFailureClosesRegisteredSession(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + options: ClientOptions{SessionFS: &SessionFSConfig{ + InitialWorkingDirectory: "/", + SessionStatePath: "/session-state", + Conventions: rpc.SessionFSSetProviderConventionsPosix, + Capabilities: &SessionFSCapabilities{Sqlite: true}, + }}, + } + + var captured *Session + _, err := client.CreateSession(t.Context(), &SessionConfig{ + SessionID: "failed-session-fs", + CreateSessionFSProvider: func(session *Session) SessionFSProvider { + captured = session + return noSQLiteSessionFSProvider{} + }, + }) + if err == nil || !strings.Contains(err.Error(), "does not implement SessionFSSqliteProvider") { + t.Fatalf("CreateSession error = %v, want SQLite provider validation error", err) + } + if captured == nil { + t.Fatal("CreateSessionFSProvider did not receive the registered session") + } + assertSessionEventChannelClosed(t, captured) + assertSessionNotRegistered(t, client, "failed-session-fs") +} + +func TestClient_ResumeSessionFailureClosesRegisteredSession(t *testing.T) { + tests := []struct { + name string + response func(string) (json.RawMessage, *jsonrpc2.Error) + wantErrSub string + }{ + { + name: "RPC failure", + response: func(string) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "session resume failed"} + }, + wantErrSub: "failed to resume session", + }, + { + name: "invalid response", + response: func(string) (json.RawMessage, *jsonrpc2.Error) { + return json.RawMessage(`"invalid"`), nil + }, + wantErrSub: "failed to unmarshal response", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + } + + captured := make(chan *Session, 1) + server.SetRequestHandler("session.resume", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + sessionID := sessionIDFromParams(t, params) + client.sessionsMux.Lock() + session := client.sessions[sessionID] + client.sessionsMux.Unlock() + captured <- session + return tt.response(sessionID) + }) + + _, err := client.ResumeSession(t.Context(), "resumed-session", &ResumeSessionConfig{}) + if err == nil || !strings.Contains(err.Error(), tt.wantErrSub) { + t.Fatalf("ResumeSession error = %v, want substring %q", err, tt.wantErrSub) + } + + session := <-captured + if session == nil { + t.Fatal("session was not registered before session.resume") + } + assertSessionEventChannelClosed(t, session) + assertSessionNotRegistered(t, client, "resumed-session") + }) + } +} + +func TestClient_ResumeSessionInitializationFailureClosesRegisteredSession(t *testing.T) { + rpcClient, server, _ := newRuntimeShutdownRpcPair(t) + t.Cleanup(server.Stop) + client := &Client{ + client: rpcClient, + RPC: rpc.NewServerRPC(rpcClient), + sessions: make(map[string]*Session), + options: ClientOptions{SessionFS: &SessionFSConfig{ + InitialWorkingDirectory: "/", + SessionStatePath: "/session-state", + Conventions: rpc.SessionFSSetProviderConventionsPosix, + Capabilities: &SessionFSCapabilities{Sqlite: true}, + }}, + } + + var captured *Session + _, err := client.ResumeSession(t.Context(), "resumed-session-fs", &ResumeSessionConfig{ + CreateSessionFSProvider: func(session *Session) SessionFSProvider { + captured = session + return noSQLiteSessionFSProvider{} + }, + }) + if err == nil || !strings.Contains(err.Error(), "does not implement SessionFSSqliteProvider") { + t.Fatalf("ResumeSession error = %v, want SQLite provider validation error", err) + } + if captured == nil { + t.Fatal("CreateSessionFSProvider did not receive the registered session") + } + assertSessionEventChannelClosed(t, captured) + assertSessionNotRegistered(t, client, "resumed-session-fs") +} + +func assertSessionEventChannelClosed(t *testing.T, session *Session) { + t.Helper() + select { + case <-session.eventDone: + case <-time.After(time.Second): + t.Fatal("timed out waiting for session event processing to stop") + } +} + +func assertSessionNotRegistered(t *testing.T, client *Client, sessionID string) { + t.Helper() + client.sessionsMux.Lock() + defer client.sessionsMux.Unlock() + if _, ok := client.sessions[sessionID]; ok { + t.Fatalf("session %q is still registered", sessionID) + } +} + +type noSQLiteSessionFSProvider struct{} + +func (noSQLiteSessionFSProvider) ReadFile(string) (string, error) { return "", nil } +func (noSQLiteSessionFSProvider) WriteFile(string, string, *int) error { return nil } +func (noSQLiteSessionFSProvider) AppendFile(string, string, *int) error { return nil } +func (noSQLiteSessionFSProvider) Exists(string) (bool, error) { return false, nil } +func (noSQLiteSessionFSProvider) Stat(string) (*SessionFSFileInfo, error) { return nil, nil } +func (noSQLiteSessionFSProvider) MakeDirectory(string, bool, *int) error { return nil } +func (noSQLiteSessionFSProvider) ReadDirectory(string) ([]string, error) { return nil, nil } +func (noSQLiteSessionFSProvider) ReadDirectoryWithTypes(string) ([]rpc.SessionFSReaddirWithTypesEntry, error) { + return nil, nil +} +func (noSQLiteSessionFSProvider) Remove(string, bool, bool) error { return nil } +func (noSQLiteSessionFSProvider) Rename(string, string) error { return nil } + func assertRuntimeShutdownNotCalled(t *testing.T, shutdownCalled <-chan struct{}) { t.Helper() select { @@ -1199,15 +1633,36 @@ func TestClient_SessionIdleTimeoutSeconds(t *testing.T) { }) } -func findCLIPathForTest() string { - base, err := filepath.Abs("../nodejs/node_modules/@github") - if err == nil { - matches, _ := filepath.Glob(filepath.Join(base, "copilot-*", "index.js")) - if len(matches) > 0 { - return matches[0] +func findCLIPathForTest(t *testing.T) string { + t.Helper() + + if cliPath := os.Getenv("COPILOT_CLI_PATH"); cliPath != "" { + if info, err := os.Stat(cliPath); err == nil && !info.IsDir() { + return cliPath } + t.Fatalf("COPILOT_CLI_PATH does not point to a file: %s", cliPath) + } + + nodeDir, err := filepath.Abs("../nodejs") + if err != nil { + t.Fatal(err) + } + command := exec.Command( + "node", + "node_modules/tsx/dist/cli.mjs", + "scripts/prepare-runtime.ts", + "--print-path", + ) + command.Dir = nodeDir + output, err := command.CombinedOutput() + if err != nil { + t.Fatalf("failed to prepare pinned Copilot CLI: %v\n%s", err, output) + } + cliPath := strings.TrimSpace(string(output)) + if info, err := os.Stat(cliPath); err != nil || info.IsDir() { + t.Fatalf("prepared Copilot CLI path is not a file: %q", cliPath) } - return "" + return cliPath } func TestCreateSessionRequest_ClientName(t *testing.T) { @@ -1922,6 +2377,21 @@ func TestClient_ResumeSession_AllowsMissingPermissionHandler(t *testing.T) { }) } +func TestModelInfoPreservesProviderMetadata(t *testing.T) { + var model ModelInfo + if err := json.Unmarshal([]byte(`{ + "id":"provider/model", + "name":"Provider Model", + "capabilities":{"supports":{},"limits":{}}, + "metadata":{"provider":"acme","contextWindow":200000} + }`), &model); err != nil { + t.Fatalf("unmarshal model info: %v", err) + } + if model.Metadata["provider"] != "acme" || model.Metadata["contextWindow"] != float64(200000) { + t.Fatalf("metadata = %#v", model.Metadata) + } +} + func TestListModelsWithCustomHandler(t *testing.T) { customModels := []ModelInfo{ { @@ -2054,10 +2524,7 @@ func TestListModelsHandlerCachesResults(t *testing.T) { } func TestClient_StartContextCancellationDoesNotKillProcess(t *testing.T) { - cliPath := findCLIPathForTest() - if cliPath == "" { - t.Skip("CLI not found") - } + cliPath := findCLIPathForTest(t) client := NewClient(&ClientOptions{Connection: StdioConnection{Path: cliPath}}) t.Cleanup(func() { client.ForceStop() }) @@ -2080,10 +2547,7 @@ func TestClient_StartContextCancellationDoesNotKillProcess(t *testing.T) { } func TestClient_StartStopRace(t *testing.T) { - cliPath := findCLIPathForTest() - if cliPath == "" { - t.Skip("CLI not found") - } + cliPath := findCLIPathForTest(t) client := NewClient(&ClientOptions{Connection: StdioConnection{Path: cliPath}}) defer client.ForceStop() errChan := make(chan error) @@ -2514,8 +2978,10 @@ func serveInMemoryRuntime(t *testing.T, stdinR *io.PipeReader, stdoutW *io.PipeW result = map[string]any{"id": "interest-1"} case "session.options.update": result = map[string]any{"success": true} - case "session.skills.reload", "session.destroy": + case "session.skills.reload": result = map[string]any{} + case "session.detach": + result = map[string]any{"success": true} default: t.Errorf("unexpected JSON-RPC method %s", request.Method) return @@ -3528,7 +3994,9 @@ func TestClient_ForwardsGitHubTelemetryForwardingOnConnect(t *testing.T) { if err := client.verifyProtocolVersion(t.Context()); err != nil { t.Fatalf("verifyProtocolVersion failed: %v", err) } - assertForwardingFlagTrue(t, <-connectParams) + params := <-connectParams + assertForwardingFlagTrue(t, params) + assertSupportedTaskKinds(t, params) } func TestClient_OmitsGitHubTelemetryForwardingOnConnectWhenNoHandler(t *testing.T) { @@ -3554,6 +4022,20 @@ func TestClient_OmitsGitHubTelemetryForwardingOnConnectWhenNoHandler(t *testing. assertForwardingFlagAbsent(t, <-connectParams) } +func assertSupportedTaskKinds(t *testing.T, params json.RawMessage) { + t.Helper() + var decoded struct { + SupportedTaskKinds []rpc.TaskKind `json:"supportedTaskKinds"` + } + if err := json.Unmarshal(params, &decoded); err != nil { + t.Fatalf("unmarshal connect params: %v", err) + } + expected := []rpc.TaskKind{rpc.TaskKindAgent, rpc.TaskKindClient, rpc.TaskKindShell} + if !reflect.DeepEqual(decoded.SupportedTaskKinds, expected) { + t.Fatalf("supportedTaskKinds = %v, want %v", decoded.SupportedTaskKinds, expected) + } +} + func TestGitHubTelemetryNotificationRoutesToCallback(t *testing.T) { // The runtime forwards telemetry via a JSON-RPC *notification* (no id). // Drive a real Content-Length-framed notification through the transport and diff --git a/go/cmd/bundler/main.go b/go/cmd/bundler/main.go index 89f99daf1b..41b5863347 100644 --- a/go/cmd/bundler/main.go +++ b/go/cmd/bundler/main.go @@ -8,7 +8,7 @@ // --platform: Target platform using Go conventions (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, windows/arm64). Defaults to current platform. // --output: Output directory for embedded artifacts. Defaults to the current directory. // --cli-version: CLI version to download. If not specified, automatically detects from the copilot-sdk version in go.mod. -// --check-only: Check that embedded CLI version matches the detected version from package-lock.json without downloading. Exits with error if versions don't match. +// --check-only: Check that embedded CLI version matches the detected version from package.json without downloading. Exits with error if versions don't match. package main import ( @@ -30,33 +30,35 @@ import ( "regexp" "runtime" "strings" + "time" "github.com/klauspost/compress/zstd" ) const ( // Keep these URLs centralized so reviewers can verify all outbound calls in one place. - sdkModule = "github.com/github/copilot-sdk/go" - packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" - tarballURLFmt = "https://registry.npmjs.org/@github/copilot-%s/-/copilot-%s-%s.tgz" - licenseTarballFmt = "https://registry.npmjs.org/@github/copilot/-/copilot-%s.tgz" - defaultPackageName = "main" + sdkModule = "github.com/github/copilot-sdk/go" + packageJSONURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package.json" + packageLockURLFmt = "https://raw.githubusercontent.com/github/copilot-sdk/%s/nodejs/package-lock.json" + defaultCLIDownloadBaseURL = "https://github.com/github/copilot-cli/releases/download" + cliDownloadBaseURLEnvironment = "COPILOT_CLI_DOWNLOAD_BASE_URL" + defaultPackageName = "main" ) -// Platform info: npm package suffix, binary name +// Platform info: release asset platform suffix, binary name type platformInfo struct { - npmPlatform string - binaryName string + runtimePlatform string + binaryName string } -// Map from GOOS/GOARCH to npm platform info +// Map from GOOS/GOARCH to release asset platform info. var platforms = map[string]platformInfo{ - "linux/amd64": {npmPlatform: "linux-x64", binaryName: "copilot"}, - "linux/arm64": {npmPlatform: "linux-arm64", binaryName: "copilot"}, - "darwin/amd64": {npmPlatform: "darwin-x64", binaryName: "copilot"}, - "darwin/arm64": {npmPlatform: "darwin-arm64", binaryName: "copilot"}, - "windows/amd64": {npmPlatform: "win32-x64", binaryName: "copilot.exe"}, - "windows/arm64": {npmPlatform: "win32-arm64", binaryName: "copilot.exe"}, + "linux/amd64": {runtimePlatform: "linux-x64", binaryName: "copilot"}, + "linux/arm64": {runtimePlatform: "linux-arm64", binaryName: "copilot"}, + "darwin/amd64": {runtimePlatform: "darwin-x64", binaryName: "copilot"}, + "darwin/arm64": {runtimePlatform: "darwin-arm64", binaryName: "copilot"}, + "windows/amd64": {runtimePlatform: "win32-x64", binaryName: "copilot.exe"}, + "windows/arm64": {runtimePlatform: "win32-arm64", binaryName: "copilot.exe"}, } // main is the CLI entry point. @@ -69,7 +71,7 @@ func main() { // Resolve version first so the default output name can include it. version := resolveCLIVersion(*cliVersion) - // Resolve platform once to validate input and get the npm package mapping. + // Resolve platform once to validate input and get the release asset mapping. goos, goarch, info, err := resolvePlatform(*platform) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) @@ -100,7 +102,7 @@ func main() { fmt.Printf("Building bundle for %s (CLI version %s)\n", *platform, version) - bundle, err := buildBundle(info, version, outputPath, goos) + bundle, err := buildBundle(info, version, outputPath, goos, true) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) @@ -109,8 +111,8 @@ func main() { var muslBundle bundleArtifacts if goos == "linux" { muslInfo := platformInfo{ - npmPlatform: strings.Replace(info.npmPlatform, "linux-", "linuxmusl-", 1), - binaryName: info.binaryName, + runtimePlatform: strings.Replace(info.runtimePlatform, "linux-", "linuxmusl-", 1), + binaryName: info.binaryName, } muslOutputPath := filepath.Join(*output, defaultOutputFileName(version, "linuxmusl", goarch, info.binaryName)) muslBundle, err = buildBundle( @@ -118,17 +120,13 @@ func main() { version, muslOutputPath, goos, + false, ) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } - if err := downloadCLILicense(version, outputPath); err != nil { - fmt.Fprintf(os.Stderr, "Error: failed to download CLI license: %v\n", err) - os.Exit(1) - } - // Generate the Go file with embed directive if err := generateGoFile( goos, @@ -262,8 +260,8 @@ func detectPackageName(dir, goos, goarch string) (string, error) { // detectCLIVersion detects the CLI version by: // 1. Running "go list -m" to get the copilot-sdk version from the user's go.mod -// 2. Fetching the package-lock.json from the SDK repo at that version -// 3. Extracting the @github/copilot CLI version from it +// 2. Fetching package.json from the SDK repo at that version +// 3. Extracting the pinned Copilot CLI version from it func detectCLIVersion() (string, error) { // Get the SDK version from the user's go.mod sdkVersion, err := getSDKVersion() @@ -273,7 +271,7 @@ func detectCLIVersion() (string, error) { fmt.Printf("Found copilot-sdk %s in go.mod\n", sdkVersion) - // Fetch package-lock.json from the SDK repo at that version + // Fetch package.json from the SDK repo at that version cliVersion, err := fetchCLIVersionFromRepo(sdkVersion) if err != nil { return "", fmt.Errorf("failed to fetch CLI version: %w", err) @@ -301,7 +299,7 @@ func getSDKVersion() (string, error) { return version, nil } -// fetchCLIVersionFromRepo fetches package-lock.json from GitHub and extracts the CLI version. +// fetchCLIVersionFromRepo fetches package.json from GitHub and extracts the CLI version. func fetchCLIVersionFromRepo(sdkVersion string) (string, error) { // Convert Go module version to Git ref // v0.1.0 -> v0.1.0 @@ -319,7 +317,7 @@ func fetchCLIVersionFromRepo(sdkVersion string) (string, error) { } } - url := fmt.Sprintf(packageLockURLFmt, gitRef) + url := fmt.Sprintf(packageJSONURLFmt, gitRef) fmt.Printf("Fetching %s...\n", url) resp, err := http.Get(url) @@ -329,7 +327,35 @@ func fetchCLIVersionFromRepo(sdkVersion string) (string, error) { defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to fetch package-lock.json: %s", resp.Status) + return "", fmt.Errorf("failed to fetch package.json: %s", resp.Status) + } + + var packageJSON struct { + CopilotCLIVersion string `json:"copilotCliVersion"` + } + + if err := json.NewDecoder(resp.Body).Decode(&packageJSON); err != nil { + return "", fmt.Errorf("failed to parse package.json: %w", err) + } + + if packageJSON.CopilotCLIVersion == "" { + return fetchLegacyCLIVersionFromRepo(gitRef) + } + + return packageJSON.CopilotCLIVersion, nil +} + +func fetchLegacyCLIVersionFromRepo(gitRef string) (string, error) { + url := fmt.Sprintf(packageLockURLFmt, gitRef) + fmt.Printf("Falling back to %s...\n", url) + + resp, err := http.Get(url) + if err != nil { + return "", fmt.Errorf("failed to fetch legacy package-lock.json: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to fetch legacy package-lock.json: %s", resp.Status) } var packageLock struct { @@ -337,16 +363,13 @@ func fetchCLIVersionFromRepo(sdkVersion string) (string, error) { Version string `json:"version"` } `json:"packages"` } - if err := json.NewDecoder(resp.Body).Decode(&packageLock); err != nil { - return "", fmt.Errorf("failed to parse package-lock.json: %w", err) + return "", fmt.Errorf("failed to parse legacy package-lock.json: %w", err) } - pkg, ok := packageLock.Packages["node_modules/@github/copilot"] if !ok || pkg.Version == "" { - return "", fmt.Errorf("could not find @github/copilot version in package-lock.json") + return "", fmt.Errorf("could not find copilotCliVersion in package.json or @github/copilot in package-lock.json") } - return pkg.Version, nil } @@ -371,19 +394,23 @@ type bundleArtifacts struct { assetsHash []byte } -// buildBundle downloads the CLI and native runtime artifacts from one platform package. -func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (bundleArtifacts, error) { +// buildBundle downloads the CLI and native runtime artifacts from one release package. +func buildBundle(info platformInfo, cliVersion, outputPath, goos string, includeLicense bool) (bundleArtifacts, error) { outputDir := filepath.Dir(outputPath) if outputDir == "" { outputDir = "." } - runtimeArtifactPath := filepath.Join(outputDir, runtimeLibArtifactName(cliVersion, info.npmPlatform, goos)) - wrapperArtifactPath := filepath.Join(outputDir, runtimeWrapperArtifactName(cliVersion, info.npmPlatform, info.binaryName)) - assetsArtifactPath := filepath.Join(outputDir, runtimeAssetsArtifactName(cliVersion, info.npmPlatform)) + runtimeArtifactPath := filepath.Join(outputDir, runtimeLibArtifactName(cliVersion, info.runtimePlatform, goos)) + wrapperArtifactPath := filepath.Join(outputDir, runtimeWrapperArtifactName(cliVersion, info.runtimePlatform, info.binaryName)) + assetsArtifactPath := filepath.Join(outputDir, runtimeAssetsArtifactName(cliVersion, info.runtimePlatform)) + requiredPaths := []string{outputPath, runtimeArtifactPath, wrapperArtifactPath, assetsArtifactPath} + if includeLicense { + requiredPaths = append(requiredPaths, licensePathForOutput(outputPath)) + } - if filesExist(outputPath, runtimeArtifactPath, wrapperArtifactPath, assetsArtifactPath) { + if filesExist(requiredPaths...) { // Idempotent output avoids re-downloading in CI or local rebuilds. - fmt.Printf("Output runtime bundle for %s already exists, skipping download\n", info.npmPlatform) + fmt.Printf("Output runtime bundle for %s already exists, skipping download\n", info.runtimePlatform) binaryHash, err := sha256FileFromCompressed(outputPath) if err != nil { return bundleArtifacts{}, fmt.Errorf("failed to hash existing output: %w", err) @@ -410,7 +437,7 @@ func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (bundle } defer os.RemoveAll(tempDir) - binaryPath, tarballPath, err := downloadCLIBinary(info.npmPlatform, info.binaryName, cliVersion, tempDir) + binaryPath, tarballPath, err := downloadCLIBinary(info.runtimePlatform, info.binaryName, cliVersion, tempDir) if err != nil { return bundleArtifacts{}, fmt.Errorf("failed to download CLI binary: %w", err) } @@ -420,6 +447,11 @@ func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (bundle return bundleArtifacts{}, fmt.Errorf("failed to create output directory: %w", err) } } + if includeLicense { + if err := extractCLILicense(tarballPath, outputPath); err != nil { + return bundleArtifacts{}, fmt.Errorf("failed to extract CLI license: %w", err) + } + } binaryHash, err := sha256File(binaryPath) if err != nil { @@ -433,10 +465,10 @@ func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (bundle if err := extractFileFromTarball( tarballPath, tempDir, - "package/prebuilds/"+info.npmPlatform+"/runtime.node", + "package/prebuilds/"+info.runtimePlatform+"/runtime.node", "runtime.node", ); err != nil { - return bundleArtifacts{}, fmt.Errorf("runtime package is missing prebuilds/%s/runtime.node: %w", info.npmPlatform, err) + return bundleArtifacts{}, fmt.Errorf("runtime package is missing prebuilds/%s/runtime.node: %w", info.runtimePlatform, err) } runtimeHash, err := sha256File(rawLibPath) if err != nil { @@ -451,10 +483,10 @@ func buildBundle(info platformInfo, cliVersion, outputPath, goos string) (bundle if err := extractFileFromTarball( tarballPath, tempDir, - "package/prebuilds/"+info.npmPlatform+"/"+wrapperName, + "package/prebuilds/"+info.runtimePlatform+"/"+wrapperName, wrapperName, ); err != nil { - return bundleArtifacts{}, fmt.Errorf("runtime package is missing prebuilds/%s/%s: %w", info.npmPlatform, wrapperName, err) + return bundleArtifacts{}, fmt.Errorf("runtime package is missing prebuilds/%s/%s: %w", info.runtimePlatform, wrapperName, err) } wrapperHash, err := sha256File(rawWrapperPath) if err != nil { @@ -488,16 +520,16 @@ func filesExist(paths ...string) bool { } // runtimeLibArtifactName builds the compressed runtime-library artifact filename. -func runtimeLibArtifactName(version, npmPlatform, goos string) string { - return fmt.Sprintf("zcopilotruntime_%s_%s.%s.zst", version, npmPlatform, runtimeLibExt(goos)) +func runtimeLibArtifactName(version, runtimePlatform, goos string) string { + return fmt.Sprintf("zcopilotruntime_%s_%s.%s.zst", version, runtimePlatform, runtimeLibExt(goos)) } -func runtimeWrapperArtifactName(version, npmPlatform, binaryName string) string { - return fmt.Sprintf("zcopilotruntimewrapper_%s_%s_%s.zst", version, npmPlatform, runtimeWrapperName(binaryName)) +func runtimeWrapperArtifactName(version, runtimePlatform, binaryName string) string { + return fmt.Sprintf("zcopilotruntimewrapper_%s_%s_%s.zst", version, runtimePlatform, runtimeWrapperName(binaryName)) } -func runtimeAssetsArtifactName(version, npmPlatform string) string { - return fmt.Sprintf("zcopilotruntimeassets_%s_%s.tgz", version, npmPlatform) +func runtimeAssetsArtifactName(version, runtimePlatform string) string { + return fmt.Sprintf("zcopilotruntimeassets_%s_%s.tgz", version, runtimePlatform) } func runtimeWrapperName(binaryName string) string { @@ -509,12 +541,12 @@ func runtimeWrapperName(binaryName string) string { var hostlessExcludedTopLevel = map[string]bool{ "app.js": true, "assets": true, "changelog.json": true, "copilot": true, "copilot.exe": true, - "copilot-sdk": true, "foundry-local-sdk": true, "index.js": true, "napi-oop-runtime": true, - "LICENSE.md": true, "npm-loader.js": true, "package.json": true, "preloads": true, "pvrecorder": true, - "queries": true, "README.md": true, "sdk": true, "sea-loader.js": true, "webview": true, + "foundry-local-sdk": true, "index.js": true, "napi-oop-runtime": true, "LICENSE.md": true, + "npm-loader.js": true, "package.json": true, "pvrecorder": true, "queries": true, "README.md": true, + "sea-loader.js": true, "webview": true, } -func hostlessRuntimePath(name, npmPlatform, wrapperName string) (string, bool) { +func hostlessRuntimePath(name, runtimePlatform, wrapperName string) (string, bool) { relative, ok := strings.CutPrefix(name, "package/") if !ok { return "", false @@ -535,7 +567,7 @@ func hostlessRuntimePath(name, npmPlatform, wrapperName string) (string, bool) { } } if topLevel == "prebuilds" { - if len(parts) < 3 || parts[1] != npmPlatform { + if len(parts) < 3 || parts[1] != runtimePlatform { return "", false } return strings.Join(parts[2:], "/"), true @@ -576,7 +608,7 @@ func createRuntimeAssetsArchive(tarballPath, outputPath string, info platformInf } destination, include := hostlessRuntimePath( header.Name, - info.npmPlatform, + info.runtimePlatform, runtimeWrapperName(info.binaryName), ) if !include { @@ -892,15 +924,82 @@ func mustDecodeBase64(s string) []byte { `, buildConstraint, pkgName, binaryName, licenseName, runtimeEmbed, muslEmbed, cliVersion, hashBase64, runtimeConfig, muslConfig, runtimeReader, muslReaders) } -// downloadCLIBinary downloads the npm tarball and extracts the CLI binary. It +var ( + releaseChecksumCache = map[string]map[string]string{} + releaseHTTPClient = &http.Client{Timeout: 10 * time.Minute} +) + +func cliDownloadBaseURL() string { + if override := strings.TrimRight(os.Getenv(cliDownloadBaseURLEnvironment), "/"); override != "" { + return override + } + return defaultCLIDownloadBaseURL +} + +func releaseAssetName(version, runtimePlatform string) string { + return fmt.Sprintf("github-copilot-%s-%s.tgz", version, runtimePlatform) +} + +func releaseDownloadURL(version, assetName string) string { + return fmt.Sprintf("%s/v%s/%s", cliDownloadBaseURL(), version, assetName) +} + +func parseReleaseChecksums(contents string) map[string]string { + checksums := make(map[string]string) + hashPattern := regexp.MustCompile(`^[0-9a-fA-F]{64}$`) + for _, line := range strings.Split(contents, "\n") { + fields := strings.Fields(line) + if len(fields) != 2 || !hashPattern.MatchString(fields[0]) { + continue + } + checksums[strings.TrimPrefix(fields[1], "*")] = strings.ToLower(fields[0]) + } + return checksums +} + +func getReleaseChecksum(version, assetName string) (string, error) { + baseURL := cliDownloadBaseURL() + cacheKey := baseURL + "\x00" + version + checksums, ok := releaseChecksumCache[cacheKey] + if !ok { + checksumsURL := fmt.Sprintf("%s/v%s/SHA256SUMS.txt", baseURL, version) + fmt.Printf("Downloading checksums from %s...\n", checksumsURL) + resp, err := releaseHTTPClient.Get(checksumsURL) + if err != nil { + return "", fmt.Errorf("failed to download checksums: %w", err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to download checksums: %s", resp.Status) + } + contents, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read checksums: %w", err) + } + checksums = parseReleaseChecksums(string(contents)) + releaseChecksumCache[cacheKey] = checksums + } + checksum, ok := checksums[assetName] + if !ok { + return "", fmt.Errorf("SHA256SUMS.txt does not contain %s", assetName) + } + return checksum, nil +} + +// downloadCLIBinary downloads the verified release package and extracts the CLI binary. It // returns the extracted binary path and the downloaded tarball path (retained so // callers can extract additional files, such as the runtime library). -func downloadCLIBinary(npmPlatform, binaryName, cliVersion, destDir string) (string, string, error) { - tarballURL := fmt.Sprintf(tarballURLFmt, npmPlatform, npmPlatform, cliVersion) +func downloadCLIBinary(runtimePlatform, binaryName, cliVersion, destDir string) (string, string, error) { + assetName := releaseAssetName(cliVersion, runtimePlatform) + expectedChecksum, err := getReleaseChecksum(cliVersion, assetName) + if err != nil { + return "", "", err + } + tarballURL := releaseDownloadURL(cliVersion, assetName) fmt.Printf("Downloading from %s...\n", tarballURL) - resp, err := http.Get(tarballURL) + resp, err := releaseHTTPClient.Get(tarballURL) if err != nil { return "", "", fmt.Errorf("failed to download: %w", err) } @@ -911,24 +1010,43 @@ func downloadCLIBinary(npmPlatform, binaryName, cliVersion, destDir string) (str } // Save tarball to temp file - tarballPath := filepath.Join(destDir, fmt.Sprintf("copilot-%s-%s.tgz", npmPlatform, cliVersion)) + tarballPath := filepath.Join(destDir, assetName) tarballFile, err := os.Create(tarballPath) if err != nil { return "", "", fmt.Errorf("failed to create tarball file: %w", err) } - if _, err := io.Copy(tarballFile, resp.Body); err != nil { + hasher := sha256.New() + if _, err := io.Copy(io.MultiWriter(tarballFile, hasher), resp.Body); err != nil { tarballFile.Close() return "", "", fmt.Errorf("failed to save tarball: %w", err) } if err := tarballFile.Close(); err != nil { return "", "", fmt.Errorf("failed to close tarball file: %w", err) } + actualChecksum := fmt.Sprintf("%x", hasher.Sum(nil)) + if actualChecksum != expectedChecksum { + return "", "", fmt.Errorf( + "checksum mismatch for %s: expected %s, got %s", + assetName, + expectedChecksum, + actualChecksum, + ) + } - // Extract only the CLI binary to avoid unpacking the full package tree. + // The SDK release package intentionally omits the legacy SEA binary. Preserve + // embeddedcli.Path compatibility by installing the runtime wrapper under the + // historical copilot[.exe] name; the normal client path uses the adjacent + // wrapper/runtime.node pair directly. binaryPath := filepath.Join(destDir, binaryName) - if err := extractFileFromTarball(tarballPath, destDir, "package/"+binaryName, binaryName); err != nil { - return "", "", fmt.Errorf("failed to extract binary: %w", err) + wrapperName := runtimeWrapperName(binaryName) + if err := extractFileFromTarball( + tarballPath, + destDir, + "package/prebuilds/"+runtimePlatform+"/"+wrapperName, + binaryName, + ); err != nil { + return "", "", fmt.Errorf("failed to extract runtime wrapper compatibility entrypoint: %w", err) } // Verify binary exists @@ -953,8 +1071,8 @@ func downloadCLIBinary(npmPlatform, binaryName, cliVersion, destDir string) (str return binaryPath, tarballPath, nil } -// downloadCLILicense downloads the @github/copilot package and writes its license next to outputPath. -func downloadCLILicense(cliVersion, outputPath string) error { +// extractCLILicense writes the license from the verified release package next to outputPath. +func extractCLILicense(tarballPath, outputPath string) error { outputDir := filepath.Dir(outputPath) if outputDir == "" { outputDir = "." @@ -964,18 +1082,13 @@ func downloadCLILicense(cliVersion, outputPath string) error { return nil } - licenseURL := fmt.Sprintf(licenseTarballFmt, cliVersion) - resp, err := http.Get(licenseURL) + source, err := os.Open(tarballPath) if err != nil { - return fmt.Errorf("failed to download license tarball: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to download license tarball: %s", resp.Status) + return fmt.Errorf("failed to open release package: %w", err) } + defer source.Close() - gzReader, err := gzip.NewReader(resp.Body) + gzReader, err := gzip.NewReader(source) if err != nil { return fmt.Errorf("failed to create gzip reader: %w", err) } diff --git a/go/cmd/bundler/main_test.go b/go/cmd/bundler/main_test.go index f41afe43f5..7dd376d40b 100644 --- a/go/cmd/bundler/main_test.go +++ b/go/cmd/bundler/main_test.go @@ -4,9 +4,13 @@ import ( "archive/tar" "bytes" "compress/gzip" + "crypto/sha256" + "fmt" "go/parser" "go/token" "io" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" @@ -22,20 +26,27 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t "package/prebuilds/linux-x64/copilot-runtime": "wrapper", "package/ripgrep/bin/linux-x64/rg": "ripgrep", "package/definitions/future.json": "{}", + "package/copilot-sdk/extension.js": "extension", + "package/preloads/extension_bootstrap.mjs": "preload", + "package/sdk/factory.js": "factory", "package/app.js": "excluded", "package/LICENSE.md": "excluded", "package/README.md": "excluded", }) if err := createRuntimeAssetsArchive(source, output, platformInfo{ - npmPlatform: "linux-x64", - binaryName: "copilot", + runtimePlatform: "linux-x64", + binaryName: "copilot", }); err != nil { t.Fatal(err) } files := readTarGz(t, output) - if files["ripgrep/bin/linux-x64/rg"] != "ripgrep" || files["definitions/future.json"] != "{}" { + if files["ripgrep/bin/linux-x64/rg"] != "ripgrep" || + files["definitions/future.json"] != "{}" || + files["copilot-sdk/extension.js"] != "extension" || + files["preloads/extension_bootstrap.mjs"] != "preload" || + files["sdk/factory.js"] != "factory" { t.Fatalf("retained assets = %#v", files) } for _, excluded := range []string{ @@ -47,6 +58,93 @@ func TestCreateRuntimeAssetsArchiveRetainsUnknownAssetsAndFiltersCLIContent(t *t } } +func TestDownloadCLIBinaryUsesVerifiedReleasePackage(t *testing.T) { + dir := t.TempDir() + archivePath := filepath.Join(dir, "source.tgz") + writeTarGz(t, archivePath, map[string]string{ + "package/prebuilds/linux-x64/copilot-runtime": "runtime wrapper", + }) + archive, err := os.ReadFile(archivePath) + if err != nil { + t.Fatal(err) + } + checksum := fmt.Sprintf("%x", sha256.Sum256(archive)) + version := "1.2.3" + assetName := releaseAssetName(version, "linux-x64") + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + switch request.URL.Path { + case "/v1.2.3/SHA256SUMS.txt": + fmt.Fprintf(writer, "%s %s\n", checksum, assetName) + case "/v1.2.3/" + assetName: + writer.Write(archive) + default: + http.NotFound(writer, request) + } + })) + defer server.Close() + t.Setenv(cliDownloadBaseURLEnvironment, server.URL) + releaseChecksumCache = map[string]map[string]string{} + + binaryPath, downloadedArchive, err := downloadCLIBinary( + "linux-x64", + "copilot", + version, + t.TempDir(), + ) + if err != nil { + t.Fatal(err) + } + if got, err := os.ReadFile(binaryPath); err != nil || string(got) != "runtime wrapper" { + t.Fatalf("downloaded CLI = %q, %v", got, err) + } + if filepath.Base(downloadedArchive) != assetName { + t.Fatalf("downloaded archive = %q, want basename %q", downloadedArchive, assetName) + } +} + +func TestDownloadCLIBinaryRejectsChecksumMismatch(t *testing.T) { + dir := t.TempDir() + archivePath := filepath.Join(dir, "source.tgz") + writeTarGz(t, archivePath, map[string]string{ + "package/prebuilds/linux-x64/copilot-runtime": "runtime wrapper", + }) + archive, err := os.ReadFile(archivePath) + if err != nil { + t.Fatal(err) + } + version := "1.2.3" + assetName := releaseAssetName(version, "linux-x64") + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + switch request.URL.Path { + case "/v1.2.3/SHA256SUMS.txt": + fmt.Fprintf(writer, "%s %s\n", strings.Repeat("0", 64), assetName) + case "/v1.2.3/" + assetName: + _, _ = writer.Write(archive) + default: + http.NotFound(writer, request) + } + })) + defer server.Close() + t.Setenv(cliDownloadBaseURLEnvironment, server.URL) + releaseChecksumCache = map[string]map[string]string{} + + _, _, err = downloadCLIBinary("linux-x64", "copilot", version, t.TempDir()) + if err == nil || !strings.Contains(err.Error(), "checksum mismatch") { + t.Fatalf("downloadCLIBinary() error = %v, want checksum mismatch", err) + } +} + +func TestParseReleaseChecksums(t *testing.T) { + hash := strings.Repeat("a", 64) + checksums := parseReleaseChecksums( + "invalid\n" + + strings.ToUpper(hash) + " *github-copilot-1.2.3-linux-x64.tgz\n", + ) + if got := checksums["github-copilot-1.2.3-linux-x64.tgz"]; got != hash { + t.Fatalf("checksum = %q, want %q", got, hash) + } +} + func writeTarGz(t *testing.T, path string, files map[string]string) { t.Helper() var buffer bytes.Buffer diff --git a/go/github_token_provider_test.go b/go/github_token_provider_test.go index f00837379f..51e8f3f7f0 100644 --- a/go/github_token_provider_test.go +++ b/go/github_token_provider_test.go @@ -48,8 +48,8 @@ func TestGitHubTokenProviderCreateRequestAndCallback(t *testing.T) { sessionID := sessionIDFromParams(t, params) return []byte(`{"sessionId":"` + sessionID + `","workspacePath":"/workspace"}`), nil }) - server.SetRequestHandler("session.destroy", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - return []byte(`{}`), nil + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return []byte(`{"success":true}`), nil }) var gotArgs GitHubTokenProviderArgs @@ -201,8 +201,8 @@ func TestGitHubTokenStringRedactsAccessToken(t *testing.T) { func TestGitHubTokenProviderCleanupOnDisconnectError(t *testing.T) { rpcClient, server, _ := newRuntimeShutdownRpcPair(t) t.Cleanup(server.Stop) - server.SetRequestHandler("session.destroy", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { - return nil, &jsonrpc2.Error{Code: -32000, Message: "destroy failed"} + server.SetRequestHandler("session.detach", func(json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + return nil, &jsonrpc2.Error{Code: -32000, Message: "detach failed"} }) client := &Client{} registrationID := client.registerGitHubTokenProvider(func(GitHubTokenProviderArgs) (*GitHubTokenProviderResult, error) { @@ -213,7 +213,7 @@ func TestGitHubTokenProviderCleanupOnDisconnectError(t *testing.T) { client.unregisterGitHubTokenProvider(registrationID) }) - if err := session.Disconnect(); err == nil || !strings.Contains(err.Error(), "destroy failed") { + if err := session.Disconnect(); err == nil || !strings.Contains(err.Error(), "detach failed") { t.Fatalf("Disconnect error = %v", err) } if len(client.gitHubTokenProviders) != 0 { diff --git a/go/internal/e2e/auto_tier_e2e_test.go b/go/internal/e2e/auto_tier_e2e_test.go new file mode 100644 index 0000000000..e974f95927 --- /dev/null +++ b/go/internal/e2e/auto_tier_e2e_test.go @@ -0,0 +1,140 @@ +package e2e + +import ( + "testing" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" +) + +// Mirrors nodejs/test/e2e/auto_tier.e2e.test.ts (snapshot category "auto_tier"). +// +// The runtime stages an Auto routing preference instead of applying it immediately: a +// request stays unclaimed until a later turn using the "auto" model mints a usable +// model and token pair. These tests observe that staged state through Model.GetCurrent, +// so they assert what the runtime actually recorded rather than what the SDK serialized. +func TestAutoTierE2E(t *testing.T) { + autoTier := func(tier copilot.AutoTier) *copilot.AutoTier { return &tier } + + pendingTier := func(t *testing.T, session *copilot.Session) *rpc.AutoTier { + t.Helper() + current, err := session.RPC.Model.GetCurrent(t.Context()) + if err != nil { + t.Fatalf("Model.GetCurrent failed: %v", err) + } + return current.PendingAutoTier + } + + assertPending := func(t *testing.T, session *copilot.Session, want rpc.AutoTier) { + t.Helper() + got := pendingTier(t, session) + if got == nil || *got != want { + t.Fatalf("Expected pending auto tier %q, got %v", want, got) + } + } + + assertNoPending := func(t *testing.T, session *copilot.Session) { + t.Helper() + if got := pendingTier(t, session); got != nil { + t.Fatalf("Expected no pending auto tier, got %q", *got) + } + } + + newAutoSession := func(t *testing.T) *copilot.Session { + t.Helper() + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + ctx.ConfigureForTest(t) + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + Model: "auto", + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + return session + } + + t.Run("should stage and reset auto tier preference", func(t *testing.T) { + session := newAutoSession(t) + assertNoPending(t, session) + + staged, err := session.SetAutoTier(t.Context(), autoTier(copilot.AutoTierEfficiency)) + if err != nil { + t.Fatalf("SetAutoTier(efficiency) failed: %v", err) + } + if staged.Status != rpc.ModelSwitchAutoTierStatusPending { + t.Fatalf("Expected status pending, got %q", staged.Status) + } + if staged.PendingAutoTier == nil || *staged.PendingAutoTier != rpc.AutoTierEfficiency { + t.Fatalf("Expected pending efficiency in result, got %+v", staged) + } + assertPending(t, session, rpc.AutoTierEfficiency) + + // A second request replaces the first and reports the one it displaced. + superseded, err := session.SetAutoTier(t.Context(), autoTier(copilot.AutoTierIntelligence)) + if err != nil { + t.Fatalf("SetAutoTier(intelligence) failed: %v", err) + } + if superseded.Status != rpc.ModelSwitchAutoTierStatusPending { + t.Fatalf("Expected status pending, got %q", superseded.Status) + } + if superseded.SupersededAutoTier == nil || *superseded.SupersededAutoTier != rpc.AutoTierEfficiency { + t.Fatalf("Expected superseded efficiency, got %+v", superseded) + } + assertPending(t, session, rpc.AutoTierIntelligence) + + // A nil tier returns the session to provider-default routing. The status is + // unchanged because provider-default was already the committed preference; + // the request's effect is cancelling the staged one. + reset, err := session.SetAutoTier(t.Context(), nil) + if err != nil { + t.Fatalf("SetAutoTier(nil) failed: %v", err) + } + if reset.Status != rpc.ModelSwitchAutoTierStatusUnchanged { + t.Fatalf("Expected status unchanged, got %q", reset.Status) + } + if reset.SupersededAutoTier == nil || *reset.SupersededAutoTier != rpc.AutoTierIntelligence { + t.Fatalf("Expected superseded intelligence, got %+v", reset) + } + assertNoPending(t, session) + }) + + t.Run("should preserve auto tier when set model omits it", func(t *testing.T) { + session := newAutoSession(t) + + if _, err := session.SetAutoTier(t.Context(), autoTier(copilot.AutoTierBalance)); err != nil { + t.Fatalf("SetAutoTier(balance) failed: %v", err) + } + assertPending(t, session, rpc.AutoTierBalance) + + // Leaving AutoTier nil without asking for a reset leaves the staged preference alone. + if err := session.SetModel(t.Context(), "auto", nil); err != nil { + t.Fatalf("SetModel without options failed: %v", err) + } + assertPending(t, session, rpc.AutoTierBalance) + + // Supplying a tier replaces it. + if err := session.SetModel(t.Context(), "auto", &copilot.SetModelOptions{ + AutoTier: autoTier(copilot.AutoTierIntelligence), + }); err != nil { + t.Fatalf("SetModel with AutoTier failed: %v", err) + } + assertPending(t, session, rpc.AutoTierIntelligence) + + // ResetAutoTier clears it. Omission, a value, and a reset are three distinct + // outcomes, which is why a single nillable field cannot express the request. + if err := session.SetModel(t.Context(), "auto", &copilot.SetModelOptions{ + ResetAutoTier: true, + }); err != nil { + t.Fatalf("SetModel with ResetAutoTier failed: %v", err) + } + assertNoPending(t, session) + }) +} diff --git a/go/internal/e2e/client_options_e2e_test.go b/go/internal/e2e/client_options_e2e_test.go index 86332eb6f1..54461443ca 100644 --- a/go/internal/e2e/client_options_e2e_test.go +++ b/go/internal/e2e/client_options_e2e_test.go @@ -329,7 +329,7 @@ func TestClientOptionsE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ SessionID: sessionID, ClientName: "go-sdk-e2e-client", - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", ReasoningEffort: "low", ReasoningSummary: copilot.ReasoningSummaryNone, ContextTier: copilot.ContextTierLongContext, @@ -388,7 +388,7 @@ func TestClientOptionsE2E(t *testing.T) { expectedValues := map[string]any{ "sessionId": sessionID, "clientName": "go-sdk-e2e-client", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "reasoningEffort": "low", "reasoningSummary": "none", "contextTier": "long_context", @@ -869,6 +869,10 @@ function handleMessage(message) { writeResponse(message.id, { sessionId, workspacePath: null, capabilities: null }); return; } + if (message.method === "session.detach") { + writeResponse(message.id, { success: true }); + return; + } if (message.method === "session.resume") { const sessionId = (message.params && message.params.sessionId) || "fake-session"; writeResponse(message.id, { sessionId, workspacePath: null, capabilities: null }); diff --git a/go/internal/e2e/copilot_request_helpers_test.go b/go/internal/e2e/copilot_request_helpers_test.go index 81d14f4d94..cd1ac63cf7 100644 --- a/go/internal/e2e/copilot_request_helpers_test.go +++ b/go/internal/e2e/copilot_request_helpers_test.go @@ -47,8 +47,8 @@ func sseFrame(eventType string, data map[string]any) string { func modelCatalogJSON(supportedEndpoints []string) string { model := map[string]any{ - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -56,7 +56,7 @@ func modelCatalogJSON(supportedEndpoints []string) string { "model_picker_enabled": true, "capabilities": map[string]any{ "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": map[string]any{ "max_context_window_tokens": 200000, @@ -141,7 +141,7 @@ func buildAnthropicMessageSSEBody(text string) string { "type": "message_start", "message": map[string]any{ "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", "content": []any{}, + "model": "claude-sonnet-5", "content": []any{}, "stop_reason": nil, "stop_sequence": nil, "usage": map[string]any{"input_tokens": 5, "output_tokens": 1}, }, @@ -188,7 +188,7 @@ func buildInferenceResponse(url string, bodyText string) *http.Response { base := func() map[string]any { return map[string]any{ "id": "chatcmpl-stub-1", "object": "chat.completion.chunk", - "created": 1, "model": "claude-sonnet-4.5", + "created": 1, "model": "claude-sonnet-5", } } c1 := base() @@ -215,7 +215,7 @@ func buildInferenceResponse(url string, bodyText string) *http.Response { "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": []any{map[string]any{"type": "text", "text": syntheticResponseText}}, "stop_reason": "end_turn", "stop_sequence": nil, @@ -225,7 +225,7 @@ func buildInferenceResponse(url string, bodyText string) *http.Response { } raw, _ := json.Marshal(map[string]any{ - "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, "model": "claude-sonnet-4.5", + "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, "model": "claude-sonnet-5", "choices": []any{map[string]any{"index": 0, "message": map[string]any{"role": "assistant", "content": syntheticResponseText}, "finish_reason": "stop"}}, "usage": map[string]any{"prompt_tokens": 5, "completion_tokens": 7, "total_tokens": 12}, }) diff --git a/go/internal/e2e/copilot_request_session_id_e2e_test.go b/go/internal/e2e/copilot_request_session_id_e2e_test.go index f7673bd457..46a62db1ab 100644 --- a/go/internal/e2e/copilot_request_session_id_e2e_test.go +++ b/go/internal/e2e/copilot_request_session_id_e2e_test.go @@ -139,14 +139,14 @@ func TestCopilotRequestSessionID(t *testing.T) { before := len(transport.inferenceRecords()) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: &copilot.ProviderConfig{ Type: "openai", WireAPI: "responses", BaseURL: "https://byok.invalid/v1", APIKey: "byok-secret", - ModelID: "claude-sonnet-4.5", - WireModel: "claude-sonnet-4.5", + ModelID: "claude-sonnet-5", + WireModel: "claude-sonnet-5", }, }) if err != nil { diff --git a/go/internal/e2e/event_fidelity_e2e_test.go b/go/internal/e2e/event_fidelity_e2e_test.go index e7cc4bfb37..8606da22d9 100644 --- a/go/internal/e2e/event_fidelity_e2e_test.go +++ b/go/internal/e2e/event_fidelity_e2e_test.go @@ -333,8 +333,8 @@ func TestEventFidelityE2E(t *testing.T) { } idleIdx := lastEventFidelityTypeIndex(types, copilot.SessionEventTypeSessionIdle) - if idleIdx != len(types)-1 { - t.Fatalf("Expected session.idle to be last event; idleIdx=%d len=%d types=%v", idleIdx, len(types), types) + if idleIdx < 0 || assistantIdx >= idleIdx { + t.Fatalf("Expected last assistant.message before session.idle; assistantIdx=%d idleIdx=%d types=%v", assistantIdx, idleIdx, types) } }) diff --git a/go/internal/e2e/external_tool_cancellation_e2e_test.go b/go/internal/e2e/external_tool_cancellation_e2e_test.go new file mode 100644 index 0000000000..1c865ca278 --- /dev/null +++ b/go/internal/e2e/external_tool_cancellation_e2e_test.go @@ -0,0 +1,77 @@ +package e2e + +import ( + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" +) + +func TestExternalToolCancellationE2E(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + t.Run("should_cancel_tool_handler_when_session_disconnects", func(t *testing.T) { + ctx.ConfigureForTest(t) + + type ValueParams struct { + Value string `json:"value" jsonschema:"Value to analyze"` + } + toolStarted := make(chan struct{}, 1) + toolCancelled := make(chan struct{}, 1) + releaseTool := make(chan string, 1) + + slowTool := copilot.DefineTool("slow_analysis", "A slow analysis tool that blocks until released", + func(_ ValueParams, inv copilot.ToolInvocation) (string, error) { + select { + case toolStarted <- struct{}{}: + default: + } + select { + case value := <-releaseTool: + return value, nil + case <-inv.TraceContext.Done(): + select { + case toolCancelled <- struct{}{}: + default: + } + return "", inv.TraceContext.Err() + } + }) + slowTool.SkipPermission = true + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Tools: []copilot.Tool{slowTool}, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + t.Cleanup(func() { _ = session.Disconnect() }) + + go func() { + _, _ = session.Send(t.Context(), copilot.MessageOptions{ + Prompt: "Use slow_analysis with value 'test_abort'. Wait for the result.", + }) + }() + + select { + case <-toolStarted: + case <-time.After(60 * time.Second): + t.Fatal("Timed out waiting for tool handler to start") + } + + if err := session.Disconnect(); err != nil { + t.Fatalf("Disconnect failed: %v", err) + } + + select { + case <-toolCancelled: + case <-time.After(60 * time.Second): + t.Fatal("Timed out waiting for tool handler cancellation") + } + + }) +} diff --git a/go/internal/e2e/mcp_oauth_e2e_test.go b/go/internal/e2e/mcp_oauth_e2e_test.go index 95de73eddd..356805371b 100644 --- a/go/internal/e2e/mcp_oauth_e2e_test.go +++ b/go/internal/e2e/mcp_oauth_e2e_test.go @@ -163,16 +163,21 @@ func TestMCPOAuthE2E(t *testing.T) { } t.Cleanup(func() { session.Disconnect() }) + if _, err := session.RPC.MCP.Reload(t.Context()); err != nil { + t.Fatalf("Failed to reload MCP servers: %v", err) + } waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) callWhoami(t, session, serverName, "refresh") callWhoami(t, session, serverName, "upscope") callWhoami(t, session, serverName, "reauth") mu.Lock() + observedReasons = slices.DeleteFunc(observedReasons, func(reason copilot.MCPOauthRequestReason) bool { + return reason == copilot.MCPOauthRequestReasonInitial + }) reasons := append([]copilot.MCPOauthRequestReason(nil), observedReasons...) mu.Unlock() expectedReasons := []copilot.MCPOauthRequestReason{ - copilot.MCPOauthRequestReasonInitial, copilot.MCPOauthRequestReasonRefresh, copilot.MCPOauthRequestReasonUpscope, copilot.MCPOauthRequestReasonRefresh, @@ -256,6 +261,7 @@ func TestMCPOAuthE2E(t *testing.T) { }) t.Run("resolve pending MCP OAuth request through RPC", func(t *testing.T) { + testharness.SkipIfInProcess(t, "blocked on github/copilot-agent-runtime#18961 MCP OAuth connection stall") ctx := testharness.NewTestContext(t) ctx.ConfigureWithoutSnapshot(t) client := ctx.NewClient() diff --git a/go/internal/e2e/rewind_e2e_test.go b/go/internal/e2e/rewind_e2e_test.go index ab73feadda..701e610775 100644 --- a/go/internal/e2e/rewind_e2e_test.go +++ b/go/internal/e2e/rewind_e2e_test.go @@ -32,7 +32,7 @@ func TestRewindE2E(t *testing.T) { t.Fatalf("Failed to create original file: %v", err) } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", EnableFileChangeTracking: copilot.Bool(true), OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) diff --git a/go/internal/e2e/rpc_e2e_test.go b/go/internal/e2e/rpc_e2e_test.go index fcf843814e..4380415701 100644 --- a/go/internal/e2e/rpc_e2e_test.go +++ b/go/internal/e2e/rpc_e2e_test.go @@ -128,7 +128,7 @@ func TestSessionRPCE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -150,7 +150,7 @@ func TestSessionRPCE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) @@ -194,7 +194,7 @@ func TestSessionRPCE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", }) if err != nil { t.Fatalf("Failed to create session: %v", err) diff --git a/go/internal/e2e/rpc_server_e2e_test.go b/go/internal/e2e/rpc_server_e2e_test.go index 6ea9ad6851..f1aa5a19c7 100644 --- a/go/internal/e2e/rpc_server_e2e_test.go +++ b/go/internal/e2e/rpc_server_e2e_test.go @@ -64,12 +64,12 @@ func TestRPCServerE2E(t *testing.T) { if strings.TrimSpace(model.Name) == "" { t.Errorf("Model %q has empty Name", model.ID) } - if model.ID == "claude-sonnet-4.5" { + if model.ID == "claude-sonnet-5" { hasClaude = true } } if !hasClaude { - t.Errorf("Expected models list to contain 'claude-sonnet-4.5'") + t.Errorf("Expected models list to contain 'claude-sonnet-5'") } }) @@ -532,6 +532,7 @@ func TestRPCServerE2E(t *testing.T) { t.Run("should report implemented error when connecting unknown remote session", func(t *testing.T) { ctx := testharness.NewTestContext(t) + ctx.ConfigureWithoutSnapshot(t) client := ctx.NewClient() t.Cleanup(func() { client.ForceStop() }) if err := client.Start(t.Context()); err != nil { diff --git a/go/internal/e2e/rpc_session_state_e2e_test.go b/go/internal/e2e/rpc_session_state_e2e_test.go index 88673a9ba9..f4870dbac6 100644 --- a/go/internal/e2e/rpc_session_state_e2e_test.go +++ b/go/internal/e2e/rpc_session_state_e2e_test.go @@ -26,7 +26,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Run("should call session rpc model getCurrent", func(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { @@ -37,8 +37,8 @@ func TestRPCSessionStateE2E(t *testing.T) { if err != nil { t.Fatalf("Model.GetCurrent failed: %v", err) } - if result.ModelID == nil || *result.ModelID != "claude-sonnet-4.5" { - t.Fatalf("Expected current model claude-sonnet-4.5, got %+v", result) + if result.ModelID == nil || *result.ModelID != "claude-sonnet-5" { + t.Fatalf("Expected current model claude-sonnet-5, got %+v", result) } }) @@ -58,7 +58,7 @@ func TestRPCSessionStateE2E(t *testing.T) { switchCtx.ConfigureForTest(t) session, err := switchClient.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { @@ -485,7 +485,7 @@ func TestRPCSessionStateE2E(t *testing.T) { branch := "rpc-context-" + randomHex(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", WorkingDirectory: firstDirectory, OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) @@ -498,7 +498,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Fatalf("Metadata.Snapshot failed: %v", err) } if initial.SessionID != session.SessionID || initial.CurrentMode != rpc.MetadataSnapshotCurrentModeInteractive || - initial.SelectedModel == nil || *initial.SelectedModel != "claude-sonnet-4.5" || + initial.SelectedModel == nil || *initial.SelectedModel != "claude-sonnet-5" || initial.IsRemote || initial.AlreadyInUse || initial.StartTime.IsZero() || initial.ModifiedTime.IsZero() || initial.Workspace == nil || initial.WorkspacePath == nil || *initial.WorkspacePath == "" { t.Fatalf("Unexpected initial metadata snapshot: %+v", initial) @@ -630,7 +630,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Run("should set reasoning effort and auto name", func(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", OnPermissionRequest: copilot.PermissionHandler.ApproveAll, }) if err != nil { @@ -648,9 +648,9 @@ func TestRPCSessionStateE2E(t *testing.T) { if err != nil { t.Fatalf("Model.GetCurrent failed: %v", err) } - if current.ModelID == nil || *current.ModelID != "claude-sonnet-4.5" || + if current.ModelID == nil || *current.ModelID != "claude-sonnet-5" || current.ReasoningEffort == nil || *current.ReasoningEffort != "high" { - t.Fatalf("Expected current model claude-sonnet-4.5/high, got %+v", current) + t.Fatalf("Expected current model claude-sonnet-5/high, got %+v", current) } autoName := "Auto Session " + randomHex(t) @@ -760,7 +760,7 @@ func TestRPCSessionStateE2E(t *testing.T) { t.Fatal("Expected fresh session to be idle") } - model := "claude-sonnet-4.5" + model := "claude-sonnet-5" contextInfo, err := session.RPC.Metadata.ContextInfo(t.Context(), &rpc.MetadataContextInfoRequest{ PromptTokenLimit: 128000, OutputTokenLimit: 4096, diff --git a/go/internal/e2e/rpc_session_state_extras_e2e_test.go b/go/internal/e2e/rpc_session_state_extras_e2e_test.go index 99dc0b7572..c74ed45cc8 100644 --- a/go/internal/e2e/rpc_session_state_extras_e2e_test.go +++ b/go/internal/e2e/rpc_session_state_extras_e2e_test.go @@ -22,7 +22,7 @@ func TestRpcSessionStateExtras(t *testing.T) { authClient := newAuthenticatedClient(ctx, token) defer authClient.ForceStop() - session := createPortedSession(t, authClient, &copilot.SessionConfig{Model: "claude-sonnet-4.5"}) + session := createPortedSession(t, authClient, &copilot.SessionConfig{Model: "claude-sonnet-5"}) defer session.Disconnect() result, err := session.RPC.Model.List(t.Context()) @@ -38,13 +38,13 @@ func TestRpcSessionStateExtras(t *testing.T) { found := false for _, model := range result.List { data, err := json.Marshal(model) - if err == nil && strings.Contains(string(data), "claude-sonnet-4.5") { + if err == nil && strings.Contains(string(data), "claude-sonnet-5") { found = true break } } if !found { - t.Fatalf("Expected model list to include claude-sonnet-4.5, got %+v", result.List) + t.Fatalf("Expected model list to include claude-sonnet-5, got %+v", result.List) } }) diff --git a/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go b/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go index 81b8471dac..a426f6bdc9 100644 --- a/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go +++ b/go/internal/e2e/rpc_shell_and_fleet_e2e_test.go @@ -30,12 +30,16 @@ func TestRPCShellAndFleetE2E(t *testing.T) { t.Fatalf("Failed to create session: %v", err) } - markerPath := filepath.Join(ctx.WorkDir, "shell-rpc-"+randomHex(t)+".txt") + commandDir := filepath.Join(ctx.WorkDir, "shell-rpc-"+randomHex(t)) + if err := os.Mkdir(commandDir, 0755); err != nil { + t.Fatalf("Failed to create shell command directory: %v", err) + } + markerPath := filepath.Join(commandDir, "marker.txt") const marker = "copilot-sdk-shell-rpc" - cwd := ctx.WorkDir + cwd := commandDir result, err := session.RPC.Shell.Exec(t.Context(), &rpc.ShellExecRequest{ - Command: writeFileCommand(markerPath, marker), + Command: writeFileCommand(filepath.Base(markerPath), marker), Cwd: &cwd, }) if err != nil { @@ -174,11 +178,11 @@ func randomHex(t *testing.T) string { return hex.EncodeToString(buf[:]) } -func writeFileCommand(markerPath, marker string) string { +func writeFileCommand(markerName, marker string) string { if runtime.GOOS == "windows" { - return fmt.Sprintf("powershell -NoLogo -NoProfile -Command \"Set-Content -LiteralPath '%s' -Value '%s'\"", markerPath, marker) + return fmt.Sprintf("echo %s>\"%s\"", marker, markerName) } - return fmt.Sprintf("sh -c \"printf '%%s' '%s' > '%s'\"", marker, markerPath) + return fmt.Sprintf("sh -c \"printf '%%s' '%s' > '%s'\"", marker, markerName) } func waitForFileText(t *testing.T, path, expected string) { diff --git a/go/internal/e2e/session_config_e2e_test.go b/go/internal/e2e/session_config_e2e_test.go index 2ce48e3b33..f1c267e230 100644 --- a/go/internal/e2e/session_config_e2e_test.go +++ b/go/internal/e2e/session_config_e2e_test.go @@ -119,8 +119,8 @@ func createAnthropicProvider() *copilot.ProviderConfig { Type: "anthropic", BaseURL: "https://anthropic-citations.invalid/v1", APIKey: "test-provider-key", - ModelID: "claude-sonnet-4.5", - WireModel: "claude-sonnet-4.5", + ModelID: "claude-sonnet-5", + WireModel: "claude-sonnet-5", } } @@ -184,6 +184,7 @@ func TestSessionConfigE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Model: "claude-sonnet-5", ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(false), @@ -208,7 +209,7 @@ func TestSessionConfigE2E(t *testing.T) { } // Switch vision on - if err := session.SetModel(t.Context(), "claude-sonnet-4.5", &copilot.SetModelOptions{ + if err := session.SetModel(t.Context(), "claude-sonnet-5", &copilot.SetModelOptions{ ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(true), @@ -238,6 +239,7 @@ func TestSessionConfigE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + Model: "claude-sonnet-5", ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(true), @@ -262,7 +264,7 @@ func TestSessionConfigE2E(t *testing.T) { } // Switch vision off - if err := session.SetModel(t.Context(), "claude-sonnet-4.5", &copilot.SetModelOptions{ + if err := session.SetModel(t.Context(), "claude-sonnet-5", &copilot.SetModelOptions{ ModelCapabilities: &copilot.ModelCapabilitiesOverride{ Supports: &copilot.ModelCapabilitiesOverrideSupports{ Vision: copilot.Bool(false), @@ -420,7 +422,7 @@ func TestSessionConfigNewOptionsCopilotRequestE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", EnableCitations: copilot.Bool(true), Provider: createAnthropicProvider(), }) @@ -481,7 +483,7 @@ func TestSessionConfigNewOptionsCopilotRequestE2E(t *testing.T) { session2, err := resumeClient.ResumeSessionWithOptions(t.Context(), session1.SessionID, &copilot.ResumeSessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", EnableCitations: copilot.Bool(true), Provider: createAnthropicProvider(), }) @@ -589,7 +591,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: createProxyProvider(ctx, providerHeaderName, "create-provider-header"), }) if err != nil { @@ -633,7 +635,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { session2, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: createProxyProvider(ctx, providerHeaderName, "resume-provider-header"), }) if err != nil { @@ -677,7 +679,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { maxOutputTokens := 1024 session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ OnPermissionRequest: copilot.PermissionHandler.ApproveAll, - Model: "claude-sonnet-4.5", + Model: "claude-sonnet-5", Provider: &copilot.ProviderConfig{ Type: "openai", BaseURL: ctx.ProxyURL, @@ -719,7 +721,7 @@ func TestSessionConfigExtrasE2E(t *testing.T) { Type: "openai", BaseURL: ctx.ProxyURL, APIKey: "test-provider-key", - ModelID: "claude-sonnet-4.5", + ModelID: "claude-sonnet-5", }, }) if err != nil { @@ -738,8 +740,8 @@ func TestSessionConfigExtrasE2E(t *testing.T) { if len(exchanges) != 1 { t.Fatalf("Expected exactly 1 exchange, got %d", len(exchanges)) } - if exchanges[0].Request.Model != "claude-sonnet-4.5" { - t.Errorf("Expected request model to be 'claude-sonnet-4.5', got %q", exchanges[0].Request.Model) + if exchanges[0].Request.Model != "claude-sonnet-5" { + t.Errorf("Expected request model to be 'claude-sonnet-5', got %q", exchanges[0].Request.Model) } }) diff --git a/go/internal/e2e/session_e2e_test.go b/go/internal/e2e/session_e2e_test.go index cf39b6784c..12550e6e2d 100644 --- a/go/internal/e2e/session_e2e_test.go +++ b/go/internal/e2e/session_e2e_test.go @@ -10,6 +10,8 @@ import ( "testing" "time" + "github.com/google/uuid" + copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" "github.com/github/copilot-sdk/go/rpc" @@ -23,7 +25,7 @@ func TestSessionE2E(t *testing.T) { t.Run("should create and disconnect sessions", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Model: "claude-sonnet-4.5"}) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll, Model: "claude-sonnet-5"}) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -47,8 +49,8 @@ func TestSessionE2E(t *testing.T) { t.Errorf("Expected session.start sessionId to match") } - if !startOk || startData.SelectedModel == nil || *startData.SelectedModel != "claude-sonnet-4.5" { - t.Errorf("Expected selectedModel to be 'claude-sonnet-4.5', got %v", startData) + if !startOk || startData.SelectedModel == nil || *startData.SelectedModel != "claude-sonnet-5" { + t.Errorf("Expected selectedModel to be 'claude-sonnet-5', got %v", startData) } if err := session.Disconnect(); err != nil { @@ -526,6 +528,62 @@ func TestSessionE2E(t *testing.T) { } }) + t.Run("should recover marker after cold resume with explicit session id", func(t *testing.T) { + ctx.ConfigureForTest(t) + + sessionID := "e2e-cold-resume-" + uuid.NewString() + + client1 := ctx.NewClient() + session1, err := client1.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + SessionID: sessionID, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + if session1.SessionID != sessionID { + t.Fatalf("Expected explicit session ID %q, got %q", sessionID, session1.SessionID) + } + + answer, err := session1.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: `Please remember this exact secret marker for later - MARKER-7f3ac21e. Reply with only the single word "Acknowledged".`, + }) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + if ad, ok := answer.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(ad.Content, "Acknowledged") { + t.Errorf("Expected answer to contain 'Acknowledged', got %v", answer.Data) + } + + if err := session1.Disconnect(); err != nil { + t.Fatalf("Failed to disconnect session: %v", err) + } + client1.ForceStop() + + client2 := ctx.NewClient() + defer client2.ForceStop() + + session2, err := client2.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to resume session: %v", err) + } + if session2.SessionID != sessionID { + t.Errorf("Expected resumed session ID to match, got %q vs %q", session2.SessionID, sessionID) + } + + answer2, err := session2.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "What was the exact secret marker I asked you to remember earlier? Reply with only that marker value and nothing else.", + }) + if err != nil { + t.Fatalf("Failed to send message after resume: %v", err) + } + if ad, ok := answer2.Data.(*copilot.AssistantMessageData); !ok || !strings.Contains(ad.Content, "MARKER-7f3ac21e") { + t.Errorf("Expected resumed answer to contain marker, got %v", answer2.Data) + } + }) + t.Run("should throw error when resuming non-existent session", func(t *testing.T) { ctx.ConfigureForTest(t) diff --git a/go/internal/e2e/session_event_loop_leak_e2e_test.go b/go/internal/e2e/session_event_loop_leak_e2e_test.go new file mode 100644 index 0000000000..c6974f74d8 --- /dev/null +++ b/go/internal/e2e/session_event_loop_leak_e2e_test.go @@ -0,0 +1,102 @@ +package e2e + +import ( + "runtime" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" +) + +// TestSessionEventLoopLeakE2E is a regression test for the goroutine leak fixed +// alongside PR #2360: CreateSession/ResumeSession construct a *Session and start +// its event-dispatch goroutine eagerly, before the server confirms the session, +// so the CLI can route session-scoped requests to it while session.create (or +// session.resume) is still being processed. Every failure path must stop that +// goroutine — otherwise each failed call leaks one goroutine forever, since no +// caller ever receives the failed session to Disconnect() it. This test fails +// without the fix and passes with it. +func TestSessionEventLoopLeakE2E(t *testing.T) { + ctx := testharness.NewTestContext(t) + + t.Run("CreateSession failure does not leak the event loop goroutine", func(t *testing.T) { + ctx.ConfigureForTest(t) + + // Redirect the CLI's GitHub API calls at the replaying proxy so an + // invalid per-session token makes the real CLI reject session.create + // with a genuine RPC error (401 Unauthorized), exercising the same + // failure path a real user would hit — not a mocked transport. + client := ctx.NewClient(func(opts *copilot.ClientOptions) { + opts.Env = append(opts.Env, "COPILOT_DEBUG_GITHUB_API_URL="+ctx.ProxyURL) + }) + t.Cleanup(func() { client.ForceStop() }) + + createFailing := func() { + if _, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + GitHubToken: "invalid-token", + }); err == nil { + t.Fatal("expected CreateSession to fail with an invalid token") + } + } + + // Warm up: the first call spawns the CLI subprocess and its steady-state + // goroutines (read loop, etc.), which must not be counted as leaks. + createFailing() + + assertNoGoroutineLeak(t, 20, createFailing) + }) + + t.Run("ResumeSession failure does not leak the event loop goroutine", func(t *testing.T) { + ctx.ConfigureForTest(t) + + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + resumeNonExistent := func() { + if _, err := client.ResumeSession(t.Context(), "non-existent-leak-check-session", &copilot.ResumeSessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }); err == nil { + t.Fatal("expected ResumeSession for a non-existent session to fail") + } + } + + // Warm up: the first call spawns the CLI subprocess and its steady-state + // goroutines (read loop, etc.), which must not be counted as leaks. + resumeNonExistent() + + assertNoGoroutineLeak(t, 20, resumeNonExistent) + }) +} + +// assertNoGoroutineLeak runs fn n times and fails if the goroutine count grows +// roughly in proportion to n afterward, which would indicate one goroutine +// leaked per call rather than transient goroutines that already exited. +func assertNoGoroutineLeak(t *testing.T, n int, fn func()) { + t.Helper() + runtime.GC() + before := runtime.NumGoroutine() + + for i := 0; i < n; i++ { + fn() + } + + // Give any goroutines that exit promptly (but not synchronously with the + // call returning) a brief window to actually terminate before measuring. + deadline := time.Now().Add(2 * time.Second) + var after int + for { + runtime.GC() + after = runtime.NumGoroutine() + if after <= before+3 || time.Now().After(deadline) { + break + } + time.Sleep(50 * time.Millisecond) + } + + t.Logf("goroutines before=%d after=%d (n=%d)", before, after, n) + if after > before+3 { + t.Fatalf("goroutine count grew from %d to %d after %d failed calls; suspected event-loop leak", before, after, n) + } +} diff --git a/go/internal/e2e/testharness/context.go b/go/internal/e2e/testharness/context.go index 037265f9de..060997d264 100644 --- a/go/internal/e2e/testharness/context.go +++ b/go/internal/e2e/testharness/context.go @@ -1,7 +1,9 @@ package testharness import ( + "fmt" "os" + "os/exec" "path/filepath" "regexp" "runtime" @@ -29,15 +31,18 @@ func CLIPath() string { return } - // Look for CLI in sibling nodejs directory's node_modules. As of CLI - // 1.0.64-1 the @github/copilot package is a thin loader; the runnable - // index.js ships in the installed platform package - // (e.g. @github/copilot-linux-x64). - base := RepoPath("nodejs", "node_modules", "@github") - matches, _ := filepath.Glob(filepath.Join(base, "copilot-*", "index.js")) - if len(matches) > 0 { - cliPath = matches[0] - return + npm := "npm" + if runtime.GOOS == "windows" { + npm = "npm.cmd" + } + command := exec.Command(npm, "run", "--silent", "prepare:runtime", "--", "--print-path") + command.Dir = RepoPath("nodejs") + output, err := command.Output() + if err == nil { + candidate := strings.TrimSpace(string(output)) + if info, statErr := os.Stat(candidate); statErr == nil && !info.IsDir() { + cliPath = candidate + } } }) return cliPath @@ -159,6 +164,17 @@ func NewTestContext(t *testing.T) *TestContext { os.RemoveAll(workDir) t.Fatalf("Failed to start proxy: %v", err) } + // Initialize the proxy before any client can start runtime requests. Tests that + // use a snapshot replace this empty configuration before model traffic begins. + dummySnapshotPath := filepath.Join(workDir, "__no_snapshot__.yaml") + if err := proxy.Configure(dummySnapshotPath, workDir); err != nil { + if stopErr := proxy.StopWithOptions(true); stopErr != nil { + t.Logf("Failed to stop proxy after initialization error: %v", stopErr) + } + os.RemoveAll(homeDir) + os.RemoveAll(workDir) + t.Fatalf("Failed to initialize proxy: %v", err) + } if err := proxy.SetCopilotUserByToken(defaultGitHubToken, map[string]interface{}{ "login": "e2e-test-user", "copilot_plan": "individual_pro", @@ -168,7 +184,9 @@ func NewTestContext(t *testing.T) *TestContext { }, "analytics_tracking_id": "e2e-test-tracking-id", }); err != nil { - proxy.StopWithOptions(true) + if stopErr := proxy.StopWithOptions(true); stopErr != nil { + t.Logf("Failed to stop proxy after configuration error: %v", stopErr) + } os.RemoveAll(homeDir) os.RemoveAll(workDir) t.Fatalf("Failed to configure default Copilot user: %v", err) @@ -251,7 +269,9 @@ func (c *TestContext) ConfigureWithoutSnapshot(t *testing.T) { func (c *TestContext) Close(testFailed bool) { c.restoreInProcessEnvironment() if c.proxy != nil { - c.proxy.StopWithOptions(testFailed) + if err := c.proxy.StopWithOptions(testFailed); err != nil { + fmt.Fprintf(os.Stderr, "Failed to stop E2E proxy: %v\n", err) + } } if c.HomeDir != "" { os.RemoveAll(c.HomeDir) diff --git a/go/internal/e2e/testharness/proxy.go b/go/internal/e2e/testharness/proxy.go index 2545882bce..990c066eea 100644 --- a/go/internal/e2e/testharness/proxy.go +++ b/go/internal/e2e/testharness/proxy.go @@ -4,16 +4,22 @@ import ( "bufio" "bytes" "encoding/json" + "errors" "fmt" "io" "net/http" "os" "os/exec" "regexp" + "runtime" + "strconv" "strings" "sync" + "time" ) +const proxyShutdownTimeout = 5 * time.Second + // CapiProxy manages a child process that acts as a replaying proxy to AI endpoints. // It spawns the shared test harness server from test/harness/server.ts. type CapiProxy struct { @@ -118,6 +124,11 @@ func (p *CapiProxy) StopWithOptions(skipWritingCache bool) error { if p.cmd == nil || p.cmd.Process == nil { return nil } + cmd := p.cmd + defer func() { + p.cmd = nil + p.proxyURL = "" + }() // Send stop request to the server if p.proxyURL != "" { @@ -126,20 +137,61 @@ func (p *CapiProxy) StopWithOptions(skipWritingCache bool) error { stopURL += "?skipWritingCache=true" } // Best effort - ignore errors - resp, err := http.Post(stopURL, "application/json", nil) + client := http.Client{Timeout: proxyShutdownTimeout} + resp, err := client.Post(stopURL, "application/json", nil) if err == nil { resp.Body.Close() } } - // Wait for process to exit - p.cmd.Wait() - p.cmd = nil - p.proxyURL = "" + exited := make(chan struct{}, 1) + go func() { + _ = cmd.Wait() + exited <- struct{}{} + }() + if !waitForProcessExit(exited, proxyShutdownTimeout) { + if err := killProcessTree(cmd); err != nil { + return fmt.Errorf("failed to kill proxy process: %w", err) + } + if !waitForProcessExit(exited, proxyShutdownTimeout) { + return fmt.Errorf("proxy process did not exit after being killed") + } + } + return nil +} +func killProcessTree(cmd *exec.Cmd) error { + if runtime.GOOS == "windows" { + taskkill := exec.Command( + "taskkill", + "/PID", + strconv.Itoa(cmd.Process.Pid), + "/T", + "/F", + ) + if err := taskkill.Run(); err == nil { + return nil + } + } + + if err := cmd.Process.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) { + return err + } return nil } +func waitForProcessExit(exited <-chan struct{}, timeout time.Duration) bool { + timer := time.NewTimer(timeout) + defer timer.Stop() + + select { + case <-exited: + return true + case <-timer.C: + return false + } +} + // Configure sends configuration to the proxy. func (p *CapiProxy) Configure(filePath, workDir string) error { p.mu.Lock() diff --git a/go/internal/embeddedcli/embeddedcli.go b/go/internal/embeddedcli/embeddedcli.go index 2535cf5f20..2e3b8add16 100644 --- a/go/internal/embeddedcli/embeddedcli.go +++ b/go/internal/embeddedcli/embeddedcli.go @@ -26,7 +26,7 @@ import ( // when provided, is written next to the installed binary. // // RuntimeExecutable and RuntimeNode form the adjacent out-of-process runtime -// pair. RuntimeAssets is a filtered npm package archive containing auxiliary +// pair. RuntimeAssets is a filtered release package archive containing auxiliary // binaries and resources. RuntimeLib is the same cdylib bytes installed under // the natural platform name for the optional in-process transport. type Config struct { @@ -264,6 +264,13 @@ func installAt(installDir string) (string, error) { if !bytes.Equal(existingHash, config.CliHash) { return "", fmt.Errorf("existing binary hash mismatch") } + if config.RuntimeExecutable != nil { + path, err := installRuntimePair(installDir) + if err != nil { + return "", err + } + runtimePath = path + } if config.RuntimeLib != nil { libPath, err := installRuntimeLib(installDir) if err != nil { @@ -298,6 +305,14 @@ func installAt(installDir string) (string, error) { } } + if config.RuntimeExecutable != nil { + path, err := installRuntimePair(installDir) + if err != nil { + return "", err + } + runtimePath = path + } + // Install the native in-process runtime library (if bundled) next to the CLI. // Fail closed on any hash mismatch; never place unverified native code. if config.RuntimeLib != nil { @@ -305,11 +320,11 @@ func installAt(installDir string) (string, error) { if err != nil { return "", err } - if err := installRuntimeAssets(installDir); err != nil { - return "", err - } runtimeLibPath = libPath } + if err := installRuntimeAssets(installDir); err != nil { + return "", err + } return finalPath, nil } diff --git a/go/internal/embeddedcli/embeddedcli_test.go b/go/internal/embeddedcli/embeddedcli_test.go index 159b6e1505..a56dd8106e 100644 --- a/go/internal/embeddedcli/embeddedcli_test.go +++ b/go/internal/embeddedcli/embeddedcli_test.go @@ -259,6 +259,46 @@ func TestInstallAtWritesBinaryAndLicense(t *testing.T) { } } +func TestInstallAtInstallsRuntimePairAndAssetsWithoutRuntimeLib(t *testing.T) { + resetGlobals() + tempDir := t.TempDir() + wrapper := []byte("wrapper") + node := []byte("runtime") + assets := runtimeAssetsArchive(t, map[string]assetFixture{ + "definitions/future.json": {content: []byte("{}"), mode: 0644}, + }) + wrapperHash := sha256.Sum256(wrapper) + nodeHash := sha256.Sum256(node) + assetsHash := sha256.Sum256(assets) + Setup(Config{ + Cli: bytes.NewReader(wrapper), + CliHash: wrapperHash[:], + RuntimeExecutable: bytes.NewReader(wrapper), + RuntimeExecutableHash: wrapperHash[:], + RuntimeNode: bytes.NewReader(node), + RuntimeNodeHash: nodeHash[:], + RuntimeAssets: bytes.NewReader(assets), + RuntimeAssetsHash: assetsHash[:], + Version: "1.2.3", + Dir: tempDir, + }) + + path, err := installAt(tempDir) + if err != nil { + t.Fatal(err) + } + installDir := filepath.Dir(path) + if got, err := os.ReadFile(filepath.Join(installDir, runtimeExecutableName())); err != nil || !bytes.Equal(got, wrapper) { + t.Fatalf("runtime wrapper content=%q err=%v", got, err) + } + if got, err := os.ReadFile(filepath.Join(installDir, "runtime.node")); err != nil || !bytes.Equal(got, node) { + t.Fatalf("runtime.node content=%q err=%v", got, err) + } + if got, err := os.ReadFile(filepath.Join(installDir, "definitions", "future.json")); err != nil || string(got) != "{}" { + t.Fatalf("definition content=%q err=%v", got, err) + } +} + func TestInstallAtExistingBinaryHashMismatch(t *testing.T) { resetGlobals() tempDir := t.TempDir() diff --git a/go/rpc/generated_rpc_api_shape_test.go b/go/rpc/generated_rpc_api_shape_test.go index 05774f4847..dcfe0feb8d 100644 --- a/go/rpc/generated_rpc_api_shape_test.go +++ b/go/rpc/generated_rpc_api_shape_test.go @@ -40,6 +40,9 @@ func TestGeneratedRPCAPIShape(t *testing.T) { assertStructFieldType(t, file, fileSet, "MCPConfigUpdateRequest", "Config", "MCPSerializableServerConfig") assertStructFieldType(t, file, fileSet, "MCPServerConfigHTTP", "FilterMapping", "FilterMapping") assertStructFieldType(t, file, fileSet, "MCPServerConfigStdio", "FilterMapping", "FilterMapping") + assertStructFieldType(t, file, fileSet, "ModelSwitchToRequest", "AutoTier", "**AutoTier") + assertStructFieldType(t, file, fileSet, "TaskClientUpdateProgress", "Percentage", "**float64") + assertStructFieldType(t, file, fileSet, "TaskClientUpdateProgress", "Phase", "**string") assertInterfaceType(t, file, "UIElicitationFieldValue") assertTypeExpr(t, fileSet, findTypeSpec(t, file, "UIElicitationStringArrayValue").Type, "[]string") diff --git a/go/rpc/generated_rpc_union_test.go b/go/rpc/generated_rpc_union_test.go index 92bcb4c077..64d723f21a 100644 --- a/go/rpc/generated_rpc_union_test.go +++ b/go/rpc/generated_rpc_union_test.go @@ -14,6 +14,7 @@ func TestExternalToolResultJSONUnion(t *testing.T) { if err != nil { t.Fatalf("marshal string result: %v", err) } + if string(raw) != `"tool result"` { t.Fatalf("marshal string result = %s", raw) } @@ -46,6 +47,29 @@ func TestExternalToolResultJSONUnion(t *testing.T) { } } +func TestOptionalNullableFieldsPreserveOmittedAndNull(t *testing.T) { + unset, err := json.Marshal(TaskClientUpdateProgress{}) + if err != nil { + t.Fatalf("marshal unset progress: %v", err) + } + if string(unset) != `{"kind":"progress"}` { + t.Fatalf("marshal unset progress = %s", unset) + } + + var clearedPercentage *float64 + var clearedPhase *string + cleared, err := json.Marshal(TaskClientUpdateProgress{ + Percentage: &clearedPercentage, + Phase: &clearedPhase, + }) + if err != nil { + t.Fatalf("marshal cleared progress: %v", err) + } + if string(cleared) != `{"kind":"progress","percentage":null,"phase":null}` { + t.Fatalf("marshal cleared progress = %s", cleared) + } +} + func TestFilterMappingJSONUnion(t *testing.T) { var mapping FilterMapping = FilterMappingEnumMap{"secret": ContentFilterModeHiddenCharacters} raw, err := json.Marshal(mapping) diff --git a/go/rpc/message_identity_test.go b/go/rpc/message_identity_test.go new file mode 100644 index 0000000000..6124d7084a --- /dev/null +++ b/go/rpc/message_identity_test.go @@ -0,0 +1,101 @@ +package rpc + +import ( + "encoding/json" + "testing" +) + +func TestQueuePendingItemsMessageIDJSONCompatibility(t *testing.T) { + var item QueuePendingItems + if err := json.Unmarshal([]byte(`{ + "id": "queue-1", + "messageId": "message-1", + "kind": "message", + "displayText": "hello", + "agentMode": "interactive" + }`), &item); err != nil { + t.Fatal(err) + } + if item.MessageID == nil || *item.MessageID != "message-1" { + t.Fatalf("MessageID = %v, want message-1", item.MessageID) + } + + encoded, err := json.Marshal(item) + if err != nil { + t.Fatal(err) + } + var wire map[string]any + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatal(err) + } + if got := wire["messageId"]; got != "message-1" { + t.Fatalf("messageId = %v, want message-1", got) + } + + var olderItem QueuePendingItems + if err := json.Unmarshal([]byte(`{ + "id": "queue-2", + "kind": "command", + "displayText": "/help", + "agentMode": "interactive" + }`), &olderItem); err != nil { + t.Fatal(err) + } + if olderItem.MessageID != nil { + t.Fatalf("MessageID = %v, want nil", olderItem.MessageID) + } + + encoded, err = json.Marshal(olderItem) + if err != nil { + t.Fatal(err) + } + wire = nil + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatal(err) + } + if _, ok := wire["messageId"]; ok { + t.Fatal("messageId should be omitted when absent") + } +} + +func TestUserMessageDataMessageIDJSONCompatibility(t *testing.T) { + var message UserMessageData + if err := json.Unmarshal([]byte(`{"content":"hello","messageId":"message-1"}`), &message); err != nil { + t.Fatal(err) + } + if message.MessageID == nil || *message.MessageID != "message-1" { + t.Fatalf("MessageID = %v, want message-1", message.MessageID) + } + + encoded, err := json.Marshal(message) + if err != nil { + t.Fatal(err) + } + var wire map[string]any + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatal(err) + } + if got := wire["messageId"]; got != "message-1" { + t.Fatalf("messageId = %v, want message-1", got) + } + + var olderMessage UserMessageData + if err := json.Unmarshal([]byte(`{"content":"hello"}`), &olderMessage); err != nil { + t.Fatal(err) + } + if olderMessage.MessageID != nil { + t.Fatalf("MessageID = %v, want nil", olderMessage.MessageID) + } + + encoded, err = json.Marshal(olderMessage) + if err != nil { + t.Fatal(err) + } + wire = nil + if err := json.Unmarshal(encoded, &wire); err != nil { + t.Fatal(err) + } + if _, ok := wire["messageId"]; ok { + t.Fatal("messageId should be omitted when absent") + } +} diff --git a/go/rpc/sandbox_config_test.go b/go/rpc/sandbox_config_test.go new file mode 100644 index 0000000000..58be07e1d6 --- /dev/null +++ b/go/rpc/sandbox_config_test.go @@ -0,0 +1,43 @@ +package rpc + +import ( + "encoding/json" + "testing" +) + +func TestSandboxConfigAllowBypassJSON(t *testing.T) { + allowBypass := true + configured := SandboxConfig{Enabled: true, AllowBypass: &allowBypass} + + data, err := json.Marshal(configured) + if err != nil { + t.Fatalf("marshal configured sandbox: %v", err) + } + var wire map[string]any + if err := json.Unmarshal(data, &wire); err != nil { + t.Fatalf("unmarshal configured sandbox: %v", err) + } + if got := wire["allowBypass"]; got != true { + t.Fatalf("allowBypass = %v, want true", got) + } + + var roundTripped SandboxConfig + if err := json.Unmarshal(data, &roundTripped); err != nil { + t.Fatalf("round-trip configured sandbox: %v", err) + } + if roundTripped.AllowBypass == nil || !*roundTripped.AllowBypass { + t.Fatal("round-tripped allowBypass = nil or false, want true") + } + + data, err = json.Marshal(SandboxConfig{Enabled: true}) + if err != nil { + t.Fatalf("marshal sandbox without bypass: %v", err) + } + wire = make(map[string]any) + if err := json.Unmarshal(data, &wire); err != nil { + t.Fatalf("unmarshal sandbox without bypass: %v", err) + } + if _, ok := wire["allowBypass"]; ok { + t.Fatal("allowBypass was serialized when absent") + } +} diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 67121c30ed..3c9d2d46de 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -170,7 +170,7 @@ type AgentGetCurrentResult struct { Agent *AgentInfo `json:"agent,omitempty"` } -// Agent metadata, including identifiers, display details, source, tools, model, MCP +// Agent metadata, including identifiers, display details, source, tools, model, models, MCP // servers, skills, and file path. // Experimental: AgentInfo is part of an experimental API and may change or be removed. type AgentInfo struct { @@ -189,6 +189,11 @@ type AgentInfo struct { // Authored preferred model id for this agent. Runtime model selection may choose a // different model; omitted means no authored preference. Model *string `json:"model,omitempty"` + // Whether authored models are preferences or required constraints. + ModelPolicy *AgentModelPolicy `json:"modelPolicy,omitempty"` + // Authored preferred model ids for this agent, in priority order. Runtime model selection + // chooses the first available model; omitted means no authored preference. + Models []string `json:"models,omitzero"` // Name of the agent. Use `id` as the stable selection identifier. Name string `json:"name"` // Absolute local file path of the agent definition. Only set for file-based agents loaded @@ -1065,6 +1070,49 @@ type AuthValidationError struct { // removed. type AuthValidationErrors []AuthValidationError +// Current per-window credit limit and consumption for an autopilot objective. +// Experimental: AutopilotObjectiveCreditLimit is part of an experimental API and may change +// or be removed. +type AutopilotObjectiveCreditLimit struct { + // Configured AI-credit cap, when one is set. + Credits *float64 `json:"credits,omitempty"` + // Window consumption in fractional AI credits, for display. + CreditsUsed float64 `json:"creditsUsed"` + // Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. + CreditsUsedNanoAiu string `json:"creditsUsedNanoAiu"` +} + +// Canonical runtime state for the session's current autopilot objective. +// Experimental: AutopilotObjectiveGetStateResult is part of an experimental API and may +// change or be removed. +type AutopilotObjectiveGetStateResult struct { + // Current objective state, or `null` when the session has no objective. + State *AutopilotObjectiveState `json:"state"` +} + +// Public, persistence-independent projection of an autopilot objective. +// Experimental: AutopilotObjectiveState is part of an experimental API and may change or be +// removed. +type AutopilotObjectiveState struct { + // Optional summary recorded when the objective completed. + CompletionSummary *string `json:"completionSummary,omitempty"` + // Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a + // decimal string. + CreditCountNanoAiu string `json:"creditCountNanoAiu"` + // Current per-window consumption and optional cap, when a credit-tracking window is present. + CreditLimit *AutopilotObjectiveCreditLimit `json:"creditLimit,omitempty"` + // Session-local objective identifier. + ID int64 `json:"id"` + // User-provided objective text. + Objective string `json:"objective"` + // Optional reason the objective is paused. + PauseReason *string `json:"pauseReason,omitempty"` + // Current normalized lifecycle status. + Status AutopilotObjectiveStatus `json:"status"` + // Number of objective turns started. + TurnCount int64 `json:"turnCount"` +} + // The running runtime's complete catalog of well-known built-in model IDs, including // supported models and additional IDs with built-in metadata. // Experimental: BuiltInModelCatalog is part of an experimental API and may change or be @@ -1361,9 +1409,12 @@ type CanvasSessionContext struct { // Experimental: CapiSessionOptions is part of an experimental API and may change or be // removed. type CapiSessionOptions struct { - // Routing preference used when the session model is `auto`. The runtime persists the - // preference across cold resume. When omitted, the default routing behavior is used. - // Resuming an already-resident session cannot change its preference. + // Routing preference for sessions whose model is `auto`. On create or cold resume, this + // establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold + // resume, the runtime restores the last committed preference. On resident resume, a + // different value requests a safe switch after resume succeeds and cannot change an + // in-flight turn. Successful switches are persisted for later cold resume. When no + // preference is supplied or restored, CAPI default routing is used. AutoTier *AutoTier `json:"autoTier,omitempty"` // Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when // the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses @@ -1604,7 +1655,8 @@ type CatalogSearchRequest struct { Kinds []CatalogCandidateKind `json:"kinds,omitzero"` // Maximum number of candidates to return. Defaults to 10 when omitted. Limit *int32 `json:"limit,omitempty"` - // Free-text search query. Never written to logs or telemetry. + // Free-text search query. Persisted as tool input for session continuity, but omitted from + // telemetry. Query string `json:"query"` } @@ -1737,6 +1789,10 @@ type CatalogNetworkFailureError struct { Message string `json:"message"` // Categorised failure, low cardinality so it can be aggregated without carrying a URL. Reason CatalogNetworkFailureReason `json:"reason"` + // Bounded cooldown in seconds before another catalog request should be attempted, when the + // authority supplied a numeric Retry-After value or the runtime applied its documented + // fallback. + RetryAfterSeconds *int32 `json:"retryAfterSeconds,omitempty"` // HTTP status code, when the failure was a rejected response. StatusCode *int32 `json:"statusCode,omitempty"` } @@ -1840,6 +1896,30 @@ func (CatalogUnsupportedKindError) Kind() CatalogSearchResultKind { return CatalogSearchResultKindUnsupportedKind } +// Runtime-to-owner cancellation request for a client-owned task. +// Experimental: ClientTaskCancelRequest is part of an experimental API and may change or be +// removed. +type ClientTaskCancelRequest struct { + // Opaque identifier shared by coalesced cancellation callers + CancellationID string `json:"cancellationId"` + // Owner-scoped task key included for correlation + ClientTaskID string `json:"clientTaskId"` + // Canonical runtime-generated task identifier + ID string `json:"id"` + // Reason the runtime requests cancellation + Reason ClientTaskCancelReason `json:"reason"` + // Session that owns the client task + SessionID string `json:"sessionId"` +} + +// Whether the client authoritatively confirmed its external work stopped. +// Experimental: ClientTaskCancelResult is part of an experimental API and may change or be +// removed. +type ClientTaskCancelResult struct { + // True only when the owner confirms that external work stopped before responding + Cancelled bool `json:"cancelled"` +} + // Slash commands available in the session, after applying any include/exclude filters. // Experimental: CommandList is part of an experimental API and may change or be removed. type CommandList struct { @@ -2063,6 +2143,9 @@ type ConnectRequest struct { // using the process-global gate for ordinary events and an explicit session-scoped decision // for host-only events. EnableGitHubTelemetryForwarding *bool `json:"enableGitHubTelemetryForwarding,omitempty"` + // Task kinds this connection can decode when observing session tasks. Omit to retain agent + // and shell compatibility. + SupportedTaskKinds []TaskKind `json:"supportedTaskKinds,omitzero"` // Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN Token *string `json:"token,omitempty"` } @@ -2075,6 +2158,8 @@ type ConnectResult struct { Ok bool `json:"ok"` // Server protocol version number ProtocolVersion int64 `json:"protocolVersion"` + // Task kinds the server may return to this connection. + TaskKinds []TaskKind `json:"taskKinds,omitzero"` // Server package version Version string `json:"version"` } @@ -2331,15 +2416,24 @@ type CopilotUserResponseQuotaSnapshotsPremiumInteractions struct { Unlimited *bool `json:"unlimited,omitempty"` } -// The currently selected model, reasoning effort, and context tier for the session. The -// context tier reflects `Session.getContextTier()`, restored from the session journal on -// resume. +// The session's authoritative model snapshot. Auto preference fields are configuration for +// the virtual `auto` model and do not change the selected model identifier. The context +// tier reflects `Session.getContextTier()`, restored from the session journal on resume. // Experimental: CurrentModel is part of an experimental API and may change or be removed. type CurrentModel struct { + // Auto preference currently claimed by an in-progress activation. Null means the activation + // is returning to provider-default routing. + ActivatingAutoTier *AutoTier `json:"activatingAutoTier,omitempty"` + // Auto preference currently committed for the session. This can remain available while + // another model is selected so a later switch to `auto` can reuse it. + AutoTier *AutoTier `json:"autoTier,omitempty"` // Context tier for models that support multiple context-window sizes. ContextTier *ContextTier `json:"contextTier,omitempty"` // Currently active model identifier ModelID *string `json:"modelId,omitempty"` + // Latest unclaimed Auto preference waiting for a future user turn. Null means the pending + // request is returning to provider-default routing. + PendingAutoTier *AutoTier `json:"pendingAutoTier,omitempty"` // Reasoning effort level currently applied to the active model, when one is set. Reads // `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the // two values are reported as a snapshot. @@ -2577,6 +2671,36 @@ type DiscoveredExtensionsEnableRequest struct { IDs []string `json:"ids"` } +// One server-discovered hook action from user, repository, plugin, or managed-policy +// configuration. +// Experimental: DiscoveredHook is part of an experimental API and may change or be removed. +type DiscoveredHook struct { + // Durable content hash used by hook enablement. Identical actions may intentionally share + // this key. Omitted when changing the user's disabled-hooks setting cannot change the + // action's current server-discovered state, including managed-policy hooks, session-start + // prompt actions, actions suppressed by disable-all settings, and projectless plugin + // actions that require project-directory expansion. + DisableKey *string `json:"disableKey,omitempty"` + // Whether this action is enabled under the server-side discovery settings. Concrete + // sessions may differ because they can add session-specific directories, plugins, or trust. + // False when its disable key is present in the user's disabled-hooks setting or disable-all + // settings suppress the action. + Enabled bool `json:"enabled"` + // Hook event that invokes this action. + HookType HookType `json:"hookType"` + // Deterministic identifier for this server-discovered action row. It remains stable while + // the project, origin, source, event, action content, and duplicate ordinal are unchanged. + // This is row identity, not the key persisted in disabledHooks. + ID string `json:"id"` + // Configuration tier that contributed this hook action. + Origin HookOrigin `json:"origin"` + // Input project path for which this server-side action was resolved. Set on every row + // returned for project-scoped discovery, including repeated user and policy actions. + ProjectPath *string `json:"projectPath,omitempty"` + // Human-readable source label, such as a hook file path, settings source, or plugin name. + Source *string `json:"source,omitempty"` +} + // MCP server discovered by `mcp.discover`, with config source, optional plugin source, // transport type, and enabled state. // Experimental: DiscoveredMCPServer is part of an experimental API and may change or be @@ -3564,6 +3688,18 @@ func (FactoryRunFailureFactoryLimitReached) Type() FactoryRunFailureType { return FactoryRunFailureTypeFactoryLimitReached } +// The extension that owns the factory disconnected while the run was executing, so the host +// halted it. The run's journaled subagent results are preserved so a resume can reuse them. +type FactoryRunFailureFactoryProviderDisconnected struct { + // Factory run identifier. + RunID string `json:"runId"` +} + +func (FactoryRunFailureFactoryProviderDisconnected) factoryRunFailure() {} +func (FactoryRunFailureFactoryProviderDisconnected) Type() FactoryRunFailureType { + return FactoryRunFailureTypeFactoryProviderDisconnected +} + type FactoryRunFailureFactoryResumeDeclined struct { // Human-readable reason the resume did not proceed. Reason string `json:"reason"` @@ -3609,9 +3745,12 @@ type FactoryRunRequest struct { // Experimental: FactoryRunResult is part of an experimental API and may change or be // removed. type FactoryRunResult struct { + // One-based execution attempt represented by this envelope. Absent before the first attempt + // starts or when returned by an older runtime. + Attempt *int64 `json:"attempt,omitempty"` // Error message for an errored run. Error *string `json:"error,omitempty"` - // Machine-readable failure details for an errored run. + // Machine-readable failure details for a halted or errored run. Failure FactoryRunFailure `json:"failure,omitempty"` // Reason for a halted or cancelled run. Reason *string `json:"reason,omitempty"` @@ -4209,8 +4348,6 @@ type HistoryTruncateResult struct { // removed. // Internal: HookInvokeRequest is an internal SDK API and is not part of the public surface. type HookInvokeRequest struct { - // Internal: HookType is part of the SDK's internal API surface and is not intended for - // external use. HookType HookType `json:"hookType"` Input any `json:"input"` SessionID string `json:"sessionId"` @@ -4224,6 +4361,40 @@ type HookInvokeResponse struct { Output any `json:"output,omitempty"` } +// Optional project paths and host-exclusion behavior for server-scoped hook discovery. +// Experimental: HooksDiscoverRequest is part of an experimental API and may change or be +// removed. +type HooksDiscoverRequest struct { + // When true, omit host-owned user and plugin hook rows and their diagnostics. + // Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks + // still contribute to each remaining row's effective enabled state. This filters sources + // rather than simulating a host with no settings. + ExcludeHostHooks *bool `json:"excludeHostHooks,omitempty"` + // Optional project directory paths whose trusted repository and project-expanded plugin + // hooks should be discovered. When omitted or empty, user, managed-policy, and globally + // enabled installed or explicit plugin hooks are returned without project expansion. + ProjectPaths []string `json:"projectPaths,omitzero"` +} + +// Server-discovered hook actions and partial-load diagnostics from user, repository, +// plugin, and managed-policy sources. Concrete sessions may include additional +// session-specific hook sources. +// Experimental: HooksDiscoverResult is part of an experimental API and may change or be +// removed. +type HooksDiscoverResult struct { + // Errors for hook sources or actions that could not be loaded, making the result partially + // incomplete. Other valid actions are still returned. Project-resolution and + // repository-settings errors are prefixed with their project path. + Errors []string `json:"errors"` + // All discovered hook actions. Byte-identical actions remain separate rows even when they + // share a disable key. + Hooks []DiscoveredHook `json:"hooks"` + // Non-fatal source-loading warnings. Discovery remains complete for the affected source, + // although the source had a recoverable issue. Repository-settings warnings are prefixed + // with their project path when attribution is available. + Warnings []string `json:"warnings"` +} + // Installed plugin record from global state, with marketplace, version, install time, // enabled state, cache path, and source. // Experimental: InstalledPlugin is part of an experimental API and may change or be removed. @@ -4685,6 +4856,11 @@ type LspInitializeRequest struct { WorkingDirectory *string `json:"workingDirectory,omitempty"` } +// Experimental: ManagedSettingsClearCacheResult is part of an experimental API and may +// change or be removed. +type ManagedSettingsClearCacheResult struct { +} + // Validated device-managed settings discovered before a session exists. // Experimental: ManagedSettingsReadResult is part of an experimental API and may change or // be removed. @@ -5035,6 +5211,8 @@ type MCPConfigReloadResult struct { // Experimental: MCPConfigRemoveRequest is part of an experimental API and may change or be // removed. type MCPConfigRemoveRequest struct { + // OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. + AuthClientIDMetadataURL *string `json:"authClientIdMetadataUrl,omitempty"` // Name of the MCP server to remove Name string `json:"name"` } @@ -6387,6 +6565,10 @@ type MCPServer struct { Error *string `json:"error,omitempty"` // Server name (config key) Name string `json:"name"` + // Server-advertised metadata for a connected server. Omitted when no live connection + // metadata is available, including while pending or when failed, disabled, stopped, or not + // configured. + ServerMetadata *MCPServerMetadata `json:"serverMetadata,omitempty"` // Configuration source: user, workspace, plugin, or builtin Source *MCPServerSource `json:"source,omitempty"` // Plugin name that provided this server, when source is plugin. @@ -6559,6 +6741,15 @@ type MCPServerList struct { Servers []MCPServer `json:"servers"` } +// Server-advertised metadata learned through modern discovery or legacy initialization. +// Experimental: MCPServerMetadata is part of an experimental API and may change or be +// removed. +type MCPServerMetadata struct { + // Non-empty natural-language guidance for using the server, or null when the server omitted + // instructions or advertised an empty string. + Instructions *string `json:"instructions"` +} + // Recorded MCP server pending-auth state. // Experimental: MCPServerNeedsAuthInfo is part of an experimental API and may change or be // removed. @@ -6846,6 +7037,10 @@ type Model struct { // a recommended alternative. Present only when the service published at least one notice. // Hosts should surface these without implying anything is wrong with the model. InfoMessages []ModelMessage `json:"infoMessages,omitzero"` + // Provider-supplied model metadata. Keys and JSON-compatible values are preserved + // unchanged. This is factual metadata published by the model provider; it carries no picker + // or UX semantics. + Metadata map[string]any `json:"metadata,omitzero"` // Model capability category for grouping in the model picker ModelPickerCategory *ModelPickerCategory `json:"modelPickerCategory,omitempty"` // Relative cost tier for token-based billing users @@ -6880,6 +7075,10 @@ type ModelApplyStartupOverlayRequest struct { DeferredResume *bool `json:"deferredResume,omitempty"` // Model required by device-managed policy, when configured. DeviceManagedModel *string `json:"deviceManagedModel,omitempty"` + // Startup default model from the enterprise policy helper, when configured. Weakest of the + // managed sources: it applies only when neither device nor server policy names a model, and + // an explicit user selection still wins. + PolicyHelperModel *string `json:"policyHelperModel,omitempty"` // Context tier selected by repository settings, when configured. RepoContextTier *string `json:"repoContextTier,omitempty"` // Model selected by repository settings, when configured. @@ -6921,6 +7120,12 @@ type ModelBillingPromo struct { // Human-readable promotion message. Does not include the expiry timestamp; consumers may // format endsAt and append it when present. Message *string `json:"message,omitempty"` + // Whether the service asked hosts to give this promotion a prominent surface, such as a + // dedicated banner, in addition to listing it with the model. `true` requests that surface + // and `false` asks for the model list only. Absent means the service expressed no + // preference — for example a response that predates the field — so hosts should apply their + // own default rather than read it as `false`. + ShowBanner *bool `json:"showBanner,omitempty"` } // Token-level pricing information for this model @@ -7164,6 +7369,38 @@ type ModelsListRequest struct { SelectionID *string `json:"selectionId,omitempty"` } +// An Auto preference request for the session. This updates Auto configuration only; it does +// not change the selected model to `auto`. +// Experimental: ModelSwitchAutoTierRequest is part of an experimental API and may change or +// be removed. +type ModelSwitchAutoTierRequest struct { + // Auto preference to activate when a future user turn using the `auto` model safely mints a + // replacement model and token pair. Pass null to return to provider-default Auto routing. + AutoTier *AutoTier `json:"autoTier"` + // Origin to record on the effective `session.model_change` event. Defaults to `sdk` when + // omitted. + Source *ModelChangeSource `json:"source,omitempty"` +} + +// Immediate acknowledgement and Auto preference snapshot after a switch request. This +// result never implies that a pending preference committed. +// Experimental: ModelSwitchAutoTierResult is part of an experimental API and may change or +// be removed. +type ModelSwitchAutoTierResult struct { + // Auto preference currently claimed by an in-progress activation. Null means the activation + // is returning to provider-default routing. + ActivatingAutoTier *AutoTier `json:"activatingAutoTier,omitempty"` + // Auto preference currently committed for the session. + EffectiveAutoTier *AutoTier `json:"effectiveAutoTier,omitempty"` + // Latest unclaimed Auto preference waiting for a future user turn. + PendingAutoTier *AutoTier `json:"pendingAutoTier,omitempty"` + // Immediate request status. `pending` means accepted but not committed. + Status ModelSwitchAutoTierStatus `json:"status"` + // Earlier unclaimed preference replaced by this request. This can be present with either + // status, including when selecting the effective preference cancels pending work. + SupersededAutoTier *AutoTier `json:"supersededAutoTier,omitempty"` +} + // Experimental: ModelSwitchConfirmation is part of an experimental API and may change or be // removed. type ModelSwitchConfirmation struct { @@ -7180,6 +7417,10 @@ type ModelSwitchConfirmation struct { // Experimental: ModelSwitchToRequest is part of an experimental API and may change or be // removed. type ModelSwitchToRequest struct { + // Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to + // return to provider-default Auto routing. This field is rejected when `modelId` is not + // `auto`. + AutoTier **AutoTier `json:"autoTier,omitempty"` // Explicit response to a model-switch compaction preflight. Omit to request a confirmation // projection when compaction is necessary. CompactionDecision *string `json:"compactionDecision,omitempty"` @@ -7214,8 +7455,8 @@ type ModelSwitchToRequest struct { RequireAvailable *bool `json:"requireAvailable,omitempty"` // When true, evaluate context-window compaction policy before applying the switch. RunCompactionPreflight *bool `json:"runCompactionPreflight,omitempty"` - // Origin to record on the effective `session.model_change` event. Defaults to `sdk` when - // omitted. + // Origin to record on the effective `session.model_change` event for trusted in-process + // calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. Source *ModelChangeSource `json:"source,omitempty"` // Output verbosity level to request for supported models Verbosity *Verbosity `json:"verbosity,omitempty"` @@ -7238,6 +7479,9 @@ type ModelSwitchToResult struct { Message *string `json:"message,omitempty"` // Currently active model identifier after the switch ModelID *string `json:"modelId,omitempty"` + // Authoritative model and Auto preference state after an immediate switch. For deferred + // switches this remains the current state until the queued change drains. + ModelState *CurrentModel `json:"modelState,omitempty"` // Persistence failure encountered after applying the model switch. PersistenceError *string `json:"persistenceError,omitempty"` // Lifecycle result for the requested switch @@ -8823,6 +9067,8 @@ type PluginInstallResult struct { PostInstallMessage *string `json:"postInstallMessage,omitempty"` // Number of skills discovered and installed from the plugin SkillsInstalled int64 `json:"skillsInstalled"` + // Where the completed plugin tree was staged before atomic promotion + StagingMode *PluginInstallStagingMode `json:"stagingMode,omitempty"` } // Plugins installed for the session, with their enabled state and version metadata. @@ -9795,6 +10041,9 @@ type QueuePendingItems struct { ID string `json:"id"` // Whether this item is a queued user message or a queued slash command / model change Kind QueuePendingItemsKind `json:"kind"` + // Stable identity of the queued user message. Present for message rows and absent for slash + // commands and model changes. + MessageID *string `json:"messageId,omitempty"` } // Snapshot of the session's pending queued items and immediate-steering messages. @@ -10216,6 +10465,12 @@ type RuntimeShutdownResult struct { type SandboxConfig struct { // Whether to auto-add the current working directory to readwritePaths. Default: true. AddCurrentWorkingDirectory *bool `json:"addCurrentWorkingDirectory,omitempty"` + // Whether the agent may request that an individual command run outside the sandbox, which + // the host then approves or denies through the usual permission flow. A host capability + // flag rather than part of the policy: it is stripped from the effective spawn policy and + // only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this + // object: omitting it offers no bypass. Default: false (opt-in). + AllowBypass *bool `json:"allowBypass,omitempty"` // Whether to auto-grant read access to tool directories discovered on PATH and in toolchain // environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common // developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the @@ -10232,6 +10487,27 @@ type SandboxConfig struct { Auth *SandboxConfigAuth `json:"auth,omitempty"` // Whether sandboxing is enabled for the session. Enabled bool `json:"enabled"` + // The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + // Internal: ManagedLspRoutingLocked is part of the SDK's internal API surface and is not + // intended for external use. + ManagedLspRoutingLocked *bool `json:"managedLspRoutingLocked,omitempty"` + // Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local + // opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at + // the administrator instead of a setting the next managed merge would override, and it is + // ignored when comparing two configs for change. Only the managed merge may set it; a + // caller-supplied value is stripped. + // Internal: ManagedMCPRoutingLocked is part of the SDK's internal API surface and is not + // intended for external use. + ManagedMCPRoutingLocked *bool `json:"managedMcpRoutingLocked,omitempty"` + // Whether language servers the session launches are confined by the sandbox. Only an + // explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by + // default; set to false to opt out). + SandboxLspServers *bool `json:"sandboxLspServers,omitempty"` + // Whether MCP servers the session launches are confined by the sandbox. Only an explicit + // `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and + // `enabled` are always read together. Ignored while `enabled` is false. Default: true + // (enabled by default; set to false to opt out). + SandboxMCPServers *bool `json:"sandboxMcpServers,omitempty"` // User-managed sandbox policy fragment merged into the auto-discovered base policy. UserPolicy *SandboxConfigUserPolicy `json:"userPolicy,omitempty"` } @@ -10306,11 +10582,14 @@ type SandboxConfigUserPolicyNetwork struct { AllowLocalNetwork *bool `json:"allowLocalNetwork,omitempty"` // Whether outbound network traffic is allowed at all. AllowOutbound *bool `json:"allowOutbound,omitempty"` - // HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and - // cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. - // Credentials go in the separate `username`/`password` fields. A credential-free http:// - // loopback proxy URL is routed through the localhost proxy automatically; an https:// or - // authenticated loopback URL is used as-is. + // HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, + // requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is + // accepted and routed through the IPv4 gateway), and does not support proxy credentials. + // macOS relies on applications honoring proxy environment variables. Windows also + // configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's + // networking stack. Configure supported credentials in the separate `username` and + // `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, + // while an https:// or authenticated loopback URL uses the URL form. Proxy *SandboxConfigUserPolicyNetworkProxy `json:"proxy,omitempty"` } @@ -10326,12 +10605,12 @@ type SandboxConfigUserPolicyNetworkProxy struct { // settings.json); the field is masked in the dialog and redacted by /settings show. Password *string `json:"password,omitempty"` // Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the - // scheme's standard port when omitted. Credentials must not be embedded here — a - // `user:pass@` authority is rejected; put them in the separate `username`/`password` - // fields. A credential-free http:// loopback URL is routed through the localhost proxy - // automatically; loopback covers localhost and any *.localhost subdomain, the whole - // 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or - // one with a username/password set, is used as-is. + // scheme's standard port when omitted; an explicit port must be between 1 and 65535. + // Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in + // the separate `username`/`password` fields. A credential-free http:// loopback proxy URL + // is routed through the localhost proxy automatically; loopback covers localhost and any + // *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback + // (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. URL string `json:"url"` // Optional username for proxy authentication. Combined with the URL (and `password`) into // `user:pass@host` when the sandboxed process routes through the proxy. @@ -11891,6 +12170,8 @@ type SessionOpenOptions struct { AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` // Whether ask_user is explicitly disabled. AskUserDisabled *bool `json:"askUserDisabled,omitempty"` + // OAuth Client ID Metadata Document URL used by this host for MCP authorization. + AuthClientIDMetadataURL *string `json:"authClientIdMetadataUrl,omitempty"` // Initial authentication info for the session. AuthInfo AuthInfo `json:"authInfo,omitempty"` // Allowlist of available tool names. @@ -11954,6 +12235,9 @@ type SessionOpenOptions struct { EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` // Whether shell-script safety heuristics are enabled. EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` + // Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by + // default. + EnableSkills *bool `json:"enableSkills,omitempty"` // Whether model responses stream as delta events. EnableStreaming *bool `json:"enableStreaming,omitempty"` // How MCP server environment values are interpreted. @@ -11977,6 +12261,16 @@ type SessionOpenOptions struct { ExpAssignments any `json:"expAssignments,omitempty"` // Feature-flag values resolved by the host. FeatureFlags map[string]bool `json:"featureFlags,omitzero"` + // Whether the requesting SDK session has a skill provider. The provider remains ephemeral + // and is never persisted in session options or history. When enableSkills is false, it + // remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw + // sessions.open flows reject it because they cannot safely pre-register the callback + // handler. + // Experimental: HasSkillProvider is part of an experimental API and may change or be + // removed. + // Internal: HasSkillProvider is part of the SDK's internal API surface and is not intended + // for external use. + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` // Built-in subagent names to include in this session. When specified, only these built-ins // are available, subject to runtime availability and exclusions. Custom agents with the // same name remain available. @@ -12872,6 +13166,22 @@ type SessionsPruneOldRequest struct { OlderThanDays int64 `json:"olderThanDays"` } +// Pagination options for reading an inactive or active local session's persisted event +// journal. +// Experimental: SessionsReadPersistedEventsRequest is part of an experimental API and may +// change or be removed. +type SessionsReadPersistedEventsRequest struct { + // Opaque cursor returned by a previous persisted-event read. Omit on the first call. + Cursor *string `json:"cursor,omitempty"` + // Direction to page through persisted history. Forward starts at the beginning; backward + // starts with the newest events. Events in each page remain chronological. + Direction *EventsReadDirection `json:"direction,omitempty"` + // Maximum number of events to return in this batch (1–1000, default 200). + Max *int64 `json:"max,omitempty"` + // Session ID whose persisted event journal should be read. + SessionID string `json:"sessionId"` +} + // Optional registration options. // Experimental: SessionsRegisterExtensionToolsOnSessionOptions is part of an experimental // API and may change or be removed. @@ -13060,8 +13370,9 @@ type SessionUpdateOptionsParams struct { EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` // Whether to enable cross-session store writes and reads. EnableSessionStore *bool `json:"enableSessionStore,omitempty"` - // Whether to enable skill directory scanning and loading. Falls back to - // enableConfigDiscovery when unset. + // Whether skill loading is enabled. Explicit false disables every source, including a bound + // SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, + // creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. EnableSkills *bool `json:"enableSkills,omitempty"` // Whether to stream model responses. EnableStreaming *bool `json:"enableStreaming,omitempty"` @@ -13479,6 +13790,66 @@ type SkillList struct { Skills []Skill `json:"skills"` } +// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched +// separately and lazily. +// Experimental: SkillProviderDescriptor is part of an experimental API and may change or be +// removed. +type SkillProviderDescriptor struct { + // Optional freeform argument hint used by slash-command catalogs. + ArgumentHint *string `json:"argumentHint,omitempty"` + // Description used in skill catalogs without fetching content. + Description string `json:"description"` + // Whether model invocation is disabled. Defaults to false. + DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` + // Invocation and display name. + Name string `json:"name"` + // Whether users may invoke the skill directly. Defaults to true. + UserInvocable *bool `json:"userInvocable,omitempty"` +} + +// Identifies the target session. +// Experimental: SkillProviderListRequest is part of an experimental API and may change or +// be removed. +type SkillProviderListRequest struct { + // Target session identifier + SessionID string `json:"sessionId"` +} + +// Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to +// 1024 descriptors and 1 MiB of aggregate metadata. +// Experimental: SkillProviderListResult is part of an experimental API and may change or be +// removed. +// Internal: SkillProviderListResult is an internal SDK API and is not part of the public +// surface. +type SkillProviderListResult struct { + // Skill descriptors in provider order. Invocation names must be unique under + // case-insensitive comparison. + Skills []SkillProviderDescriptor `json:"skills"` +} + +// Identifies one SDK-provided skill by invocation name. +// Experimental: SkillProviderReadRequest is part of an experimental API and may change or +// be removed. +// Internal: SkillProviderReadRequest is an internal SDK API and is not part of the public +// surface. +type SkillProviderReadRequest struct { + // Invocation name of the skill to read. + Name string `json:"name"` + // Target session identifier + SessionID string `json:"sessionId"` +} + +// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related +// files and assets are not supported. +// Experimental: SkillProviderReadResult is part of an experimental API and may change or be +// removed. +// Internal: SkillProviderReadResult is an internal SDK API and is not part of the public +// surface. +type SkillProviderReadResult struct { + // Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. + Markdown string `json:"markdown"` +} + // Skill names to mark as disabled in global configuration, replacing any previous list. // Experimental: SkillsConfigSetDisabledSkillsRequest is part of an experimental API and may // change or be removed. @@ -13565,11 +13936,14 @@ type SkillsInvokedSkill struct { AllowedTools []string `json:"allowedTools,omitzero"` // Full content of the skill file Content string `json:"content"` + // Whether model invocation was disabled when this skill was invoked + DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` // Turn number when the skill was invoked InvokedAtTurn int64 `json:"invokedAtTurn"` // Unique identifier for the skill Name string `json:"name"` - // Path to the SKILL.md file + // Path to the SKILL.md file, or an empty string for an SDK-provided skill without a + // filesystem identity Path string `json:"path"` } @@ -13703,6 +14077,8 @@ func (SlashCommandAgentPromptResult) Kind() SlashCommandInvocationResultKind { type SlashCommandCompletedResult struct { // Optional user-facing message describing the completed command Message *string `json:"message,omitempty"` + // Optional target session mode applied without submitting an agent prompt + Mode *SessionMode `json:"mode,omitempty"` // True when the invocation mutated user runtime settings; consumers caching settings should // refresh RuntimeSettingsChanged *bool `json:"runtimeSettingsChanged,omitempty"` @@ -13837,6 +14213,10 @@ type SlashCommandSelectSubcommandOption struct { // Experimental: SlashCommandTimelineEntry is part of an experimental API and may change or // be removed. type SlashCommandTimelineEntry struct { + // What the user must do to recover, when the entry reports a failure the runtime knows an + // action for. The `text` never names a client affordance, so a client that offers one + // renders it from this value. + Remediation *RemediationAction `json:"remediation,omitempty"` // Text displayed for the timeline entry. Text string `json:"text"` // Timeline entry presentation type. @@ -13870,6 +14250,104 @@ type SubagentSettingsEntry struct { EffortLevel *string `json:"effortLevel,omitempty"` // Model override for matching subagents Model *string `json:"model,omitempty"` + // Whether the configured model strategy is preferred or required + ModelPolicy *AgentModelPolicy `json:"modelPolicy,omitempty"` +} + +// Public owner attribution for a client-owned task. Identifiers are opaque and never +// authorize requests. +// Experimental: TaskClientOwner is part of an experimental API and may change or be removed. +type TaskClientOwner struct { + // ISO 8601 timestamp when the bound join disconnected + DisconnectedAt *time.Time `json:"disconnectedAt,omitempty"` + // Display-only owner name + DisplayName *string `json:"displayName,omitempty"` + // Opaque identity of the currently or most recently bound session join + JoinID string `json:"joinId"` + // Class of the task owner + Kind TaskClientOwnerKind `json:"kind"` + // Opaque session-scoped participant identity + ParticipantID string `json:"participantId"` + // Whether this task's bound join is currently connected + Presence TaskClientOwnerPresence `json:"presence"` + // Display-only owner source + Source *string `json:"source,omitempty"` +} + +// Progress or terminal update for a client-owned task. +// Experimental: TaskClientUpdate is part of an experimental API and may change or be +// removed. +type TaskClientUpdate interface { + taskClientUpdate() + Kind() TaskClientUpdateKind +} + +type RawTaskClientUpdateData struct { + Discriminator TaskClientUpdateKind + Raw json.RawMessage +} + +func (RawTaskClientUpdateData) taskClientUpdate() {} +func (r RawTaskClientUpdateData) Kind() TaskClientUpdateKind { + return r.Discriminator +} + +// Reports terminal cancellation after external work stopped. +type TaskClientUpdateCancelled struct { + // Optional final progress message + Message *string `json:"message,omitempty"` + // Optional human-readable cancellation reason + Reason *string `json:"reason,omitempty"` +} + +func (TaskClientUpdateCancelled) taskClientUpdate() {} +func (TaskClientUpdateCancelled) Kind() TaskClientUpdateKind { + return TaskClientUpdateKindCancelled +} + +// Reports successful terminal completion. +type TaskClientUpdateCompleted struct { + // Optional final progress message + Message *string `json:"message,omitempty"` + // Optional opaque successful terminal result + Result any `json:"result,omitempty"` +} + +func (TaskClientUpdateCompleted) taskClientUpdate() {} +func (TaskClientUpdateCompleted) Kind() TaskClientUpdateKind { + return TaskClientUpdateKindCompleted +} + +// Reports terminal failure. +type TaskClientUpdateFailed struct { + // Optional owner-supplied terminal failure code + Code *string `json:"code,omitempty"` + // Human-readable terminal failure message + Error string `json:"error"` + // Optional final progress message + Message *string `json:"message,omitempty"` +} + +func (TaskClientUpdateFailed) taskClientUpdate() {} +func (TaskClientUpdateFailed) Kind() TaskClientUpdateKind { + return TaskClientUpdateKindFailed +} + +// Publishes nonterminal progress for a running or idle client task. +type TaskClientUpdateProgress struct { + // Optional progress message appended to recent activity when nonempty + Message *string `json:"message,omitempty"` + // Optional completion percentage; null clears the current percentage + Percentage **float64 `json:"percentage,omitempty"` + // Optional progress phase; null clears the current phase + Phase **string `json:"phase,omitempty"` + // Optional active status transition + Status *TaskClientActiveStatus `json:"status,omitempty"` +} + +func (TaskClientUpdateProgress) taskClientUpdate() {} +func (TaskClientUpdateProgress) Kind() TaskClientUpdateKind { + return TaskClientUpdateKindProgress } // Task completion notification with summary from the agent @@ -13911,7 +14389,7 @@ type TaskCompletionDecision struct { ReviewerResultMeta any `json:"reviewerResultMeta,omitempty"` } -// Tracked task union returned by task APIs, containing either an agent task or a shell task. +// Tracked task union returned by task APIs, containing an agent, client, or shell task. // Experimental: TaskInfo is part of an experimental API and may change or be removed. type TaskInfo interface { taskInfo() @@ -13980,6 +14458,58 @@ func (TaskAgentInfo) Type() TaskInfoType { return TaskInfoTypeAgent } +// Tracked client-owned task metadata. +// Experimental: TaskClientInfo is part of an experimental API and may change or be removed. +type TaskClientInfo struct { + // ISO 8601 timestamp when the current active segment started + ActiveStartedAt *time.Time `json:"activeStartedAt,omitempty"` + // Accumulated active execution time in milliseconds + ActiveTimeMs int64 `json:"activeTimeMs"` + // Whether the currently bound owner can receive a cancellation request + CanCancel bool `json:"canCancel"` + // Human-readable reason for terminal cancellation + CancellationReason *string `json:"cancellationReason,omitempty"` + // Owner-scoped registration and reclaim key + ClientTaskID string `json:"clientTaskId"` + // ISO 8601 timestamp when the task reached a terminal status + CompletedAt *time.Time `json:"completedAt,omitempty"` + // Task description + Description string `json:"description"` + // Optional task display name + DisplayName *string `json:"displayName,omitempty"` + // Human-readable terminal failure message + Error *string `json:"error,omitempty"` + // Optional owner-supplied terminal failure code + ErrorCode *string `json:"errorCode,omitempty"` + // Execution mode, which is always background for client-owned tasks + ExecutionMode TaskClientExecutionMode `json:"executionMode"` + // Canonical runtime-generated task identifier + ID string `json:"id"` + // ISO 8601 timestamp when the connected owner entered idle status + IdleSince *time.Time `json:"idleSince,omitempty"` + // ISO 8601 timestamp of the most recent orphan transition + OrphanedAt *time.Time `json:"orphanedAt,omitempty"` + // Public attribution and presence for the task owner + Owner TaskClientOwner `json:"owner"` + // ISO 8601 timestamp of the most recent successful reclaim + ReclaimedAt *time.Time `json:"reclaimedAt,omitempty"` + // Opaque successful terminal result supplied by the task owner + Result any `json:"result,omitempty"` + // Sequence number of the latest accepted owner update + Sequence int64 `json:"sequence"` + // ISO 8601 timestamp when the task started + StartedAt time.Time `json:"startedAt"` + // Client task lifecycle status + Status TaskClientStatus `json:"status"` + // ISO 8601 timestamp of the latest accepted lifecycle change + UpdatedAt time.Time `json:"updatedAt"` +} + +func (TaskClientInfo) taskInfo() {} +func (TaskClientInfo) Type() TaskInfoType { + return TaskInfoTypeClient +} + // Tracked shell task metadata, including ID, command, status, timing, attachment/execution // mode, log path, and PID. // Experimental: TaskShellInfo is part of an experimental API and may change or be removed. @@ -14021,6 +14551,8 @@ type TaskList struct { Tasks []TaskInfo `json:"tasks"` } +// Progress information for the task, discriminated by type. Returns null when no task with +// this ID is currently tracked. // Experimental: TaskProgress is part of an experimental API and may change or be removed. type TaskProgress interface { taskProgress() @@ -14053,6 +14585,31 @@ func (TaskAgentProgress) Type() TaskProgressType { return TaskProgressTypeAgent } +// Generic progress for a client-owned task. +// Experimental: TaskClientProgress is part of an experimental API and may change or be +// removed. +type TaskClientProgress struct { + // Most recent nonempty progress message + LastMessage *string `json:"lastMessage,omitempty"` + // Current completion percentage from zero through one hundred + Percentage *float64 `json:"percentage,omitempty"` + // Current owner-defined progress phase + Phase *string `json:"phase,omitempty"` + // Recent server-timestamped progress messages + RecentActivity []TaskProgressLine `json:"recentActivity"` + // Sequence number of the latest accepted owner update + Sequence int64 `json:"sequence"` + // Current client task lifecycle status + Status TaskClientStatus `json:"status"` + // ISO 8601 timestamp of the latest accepted lifecycle change + UpdatedAt time.Time `json:"updatedAt"` +} + +func (TaskClientProgress) taskProgress() {} +func (TaskClientProgress) Type() TaskProgressType { + return TaskProgressTypeClient +} + // Progress snapshot for a shell task, with recent stdout/stderr output and optional process // ID. // Experimental: TaskShellProgress is part of an experimental API and may change or be @@ -14156,6 +14713,36 @@ type TasksPromoteToBackgroundResult struct { type TasksRefreshResult struct { } +// Registers or reclaims a client-owned task. +// Experimental: TasksRegisterRequest is part of an experimental API and may change or be +// removed. +type TasksRegisterRequest struct { + // Whether the owner supports runtime cancellation requests + Cancellable bool `json:"cancellable"` + // Owner-scoped idempotency key used for registration and reclaim + ClientTaskID string `json:"clientTaskId"` + // Human-readable description of the external work + Description string `json:"description"` + // Optional short display name for the external work + DisplayName *string `json:"displayName,omitempty"` + // Expected current sequence for idempotent registration or orphan reclaim + ExpectedSequence *int64 `json:"expectedSequence,omitempty"` + // Task kind + Type TaskClientType `json:"type"` +} + +// Result of registering or reclaiming a client-owned task. +// Experimental: TasksRegisterResult is part of an experimental API and may change or be +// removed. +type TasksRegisterResult struct { + // True only when this invocation created a new task + Created bool `json:"created"` + // True only when this invocation reclaimed an orphaned task + Reclaimed bool `json:"reclaimed"` + // Authoritative registered or reclaimed task + Task TaskClientInfo `json:"task"` +} + // Identifier of the completed or cancelled task to remove from tracking. // Experimental: TasksRemoveRequest is part of an experimental API and may change or be // removed. @@ -14220,6 +14807,30 @@ type TasksStartAgentResult struct { AgentID string `json:"agentId"` } +// Updates a client-owned task. +// Experimental: TasksUpdateRequest is part of an experimental API and may change or be +// removed. +type TasksUpdateRequest struct { + // Canonical runtime-generated task identifier + ID string `json:"id"` + // Owner update sequence to apply + Sequence int64 `json:"sequence"` + // Progress or terminal update payload + Update TaskClientUpdate `json:"update"` +} + +// Result of publishing a client-owned task update. +// Experimental: TasksUpdateResult is part of an experimental API and may change or be +// removed. +type TasksUpdateResult struct { + // Whether this invocation changed task state + Applied bool `json:"applied"` + // Whether this invocation repeated the latest accepted update + Duplicate bool `json:"duplicate"` + // Authoritative task after processing the update + Task TaskClientInfo `json:"task"` +} + // Wait until all in-flight background tasks (agents + shells) and any follow-up turns // scheduled by their completions have settled. Returns when the runtime is fully drained or // after an internal timeout (default 10 minutes; configurable via @@ -15681,6 +16292,18 @@ const ( AgentInfoSourceUser AgentInfoSource = "user" ) +// Whether configured models are advisory preferences or required constraints +// Experimental: AgentModelPolicy is part of an experimental API and may change or be +// removed. +type AgentModelPolicy string + +const ( + // Treat the authored models as advisory preferences that callers may override. + AgentModelPolicyPreferred AgentModelPolicy = "preferred" + // Require subagent execution to use one of the authored models. + AgentModelPolicyRequired AgentModelPolicy = "required" +) + // Kind of attention required when status === "attention". Meaningful only when status === // "attention". // Experimental: AgentRegistryLiveTargetEntryAttentionKind is part of an experimental API @@ -15868,6 +16491,20 @@ const ( AuthInfoTypeUser AuthInfoType = "user" ) +// Current normalized autopilot objective lifecycle status. +// Experimental: AutopilotObjectiveStatus is part of an experimental API and may change or +// be removed. +type AutopilotObjectiveStatus string + +const ( + // The objective is actively running. + AutopilotObjectiveStatusActive AutopilotObjectiveStatus = "active" + // The objective completed. + AutopilotObjectiveStatusCompleted AutopilotObjectiveStatus = "completed" + // The objective is paused and may be resumed. + AutopilotObjectiveStatusPaused AutopilotObjectiveStatus = "paused" +) + // Routing preference used when the session model is `auto`. // Experimental: AutoTier is part of an experimental API and may change or be removed. type AutoTier string @@ -16122,14 +16759,20 @@ const ( CatalogNetworkFailureReasonConnectionRefused CatalogNetworkFailureReason = "connection-refused" // The authority's name could not be resolved. CatalogNetworkFailureReasonDns CatalogNetworkFailureReason = "dns" - // The authority returned a status the runtime treats as a failure. + // The authority returned another status the runtime treats as a failure. CatalogNetworkFailureReasonHTTPStatus CatalogNetworkFailureReason = "http-status" // No network is available, so nothing was attempted. CatalogNetworkFailureReasonOffline CatalogNetworkFailureReason = "offline" + // The configured proxy returned 407 and requires authentication. + CatalogNetworkFailureReasonProxyAuthenticationRequired CatalogNetworkFailureReason = "proxy-authentication-required" + // The authority rate-limited requests and supplied or implied a bounded cooldown. + CatalogNetworkFailureReasonRateLimited CatalogNetworkFailureReason = "rate-limited" // A redirect was refused by the runtime's redirect policy. CatalogNetworkFailureReasonRedirectRejected CatalogNetworkFailureReason = "redirect-rejected" // The response exceeded the permitted size. CatalogNetworkFailureReasonResponseTooLarge CatalogNetworkFailureReason = "response-too-large" + // The authority returned a transient 5xx response. + CatalogNetworkFailureReasonServiceUnavailable CatalogNetworkFailureReason = "service-unavailable" // The request exceeded its time budget. CatalogNetworkFailureReasonTimeout CatalogNetworkFailureReason = "timeout" // The TLS handshake or certificate validation failed. @@ -16217,6 +16860,18 @@ const ( CatalogUnsafeRetrievalReasonRedirectToBlockedAddress CatalogUnsafeRetrievalReason = "redirect-to-blocked-address" ) +// Why the runtime requests client-task cancellation. +// Experimental: ClientTaskCancelReason is part of an experimental API and may change or be +// removed. +type ClientTaskCancelReason string + +const ( + // A caller requested task cancellation. + ClientTaskCancelReasonCancelRequested ClientTaskCancelReason = "cancel_requested" + // The session is shutting down. + ClientTaskCancelReasonSessionShutdown ClientTaskCancelReason = "session_shutdown" +) + // Whether a pending slash-command invocation effect was applied or cancelled by the host. // Experimental: CommandsInvocationEffectOutcome is part of an experimental API and may // change or be removed. @@ -16592,6 +17247,7 @@ const ( FactoryRunFailureTypeFactoryAccountingIncomplete FactoryRunFailureType = "factory_accounting_incomplete" FactoryRunFailureTypeFactoryDurableFailure FactoryRunFailureType = "factory_durable_failure" FactoryRunFailureTypeFactoryLimitReached FactoryRunFailureType = "factory_limit_reached" + FactoryRunFailureTypeFactoryProviderDisconnected FactoryRunFailureType = "factory_provider_disconnected" FactoryRunFailureTypeFactoryResumeDeclined FactoryRunFailureType = "factory_resume_declined" ) @@ -16745,7 +17401,24 @@ const ( HMACAuthInfoHostHTTPSGitHubCom HMACAuthInfoHost = "https://github.com" ) -// Hook event name dispatched through the SDK callback transport. +// Configuration tier that contributed a discovered hook action. +// Experimental: HookOrigin is part of an experimental API and may change or be removed. +type HookOrigin string + +const ( + // Hook provided by an enabled installed or explicit plugin. Projectless rows omit + // projectPath and do not expand a project directory. + HookOriginPlugin HookOrigin = "plugin" + // Hook enforced by centrally managed policy. + HookOriginPolicy HookOrigin = "policy" + // Hook loaded from repository settings or the repository hook directory. + HookOriginRepository HookOrigin = "repository" + // Hook loaded from user settings or the user's hook directory. + HookOriginUser HookOrigin = "user" +) + +// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally +// support callback-only events. // Experimental: HookType is part of an experimental API and may change or be removed. type HookType string @@ -17575,6 +18248,22 @@ const ( ModelPolicyStateUnconfigured ModelPolicyState = "unconfigured" ) +// Whether the requested preference was already effective or was accepted for later +// transactional activation. +// Experimental: ModelSwitchAutoTierStatus is part of an experimental API and may change or +// be removed. +type ModelSwitchAutoTierStatus string + +const ( + // The request was accepted but has not committed. A later user turn using the `auto` model + // must mint and validate the replacement before it becomes effective. + ModelSwitchAutoTierStatusPending ModelSwitchAutoTierStatus = "pending" + // The requested preference is already effective. No activation is pending for it, although + // this request may have cancelled an earlier unclaimed preference reported in + // `supersededAutoTier`. + ModelSwitchAutoTierStatusUnchanged ModelSwitchAutoTierStatus = "unchanged" +) + // Why the binary data is absent: it exceeded the inline size limit, or its asset was // unavailable // Experimental: OmittedBinaryOmittedReason is part of an experimental API and may change or @@ -17882,6 +18571,18 @@ const ( PermissionsSetApproveAllSourceUserSetting PermissionsSetApproveAllSource = "user_setting" ) +// Where completed plugin content was staged before atomic promotion. +// Experimental: PluginInstallStagingMode is part of an experimental API and may change or +// be removed. +type PluginInstallStagingMode string + +const ( + // A sibling of the destination plugin directory, used when external staging is unavailable. + PluginInstallStagingModeDestinationSibling PluginInstallStagingMode = "destination_sibling" + // A sibling of the installed-plugins root, outside the recursively watched tree. + PluginInstallStagingModeExternal PluginInstallStagingMode = "external" +) + // Controls whether the runtime may defer loading an external tool definition. // Experimental: ProtocolExternalToolDefer is part of an experimental API and may change or // be removed. @@ -18031,6 +18732,31 @@ const ( ReasoningSummaryNone ReasoningSummary = "none" ) +// What the user must do to recover from a failure, named as an action rather than as one +// client's affordance. The runtime cannot know which affordance a client offers — a slash +// command, a settings pane, a link — so the accompanying message stays host-agnostic and +// each client renders its own copy from this value. Absent when the runtime knows of no +// action the user can take. +// Experimental: RemediationAction is part of an experimental API and may change or be +// removed. +type RemediationAction string + +const ( + // Permit outbound network access in the sandbox policy. + RemediationActionAllowSandboxOutbound RemediationAction = "allow_sandbox_outbound" + // Review or widen the sandbox policy. The blocked path or host is named by the accompanying + // message or by the tool result the action arrived with. + RemediationActionReviewSandboxPolicy RemediationAction = "review_sandbox_policy" + // Inspect which account is currently authenticated before deciding what to change. + RemediationActionShowAccount RemediationAction = "show_account" + // Authenticate again with the Copilot backend. The current credential is absent, expired, + // or rejected. + RemediationActionSignIn RemediationAction = "sign_in" + // Authenticate as a different account. The current account exists but lacks access to the + // requested resource. + RemediationActionSwitchAccount RemediationAction = "switch_account" +) + // State discriminator for RemoteControlStatus. type RemoteControlStatusState string @@ -18650,7 +19376,7 @@ const ( SkillDiscoveryScopeProject SkillDiscoveryScope = "project" ) -// Source location type (e.g., project, personal-copilot, plugin, builtin) +// Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) // Experimental: SkillSource is part of an experimental API and may change or be removed. type SkillSource string @@ -18669,6 +19395,8 @@ const ( SkillSourcePlugin SkillSource = "plugin" // Skill defined in the current project's skill directories. SkillSourceProject SkillSource = "project" + // Pathless skill supplied lazily by an SDK skill provider. + SkillSourceSDK SkillSource = "sdk" ) // Optional completion hint for the input (e.g. 'directory' for filesystem path completion) @@ -18731,6 +19459,89 @@ const ( SubagentSettingsEntryContextTierLongContext SubagentSettingsEntryContextTier = "long_context" ) +// Active status a client owner may publish with a progress update. +// Experimental: TaskClientActiveStatus is part of an experimental API and may change or be +// removed. +type TaskClientActiveStatus string + +const ( + // The external owner is connected but waiting. + TaskClientActiveStatusIdle TaskClientActiveStatus = "idle" + // The external owner is actively working. + TaskClientActiveStatusRunning TaskClientActiveStatus = "running" +) + +// Client-owned tasks always execute outside the runtime in background mode. +// Experimental: TaskClientExecutionMode is part of an experimental API and may change or be +// removed. +type TaskClientExecutionMode string + +const ( + TaskClientExecutionModeBackground TaskClientExecutionMode = "background" +) + +// Connection class owning a client task. +// Experimental: TaskClientOwnerKind is part of an experimental API and may change or be +// removed. +type TaskClientOwnerKind string + +const ( + // A discovered extension connection owns the task. + TaskClientOwnerKindExtension TaskClientOwnerKind = "extension" + // A generic SDK connection owns the task. + TaskClientOwnerKindSDK TaskClientOwnerKind = "sdk" +) + +// Presence of the task's bound join. +// Experimental: TaskClientOwnerPresence is part of an experimental API and may change or be +// removed. +type TaskClientOwnerPresence string + +const ( + // The bound session join is connected. + TaskClientOwnerPresenceConnected TaskClientOwnerPresence = "connected" + // The bound session join is disconnected. + TaskClientOwnerPresenceDisconnected TaskClientOwnerPresence = "disconnected" +) + +// Lifecycle status of a client-owned task. +// Experimental: TaskClientStatus is part of an experimental API and may change or be +// removed. +type TaskClientStatus string + +const ( + // The owner reported or confirmed cancellation. + TaskClientStatusCancelled TaskClientStatus = "cancelled" + // The owner reported successful completion. + TaskClientStatusCompleted TaskClientStatus = "completed" + // The owner reported failure. + TaskClientStatusFailed TaskClientStatus = "failed" + // The external owner is connected but waiting. + TaskClientStatusIdle TaskClientStatus = "idle" + // The bound owner join disappeared; external executor state is unknown. + TaskClientStatusOrphaned TaskClientStatus = "orphaned" + // The external owner is actively working. + TaskClientStatusRunning TaskClientStatus = "running" +) + +// Discriminator for a client-owned task. +// Experimental: TaskClientType is part of an experimental API and may change or be removed. +type TaskClientType string + +const ( + TaskClientTypeClient TaskClientType = "client" +) + +// Kind discriminator for TaskClientUpdate. +type TaskClientUpdateKind string + +const ( + TaskClientUpdateKindCancelled TaskClientUpdateKind = "cancelled" + TaskClientUpdateKindCompleted TaskClientUpdateKind = "completed" + TaskClientUpdateKindFailed TaskClientUpdateKind = "failed" + TaskClientUpdateKindProgress TaskClientUpdateKind = "progress" +) + // Semantic result of evaluating a task completion request // Experimental: TaskCompletionOutcome is part of an experimental API and may change or be // removed. @@ -18762,16 +19573,31 @@ const ( type TaskInfoType string const ( - TaskInfoTypeAgent TaskInfoType = "agent" - TaskInfoTypeShell TaskInfoType = "shell" + TaskInfoTypeAgent TaskInfoType = "agent" + TaskInfoTypeClient TaskInfoType = "client" + TaskInfoTypeShell TaskInfoType = "shell" +) + +// Closed set of public task kinds a connection can negotiate. +// Experimental: TaskKind is part of an experimental API and may change or be removed. +type TaskKind string + +const ( + // Runtime-owned background agent task. + TaskKindAgent TaskKind = "agent" + // Client-owned externally executed task. + TaskKindClient TaskKind = "client" + // Runtime-owned shell task. + TaskKindShell TaskKind = "shell" ) // Type discriminator for TaskProgress. type TaskProgressType string const ( - TaskProgressTypeAgent TaskProgressType = "agent" - TaskProgressTypeShell TaskProgressType = "shell" + TaskProgressTypeAgent TaskProgressType = "agent" + TaskProgressTypeClient TaskProgressType = "client" + TaskProgressTypeShell TaskProgressType = "shell" ) // Whether the shell runs inside a managed PTY session or as an independent background @@ -19309,6 +20135,32 @@ func (a *ServerExtensionsAPI) Enable(ctx context.Context, params *DiscoveredExte return &result, nil } +// Experimental: ServerHooksAPI contains experimental APIs that may change or be removed. +type ServerHooksAPI serverAPI + +// Discovers hook actions enabled under server-side discovery settings from user, +// repository, plugin, and managed-policy sources. +// +// RPC method: hooks.discover. +// +// Parameters: Optional project paths and host-exclusion behavior for server-scoped hook +// discovery. +// +// Returns: Server-discovered hook actions and partial-load diagnostics from user, +// repository, plugin, and managed-policy sources. Concrete sessions may include additional +// session-specific hook sources. +func (a *ServerHooksAPI) Discover(ctx context.Context, params *HooksDiscoverRequest) (*HooksDiscoverResult, error) { + raw, err := a.client.Request(ctx, "hooks.discover", params) + if err != nil { + return nil, err + } + var result HooksDiscoverResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: ServerInstructionsAPI contains experimental APIs that may change or be // removed. type ServerInstructionsAPI serverAPI @@ -19424,6 +20276,32 @@ func (a *ServerLlmInferenceAPI) SetProvider(ctx context.Context) (*LlmInferenceS // removed. type ServerManagedSettingsAPI serverAPI +// ClearCache force-refreshes enterprise managed settings for every account: wipes the +// persistent server-policy cache (the whole `/managed-settings` directory) and +// drops this runtime process's in-memory retained server policy. It does not itself fetch +// policy — the effect is that the next time a session resolves managed settings for an +// account, that resolution re-fetches the account's org policy from the network instead of +// serving a cached response. Note that `managedSettings.read` returns only device/MDM +// settings and never triggers the account server-policy fetch, so a host implementing "sync +// account policy" should start a fresh session resolution rather than treat a subsequent +// `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out +// performs, broadened from the one signing-out account to all of them; device/MDM layers +// describe the machine, not the account, and are left untouched. Rejects if the on-disk +// cache cannot be removed. +// +// RPC method: managedSettings.clearCache. +func (a *ServerManagedSettingsAPI) ClearCache(ctx context.Context) (*ManagedSettingsClearCacheResult, error) { + raw, err := a.client.Request(ctx, "managedSettings.clearCache", nil) + if err != nil { + return nil, err + } + var result ManagedSettingsClearCacheResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Read discovers device-managed settings from production MDM and managed-file sources, // validates them against the runtime-owned managed-settings schema, and returns the // canonical JSON without requiring a session. @@ -20293,6 +21171,31 @@ func (a *ServerSessionsAPI) PruneOld(ctx context.Context, params *SessionsPruneO return &result, nil } +// ReadPersistedEvents reads a page of durable events directly from a local session's +// persisted journal without creating, resuming, or activating the session. The initial +// backward read uses a bounded tail scan for fast first paint; cursor continuations +// preserve the session event-log paging semantics. Persisted events may omit payloads that +// are reconstructed only for an active session. +// +// RPC method: sessions.readPersistedEvents. +// +// Parameters: Pagination options for reading an inactive or active local session's +// persisted event journal. +// +// Returns: Batch of session events returned by a read, with cursor and continuation +// metadata. +func (a *ServerSessionsAPI) ReadPersistedEvents(ctx context.Context, params *SessionsReadPersistedEventsRequest) (*EventsReadResult, error) { + raw, err := a.client.Request(ctx, "sessions.readPersistedEvents", params) + if err != nil { + return nil, err + } + var result EventsReadResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // ReleaseLock releases the in-use lock held by this process for a session. // // RPC method: sessions.releaseLock. @@ -20666,6 +21569,7 @@ type ServerRPC struct { Catalog *ServerCatalogAPI Commands *ServerCommandsAPI Extensions *ServerExtensionsAPI + Hooks *ServerHooksAPI Instructions *ServerInstructionsAPI LlmInference *ServerLlmInferenceAPI ManagedSettings *ServerManagedSettingsAPI @@ -20730,6 +21634,7 @@ func NewServerRPC(client *jsonrpc2.Client) *ServerRPC { r.Catalog = (*ServerCatalogAPI)(&r.common) r.Commands = (*ServerCommandsAPI)(&r.common) r.Extensions = (*ServerExtensionsAPI)(&r.common) + r.Hooks = (*ServerHooksAPI)(&r.common) r.Instructions = (*ServerInstructionsAPI)(&r.common) r.LlmInference = (*ServerLlmInferenceAPI)(&r.common) r.ManagedSettings = (*ServerManagedSettingsAPI)(&r.common) @@ -21134,6 +22039,28 @@ func (a *AgentAPI) SetPrompt(ctx context.Context, params *AgentSetPromptRequest) return &result, nil } +// Experimental: AutopilotObjectiveAPI contains experimental APIs that may change or be +// removed. +type AutopilotObjectiveAPI sessionAPI + +// GetState reads the current canonical autopilot objective state for this session. +// +// RPC method: session.autopilotObjective.getState. +// +// Returns: Canonical runtime state for the session's current autopilot objective. +func (a *AutopilotObjectiveAPI) GetState(ctx context.Context) (*AutopilotObjectiveGetStateResult, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request(ctx, "session.autopilotObjective.getState", req) + if err != nil { + return nil, err + } + var result AutopilotObjectiveGetStateResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Experimental: CanvasAPI contains experimental APIs that may change or be removed. type CanvasAPI sessionAPI @@ -23552,13 +24479,15 @@ func (a *ModeAPI) Set(ctx context.Context, params *ModeSetRequest) (*ModeSetResu // Experimental: ModelAPI contains experimental APIs that may change or be removed. type ModelAPI sessionAPI -// GetCurrent gets the currently selected model for the session. +// GetCurrent gets the session's authoritative model snapshot, including the committed Auto +// preference and any newer unclaimed Auto preference waiting for a future user turn. // // RPC method: session.model.getCurrent. // -// Returns: The currently selected model, reasoning effort, and context tier for the -// session. The context tier reflects `Session.getContextTier()`, restored from the session -// journal on resume. +// Returns: The session's authoritative model snapshot. Auto preference fields are +// configuration for the virtual `auto` model and do not change the selected model +// identifier. The context tier reflects `Session.getContextTier()`, restored from the +// session journal on resume. func (a *ModelAPI) GetCurrent(ctx context.Context) (*CurrentModel, error) { req := map[string]any{"sessionId": a.sessionID} raw, err := a.client.Request(ctx, "session.model.getCurrent", req) @@ -23629,6 +24558,40 @@ func (a *ModelAPI) SetReasoningEffort(ctx context.Context, params *ModelSetReaso return &result, nil } +// SwitchAutoTier requests an Auto preference change without changing the session's selected +// model. The latest unclaimed request wins; the runtime commits it only after a later +// prompt using the `auto` model mints a usable model and token pair. A `pending` response +// confirms that the request was accepted, not that it committed. Observe eventual success +// through `session.model_change`, failure through the ephemeral +// `session.auto_tier_switch_failed` event, or current unclaimed state through +// `session.model.getCurrent`. +// +// RPC method: session.model.switchAutoTier. +// +// Parameters: An Auto preference request for the session. This updates Auto configuration +// only; it does not change the selected model to `auto`. +// +// Returns: Immediate acknowledgement and Auto preference snapshot after a switch request. +// This result never implies that a pending preference committed. +func (a *ModelAPI) SwitchAutoTier(ctx context.Context, params *ModelSwitchAutoTierRequest) (*ModelSwitchAutoTierResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["autoTier"] = params.AutoTier + if params.Source != nil { + req["source"] = *params.Source + } + } + raw, err := a.client.Request(ctx, "session.model.switchAutoTier", req) + if err != nil { + return nil, err + } + var result ModelSwitchAutoTierResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // SwitchTo switches the session to a model and optional reasoning configuration. // // RPC method: session.model.switchTo. @@ -23640,6 +24603,9 @@ func (a *ModelAPI) SetReasoningEffort(ctx context.Context, params *ModelSetReaso func (a *ModelAPI) SwitchTo(ctx context.Context, params *ModelSwitchToRequest) (*ModelSwitchToResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { + if params.AutoTier != nil { + req["autoTier"] = *params.AutoTier + } if params.CompactionDecision != nil { req["compactionDecision"] = *params.CompactionDecision } @@ -25516,6 +26482,39 @@ func (a *TasksAPI) Refresh(ctx context.Context) (*TasksRefreshResult, error) { return &result, nil } +// Registers a client-owned task, or reclaims an orphaned task belonging to the same +// extension principal. +// +// RPC method: session.tasks.register. +// +// Parameters: Registers or reclaims a client-owned task. +// +// Returns: Result of registering or reclaiming a client-owned task. +func (a *TasksAPI) Register(ctx context.Context, params *TasksRegisterRequest) (*TasksRegisterResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["cancellable"] = params.Cancellable + req["clientTaskId"] = params.ClientTaskID + req["description"] = params.Description + if params.DisplayName != nil { + req["displayName"] = *params.DisplayName + } + if params.ExpectedSequence != nil { + req["expectedSequence"] = *params.ExpectedSequence + } + req["type"] = params.Type + } + raw, err := a.client.Request(ctx, "session.tasks.register", req) + if err != nil { + return nil, err + } + var result TasksRegisterResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // Removes a completed or cancelled background task from tracking. // // RPC method: session.tasks.remove. @@ -25601,6 +26600,31 @@ func (a *TasksAPI) StartAgent(ctx context.Context, params *TasksStartAgentReques return &result, nil } +// Update publishes generic progress or a terminal outcome for a client-owned task. +// +// RPC method: session.tasks.update. +// +// Parameters: Updates a client-owned task. +// +// Returns: Result of publishing a client-owned task update. +func (a *TasksAPI) Update(ctx context.Context, params *TasksUpdateRequest) (*TasksUpdateResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["id"] = params.ID + req["sequence"] = params.Sequence + req["update"] = params.Update + } + raw, err := a.client.Request(ctx, "session.tasks.update", req) + if err != nil { + return nil, err + } + var result TasksUpdateResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + // WaitForPending waits for all in-flight background tasks and any follow-up turns to settle. // // RPC method: session.tasks.waitForPending. @@ -26607,44 +27631,45 @@ type SessionRPC struct { // Reuse a single struct instead of allocating one for each service on the heap. common sessionAPI - Agent *AgentAPI - Canvas *CanvasAPI - Commands *CommandsAPI - Completions *CompletionsAPI - ContentExclusion *ContentExclusionAPI - Debug *DebugAPI - EventLog *EventLogAPI - Extensions *ExtensionsAPI - Factory *FactoryAPI - Fleet *FleetAPI - GitHubAuth *GitHubAuthAPI - History *HistoryAPI - Instructions *InstructionsAPI - LimitPrediction *LimitPredictionAPI - Lsp *LspAPI - MCP *MCPAPI - Metadata *MetadataAPI - Mode *ModeAPI - Model *ModelAPI - Name *NameAPI - Options *OptionsAPI - Permissions *PermissionsAPI - Plan *PlanAPI - Plugins *PluginsAPI - Provider *ProviderAPI - Queue *QueueAPI - Remote *RemoteAPI - Sandbox *SandboxAPI - Schedule *ScheduleAPI - Shell *ShellAPI - Skills *SkillsAPI - Tasks *TasksAPI - Telemetry *TelemetryAPI - Tools *ToolsAPI - UI *UIAPI - Usage *UsageAPI - Visibility *VisibilityAPI - Workspaces *WorkspacesAPI + Agent *AgentAPI + AutopilotObjective *AutopilotObjectiveAPI + Canvas *CanvasAPI + Commands *CommandsAPI + Completions *CompletionsAPI + ContentExclusion *ContentExclusionAPI + Debug *DebugAPI + EventLog *EventLogAPI + Extensions *ExtensionsAPI + Factory *FactoryAPI + Fleet *FleetAPI + GitHubAuth *GitHubAuthAPI + History *HistoryAPI + Instructions *InstructionsAPI + LimitPrediction *LimitPredictionAPI + Lsp *LspAPI + MCP *MCPAPI + Metadata *MetadataAPI + Mode *ModeAPI + Model *ModelAPI + Name *NameAPI + Options *OptionsAPI + Permissions *PermissionsAPI + Plan *PlanAPI + Plugins *PluginsAPI + Provider *ProviderAPI + Queue *QueueAPI + Remote *RemoteAPI + Sandbox *SandboxAPI + Schedule *ScheduleAPI + Shell *ShellAPI + Skills *SkillsAPI + Tasks *TasksAPI + Telemetry *TelemetryAPI + Tools *ToolsAPI + UI *UIAPI + Usage *UsageAPI + Visibility *VisibilityAPI + Workspaces *WorkspacesAPI } // Aborts the current agent turn. @@ -26930,6 +27955,7 @@ func NewSessionRPC(client *jsonrpc2.Client, sessionID string) *SessionRPC { r := &SessionRPC{} r.common = sessionAPI{client: client, sessionID: sessionID} r.Agent = (*AgentAPI)(&r.common) + r.AutopilotObjective = (*AutopilotObjectiveAPI)(&r.common) r.Canvas = (*CanvasAPI)(&r.common) r.Commands = (*CommandsAPI)(&r.common) r.Completions = (*CompletionsAPI)(&r.common) @@ -27461,6 +28487,9 @@ func (a *InternalModelAPI) ApplyStartupOverlay(ctx context.Context, params *Mode if params.DeviceManagedModel != nil { req["deviceManagedModel"] = *params.DeviceManagedModel } + if params.PolicyHelperModel != nil { + req["policyHelperModel"] = *params.PolicyHelperModel + } if params.RepoContextTier != nil { req["repoContextTier"] = *params.RepoContextTier } @@ -28185,12 +29214,26 @@ type SessionFSHandler interface { WriteFile(request *SessionFSWriteFileRequest) (*SessionFSError, error) } +// Experimental: TasksHandler contains experimental APIs that may change or be removed. +type TasksHandler interface { + // Cancel asks the client currently bound to a client-owned session task to confirm that its + // external work stopped. + // + // RPC method: tasks.cancel. + // + // Parameters: Runtime-to-owner cancellation request for a client-owned task. + // + // Returns: Whether the client authoritatively confirmed its external work stopped. + Cancel(request *ClientTaskCancelRequest) (*ClientTaskCancelResult, error) +} + // ClientSessionAPIHandlers provides all client session API handler groups for a session. type ClientSessionAPIHandlers struct { Canvas CanvasHandler Factory FactoryHandler ProviderToken ProviderTokenHandler SessionFS SessionFSHandler + Tasks TasksHandler } func clientSessionHandlerError(err error) *jsonrpc2.Error { @@ -28568,6 +29611,25 @@ func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func( } return raw, nil }) + client.SetRequestHandler("tasks.cancel", func(params json.RawMessage) (json.RawMessage, *jsonrpc2.Error) { + var request ClientTaskCancelRequest + if err := json.Unmarshal(params, &request); err != nil { + return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("Invalid params: %v", err)} + } + handlers := getHandlers(request.SessionID) + if handlers == nil || handlers.Tasks == nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("No tasks handler registered for session: %s", request.SessionID)} + } + result, err := handlers.Tasks.Cancel(&request) + if err != nil { + return nil, clientSessionHandlerError(err) + } + raw, err := json.Marshal(result) + if err != nil { + return nil, &jsonrpc2.Error{Code: -32603, Message: fmt.Sprintf("Failed to marshal response: %v", err)} + } + return raw, nil + }) } // Experimental: ExtensionLaunchProviderHandler contains experimental APIs that may change diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index 78788660f5..13d190be22 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -1606,6 +1606,12 @@ func unmarshalFactoryRunFailure(data []byte) (FactoryRunFailure, error) { return nil, err } return &d, nil + case FactoryRunFailureTypeFactoryProviderDisconnected: + var d FactoryRunFailureFactoryProviderDisconnected + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case FactoryRunFailureTypeFactoryResumeDeclined: var d FactoryRunFailureFactoryResumeDeclined if err := json.Unmarshal(data, &d); err != nil { @@ -1661,6 +1667,17 @@ func (r FactoryRunFailureFactoryLimitReached) MarshalJSON() ([]byte, error) { }) } +func (r FactoryRunFailureFactoryProviderDisconnected) MarshalJSON() ([]byte, error) { + type alias FactoryRunFailureFactoryProviderDisconnected + return json.Marshal(struct { + Type FactoryRunFailureType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r FactoryRunFailureFactoryResumeDeclined) MarshalJSON() ([]byte, error) { type alias FactoryRunFailureFactoryResumeDeclined return json.Marshal(struct { @@ -1698,6 +1715,7 @@ func (r *FactoryRunTerminal) UnmarshalJSON(data []byte) error { func (r *FactoryRunResult) UnmarshalJSON(data []byte) error { type rawFactoryRunResult struct { + Attempt *int64 `json:"attempt,omitempty"` Error *string `json:"error,omitempty"` Failure json.RawMessage `json:"failure,omitempty"` Reason *string `json:"reason,omitempty"` @@ -1710,6 +1728,7 @@ func (r *FactoryRunResult) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &raw); err != nil { return err } + r.Attempt = raw.Attempt r.Error = raw.Error if raw.Failure != nil { value, err := unmarshalFactoryRunFailure(raw.Failure) @@ -5362,6 +5381,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { AgentContext *string `json:"agentContext,omitempty"` AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` AskUserDisabled *bool `json:"askUserDisabled,omitempty"` + AuthClientIDMetadataURL *string `json:"authClientIdMetadataUrl,omitempty"` AuthInfo json.RawMessage `json:"authInfo,omitempty"` AvailableTools []string `json:"availableTools,omitzero"` Capi *CapiSessionOptions `json:"capi,omitempty"` @@ -5382,6 +5402,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { EnableManagedSettings *bool `json:"enableManagedSettings,omitempty"` EnableOnDemandInstructionDiscovery *bool `json:"enableOnDemandInstructionDiscovery,omitempty"` EnableScriptSafety *bool `json:"enableScriptSafety,omitempty"` + EnableSkills *bool `json:"enableSkills,omitempty"` EnableStreaming *bool `json:"enableStreaming,omitempty"` EnvValueMode *SessionOpenOptionsEnvValueMode `json:"envValueMode,omitempty"` EventsLogDirectory *string `json:"eventsLogDirectory,omitempty"` @@ -5390,6 +5411,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { ExcludedTools []string `json:"excludedTools,omitzero"` ExpAssignments any `json:"expAssignments,omitempty"` FeatureFlags map[string]bool `json:"featureFlags,omitzero"` + HasSkillProvider *bool `json:"hasSkillProvider,omitempty"` IncludedBuiltinAgents []string `json:"includedBuiltinAgents,omitzero"` IncludedBuiltinSkills []string `json:"includedBuiltinSkills,omitzero"` InstalledPlugins []InstalledPlugin `json:"installedPlugins,omitzero"` @@ -5436,6 +5458,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.AgentContext = raw.AgentContext r.AllowAllMCPServerInstructions = raw.AllowAllMCPServerInstructions r.AskUserDisabled = raw.AskUserDisabled + r.AuthClientIDMetadataURL = raw.AuthClientIDMetadataURL if raw.AuthInfo != nil { value, err := unmarshalAuthInfo(raw.AuthInfo) if err != nil { @@ -5462,6 +5485,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.EnableManagedSettings = raw.EnableManagedSettings r.EnableOnDemandInstructionDiscovery = raw.EnableOnDemandInstructionDiscovery r.EnableScriptSafety = raw.EnableScriptSafety + r.EnableSkills = raw.EnableSkills r.EnableStreaming = raw.EnableStreaming r.EnvValueMode = raw.EnvValueMode r.EventsLogDirectory = raw.EventsLogDirectory @@ -5470,6 +5494,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.ExcludedTools = raw.ExcludedTools r.ExpAssignments = raw.ExpAssignments r.FeatureFlags = raw.FeatureFlags + r.HasSkillProvider = raw.HasSkillProvider r.IncludedBuiltinAgents = raw.IncludedBuiltinAgents r.IncludedBuiltinSkills = raw.IncludedBuiltinSkills r.InstalledPlugins = raw.InstalledPlugins @@ -5922,6 +5947,103 @@ func (r SlashCommandTextResult) MarshalJSON() ([]byte, error) { }) } +func unmarshalTaskClientUpdate(data []byte) (TaskClientUpdate, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind TaskClientUpdateKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case TaskClientUpdateKindCancelled: + var d TaskClientUpdateCancelled + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case TaskClientUpdateKindCompleted: + var d TaskClientUpdateCompleted + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case TaskClientUpdateKindFailed: + var d TaskClientUpdateFailed + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case TaskClientUpdateKindProgress: + var d TaskClientUpdateProgress + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawTaskClientUpdateData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawTaskClientUpdateData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind TaskClientUpdateKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r TaskClientUpdateCancelled) MarshalJSON() ([]byte, error) { + type alias TaskClientUpdateCancelled + return json.Marshal(struct { + Kind TaskClientUpdateKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r TaskClientUpdateCompleted) MarshalJSON() ([]byte, error) { + type alias TaskClientUpdateCompleted + return json.Marshal(struct { + Kind TaskClientUpdateKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r TaskClientUpdateFailed) MarshalJSON() ([]byte, error) { + type alias TaskClientUpdateFailed + return json.Marshal(struct { + Kind TaskClientUpdateKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r TaskClientUpdateProgress) MarshalJSON() ([]byte, error) { + type alias TaskClientUpdateProgress + return json.Marshal(struct { + Kind TaskClientUpdateKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + func unmarshalTaskInfo(data []byte) (TaskInfo, error) { if string(data) == "null" { return nil, nil @@ -5941,6 +6063,12 @@ func unmarshalTaskInfo(data []byte) (TaskInfo, error) { return nil, err } return &d, nil + case TaskInfoTypeClient: + var d TaskClientInfo + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case TaskInfoTypeShell: var d TaskShellInfo if err := json.Unmarshal(data, &d); err != nil { @@ -5974,6 +6102,17 @@ func (r TaskAgentInfo) MarshalJSON() ([]byte, error) { }) } +func (r TaskClientInfo) MarshalJSON() ([]byte, error) { + type alias TaskClientInfo + return json.Marshal(struct { + Type TaskInfoType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r TaskShellInfo) MarshalJSON() ([]byte, error) { type alias TaskShellInfo return json.Marshal(struct { @@ -6025,6 +6164,12 @@ func unmarshalTaskProgress(data []byte) (TaskProgress, error) { return nil, err } return &d, nil + case TaskProgressTypeClient: + var d TaskClientProgress + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil case TaskProgressTypeShell: var d TaskShellProgress if err := json.Unmarshal(data, &d); err != nil { @@ -6058,6 +6203,17 @@ func (r TaskAgentProgress) MarshalJSON() ([]byte, error) { }) } +func (r TaskClientProgress) MarshalJSON() ([]byte, error) { + type alias TaskClientProgress + return json.Marshal(struct { + Type TaskProgressType `json:"type"` + alias + }{ + Type: r.Type(), + alias: alias(r), + }) +} + func (r TaskShellProgress) MarshalJSON() ([]byte, error) { type alias TaskShellProgress return json.Marshal(struct { @@ -6123,6 +6279,28 @@ func (r *TasksPromoteCurrentToBackgroundResult) UnmarshalJSON(data []byte) error return nil } +func (r *TasksUpdateRequest) UnmarshalJSON(data []byte) error { + type rawTasksUpdateRequest struct { + ID string `json:"id"` + Sequence int64 `json:"sequence"` + Update json.RawMessage `json:"update"` + } + var raw rawTasksUpdateRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.ID = raw.ID + r.Sequence = raw.Sequence + if raw.Update != nil { + value, err := unmarshalTaskClientUpdate(raw.Update) + if err != nil { + return err + } + r.Update = value + } + return nil +} + func (r *ToolResultExpanded) UnmarshalJSON(data []byte) error { type rawToolResultExpanded struct { BinaryResultsForLlm []ExternalToolTextResultForLlmBinaryResultsForLlm `json:"binaryResultsForLlm,omitzero"` diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index 4c03a42c00..a220b79ad5 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -47,6 +47,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeAssistantFusionPhaseActivity: + var d AssistantFusionPhaseActivityData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeAssistantFusionPhaseCompleted: var d AssistantFusionPhaseCompletedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -383,6 +389,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionAutoTierSwitchFailed: + var d SessionAutoTierSwitchFailedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionBackgroundTasksChanged: var d SessionBackgroundTasksChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -443,6 +455,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionCompletionReceipt: + var d SessionCompletionReceiptData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionContextChanged: var d SessionContextChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -551,6 +569,18 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionMCPServerNeedsReconnect: + var d SessionMCPServerNeedsReconnectData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeSessionMCPServerRemoved: + var d SessionMCPServerRemovedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionMCPServersLoaded: var d SessionMCPServersLoadedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -575,6 +605,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeSessionModeNoticeDelivered: + var d SessionModeNoticeDeliveredData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeSessionPermissionsChanged: var d SessionPermissionsChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -858,6 +894,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { Delivery *UserMessageDelivery `json:"delivery,omitempty"` InteractionID *string `json:"interactionId,omitempty"` IsAutopilotContinuation *bool `json:"isAutopilotContinuation,omitempty"` + MessageID *string `json:"messageId,omitempty"` NativeDocumentPathFallbackPaths []string `json:"nativeDocumentPathFallbackPaths,omitzero"` ParentAgentTaskID *string `json:"parentAgentTaskId,omitempty"` Source *string `json:"source,omitempty"` @@ -884,6 +921,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { r.Delivery = raw.Delivery r.InteractionID = raw.InteractionID r.IsAutopilotContinuation = raw.IsAutopilotContinuation + r.MessageID = raw.MessageID r.NativeDocumentPathFallbackPaths = raw.NativeDocumentPathFallbackPaths r.ParentAgentTaskID = raw.ParentAgentTaskID r.Source = raw.Source @@ -2095,6 +2133,7 @@ func (r PermissionPromptRequestWrite) MarshalJSON() ([]byte, error) { func (r *PermissionRequestedData) UnmarshalJSON(data []byte) error { type rawPermissionRequestedData struct { + AgentMode *SessionMode `json:"agentMode,omitempty"` PermissionRequest json.RawMessage `json:"permissionRequest"` PromptRequest json.RawMessage `json:"promptRequest,omitempty"` RequestID string `json:"requestId"` @@ -2105,6 +2144,7 @@ func (r *PermissionRequestedData) UnmarshalJSON(data []byte) error { if err := json.Unmarshal(data, &raw); err != nil { return err } + r.AgentMode = raw.AgentMode if raw.PermissionRequest != nil { value, err := unmarshalPermissionRequest(raw.PermissionRequest) if err != nil { diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index acea96f5b9..2874e1f59b 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -55,6 +55,9 @@ type SessionEventType string const ( SessionEventTypeAbort SessionEventType = "abort" SessionEventTypeAgentInterrupted SessionEventType = "agent.interrupted" + // Experimental: SessionEventTypeAssistantFusionPhaseActivity identifies an experimental + // event that may change or be removed. + SessionEventTypeAssistantFusionPhaseActivity SessionEventType = "assistant.fusion_phase_activity" // Experimental: SessionEventTypeAssistantFusionPhaseCompleted identifies an experimental // event that may change or be removed. SessionEventTypeAssistantFusionPhaseCompleted SessionEventType = "assistant.fusion_phase_completed" @@ -125,6 +128,7 @@ const ( // that may change or be removed. SessionEventTypeSessionAutoModeResolved SessionEventType = "session.auto_mode_resolved" SessionEventTypeSessionAutopilotObjectiveChanged SessionEventType = "session.autopilot_objective_changed" + SessionEventTypeSessionAutoTierSwitchFailed SessionEventType = "session.auto_tier_switch_failed" SessionEventTypeSessionBackgroundTasksChanged SessionEventType = "session.background_tasks_changed" // Experimental: SessionEventTypeSessionBinaryAsset identifies an experimental event that // may change or be removed. @@ -146,9 +150,12 @@ const ( SessionEventTypeSessionCanvasRemoved SessionEventType = "session.canvas.removed" // Experimental: SessionEventTypeSessionCanvasUnavailable identifies an experimental event // that may change or be removed. - SessionEventTypeSessionCanvasUnavailable SessionEventType = "session.canvas.unavailable" - SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete" - SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start" + SessionEventTypeSessionCanvasUnavailable SessionEventType = "session.canvas.unavailable" + SessionEventTypeSessionCompactionComplete SessionEventType = "session.compaction_complete" + SessionEventTypeSessionCompactionStart SessionEventType = "session.compaction_start" + // Experimental: SessionEventTypeSessionCompletionReceipt identifies an experimental event + // that may change or be removed. + SessionEventTypeSessionCompletionReceipt SessionEventType = "session.completion_receipt" SessionEventTypeSessionContextChanged SessionEventType = "session.context_changed" SessionEventTypeSessionContextCleared SessionEventType = "session.context_cleared" SessionEventTypeSessionCustomAgentsUpdated SessionEventType = "session.custom_agents_updated" @@ -179,10 +186,13 @@ const ( // Experimental: SessionEventTypeSessionManagedSettingsResolved identifies an experimental // event that may change or be removed. SessionEventTypeSessionManagedSettingsResolved SessionEventType = "session.managed_settings_resolved" + SessionEventTypeSessionMCPServerNeedsReconnect SessionEventType = "session.mcp_server_needs_reconnect" + SessionEventTypeSessionMCPServerRemoved SessionEventType = "session.mcp_server_removed" SessionEventTypeSessionMCPServersLoaded SessionEventType = "session.mcp_servers_loaded" SessionEventTypeSessionMCPServerStatusChanged SessionEventType = "session.mcp_server_status_changed" SessionEventTypeSessionModeChanged SessionEventType = "session.mode_changed" SessionEventTypeSessionModelChange SessionEventType = "session.model_change" + SessionEventTypeSessionModeNoticeDelivered SessionEventType = "session.mode_notice_delivered" // Experimental: SessionEventTypeSessionPermissionsChanged identifies an experimental event // that may change or be removed. SessionEventTypeSessionPermissionsChanged SessionEventType = "session.permissions_changed" @@ -299,6 +309,21 @@ type PromptCacheBreakData struct { func (*PromptCacheBreakData) sessionEventData() {} func (*PromptCacheBreakData) Type() SessionEventType { return SessionEventTypePromptCacheBreak } +// A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. +type SessionAutoTierSwitchFailedData struct { + // Auto preference that remains effective after the failed request. + EffectiveAutoTier *AutoTier `json:"effectiveAutoTier,omitempty"` + // Low-cardinality failure outcome reported by Auto resolution. + Reason AutoTierSwitchFailureReason `json:"reason"` + // Auto preference that failed to activate, or null when returning to provider-default routing failed. + RequestedAutoTier *AutoTier `json:"requestedAutoTier"` +} + +func (*SessionAutoTierSwitchFailedData) sessionEventData() {} +func (*SessionAutoTierSwitchFailedData) Type() SessionEventType { + return SessionEventTypeSessionAutoTierSwitchFailed +} + // Agent intent description for current activity or plan type AssistantIntentData struct { // Short description of what the agent is currently doing or planning to do @@ -473,6 +498,32 @@ func (*SessionAutopilotObjectiveChangedData) Type() SessionEventType { return SessionEventTypeSessionAutopilotObjectiveChanged } +// Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +// Experimental: SessionCompletionReceiptData is part of an experimental API and may change or be removed. +type SessionCompletionReceiptData struct { + // One-based accepted completion receipt ordinal in the durable session history. + Attempt int64 `json:"attempt"` + // Inclusive durable event range summarized by this receipt. + EventRange CompletionReceiptEventRange `json:"eventRange"` + // Number of failed structured tool completions in the covered range. + FailedToolCount int64 `json:"failedToolCount"` + // Final structured tool completion in the covered range, when one exists. + FinalTool *CompletionReceiptFinalTool `json:"finalTool,omitempty"` + // Version of the completion receipt payload. + SchemaVersion int64 `json:"schemaVersion"` + // Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + SourceEventID string `json:"sourceEventId"` + // Runtime reason the completion decision was accepted. + StopReason CompletionReceiptStopReason `json:"stopReason"` + // Number of successful structured tool completions in the covered range. + SuccessfulToolCount int64 `json:"successfulToolCount"` +} + +func (*SessionCompletionReceiptData) sessionEventData() {} +func (*SessionCompletionReceiptData) Type() SessionEventType { + return SessionEventTypeSessionCompletionReceipt +} + // Canonical bytes for a content-addressed binary asset shared by reference across events type SessionBinaryAssetData struct { // Content-addressed id for this binary asset (e.g. "sha256:..."). @@ -768,7 +819,7 @@ func (*PendingMessagesModifiedData) Type() SessionEventType { return SessionEventTypePendingMessagesModified } -// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +// Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. // Experimental: SessionManagedSettingsResolvedData is part of an experimental API and may change or be removed. type SessionManagedSettingsResolvedData struct { // Whether enterprise policy disables bypass-permissions ("yolo") mode for this session. Deny-wins across layers, and forced on when `failClosed` is true. @@ -783,13 +834,15 @@ type SessionManagedSettingsResolvedData struct { ManagedKeys []string `json:"managedKeys"` // Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. PermissionsAllowIntersected *bool `json:"permissionsAllowIntersected,omitempty"` + // Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + PolicyHelperManaged *bool `json:"policyHelperManaged,omitempty"` // Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. SandboxEnabledByUndeterminedPolicy *bool `json:"sandboxEnabledByUndeterminedPolicy,omitempty"` // Whether the server (account/org) managed-settings layer was present ServerManaged bool `json:"serverManaged"` // The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. Settings any `json:"settings,omitempty"` - // Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + // Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. Source ManagedSettingsResolvedSource `json:"source"` } @@ -867,6 +920,8 @@ type SessionErrorData struct { Message string `json:"message"` // GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs ProviderCallID *string `json:"providerCallId,omitempty"` + // What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + Remediation *RemediationAction `json:"remediation,omitempty"` // Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation ServiceRequestID *string `json:"serviceRequestId,omitempty"` // Error stack trace, when available @@ -880,6 +935,34 @@ type SessionErrorData struct { func (*SessionErrorData) sessionEventData() {} func (*SessionErrorData) Type() SessionEventType { return SessionEventTypeSessionError } +// Experimental content-safe activity signal for a running HydraFusion phase. +// Experimental: AssistantFusionPhaseActivityData is part of an experimental API and may change or be removed. +type AssistantFusionPhaseActivityData struct { + // Kind of real activity observed. + Activity FusionPhaseActivityKind `json:"activity"` + // Conversation scope in which the phase executes. + ConversationScope FusionConversationScope `json:"conversationScope"` + // Identifier of the HydraFusion turn containing the phase. + FusionID string `json:"fusionId"` + // HydraFusion orchestration pattern containing the phase. + Pattern FusionPattern `json:"pattern"` + // Stable identifier for the concrete phase. + PhaseID string `json:"phaseId"` + // Kind of phase currently executing. + PhaseKind FusionPhaseKind `json:"phaseKind"` + // Semantic role assigned to the phase. + Role string `json:"role"` + // Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + ToolCallID *string `json:"toolCallId,omitempty"` + // Cumulative private response bytes observed for this model call. The event never includes response text. + TotalResponseSizeBytes *int64 `json:"totalResponseSizeBytes,omitempty"` +} + +func (*AssistantFusionPhaseActivityData) sessionEventData() {} +func (*AssistantFusionPhaseActivityData) Type() SessionEventType { + return SessionEventTypeAssistantFusionPhaseActivity +} + // Experimental durable HydraFusion phase output and lossless replay checkpoint. // Experimental: AssistantFusionPhaseCompletedData is part of an experimental API and may change or be removed. type AssistantFusionPhaseCompletedData struct { @@ -1042,6 +1125,9 @@ type SessionFusionResolvedData struct { ModelUniverseVersion *string `json:"modelUniverseVersion,omitempty"` // Validated orchestration pattern selected for the turn. Pattern FusionPattern `json:"pattern"` + // Presentation-neutral phase plan for clients that render workflow progress. + // Experimental: PhasePlan is part of an experimental API and may change or be removed. + PhasePlan []FusionPhasePlanStep `json:"phasePlan,omitzero"` // Version of the validated execution-plan format. PlanVersion *string `json:"planVersion,omitempty"` // HydraFusion routing policy used to resolve the plan. @@ -1511,12 +1597,16 @@ func (*ModelCallStartData) Type() SessionEventType { return SessionEventTypeMode // Model change details including previous and new model identifiers type SessionModelChangeData struct { + // Committed Auto preference after the model configuration change, when applicable. + AutoTier *AutoTier `json:"autoTier,omitempty"` // Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. Cause *string `json:"cause,omitempty"` // Context tier after the model change; null explicitly clears a previously selected tier ContextTier *ContextTier `json:"contextTier,omitempty"` // Newly selected model identifier NewModel string `json:"newModel"` + // Previously committed Auto preference, when one was explicitly selected. + PreviousAutoTier *AutoTier `json:"previousAutoTier,omitempty"` // Model that was previously selected, if any PreviousModel *string `json:"previousModel,omitempty"` // Reasoning effort level before the model change, if applicable @@ -1756,6 +1846,28 @@ func (*SessionExtensionsLoadedData) Type() SessionEventType { return SessionEventTypeSessionExtensionsLoaded } +// Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. +type SessionMCPServerNeedsReconnectData struct { + // Name of the MCP server that needs to reconnect + ServerName string `json:"serverName"` +} + +func (*SessionMCPServerNeedsReconnectData) sessionEventData() {} +func (*SessionMCPServerNeedsReconnectData) Type() SessionEventType { + return SessionEventTypeSessionMCPServerNeedsReconnect +} + +// Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. +type SessionMCPServerRemovedData struct { + // Name of the MCP server that was removed from the graph + ServerName string `json:"serverName"` +} + +func (*SessionMCPServerRemovedData) sessionEventData() {} +func (*SessionMCPServerRemovedData) Type() SessionEventType { + return SessionEventTypeSessionMCPServerRemoved +} + // Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error. type SessionMCPServerStatusChangedData struct { // Error message if the server entered a failed state @@ -1814,6 +1926,8 @@ type UserMessageData struct { InteractionID *string `json:"interactionId,omitempty"` // True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. IsAutopilotContinuation *bool `json:"isAutopilotContinuation,omitempty"` + // Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots + MessageID *string `json:"messageId,omitempty"` // Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit NativeDocumentPathFallbackPaths []string `json:"nativeDocumentPathFallbackPaths,omitzero"` // Parent agent task ID for background telemetry correlated to this user turn @@ -1846,6 +1960,8 @@ func (*PermissionCompletedData) Type() SessionEventType { return SessionEventTyp // Permission request notification requiring client approval with request details type PermissionRequestedData struct { + // Agent mode captured from the owning turn when permission evaluation began. + AgentMode *SessionMode `json:"agentMode,omitempty"` // Details of the permission being requested PermissionRequest PermissionRequest `json:"permissionRequest"` // Derived user-facing permission prompt details for UI consumers @@ -1960,6 +2076,19 @@ type CommandQueuedData struct { func (*CommandQueuedData) sessionEventData() {} func (*CommandQueuedData) Type() SessionEventType { return SessionEventTypeCommandQueued } +// Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +type SessionModeNoticeDeliveredData struct { + // Model-visible transition notice persisted for a mid-turn delivery + Content *string `json:"content,omitempty"` + // Mode established by the delivered transition notice + Mode SessionMode `json:"mode"` +} + +func (*SessionModeNoticeDeliveredData) sessionEventData() {} +func (*SessionModeNoticeDeliveredData) Type() SessionEventType { + return SessionEventTypeSessionModeNoticeDelivered +} + // Registered command dispatch request routed to the owning client type CommandExecuteData struct { // Raw argument string after the command name @@ -2319,17 +2448,19 @@ type SkillInvokedData struct { Content string `json:"content"` // Description of the skill from its SKILL.md frontmatter Description *string `json:"description,omitempty"` + // Whether model invocation is disabled for this skill + DisableModelInvocation *bool `json:"disableModelInvocation,omitempty"` // Model identifier active when the skill was invoked, when known Model *string `json:"model,omitempty"` // Name of the invoked skill Name string `json:"name"` - // File path to the SKILL.md definition + // File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity Path string `json:"path"` // Name of the plugin this skill originated from, when applicable PluginName *string `json:"pluginName,omitempty"` // Version of the plugin this skill originated from, when applicable PluginVersion *string `json:"pluginVersion,omitempty"` - // Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + // Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) Source *string `json:"source,omitempty"` // What triggered the skill invocation: `user-invoked` (explicit user action, such as via a slash command or UI affordance), `agent-invoked` (agent requested the skill), or `context-load` (loaded as part of another context, such as preloading skills configured on a custom agent or subagent) Trigger *SkillInvokedTrigger `json:"trigger,omitempty"` @@ -2443,6 +2574,8 @@ type SubagentCompletedData struct { FirstDispatchedModel *string `json:"firstDispatchedModel,omitempty"` // Model used by the sub-agent Model *string `json:"model,omitempty"` + // Why an explicit task-call model did not become the effective model + ModelOverrideReason *string `json:"modelOverrideReason,omitempty"` // Tool call ID of the parent tool invocation that spawned this sub-agent ToolCallID string `json:"toolCallId"` // Total tokens (input + output) consumed by the sub-agent @@ -2476,6 +2609,8 @@ type SubagentFailedData struct { FirstDispatchedModel *string `json:"firstDispatchedModel,omitempty"` // Model selected for the sub-agent, when known Model *string `json:"model,omitempty"` + // Why an explicit task-call model did not become the effective model + ModelOverrideReason *string `json:"modelOverrideReason,omitempty"` // Tool call ID of the parent tool invocation that spawned this sub-agent ToolCallID string `json:"toolCallId"` // Total tokens (input + output) consumed before the sub-agent failed @@ -2735,6 +2870,8 @@ func (*ToolUserRequestedData) Type() SessionEventType { return SessionEventTypeT type SessionWarningData struct { // Human-readable warning message for display in the timeline Message string `json:"message"` + // What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + Remediation *RemediationAction `json:"remediation,omitempty"` // Optional URL associated with this warning that the user can open in a browser URL *string `json:"url,omitempty"` // Category of warning (e.g., "subscription", "policy", "mcp") @@ -2787,7 +2924,7 @@ func (*SessionWorkspaceFileChangedData) Type() SessionEventType { // Neutral provider-tagged reasoning content blocks preserved verbatim for round-tripping // Experimental: AssistantMessageReasoningBlocks is part of an experimental API and may change or be removed. type AssistantMessageReasoningBlocks struct { - // Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + // Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. Blocks []any `json:"blocks,omitzero"` // Model provider that produced these reasoning blocks. Provider string `json:"provider"` @@ -3107,7 +3244,27 @@ type CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail struct { TokenType string `json:"tokenType"` } -// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. +// Inclusive durable event range summarized by a completion receipt. +type CompletionReceiptEventRange struct { + // Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + EndEventID string `json:"endEventId"` + // Identifier of the user message that starts the covered exchange. + StartEventID string `json:"startEventId"` +} + +// Final structured tool completion in the covered event range. +type CompletionReceiptFinalTool struct { + // Process exit code from a structured shell result, when available. + ExitCode *int64 `json:"exitCode,omitempty"` + // Structured success or failure status from the tool completion event. + Status CompletionReceiptToolStatus `json:"status"` + // Unique identifier of the completed tool call. + ToolCallID string `json:"toolCallId"` + // Tool name from the matching tool execution start event, when available. + ToolName *string `json:"toolName,omitempty"` +} + +// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. type CustomAgentsUpdatedAgent struct { // Description of what the agent does Description string `json:"description"` @@ -3117,6 +3274,10 @@ type CustomAgentsUpdatedAgent struct { ID string `json:"id"` // Model override for this agent, if set Model *string `json:"model,omitempty"` + // Whether authored models are preferences or required constraints + ModelPolicy *AgentModelPolicy `json:"modelPolicy,omitempty"` + // Authored model ids in priority order, if configured + Models []string `json:"models,omitzero"` // Internal name of the agent Name string `json:"name"` // Source location: user, project, inherited, remote, or plugin @@ -3193,6 +3354,19 @@ type FusionFollowUpRecommendation struct { UserTurn FusionFollowUpAction `json:"userTurn"` } +// Presentation-neutral phase planned for a HydraFusion turn. +// Experimental: FusionPhasePlanStep is part of an experimental API and may change or be removed. +type FusionPhasePlanStep struct { + // Whether the phase executes only when an earlier phase requests it. + Conditional bool `json:"conditional"` + // Kind of phase that may execute. + Kind FusionPhaseKind `json:"kind"` + // Semantic role assigned to the phase. + Role string `json:"role"` + // Conversation scope in which the phase executes. + Scope FusionConversationScope `json:"scope"` +} + // Aggregate concrete-model usage for one HydraFusion phase. // Experimental: FusionPhaseUsage is part of an experimental API and may change or be removed. type FusionPhaseUsage struct { @@ -3336,6 +3510,8 @@ type MCPServersLoadedServer struct { PluginName *string `json:"pluginName,omitempty"` // Version of the plugin that supplied the effective MCP server config, only when source is plugin PluginVersion *string `json:"pluginVersion,omitempty"` + // Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + ServerMetadata *MCPServerMetadata `json:"serverMetadata,omitempty"` // Configuration source: user, workspace, plugin, or builtin Source *MCPServerSource `json:"source,omitempty"` // Connection status: connected, failed, needs-auth, pending, disabled, stopped, or not_configured @@ -3657,9 +3833,9 @@ type PermissionPromptRequestURL struct { ManagedApprovalRequired *bool `json:"managedApprovalRequired,omitempty"` // Immediately preceding URL when this prompt is for a redirect target RedirectedFrom *string `json:"redirectedFrom,omitempty"` - // True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + // True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` - // Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + // What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` @@ -3913,9 +4089,9 @@ type PermissionRequestRead struct { ManagedApprovalRequired *bool `json:"managedApprovalRequired,omitempty"` // Path of the file or directory being read Path string `json:"path"` - // True when the model has requested to run this search outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + // True when the tool is asking to re-run this search outside the sandbox, after a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` - // Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + // What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` @@ -3946,9 +4122,9 @@ type PermissionRequestShell struct { PossiblePaths []string `json:"possiblePaths"` // URLs that may be accessed by the command PossibleURLs []PermissionRequestShellPossibleURL `json:"possibleUrls"` - // True when the model has requested to run this command outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + // True when the tool is asking to run this command outside the sandbox, either because the command detaches and cannot be sandboxed at all, or because a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` - // Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + // What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` @@ -3969,9 +4145,9 @@ type PermissionRequestURL struct { ManagedApprovalRequired *bool `json:"managedApprovalRequired,omitempty"` // Immediately preceding URL when this request is for a redirect target RedirectedFrom *string `json:"redirectedFrom,omitempty"` - // True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + // True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. RequestSandboxBypass *bool `json:"requestSandboxBypass,omitempty"` - // Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + // What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. RequestSandboxBypassReason *string `json:"requestSandboxBypassReason,omitempty"` // Tool call ID that triggered this permission request ToolCallID *string `json:"toolCallId,omitempty"` @@ -4339,7 +4515,7 @@ type SkillsLoadedSkill struct { Name string `json:"name"` // Absolute path to the skill file, if available Path *string `json:"path,omitempty"` - // Source location type (e.g., project, personal-copilot, plugin, builtin) + // Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk) Source SkillSource `json:"source"` // Whether the skill can be invoked by the user as a slash command UserInvocable bool `json:"userInvocable"` @@ -4656,6 +4832,8 @@ type ToolExecutionCompleteError struct { Code *string `json:"code,omitempty"` // Human-readable error message Message string `json:"message"` + // What the user must do to recover, when the runtime knows of an action. Set on sandbox policy denials, where `message` names the rule that blocked the call but never the client affordance that relaxes it. + Remediation *RemediationAction `json:"remediation,omitempty"` } // Tool execution result on success @@ -4993,6 +5171,20 @@ const ( AutopilotObjectiveChangedStatusPaused AutopilotObjectiveChangedStatus = "paused" ) +// Terminal reason an Auto preference activation failed. +type AutoTierSwitchFailureReason string + +const ( + // The candidate model was rejected by model policy. + AutoTierSwitchFailureReasonPolicyRejected AutoTierSwitchFailureReason = "policy_rejected" + // The Auto routing request failed or returned an unusable response. + AutoTierSwitchFailureReasonRequestFailed AutoTierSwitchFailureReason = "request_failed" + // The runtime could not prepare the Auto routing request. + AutoTierSwitchFailureReasonSetupFailed AutoTierSwitchFailureReason = "setup_failed" + // The provider does not support Auto routing. + AutoTierSwitchFailureReasonUnsupported AutoTierSwitchFailureReason = "unsupported" +) + // Binary result type discriminator. Use "image" for images and "resource" for other binary data. type BinaryAssetReferenceType string @@ -5052,6 +5244,34 @@ const ( CompactionTriggerThreshold CompactionTrigger = "threshold" ) +// Runtime reason the completion decision was accepted. +type CompletionReceiptStopReason string + +const ( + // The configured agentStop continuation limit was reached. + CompletionReceiptStopReasonAgentStopBlockLimit CompletionReceiptStopReason = "agent_stop_block_limit" + // The model reached a natural terminal response. + CompletionReceiptStopReasonNatural CompletionReceiptStopReason = "natural" + // A terminal tool ended the interaction. + CompletionReceiptStopReasonTerminalTool CompletionReceiptStopReason = "terminal_tool" +) + +// Structured terminal status from a tool completion event. +type CompletionReceiptToolStatus string + +const ( + // The permissions service denied the tool call. + CompletionReceiptToolStatusDenied CompletionReceiptToolStatus = "denied" + // The tool failed without a more specific structured status. + CompletionReceiptToolStatusFailure CompletionReceiptToolStatus = "failure" + // The user rejected the tool call. + CompletionReceiptToolStatusRejected CompletionReceiptToolStatus = "rejected" + // The tool completed successfully. + CompletionReceiptToolStatusSuccess CompletionReceiptToolStatus = "success" + // The tool exceeded its time budget. + CompletionReceiptToolStatusTimeout CompletionReceiptToolStatus = "timeout" +) + // The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" (dismissed) type ElicitationCompletedAction string @@ -5182,6 +5402,19 @@ const ( FusionPatternSingle FusionPattern = "single" ) +// Content-safe activity observed while a HydraFusion phase is running. +// Experimental: FusionPhaseActivityKind is part of an experimental API and may change or be removed. +type FusionPhaseActivityKind string + +const ( + // The provider produced additional private output bytes. + FusionPhaseActivityKindModelOutput FusionPhaseActivityKind = "model_output" + // A tool finished executing inside the phase. + FusionPhaseActivityKindToolCompleted FusionPhaseActivityKind = "tool_completed" + // A tool began executing inside the phase. + FusionPhaseActivityKindToolStarted FusionPhaseActivityKind = "tool_started" +) + // HydraFusion phase kind. // Experimental: FusionPhaseKind is part of an experimental API and may change or be removed. type FusionPhaseKind string @@ -5284,10 +5517,12 @@ const ( ManagedSettingsResolvedSourceClient ManagedSettingsResolvedSource = "client" // Only the device MDM/plist/registry/file channel contributed. ManagedSettingsResolvedSourceDevice ManagedSettingsResolvedSource = "device" - // More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + // More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. ManagedSettingsResolvedSourceMixed ManagedSettingsResolvedSource = "mixed" // No managed policy is in force (no channel contributed). ManagedSettingsResolvedSourceNone ManagedSettingsResolvedSource = "none" + // A policy helper registered by device or server policy contributed. Device registration takes priority when present. + ManagedSettingsResolvedSourcePolicyHelper ManagedSettingsResolvedSource = "policyHelper" // Only the server/account channel contributed. ManagedSettingsResolvedSourceServer ManagedSettingsResolvedSource = "server" ) diff --git a/go/samples/chat.go b/go/samples/chat.go index 2f34a243c9..85128a1b56 100644 --- a/go/samples/chat.go +++ b/go/samples/chat.go @@ -5,10 +5,10 @@ import ( "context" "fmt" "os" - "path/filepath" "strings" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/samples/internal/sampleutil" ) const blue = "\033[34m" @@ -16,7 +16,10 @@ const reset = "\033[0m" func main() { ctx := context.Background() - cliPath := filepath.Join("..", "..", "nodejs", "node_modules", "@github", "copilot", "index.js") + cliPath, err := sampleutil.CLIPath() + if err != nil { + panic(err) + } client := copilot.NewClient(&copilot.ClientOptions{Connection: copilot.StdioConnection{Path: cliPath}}) if err := client.Start(ctx); err != nil { panic(err) diff --git a/go/samples/internal/sampleutil/runtime.go b/go/samples/internal/sampleutil/runtime.go new file mode 100644 index 0000000000..5c51f0d8db --- /dev/null +++ b/go/samples/internal/sampleutil/runtime.go @@ -0,0 +1,48 @@ +package sampleutil + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// CLIPath resolves the pinned source-checkout runtime used by SDK samples. +func CLIPath() (string, error) { + if cliPath := os.Getenv("COPILOT_CLI_PATH"); cliPath != "" { + return cliPath, nil + } + + current, err := os.Getwd() + if err != nil { + return "", err + } + for { + nodeDir := filepath.Join(current, "nodejs") + if _, err := os.Stat(filepath.Join(nodeDir, "package.json")); err == nil { + command := exec.Command( + "node", + "node_modules/tsx/dist/cli.mjs", + "scripts/prepare-runtime.ts", + "--print-path", + ) + command.Dir = nodeDir + output, err := command.CombinedOutput() + if err != nil { + return "", fmt.Errorf("prepare pinned Copilot CLI: %w: %s", err, output) + } + cliPath := strings.TrimSpace(string(output)) + if info, err := os.Stat(cliPath); err != nil || info.IsDir() { + return "", fmt.Errorf("prepared Copilot CLI path is not a file: %q", cliPath) + } + return cliPath, nil + } + + parent := filepath.Dir(current) + if parent == current { + return "", fmt.Errorf("could not find nodejs/package.json; set COPILOT_CLI_PATH") + } + current = parent + } +} diff --git a/go/samples/manual_tool_resume/main.go b/go/samples/manual_tool_resume/main.go index a7391ff405..62806b7a78 100644 --- a/go/samples/manual_tool_resume/main.go +++ b/go/samples/manual_tool_resume/main.go @@ -4,11 +4,11 @@ import ( "context" "fmt" "os" - "path/filepath" "time" copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/rpc" + "github.com/github/copilot-sdk/go/samples/internal/sampleutil" ) const timeout = 2 * time.Minute @@ -32,7 +32,10 @@ func manualTool() copilot.Tool { } func newClient() *copilot.Client { - cliPath := filepath.Join("..", "..", "nodejs", "node_modules", "@github", "copilot", "index.js") + cliPath, err := sampleutil.CLIPath() + if err != nil { + panic(err) + } return copilot.NewClient(&copilot.ClientOptions{Connection: copilot.StdioConnection{Path: cliPath}}) } diff --git a/go/session.go b/go/session.go index 5d45d19ef2..f4bb0d38c7 100644 --- a/go/session.go +++ b/go/session.go @@ -4,6 +4,7 @@ package copilot import ( "context" "encoding/json" + "errors" "fmt" "log" "sync" @@ -65,6 +66,9 @@ type Session struct { handlerMutex sync.RWMutex toolHandlers map[string]ToolHandler toolHandlersM sync.RWMutex + pendingExternalTools map[string]*pendingExternalTool + pendingExternalToolsM sync.Mutex + externalToolsClosed bool permissionHandler PermissionHandlerFunc permissionMux sync.RWMutex managedSettings bool @@ -98,13 +102,20 @@ type Session struct { // eventCh serializes user event handler dispatch. dispatchEvent enqueues; // a single goroutine (processEvents) dequeues and invokes handlers in FIFO order. + // eventDone stops both sides without closing eventCh while a sender may be active. eventCh chan SessionEvent - closeOnce sync.Once // guards eventCh close so Disconnect is safe to call more than once + eventDone chan struct{} + closeOnce sync.Once // RPC provides typed session-scoped RPC methods. RPC *rpc.SessionRPC } +type pendingExternalTool struct { + ctx context.Context + cancel context.CancelFunc +} + // WorkspacePath returns the path to the session workspace directory when infinite // sessions are enabled. Contains checkpoints/, plan.md, and files/ subdirectories. // Returns empty string if infinite sessions are disabled. @@ -385,6 +396,7 @@ func newSession( toolHandlers: make(map[string]ToolHandler), commandHandlers: make(map[string]CommandHandler), eventCh: make(chan SessionEvent, 128), + eventDone: make(chan struct{}), RPC: rpc.NewSessionRPC(client, sessionID), } s.clientSessionAPIs.Canvas = newCanvasClientSessionAdapter(s) @@ -1389,16 +1401,24 @@ func fromRPCElicitationRequestedSchema(schema *rpc.ElicitationRequestedSchema) * // serial, FIFO dispatch without blocking the read loop. func (s *Session) dispatchEvent(event SessionEvent) { s.updateOpenCanvasesFromEvent(event) - go s.handleBroadcastEvent(event) - - // Send to the event channel in a closure with a recover guard. - // Disconnect closes eventCh, and in Go sending on a closed channel - // panics — there is no non-panicking send primitive. We only want - // to suppress that specific panic; other panics are not expected here. - func() { - defer func() { recover() }() - s.eventCh <- event - }() + + broadcastHandled := false + switch data := event.Data.(type) { + case *ExternalToolRequestedData: + s.startExternalTool(data) + broadcastHandled = true + case *ExternalToolCompletedData: + s.cancelExternalTool(data.RequestID) + broadcastHandled = true + } + if !broadcastHandled { + go s.handleBroadcastEvent(event) + } + + select { + case s.eventCh <- event: + case <-s.eventDone: + } } // processEvents is the single consumer goroutine for the event channel. @@ -1406,7 +1426,14 @@ func (s *Session) dispatchEvent(event SessionEvent) { // handlers are recovered so that one misbehaving handler does not prevent // others from receiving the event. func (s *Session) processEvents() { - for event := range s.eventCh { + for { + var event SessionEvent + select { + case event = <-s.eventCh: + case <-s.eventDone: + return + } + s.handlerMutex.RLock() handlers := make([]SessionEventHandler, 0, len(s.handlers)) for _, h := range s.handlers { @@ -1427,6 +1454,13 @@ func (s *Session) processEvents() { } } +// stopEventProcessing stops the session event consumer without making an RPC. +// CreateSession/ResumeSession use this when a locally registered session fails +// before it can be returned to the caller. +func (s *Session) stopEventProcessing() { + s.closeOnce.Do(func() { close(s.eventDone) }) +} + // handleBroadcastEvent handles broadcast request events by executing local handlers // and responding via RPC. This implements the protocol v3 broadcast model where tool // calls and permission requests are broadcast as session events to all clients. @@ -1436,20 +1470,6 @@ func (s *Session) processEvents() { // cause RPC deadlocks. func (s *Session) handleBroadcastEvent(event SessionEvent) { switch d := event.Data.(type) { - case *ExternalToolRequestedData: - handler, ok := s.getToolHandler(d.ToolName) - if !ok { - return - } - var tp, ts string - if d.Traceparent != nil { - tp = *d.Traceparent - } - if d.Tracestate != nil { - ts = *d.Tracestate - } - s.executeToolAndRespond(d.RequestID, d.ToolName, d.ToolCallID, d.Arguments, handler, tp, ts) - case *PermissionRequestedData: if d.ResolvedByHook != nil && *d.ResolvedByHook { return // Already resolved by a permissionRequest hook; no client action needed. @@ -1532,11 +1552,90 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { } } +func (s *Session) startExternalTool(data *ExternalToolRequestedData) { + handler, ok := s.getToolHandler(data.ToolName) + if !ok { + return + } + + var traceparent, tracestate string + if data.Traceparent != nil { + traceparent = *data.Traceparent + } + if data.Tracestate != nil { + tracestate = *data.Tracestate + } + traceCtx := contextWithTraceParent(context.Background(), traceparent, tracestate) + ctx, cancel := context.WithCancel(traceCtx) + pending := &pendingExternalTool{ctx: ctx, cancel: cancel} + + s.pendingExternalToolsM.Lock() + if s.externalToolsClosed { + s.pendingExternalToolsM.Unlock() + cancel() + return + } + if s.pendingExternalTools == nil { + s.pendingExternalTools = make(map[string]*pendingExternalTool) + } + if _, exists := s.pendingExternalTools[data.RequestID]; exists { + s.pendingExternalToolsM.Unlock() + cancel() + return + } + s.pendingExternalTools[data.RequestID] = pending + s.pendingExternalToolsM.Unlock() + + go s.executeToolAndRespond(data.RequestID, data.ToolName, data.ToolCallID, data.Arguments, handler, pending) +} + +func (s *Session) cancelExternalTool(requestID string) { + s.pendingExternalToolsM.Lock() + pending := s.pendingExternalTools[requestID] + delete(s.pendingExternalTools, requestID) + s.pendingExternalToolsM.Unlock() + if pending != nil { + pending.cancel() + } +} + +func (s *Session) cancelPendingExternalTools() { + s.pendingExternalToolsM.Lock() + s.externalToolsClosed = true + pendingTools := s.pendingExternalTools + s.pendingExternalTools = nil + s.pendingExternalToolsM.Unlock() + for _, pending := range pendingTools { + pending.cancel() + } +} + +func (s *Session) claimExternalTool(requestID string, pending *pendingExternalTool) bool { + s.pendingExternalToolsM.Lock() + defer s.pendingExternalToolsM.Unlock() + if s.pendingExternalTools[requestID] != pending { + return false + } + delete(s.pendingExternalTools, requestID) + return true +} + // executeToolAndRespond executes a tool handler and sends the result back via RPC. -func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, arguments any, handler ToolHandler, traceparent, tracestate string) { - ctx := contextWithTraceParent(context.Background(), traceparent, tracestate) +func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, arguments any, handler ToolHandler, pending *pendingExternalTool) { + ctx := pending.ctx + defer func() { + s.pendingExternalToolsM.Lock() + if s.pendingExternalTools[requestID] == pending { + delete(s.pendingExternalTools, requestID) + } + s.pendingExternalToolsM.Unlock() + pending.cancel() + }() defer func() { if r := recover(); r != nil { + if !s.claimExternalTool(requestID, pending) { + return + } errMsg := fmt.Sprintf("tool panic: %v", r) s.RPC.Tools.HandlePendingToolCall(ctx, &rpc.HandlePendingToolCallRequest{ RequestID: requestID, @@ -1563,8 +1662,14 @@ func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string, invocation.AvailableTools = metadata.Tools } } + if ctx.Err() != nil { + return + } result, err := handler(invocation) + if !s.claimExternalTool(requestID, pending) { + return + } if err != nil { errMsg := err.Error() s.RPC.Tools.HandlePendingToolCall(ctx, &rpc.HandlePendingToolCallRequest{ @@ -1727,9 +1832,24 @@ func (s *Session) GetEvents(ctx context.Context) ([]SessionEvent, error) { // log.Printf("Failed to disconnect session: %v", err) // } func (s *Session) Disconnect() error { - _, err := s.client.Request(context.Background(), "session.destroy", sessionDestroyRequest{SessionID: s.SessionID}) + s.cancelPendingExternalTools() + result, err := s.client.Request(context.Background(), "session.detach", sessionDetachRequest{SessionID: s.SessionID}) + if err == nil { + var response sessionDetachResponse + if decodeErr := json.Unmarshal(result, &response); decodeErr != nil { + err = fmt.Errorf("failed to decode session detach response: %w", decodeErr) + } else if !response.Success { + if response.Error == "" { + response.Error = "unknown error" + } + err = errors.New(response.Error) + } + } - s.closeOnce.Do(func() { close(s.eventCh) }) + // Local cleanup always runs, even if the detach RPC failed, so callers + // don't leak in-memory resources (event goroutines, registered + // providers/handlers) just because the runtime couldn't be reached. + s.stopEventProcessing() s.releaseGitHubTokenProviderRegistration() // Clear handlers @@ -1828,6 +1948,22 @@ type SetModelOptions struct { // ModelCapabilities overrides individual model capabilities resolved by the runtime. // Only non-nil fields are applied over the runtime-resolved capabilities. ModelCapabilities *rpc.ModelCapabilitiesOverride + // AutoTier stages an Auto routing preference atomically with selecting the + // "auto" model. Leave nil to leave the current preference alone. + // + // The runtime rejects this option when the model is anything other than + // "auto". Use [Session.SetAutoTier] to change the preference without + // changing the selected model. + // + // Experimental: AutoTier is part of an experimental Auto routing surface and + // may change or be removed. + AutoTier *AutoTier + // ResetAutoTier returns to the provider's default Auto routing as part of + // this switch. It is mutually exclusive with AutoTier. + // + // Experimental: ResetAutoTier is part of an experimental Auto routing surface + // and may change or be removed. + ResetAutoTier bool } // SetModel changes the model for this session. @@ -1844,10 +1980,25 @@ type SetModelOptions struct { func (s *Session) SetModel(ctx context.Context, model string, opts *SetModelOptions) error { params := &rpc.ModelSwitchToRequest{ModelID: model} if opts != nil { + if opts.AutoTier != nil && opts.ResetAutoTier { + return errors.New("failed to set model: AutoTier and ResetAutoTier are mutually exclusive") + } params.ReasoningEffort = opts.ReasoningEffort params.ReasoningSummary = opts.ReasoningSummary params.ContextTier = opts.ContextTier params.ModelCapabilities = opts.ModelCapabilities + + // The generated field is a double pointer so the three cases stay + // distinct on the wire: a nil outer pointer omits the field and leaves + // any staged preference alone, while a non-nil outer pointer sends the + // inner value, including an explicit null. + switch { + case opts.AutoTier != nil: + params.AutoTier = &opts.AutoTier + case opts.ResetAutoTier: + var providerDefault *AutoTier + params.AutoTier = &providerDefault + } } _, err := s.RPC.Model.SwitchTo(ctx, params) if err != nil { @@ -1857,6 +2008,41 @@ func (s *Session) SetModel(ctx context.Context, model string, opts *SetModelOpti return nil } +// SetAutoTier changes the Auto routing preference without changing the selected model. +// +// The runtime does not apply the preference immediately. It records the request and +// commits it only when a later user turn using the "auto" model successfully obtains a +// usable model from the provider. A [rpc.ModelSwitchAutoTierStatusPending] status +// therefore confirms that the request was accepted, not that it took effect. +// +// Watch for the outcome through the session.model_change event on success, or the +// ephemeral session.auto_tier_switch_failed event on failure. You can also read the +// current committed and in-flight state at any time with session.RPC.Model.GetCurrent. +// +// Only the most recent request survives: issuing a new request replaces any earlier one +// that has not yet been claimed by a turn. +// +// Pass nil to return to the provider's default Auto routing. +// +// Experimental: SetAutoTier is part of an experimental Auto routing surface and +// may change or be removed. +// +// Example: +// +// tier := copilot.AutoTierIntelligence +// result, err := session.SetAutoTier(context.Background(), &tier) +// if err != nil { +// log.Printf("Failed to set auto tier: %v", err) +// } +func (s *Session) SetAutoTier(ctx context.Context, autoTier *AutoTier) (*rpc.ModelSwitchAutoTierResult, error) { + result, err := s.RPC.Model.SwitchAutoTier(ctx, &rpc.ModelSwitchAutoTierRequest{AutoTier: autoTier}) + if err != nil { + return nil, fmt.Errorf("failed to set auto tier: %w", err) + } + + return result, nil +} + type LogOptions struct { // Level sets the log severity. Valid values are [rpc.SessionLogLevelInfo] (default), // [rpc.SessionLogLevelWarning], and [rpc.SessionLogLevelError]. diff --git a/go/session_event_serialization_test.go b/go/session_event_serialization_test.go index ee9258b225..c64b6ce598 100644 --- a/go/session_event_serialization_test.go +++ b/go/session_event_serialization_test.go @@ -14,6 +14,50 @@ var _ SessionEventData = (*rpc.UserMessageData)(nil) var _ rpc.EmbeddedTextResourceContents = EmbeddedTextResourceContents{} var _ EmbeddedTextResourceContents = rpc.EmbeddedTextResourceContents{} +func TestSessionEventAutoTier(t *testing.T) { + for _, eventType := range []string{"session.start", "session.resume"} { + for _, tier := range []AutoTier{"", AutoTierEfficiency, AutoTierBalance, AutoTierIntelligence} { + t.Run(eventType+"/"+string(tier), func(t *testing.T) { + data := map[string]any{ + "sessionId": "test-session", "version": 1, + "producer": "copilot", "copilotVersion": "1.0.82-1", + "startTime": "2026-08-28T00:00:00Z", + "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1, + } + if tier != "" { + data["autoTier"] = tier + } + wire, err := json.Marshal(map[string]any{ + "id": "00000000-0000-0000-0000-000000000001", + "timestamp": "2026-08-28T00:00:00Z", "parentId": nil, + "type": eventType, "data": data, + }) + if err != nil { + t.Fatal(err) + } + var event SessionEvent + if err := json.Unmarshal(wire, &event); err != nil { + t.Fatal(err) + } + var actual *AutoTier + switch eventType { + case "session.start": + actual = event.Data.(*SessionStartData).AutoTier + case "session.resume": + actual = event.Data.(*SessionResumeData).AutoTier + } + if tier == "" { + if actual != nil { + t.Fatalf("expected omitted autoTier, got %v", *actual) + } + } else if actual == nil || *actual != tier { + t.Fatalf("expected autoTier %q, got %v", tier, actual) + } + }) + } + } +} + func TestSessionEventAgentIDRoundTripsKnownEvent(t *testing.T) { var event SessionEvent if err := json.Unmarshal([]byte(`{ @@ -255,3 +299,70 @@ func TestManagedSettingsResolvedProvenanceRoundTrips(t *testing.T) { t.Fatalf("expected absent clientManaged to be omitted, got %v", serialized) } } + +// The failure event is ephemeral: the runtime emits it when an Auto preference +// switch cannot mint a usable model, and never persists or replays it. +func TestSessionAutoTierSwitchFailedEvent(t *testing.T) { + reasons := []AutoTierSwitchFailureReason{ + AutoTierSwitchFailureReasonPolicyRejected, + AutoTierSwitchFailureReasonRequestFailed, + AutoTierSwitchFailureReasonSetupFailed, + AutoTierSwitchFailureReasonUnsupported, + } + for _, reason := range reasons { + t.Run(string(reason), func(t *testing.T) { + wire, err := json.Marshal(map[string]any{ + "id": "00000000-0000-0000-0000-000000000001", + "timestamp": "2026-08-28T00:00:00Z", "parentId": nil, + "type": "session.auto_tier_switch_failed", + "data": map[string]any{ + "effectiveAutoTier": AutoTierBalance, + "requestedAutoTier": AutoTierIntelligence, + "reason": reason, + }, + }) + if err != nil { + t.Fatal(err) + } + var event SessionEvent + if err := json.Unmarshal(wire, &event); err != nil { + t.Fatal(err) + } + data, ok := event.Data.(*SessionAutoTierSwitchFailedData) + if !ok { + t.Fatalf("expected *SessionAutoTierSwitchFailedData, got %T", event.Data) + } + if data.Reason != reason { + t.Fatalf("expected reason %q, got %q", reason, data.Reason) + } + if data.EffectiveAutoTier == nil || *data.EffectiveAutoTier != AutoTierBalance { + t.Fatalf("expected effective tier %q, got %v", AutoTierBalance, data.EffectiveAutoTier) + } + if data.RequestedAutoTier == nil || *data.RequestedAutoTier != AutoTierIntelligence { + t.Fatalf("expected requested tier %q, got %v", AutoTierIntelligence, data.RequestedAutoTier) + } + }) + } +} + +// A null requested tier means the attempt to return to provider-default Auto +// routing is what failed. +func TestSessionAutoTierSwitchFailedEventNullRequestedTier(t *testing.T) { + wire := []byte(`{"id":"00000000-0000-0000-0000-000000000001","timestamp":"2026-08-28T00:00:00Z",` + + `"parentId":null,"type":"session.auto_tier_switch_failed","data":{"effectiveAutoTier":"efficiency",` + + `"requestedAutoTier":null,"reason":"unsupported"}}`) + var event SessionEvent + if err := json.Unmarshal(wire, &event); err != nil { + t.Fatal(err) + } + data, ok := event.Data.(*SessionAutoTierSwitchFailedData) + if !ok { + t.Fatalf("expected *SessionAutoTierSwitchFailedData, got %T", event.Data) + } + if data.RequestedAutoTier != nil { + t.Fatalf("expected nil requested tier, got %v", *data.RequestedAutoTier) + } + if data.EffectiveAutoTier == nil || *data.EffectiveAutoTier != AutoTierEfficiency { + t.Fatalf("expected effective tier %q, got %v", AutoTierEfficiency, data.EffectiveAutoTier) + } +} diff --git a/go/session_test.go b/go/session_test.go index 74e212c418..bdf14887a3 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -18,21 +18,79 @@ import ( ) // newTestSession creates a session with an event channel and starts the consumer goroutine. -// Returns a cleanup function that closes the channel (stopping the consumer). +// Returns a cleanup function that stops the consumer. func newTestSession() (*Session, func()) { s := &Session{ handlers: make([]sessionHandler, 0), commandHandlers: make(map[string]CommandHandler), eventCh: make(chan SessionEvent, 128), + eventDone: make(chan struct{}), } go s.processEvents() - return s, func() { close(s.eventCh) } + return s, s.stopEventProcessing } func newTestEvent() SessionEvent { return SessionEvent{Data: &SessionIdleData{}} } +func TestExternalToolCompletedCancelsBlockedHandler(t *testing.T) { + session, cleanup := newTestSession() + defer cleanup() + + started := make(chan struct{}) + cancelled := make(chan struct{}) + session.registerTools([]Tool{{ + Name: "blocked_tool", + Handler: func(invocation ToolInvocation) (ToolResult, error) { + close(started) + <-invocation.TraceContext.Done() + close(cancelled) + return ToolResult{}, invocation.TraceContext.Err() + }, + }}) + + session.dispatchEvent(SessionEvent{Data: &ExternalToolRequestedData{ + RequestID: "request-1", + SessionID: "session-1", + ToolCallID: "tool-call-1", + ToolName: "blocked_tool", + }}) + select { + case <-started: + case <-time.After(time.Second): + t.Fatal("tool handler did not start") + } + + session.dispatchEvent(SessionEvent{Data: &ExternalToolCompletedData{RequestID: "request-1"}}) + select { + case <-cancelled: + case <-time.After(time.Second): + t.Fatal("tool handler was not cancelled") + } +} + +func TestDispatchEventReturnsAfterEventProcessingStops(t *testing.T) { + session := &Session{ + eventCh: make(chan SessionEvent), + eventDone: make(chan struct{}), + } + + dispatched := make(chan struct{}) + go func() { + session.dispatchEvent(newTestEvent()) + close(dispatched) + }() + + session.stopEventProcessing() + + select { + case <-dispatched: + case <-time.After(time.Second): + t.Fatal("dispatchEvent remained blocked after event processing stopped") + } +} + func ptr[T any](value T) *T { return &value } @@ -58,6 +116,72 @@ func TestSession_SetModelOmitsContextTierWhenUnset(t *testing.T) { if _, ok := params["contextTier"]; ok { t.Fatalf("expected contextTier to be omitted, got %v", params["contextTier"]) } + if _, ok := params["autoTier"]; ok { + t.Fatalf("expected autoTier to be omitted, got %v", params["autoTier"]) + } +} + +func TestSession_SetModelForwardsAutoTier(t *testing.T) { + tier := AutoTierIntelligence + params := captureSetModelRequestForModel(t, "auto", &SetModelOptions{AutoTier: &tier}) + + if params["modelId"] != "auto" { + t.Fatalf("expected modelId auto, got %v", params["modelId"]) + } + if params["autoTier"] != "intelligence" { + t.Fatalf("expected autoTier intelligence, got %v", params["autoTier"]) + } +} + +func TestSession_SetModelSendsExplicitNullAutoTierWhenCleared(t *testing.T) { + params := captureSetModelRequestForModel(t, "auto", &SetModelOptions{ResetAutoTier: true}) + + // An explicit null must survive to the wire. Omitting it would mean "leave + // the preference alone" rather than "use provider-default routing". + value, ok := params["autoTier"] + if !ok { + t.Fatal("expected autoTier to be present") + } + if value != nil { + t.Fatalf("expected autoTier to be null, got %v", value) + } +} + +func TestSession_SetModelRejectsConflictingAutoTierOptions(t *testing.T) { + tier := AutoTierBalance + session := &Session{SessionID: "session-1"} + + err := session.SetModel(context.Background(), "auto", &SetModelOptions{ + AutoTier: &tier, + ResetAutoTier: true, + }) + if err == nil { + t.Fatal("expected an error when AutoTier and ResetAutoTier are both set") + } +} + +func TestSession_SetAutoTierForwardsTier(t *testing.T) { + tier := AutoTierEfficiency + params := captureSetAutoTierRequest(t, &tier) + + if params["sessionId"] != "session-1" { + t.Fatalf("expected sessionId session-1, got %v", params["sessionId"]) + } + if params["autoTier"] != "efficiency" { + t.Fatalf("expected autoTier efficiency, got %v", params["autoTier"]) + } +} + +func TestSession_SetAutoTierSendsExplicitNull(t *testing.T) { + params := captureSetAutoTierRequest(t, nil) + + value, ok := params["autoTier"] + if !ok { + t.Fatal("expected autoTier to be present") + } + if value != nil { + t.Fatalf("expected autoTier to be null, got %v", value) + } } func TestSession_MCPAuthRequestSendsHostToken(t *testing.T) { @@ -261,6 +385,28 @@ func TestMCPOauthRequiredDataAllowsOptionalMetadata(t *testing.T) { func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any { t.Helper() + return captureModelRequest(t, "session.model.switchTo", func(session *Session) error { + return session.SetModel(context.Background(), "gpt-4.1", opts) + }) +} + +func captureSetModelRequestForModel(t *testing.T, model string, opts *SetModelOptions) map[string]any { + t.Helper() + return captureModelRequest(t, "session.model.switchTo", func(session *Session) error { + return session.SetModel(context.Background(), model, opts) + }) +} + +func captureSetAutoTierRequest(t *testing.T, autoTier *AutoTier) map[string]any { + t.Helper() + return captureModelRequest(t, "session.model.switchAutoTier", func(session *Session) error { + _, err := session.SetAutoTier(context.Background(), autoTier) + return err + }) +} + +func captureModelRequest(t *testing.T, method string, invoke func(*Session) error) map[string]any { + t.Helper() stdinR, stdinW := io.Pipe() stdoutR, stdoutW := io.Pipe() @@ -292,8 +438,8 @@ func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any errCh <- err return } - if request.Method != "session.model.switchTo" { - errCh <- fmt.Errorf("expected session.model.switchTo, got %s", request.Method) + if request.Method != method { + errCh <- fmt.Errorf("expected %s, got %s", method, request.Method) return } @@ -302,7 +448,7 @@ func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any response := map[string]any{ "jsonrpc": "2.0", "id": json.RawMessage(request.ID), - "result": map[string]any{}, + "result": map[string]any{"status": "pending"}, } data, err := json.Marshal(response) if err != nil { @@ -320,8 +466,8 @@ func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any client: client, RPC: rpc.NewSessionRPC(client, "session-1"), } - if err := session.SetModel(context.Background(), "gpt-4.1", opts); err != nil { - t.Fatalf("SetModel failed: %v", err) + if err := invoke(session); err != nil { + t.Fatalf("model request failed: %v", err) } select { @@ -330,7 +476,7 @@ func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any case err := <-errCh: t.Fatal(err) case <-time.After(2 * time.Second): - t.Fatal("timed out waiting for session.model.switchTo request") + t.Fatalf("timed out waiting for %s request", method) } return nil } @@ -423,9 +569,10 @@ func TestSession_SendAndWaitSkipsAutopilotContinuationIdle(t *testing.T) { RPC: rpc.NewSessionRPC(client, "session-1"), handlers: make([]sessionHandler, 0), eventCh: make(chan SessionEvent, 8), + eventDone: make(chan struct{}), } go session.processEvents() - defer close(session.eventCh) + defer session.stopEventProcessing() resultCh := make(chan *SessionEvent, 1) go func() { diff --git a/go/test.sh b/go/test.sh index dfb7bac1dd..f5924b3e8d 100755 --- a/go/test.sh +++ b/go/test.sh @@ -15,17 +15,14 @@ fi # Determine COPILOT_CLI_PATH if [ -z "$COPILOT_CLI_PATH" ]; then - # Try to find it relative to the SDK. As of CLI 1.0.64-1 the @github/copilot - # package is a thin loader; the runnable index.js ships in the installed - # platform package (e.g. @github/copilot-linux-x64). Exactly one is installed. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" - POTENTIAL_PATH="$(ls "$SCRIPT_DIR"/../nodejs/node_modules/@github/copilot-*/index.js 2>/dev/null | head -n1)" - if [ -n "$POTENTIAL_PATH" ] && [ -f "$POTENTIAL_PATH" ]; then + if POTENTIAL_PATH="$(cd "$SCRIPT_DIR/../nodejs" && npm run --silent prepare:runtime -- --print-path)" && + [ -n "$POTENTIAL_PATH" ] && [ -f "$POTENTIAL_PATH" ]; then export COPILOT_CLI_PATH="$POTENTIAL_PATH" echo "📍 Auto-detected CLI path: $COPILOT_CLI_PATH" else echo "❌ COPILOT_CLI_PATH environment variable not set" - echo " Run: export COPILOT_CLI_PATH=/path/to/dist-cli/index.js" + echo " Run: export COPILOT_CLI_PATH=/path/to/copilot" exit 1 fi fi diff --git a/go/types.go b/go/types.go index 3577731187..4a45a4019c 100644 --- a/go/types.go +++ b/go/types.go @@ -193,6 +193,13 @@ type ClientOptions struct { // directory are accessible from GitHub web and mobile. // Ignored when connecting to an existing runtime via [URIConnection]. EnableRemoteSessions bool + // ClientInfo declares the integrating application's identity, forwarded to the + // runtime on the `server.connect` handshake. Declaring it lets the + // telemetry the runtime emits on this connection be attributed to a + // consistent surface (the application and its Copilot integration) instead of + // the runtime's own build. All fields are optional; leave it nil to keep the + // runtime's default attribution. + ClientInfo *ClientInfo // Mode controls the default tool surface and feature flags presented to // sessions created by this client. The zero value ([ModeCopilotCli]) // matches legacy CLI defaults. Set to [ModeEmpty] to opt in to @@ -204,6 +211,54 @@ type ClientOptions struct { Mode ClientMode } +// ClientInfo identifies the integrating application on the `server.connect` handshake. +// +// Declaring it lets the telemetry the runtime emits on the connection be +// attributed to a single, consistent surface instead of the runtime's own +// build. All fields are optional; an empty field is omitted from the handshake. +type ClientInfo struct { + // ApplicationName is the name of the application using the SDK. + ApplicationName string + // ApplicationVersion is the version of the application using the SDK. + ApplicationVersion string + // IntegrationName optionally identifies a specific integration within the + // application, such as an extension or plugin. + IntegrationName string + // IntegrationVersion is the optional version of the named integration. + IntegrationVersion string +} + +// toWire maps the public [ClientInfo] onto the generated connect wire shape, +// omitting empty fields. It returns nil when no identity was supplied so the +// caller drops the clientInfo field and keeps the runtime's default attribution. +func (ci *ClientInfo) toWire() *rpc.ConnectClientInfo { + if ci == nil { + return nil + } + wire := &rpc.ConnectClientInfo{} + populated := false + if ci.ApplicationName != "" { + wire.EditorName = &ci.ApplicationName + populated = true + } + if ci.ApplicationVersion != "" { + wire.EditorVersion = &ci.ApplicationVersion + populated = true + } + if ci.IntegrationName != "" { + wire.ExtensionName = &ci.IntegrationName + populated = true + } + if ci.IntegrationVersion != "" { + wire.ExtensionVersion = &ci.IntegrationVersion + populated = true + } + if !populated { + return nil + } + return wire +} + // CloudSessionRepository is GitHub repository metadata associated with a cloud session. type CloudSessionRepository struct { Owner string `json:"owner"` @@ -1688,7 +1743,9 @@ type ToolInvocation struct { // TraceContext carries the W3C Trace Context propagated from the CLI's // execute_tool span. Pass this to OpenTelemetry-aware code so that // child spans created inside the handler are parented to the CLI span. - // When no trace context is available this will be context.Background(). + // It is cancelled when the external tool request completes or the session + // disconnects, so background work must derive its own lifetime if it should + // outlive the invocation. TraceContext context.Context } @@ -2231,6 +2288,18 @@ func (p ProviderConfig) MarshalJSON() ([]byte, error) { return json.Marshal(aux) } +// AutoTier selects the routing tier for model "auto" with V2 Auto. +type AutoTier = rpc.AutoTier + +const ( + // AutoTierEfficiency selects the efficiency routing tier. + AutoTierEfficiency = rpc.AutoTierEfficiency + // AutoTierBalance selects the balance routing tier. + AutoTierBalance = rpc.AutoTierBalance + // AutoTierIntelligence selects the intelligence routing tier. + AutoTierIntelligence = rpc.AutoTierIntelligence +) + // CapiSessionOptions configures provider-scoped Copilot API (CAPI) session behavior. // // WebSocket transport is the default for the CAPI Responses API whenever the @@ -2245,6 +2314,16 @@ type CapiSessionOptions struct { // WebSocket transport. Enabled by default when the model advertises // ws:/responses support; set to Bool(false) to force HTTP Responses transport. EnableWebSocketResponses *bool `json:"enableWebSocketResponses,omitempty"` + + // AutoTier selects the routing tier for model "auto" with V2 Auto. + // Requires a runtime that supports Auto tiers; it has no effect outside V2 Auto. + // When unset, the runtime uses its default on create and restores the last + // committed tier on cold resume. On resident resume, a different tier + // requests a safe switch that takes effect after resume succeeds and never + // disturbs a turn that is already running. + // + // To change the preference on a live session, use [Session.SetAutoTier]. + AutoTier AutoTier `json:"autoTier,omitempty"` } // AzureProviderOptions contains Azure-specific provider configuration @@ -2439,6 +2518,7 @@ type ModelInfo struct { Capabilities ModelCapabilities `json:"capabilities"` Policy *ModelPolicy `json:"policy,omitempty"` Billing *ModelBilling `json:"billing,omitempty"` + Metadata map[string]any `json:"metadata,omitempty"` SupportedReasoningEfforts []string `json:"supportedReasoningEfforts,omitempty"` DefaultReasoningEffort string `json:"defaultReasoningEffort,omitempty"` } @@ -2815,11 +2895,16 @@ type sessionGetMessagesResponse struct { Events []SessionEvent `json:"events"` } -// sessionDestroyRequest is the request for session.destroy -type sessionDestroyRequest struct { +// sessionDetachRequest is the request for session.detach. +type sessionDetachRequest struct { SessionID string `json:"sessionId"` } +type sessionDetachResponse struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` +} + // sessionAbortRequest is the request for session.abort type sessionAbortRequest struct { SessionID string `json:"sessionId"` diff --git a/go/zsession_events.go b/go/zsession_events.go index 8731009064..9540968e52 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -12,6 +12,8 @@ type ( AgentInterruptedActivity = rpc.AgentInterruptedActivity AgentInterruptedCancelPhase = rpc.AgentInterruptedCancelPhase AgentInterruptedData = rpc.AgentInterruptedData + AgentModelPolicy = rpc.AgentModelPolicy + AssistantFusionPhaseActivityData = rpc.AssistantFusionPhaseActivityData AssistantFusionPhaseCompletedData = rpc.AssistantFusionPhaseCompletedData AssistantFusionPhaseFailedData = rpc.AssistantFusionPhaseFailedData AssistantFusionPhaseStartedData = rpc.AssistantFusionPhaseStartedData @@ -71,7 +73,7 @@ type ( AutoModeSwitchResponse = rpc.AutoModeSwitchResponse AutopilotObjectiveChangedOperation = rpc.AutopilotObjectiveChangedOperation AutopilotObjectiveChangedStatus = rpc.AutopilotObjectiveChangedStatus - AutoTier = rpc.AutoTier + AutoTierSwitchFailureReason = rpc.AutoTierSwitchFailureReason BinaryAssetReference = rpc.BinaryAssetReference BinaryAssetReferenceType = rpc.BinaryAssetReferenceType BinaryAssetType = rpc.BinaryAssetType @@ -98,6 +100,10 @@ type ( CompactionCompleteCompactionTokensUsed = rpc.CompactionCompleteCompactionTokensUsed CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail = rpc.CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail CompactionTrigger = rpc.CompactionTrigger + CompletionReceiptEventRange = rpc.CompletionReceiptEventRange + CompletionReceiptFinalTool = rpc.CompletionReceiptFinalTool + CompletionReceiptStopReason = rpc.CompletionReceiptStopReason + CompletionReceiptToolStatus = rpc.CompletionReceiptToolStatus ContextTier = rpc.ContextTier CustomAgentsUpdatedAgent = rpc.CustomAgentsUpdatedAgent ElicitationCompletedAction = rpc.ElicitationCompletedAction @@ -127,7 +133,9 @@ type ( FusionFollowUpAction = rpc.FusionFollowUpAction FusionFollowUpRecommendation = rpc.FusionFollowUpRecommendation FusionPattern = rpc.FusionPattern + FusionPhaseActivityKind = rpc.FusionPhaseActivityKind FusionPhaseKind = rpc.FusionPhaseKind + FusionPhasePlanStep = rpc.FusionPhasePlanStep FusionPhaseStatus = rpc.FusionPhaseStatus FusionPhaseUsage = rpc.FusionPhaseUsage FusionScores = rpc.FusionScores @@ -161,6 +169,7 @@ type ( MCPOauthWwwAuthenticateParams = rpc.MCPOauthWwwAuthenticateParams MCPPromptsListChangedData = rpc.MCPPromptsListChangedData MCPResourcesListChangedData = rpc.MCPResourcesListChangedData + MCPServerMetadata = rpc.MCPServerMetadata MCPServersLoadedServer = rpc.MCPServersLoadedServer MCPServerSource = rpc.MCPServerSource MCPServerStatus = rpc.MCPServerStatus @@ -250,12 +259,14 @@ type ( RawSystemNotification = rpc.RawSystemNotification RawToolExecutionCompleteContent = rpc.RawToolExecutionCompleteContent ReasoningSummary = rpc.ReasoningSummary + RemediationAction = rpc.RemediationAction SamplingCompletedData = rpc.SamplingCompletedData SamplingRequestedData = rpc.SamplingRequestedData SandboxDecisionData = rpc.SandboxDecisionData ScheduleOrigin = rpc.ScheduleOrigin SessionAutoModeResolvedData = rpc.SessionAutoModeResolvedData SessionAutopilotObjectiveChangedData = rpc.SessionAutopilotObjectiveChangedData + SessionAutoTierSwitchFailedData = rpc.SessionAutoTierSwitchFailedData SessionBackgroundTasksChangedData = rpc.SessionBackgroundTasksChangedData SessionBinaryAssetData = rpc.SessionBinaryAssetData SessionCanvasClosedData = rpc.SessionCanvasClosedData @@ -266,6 +277,7 @@ type ( SessionCanvasUnavailableData = rpc.SessionCanvasUnavailableData SessionCompactionCompleteData = rpc.SessionCompactionCompleteData SessionCompactionStartData = rpc.SessionCompactionStartData + SessionCompletionReceiptData = rpc.SessionCompletionReceiptData SessionContextChangedData = rpc.SessionContextChangedData SessionContextClearedData = rpc.SessionContextClearedData SessionCustomAgentsUpdatedData = rpc.SessionCustomAgentsUpdatedData @@ -290,11 +302,14 @@ type ( SessionLimitsExhaustedResponseAction = rpc.SessionLimitsExhaustedResponseAction SessionManagedSettingsEnforcedData = rpc.SessionManagedSettingsEnforcedData SessionManagedSettingsResolvedData = rpc.SessionManagedSettingsResolvedData + SessionMCPServerNeedsReconnectData = rpc.SessionMCPServerNeedsReconnectData + SessionMCPServerRemovedData = rpc.SessionMCPServerRemovedData SessionMCPServersLoadedData = rpc.SessionMCPServersLoadedData SessionMCPServerStatusChangedData = rpc.SessionMCPServerStatusChangedData SessionMode = rpc.SessionMode SessionModeChangedData = rpc.SessionModeChangedData SessionModelChangeData = rpc.SessionModelChangeData + SessionModeNoticeDeliveredData = rpc.SessionModeNoticeDeliveredData SessionPermissionsChangedData = rpc.SessionPermissionsChangedData SessionPlanChangedData = rpc.SessionPlanChangedData SessionRemoteSteerableChangedData = rpc.SessionRemoteSteerableChangedData @@ -427,6 +442,8 @@ const ( AgentInterruptedActivityToolCall = rpc.AgentInterruptedActivityToolCall AgentInterruptedCancelPhaseMidStream = rpc.AgentInterruptedCancelPhaseMidStream AgentInterruptedCancelPhasePreFirstToken = rpc.AgentInterruptedCancelPhasePreFirstToken + AgentModelPolicyPreferred = rpc.AgentModelPolicyPreferred + AgentModelPolicyRequired = rpc.AgentModelPolicyRequired AssistantMessageToolRequestCallerTypeProgram = rpc.AssistantMessageToolRequestCallerTypeProgram AssistantMessageToolRequestTypeCustom = rpc.AssistantMessageToolRequestTypeCustom AssistantMessageToolRequestTypeFunction = rpc.AssistantMessageToolRequestTypeFunction @@ -476,9 +493,10 @@ const ( AutopilotObjectiveChangedStatusCapReached = rpc.AutopilotObjectiveChangedStatusCapReached AutopilotObjectiveChangedStatusCompleted = rpc.AutopilotObjectiveChangedStatusCompleted AutopilotObjectiveChangedStatusPaused = rpc.AutopilotObjectiveChangedStatusPaused - AutoTierBalance = rpc.AutoTierBalance - AutoTierEfficiency = rpc.AutoTierEfficiency - AutoTierIntelligence = rpc.AutoTierIntelligence + AutoTierSwitchFailureReasonPolicyRejected = rpc.AutoTierSwitchFailureReasonPolicyRejected + AutoTierSwitchFailureReasonRequestFailed = rpc.AutoTierSwitchFailureReasonRequestFailed + AutoTierSwitchFailureReasonSetupFailed = rpc.AutoTierSwitchFailureReasonSetupFailed + AutoTierSwitchFailureReasonUnsupported = rpc.AutoTierSwitchFailureReasonUnsupported BinaryAssetReferenceTypeImage = rpc.BinaryAssetReferenceTypeImage BinaryAssetReferenceTypeResource = rpc.BinaryAssetReferenceTypeResource BinaryAssetTypeImage = rpc.BinaryAssetTypeImage @@ -494,6 +512,14 @@ const ( CompactionTriggerMemoryPressure = rpc.CompactionTriggerMemoryPressure CompactionTriggerModelSwitch = rpc.CompactionTriggerModelSwitch CompactionTriggerThreshold = rpc.CompactionTriggerThreshold + CompletionReceiptStopReasonAgentStopBlockLimit = rpc.CompletionReceiptStopReasonAgentStopBlockLimit + CompletionReceiptStopReasonNatural = rpc.CompletionReceiptStopReasonNatural + CompletionReceiptStopReasonTerminalTool = rpc.CompletionReceiptStopReasonTerminalTool + CompletionReceiptToolStatusDenied = rpc.CompletionReceiptToolStatusDenied + CompletionReceiptToolStatusFailure = rpc.CompletionReceiptToolStatusFailure + CompletionReceiptToolStatusRejected = rpc.CompletionReceiptToolStatusRejected + CompletionReceiptToolStatusSuccess = rpc.CompletionReceiptToolStatusSuccess + CompletionReceiptToolStatusTimeout = rpc.CompletionReceiptToolStatusTimeout ContextTierDefault = rpc.ContextTierDefault ContextTierLongContext = rpc.ContextTierLongContext ElicitationCompletedActionAccept = rpc.ElicitationCompletedActionAccept @@ -527,6 +553,9 @@ const ( FusionPatternCascade = rpc.FusionPatternCascade FusionPatternCritique = rpc.FusionPatternCritique FusionPatternSingle = rpc.FusionPatternSingle + FusionPhaseActivityKindModelOutput = rpc.FusionPhaseActivityKindModelOutput + FusionPhaseActivityKindToolCompleted = rpc.FusionPhaseActivityKindToolCompleted + FusionPhaseActivityKindToolStarted = rpc.FusionPhaseActivityKindToolStarted FusionPhaseKindCritic = rpc.FusionPhaseKindCritic FusionPhaseKindDraft = rpc.FusionPhaseKindDraft FusionPhaseKindFollowUp = rpc.FusionPhaseKindFollowUp @@ -555,6 +584,7 @@ const ( ManagedSettingsResolvedSourceDevice = rpc.ManagedSettingsResolvedSourceDevice ManagedSettingsResolvedSourceMixed = rpc.ManagedSettingsResolvedSourceMixed ManagedSettingsResolvedSourceNone = rpc.ManagedSettingsResolvedSourceNone + ManagedSettingsResolvedSourcePolicyHelper = rpc.ManagedSettingsResolvedSourcePolicyHelper ManagedSettingsResolvedSourceServer = rpc.ManagedSettingsResolvedSourceServer MCPHeadersRefreshCompletedOutcomeHeaders = rpc.MCPHeadersRefreshCompletedOutcomeHeaders MCPHeadersRefreshCompletedOutcomeNone = rpc.MCPHeadersRefreshCompletedOutcomeNone @@ -669,10 +699,16 @@ const ( ReasoningSummaryConcise = rpc.ReasoningSummaryConcise ReasoningSummaryDetailed = rpc.ReasoningSummaryDetailed ReasoningSummaryNone = rpc.ReasoningSummaryNone + RemediationActionAllowSandboxOutbound = rpc.RemediationActionAllowSandboxOutbound + RemediationActionReviewSandboxPolicy = rpc.RemediationActionReviewSandboxPolicy + RemediationActionShowAccount = rpc.RemediationActionShowAccount + RemediationActionSignIn = rpc.RemediationActionSignIn + RemediationActionSwitchAccount = rpc.RemediationActionSwitchAccount ScheduleOriginModel = rpc.ScheduleOriginModel ScheduleOriginUser = rpc.ScheduleOriginUser SessionEventTypeAbort = rpc.SessionEventTypeAbort SessionEventTypeAgentInterrupted = rpc.SessionEventTypeAgentInterrupted + SessionEventTypeAssistantFusionPhaseActivity = rpc.SessionEventTypeAssistantFusionPhaseActivity SessionEventTypeAssistantFusionPhaseCompleted = rpc.SessionEventTypeAssistantFusionPhaseCompleted SessionEventTypeAssistantFusionPhaseFailed = rpc.SessionEventTypeAssistantFusionPhaseFailed SessionEventTypeAssistantFusionPhaseStarted = rpc.SessionEventTypeAssistantFusionPhaseStarted @@ -729,6 +765,7 @@ const ( SessionEventTypeSandboxDecision = rpc.SessionEventTypeSandboxDecision SessionEventTypeSessionAutoModeResolved = rpc.SessionEventTypeSessionAutoModeResolved SessionEventTypeSessionAutopilotObjectiveChanged = rpc.SessionEventTypeSessionAutopilotObjectiveChanged + SessionEventTypeSessionAutoTierSwitchFailed = rpc.SessionEventTypeSessionAutoTierSwitchFailed SessionEventTypeSessionBackgroundTasksChanged = rpc.SessionEventTypeSessionBackgroundTasksChanged SessionEventTypeSessionBinaryAsset = rpc.SessionEventTypeSessionBinaryAsset SessionEventTypeSessionCanvasClosed = rpc.SessionEventTypeSessionCanvasClosed @@ -739,6 +776,7 @@ const ( SessionEventTypeSessionCanvasUnavailable = rpc.SessionEventTypeSessionCanvasUnavailable SessionEventTypeSessionCompactionComplete = rpc.SessionEventTypeSessionCompactionComplete SessionEventTypeSessionCompactionStart = rpc.SessionEventTypeSessionCompactionStart + SessionEventTypeSessionCompletionReceipt = rpc.SessionEventTypeSessionCompletionReceipt SessionEventTypeSessionContextChanged = rpc.SessionEventTypeSessionContextChanged SessionEventTypeSessionContextCleared = rpc.SessionEventTypeSessionContextCleared SessionEventTypeSessionCustomAgentsUpdated = rpc.SessionEventTypeSessionCustomAgentsUpdated @@ -757,10 +795,13 @@ const ( SessionEventTypeSessionLimitsExhaustedRequested = rpc.SessionEventTypeSessionLimitsExhaustedRequested SessionEventTypeSessionManagedSettingsEnforced = rpc.SessionEventTypeSessionManagedSettingsEnforced SessionEventTypeSessionManagedSettingsResolved = rpc.SessionEventTypeSessionManagedSettingsResolved + SessionEventTypeSessionMCPServerNeedsReconnect = rpc.SessionEventTypeSessionMCPServerNeedsReconnect + SessionEventTypeSessionMCPServerRemoved = rpc.SessionEventTypeSessionMCPServerRemoved SessionEventTypeSessionMCPServersLoaded = rpc.SessionEventTypeSessionMCPServersLoaded SessionEventTypeSessionMCPServerStatusChanged = rpc.SessionEventTypeSessionMCPServerStatusChanged SessionEventTypeSessionModeChanged = rpc.SessionEventTypeSessionModeChanged SessionEventTypeSessionModelChange = rpc.SessionEventTypeSessionModelChange + SessionEventTypeSessionModeNoticeDelivered = rpc.SessionEventTypeSessionModeNoticeDelivered SessionEventTypeSessionPermissionsChanged = rpc.SessionEventTypeSessionPermissionsChanged SessionEventTypeSessionPlanChanged = rpc.SessionEventTypeSessionPlanChanged SessionEventTypeSessionRemoteSteerableChanged = rpc.SessionEventTypeSessionRemoteSteerableChanged @@ -820,6 +861,7 @@ const ( SkillSourcePersonalCopilot = rpc.SkillSourcePersonalCopilot SkillSourcePlugin = rpc.SkillSourcePlugin SkillSourceProject = rpc.SkillSourceProject + SkillSourceSDK = rpc.SkillSourceSDK SystemMessageRoleDeveloper = rpc.SystemMessageRoleDeveloper SystemMessageRoleSystem = rpc.SystemMessageRoleSystem SystemNotificationAgentCompletedStatusCompleted = rpc.SystemNotificationAgentCompletedStatusCompleted diff --git a/java/README.md b/java/README.md index f3ba3f63a0..69bf8b050c 100644 --- a/java/README.md +++ b/java/README.md @@ -30,25 +30,25 @@ runtime. ### Maven -Replace `${copilot.sdk.version}` with the latest release from Maven Central. - ```xml com.github copilot-sdk-java - 1.0.13-preview.4 + 1.0.13 ``` ### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.13-preview.4' +implementation 'com.github:copilot-sdk-java:1.0.13' ``` -#### Snapshot Builds +### Snapshot builds + +Snapshot builds of the next development version are published to Maven Central Snapshots. To use them, add the snapshot repository and depend on the development version: -Snapshot builds of the next development version are published to Maven Central Snapshots. To use them, add the repository and update the dependency version in your `pom.xml`: +#### Maven ```xml @@ -62,16 +62,14 @@ Snapshot builds of the next development version are published to Maven Central S com.github copilot-sdk-java - 1.0.14-preview.4-SNAPSHOT + 1.0.14-SNAPSHOT ``` -### Gradle - -Replace `${copilot.sdk.version}` with the latest release from Maven Central. +#### Gradle ```groovy -implementation 'com.github:copilot-sdk-java:1.0.14-preview.4-SNAPSHOT' +implementation 'com.github:copilot-sdk-java:1.0.14-SNAPSHOT' ``` ## In-process mode (experimental) @@ -341,6 +339,52 @@ Chain fluent modifiers to set tool options: For design context and decision rationale, see [ADR-006](docs/adr/adr-006-tool-definition-inline.md). +## Auto routing tiers + +Use `CapiSessionOptions.setAutoTier(...)` to select `AutoTier.EFFICIENCY`, +`AutoTier.BALANCE`, or `AutoTier.INTELLIGENCE`. This option is meaningful only +with model `auto` (Auto mode V2). +It requires a runtime version that supports `capi.autoTier`. + +```java +import com.github.copilot.rpc.AutoTier; +import com.github.copilot.rpc.CapiSessionOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; + +var config = new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setModel("auto") + .setCapi(new CapiSessionOptions().setAutoTier(AutoTier.BALANCE)); +``` + +The same options work with `ResumeSessionConfig.setCapi(...)` and can be combined +with `setEnableWebSocketResponses(false)`. The SDK omits an unset (`null`) tier: +the runtime chooses its default on create and preserves the persisted/current +tier on resume. An explicit tier overrides the persisted tier on cold resume. On +resident resume, a different tier requests a safe switch applied after the +resume succeeds; it cannot change a turn that is already in flight. The SDK does not choose a default or manage tier persistence. +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) +for the lifecycle rules. + +### Changing the Auto tier during a session + +Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. + +```java +var result = session.setAutoTier(AutoTier.INTELLIGENCE).get(); +if (result.status() == ModelSwitchAutoTierStatus.PENDING) { + // Accepted, but not yet in effect. +} + +// Return to the provider's default Auto routing. +session.setAutoTier(null).get(); +``` + +`setModel(SetModelOptions)` accepts the same preference through `SetModelOptions.setAutoTier(...)`, which stages the tier atomically with selecting `auto`. Call `setResetAutoTier(true)` instead to return to provider-default routing; the two options are mutually exclusive. + ## Session Store `enableSessionStore` on `SessionConfig` enables the cross-session store for search and retrieval across sessions. When unset in the default `CopilotClientMode.COPILOT_CLI` mode, the runtime default applies (enabled). In `CopilotClientMode.EMPTY` mode, defaults to disabled. @@ -520,9 +564,9 @@ mvn jacoco:prepare-agent@wire-up-coverage-instrumentation antrun:run@print-test- #### Development Setup for native embedding -Run native-runtime Maven commands from the `java` directory. Native packaging requires Node.js and npm in addition to JDK 25 and Maven because `copilot-native/scripts/fetch-native.mjs` retrieves the pinned npm runtime package. +Run native-runtime Maven commands from the `java` directory. Native packaging requires Node.js in addition to JDK 25 and Maven because `copilot-native/scripts/fetch-native.mjs` retrieves the pinned runtime package from the corresponding GitHub release. -On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On Windows x64, Windows ARM64, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, fetches the pinned `@github/copilot-` package during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Ensure npm can authenticate to the package registry before running the build. +On a native Linux glibc host, Maven activates `native-linux-x64` or `native-linux-arm64` for the matching architecture when `copilot.native.libc=glibc` is set. On Windows x64, Windows ARM64, and Apple Silicon macOS, Maven activates `native-win32-x64`, `native-win32-arm64`, or `native-darwin-arm64` automatically. The matching profile validates the host, runs the native script tests, fetches the pinned platform package from the corresponding `github/copilot-cli` release during `generate-resources`, packages the classifier JAR during `package`, and verifies its native contents. Before opting in, validate that Node.js reports glibc for the build host: diff --git a/java/copilot-native/pom.xml b/java/copilot-native/pom.xml index c03be9909e..7624deb4fe 100644 --- a/java/copilot-native/pom.xml +++ b/java/copilot-native/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.4-SNAPSHOT + 1.0.14-SNAPSHOT ../pom.xml @@ -31,14 +31,13 @@ ${project.basedir}/../.. ${project.build.directory}/native-staging false @@ -60,11 +59,10 @@ org.codehaus.mojo diff --git a/java/copilot-native/scripts/fetch-native.mjs b/java/copilot-native/scripts/fetch-native.mjs index 4ee91b6aa6..29800708b1 100644 --- a/java/copilot-native/scripts/fetch-native.mjs +++ b/java/copilot-native/scripts/fetch-native.mjs @@ -6,10 +6,9 @@ * Downloads the native runtime artifacts for one platform classifier. * * Steps: - * 1. Read the pinned version and the SHA-512 `integrity` value for - * `@github/copilot-` from `nodejs/package-lock.json`. - * 2. `npm pack` that exact version into the staging directory. - * 3. Verify the downloaded tarball against the `integrity` value. + * 1. Read the pinned version from `nodejs/package.json`. + * 2. Download the platform tarball and `SHA256SUMS.txt` from the matching release. + * 3. Verify the downloaded tarball against the release checksum. * 4. Stage the hostless runtime tree, flattening the selected prebuild directory * beside the package's retained top-level runtime assets. * 5. Write an inventory consumed by the SDK's generic classpath extractor. @@ -29,18 +28,15 @@ const excludedTopLevel = new Set([ 'changelog.json', 'copilot', 'copilot.exe', - 'copilot-sdk', 'foundry-local-sdk', 'index.js', 'LICENSE.md', 'napi-oop-runtime', 'npm-loader.js', 'package.json', - 'preloads', 'pvrecorder', 'queries', 'README.md', - 'sdk', 'sea-loader.js', 'webview', ]); @@ -52,21 +48,14 @@ if (!repoRoot || !stagingDir || !classifier) { process.exit(1); } -const lockPath = path.join(repoRoot, 'nodejs', 'package-lock.json'); -const packageName = `@github/copilot-${classifier}`; -const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8')); -const entry = lock.packages?.[`node_modules/${packageName}`]; - -if (!entry?.version || !entry?.integrity) { - console.error(`Could not find version/integrity for ${packageName} in ${lockPath}`); - process.exit(1); -} - -const { version, integrity } = entry; -if (!integrity.startsWith('sha512-')) { - console.error(`Unsupported integrity algorithm for ${packageName}: ${integrity}`); +const packagePath = path.join(repoRoot, 'nodejs', 'package.json'); +const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); +const version = packageJson.copilotCliVersion; +if (!version) { + console.error(`Could not find copilotCliVersion in ${packagePath}`); process.exit(1); } +const assetName = `github-copilot-${version}-${classifier}.tgz`; const outDir = path.join(stagingDir, classifier); const resourceDir = path.join(outDir, 'native', classifier); @@ -77,7 +66,7 @@ const wrapperPath = path.join(resourceDir, wrapperFilename); const inventoryPath = path.join(resourceDir, 'runtime-assets.list'); const platformPropertiesPath = path.join(resourceDir, 'platform.properties'); const expectedPlatformProperties = `classifier=${classifier}\nversion=${version}\n`; -const stagingSchema = 'hostless-runtime-v2'; +const stagingSchema = 'hostless-runtime-v3'; const stampPath = path.join(outDir, '.version'); // Idempotence: skip the download only when every required staged artifact @@ -92,18 +81,16 @@ if ( const stampLines = fs.readFileSync(stampPath, 'utf8').trim().split('\n'); const stampSchema = stampLines[0] || ''; const stampVersion = stampLines[1] || ''; - const stampIntegrity = stampLines[2] || ''; const stampTreeDigest = stampLines[3] || ''; const currentTreeDigest = digestTree(resourceDir); const currentPlatformProperties = fs.readFileSync(platformPropertiesPath, 'utf8'); if ( stampSchema === stagingSchema && stampVersion === version && - stampIntegrity === integrity && stampTreeDigest === currentTreeDigest && currentPlatformProperties === expectedPlatformProperties ) { - console.log(`${packageName}@${version} already staged at ${runtimePath}`); + console.log(`${assetName} already staged at ${runtimePath}`); process.exit(0); } } @@ -111,25 +98,36 @@ if ( fs.rmSync(outDir, { recursive: true, force: true }); fs.mkdirSync(resourceDir, { recursive: true }); -console.log(`Downloading ${packageName}@${version} ...`); -const packOutput = execFileSync('npm', ['pack', `${packageName}@${version}`, '--pack-destination', outDir], { - encoding: 'utf8', - shell: process.platform === 'win32', -}); -const tarballName = packOutput.trim().split('\n').pop().trim(); -const tarballPath = path.join(outDir, tarballName); - -const actual = `sha512-${createHash('sha512').update(fs.readFileSync(tarballPath)).digest('base64')}`; -if (actual !== integrity) { - console.error(`Integrity verification failed for ${tarballPath}`); - console.error(` expected: ${integrity}`); +console.log(`Downloading ${assetName} ...`); +const releaseBase = ( + process.env.COPILOT_CLI_DOWNLOAD_BASE_URL ?? + 'https://github.com/github/copilot-cli/releases/download' +).replace(/\/+$/, ''); +let archive; +let expectedHash; +if (process.env.COPILOT_CLI_RELEASE_TARBALL) { + archive = fs.readFileSync(process.env.COPILOT_CLI_RELEASE_TARBALL); + expectedHash = process.env.COPILOT_CLI_RELEASE_SHA256; +} else { + const releaseUrl = `${releaseBase}/v${version}`; + const checksums = (await download(`${releaseUrl}/SHA256SUMS.txt`)).toString('utf8'); + expectedHash = findChecksum(checksums, assetName); + archive = await download(`${releaseUrl}/${assetName}`); +} +if (!expectedHash || !/^[a-fA-F0-9]{64}$/.test(expectedHash)) { + throw new Error(`Missing or invalid SHA-256 for ${assetName}`); +} +const actual = createHash('sha256').update(archive).digest('hex'); +if (actual !== expectedHash.toLowerCase()) { + console.error(`Integrity verification failed for ${assetName}`); + console.error(` expected: ${expectedHash}`); console.error(` actual: ${actual}`); process.exit(1); } -console.log(`Integrity verified (${integrity.slice(0, 20)}...).`); +console.log(`Integrity verified (${expectedHash.slice(0, 20)}...).`); const inventory = []; -const members = execFileSync('tar', ['-tzf', tarballPath], { encoding: 'utf8' }) +const members = execFileSync('tar', ['-tzf', '-'], { encoding: 'utf8', input: archive }) .split(/\r?\n/) .filter(Boolean); for (const member of members) { @@ -137,15 +135,19 @@ for (const member of members) { if (destinationRelative === null) { continue; } - const listing = execFileSync('tar', ['-tvzf', tarballPath, member], { encoding: 'utf8' }).trim(); + const listing = execFileSync('tar', ['-tvzf', '-', member], { + encoding: 'utf8', + input: archive, + }).trim(); if (listing.startsWith('d')) { continue; } if (!listing.startsWith('-')) { throw new Error(`Unsupported runtime package entry: ${member}`); } - const content = execFileSync('tar', ['-xOzf', tarballPath, member], { + const content = execFileSync('tar', ['-xOzf', '-', member], { encoding: null, + input: archive, maxBuffer: 512 * 1024 * 1024, }); const destination = path.resolve(resourceDir, destinationRelative); @@ -162,14 +164,12 @@ for (const member of members) { inventory.sort(); fs.writeFileSync(inventoryPath, `${inventory.join('\n')}\n`); -fs.rmSync(tarballPath, { force: true }); - if (!fs.existsSync(runtimePath) || !fs.existsSync(wrapperPath)) { - throw new Error(`Package ${packageName}@${version} is missing the runtime wrapper pair`); + throw new Error(`${assetName} is missing the runtime wrapper pair`); } fs.writeFileSync(platformPropertiesPath, expectedPlatformProperties); const treeDigest = digestTree(resourceDir); -fs.writeFileSync(stampPath, `${stagingSchema}\n${version}\n${integrity}\n${treeDigest}\n`); +fs.writeFileSync(stampPath, `${stagingSchema}\n${version}\n${expectedHash}\n${treeDigest}\n`); console.log(`Staged ${runtimePath}`); @@ -224,3 +224,37 @@ function digestTree(directory) { } return `sha512-${hash.digest('base64')}`; } + +async function download(url) { + let lastError; + for (let attempt = 0; attempt < 3; attempt++) { + try { + // lgtm[js/file-access-to-http] The repository-pinned CLI version intentionally selects the release asset. + const response = await fetch(url); + if (response.ok) { + return Buffer.from(await response.arrayBuffer()); + } + await response.body?.cancel(); + lastError = new Error(`${response.status} ${response.statusText}`); + if (response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429) { + break; + } + } catch (error) { + lastError = error; + } + if (attempt < 2) { + await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000)); + } + } + throw new Error(`Failed to download ${url}: ${lastError}`); +} + +function findChecksum(checksums, assetName) { + for (const line of checksums.split(/\r?\n/)) { + const [hash, name] = line.trim().split(/\s+/, 2); + if (name?.replace(/^\*/, '') === assetName && /^[a-fA-F0-9]{64}$/.test(hash)) { + return hash.toLowerCase(); + } + } + throw new Error(`SHA256SUMS.txt does not contain ${assetName}`); +} diff --git a/java/copilot-native/scripts/fetch-native.test.mjs b/java/copilot-native/scripts/fetch-native.test.mjs index 582ffa397f..3213d416f0 100644 --- a/java/copilot-native/scripts/fetch-native.test.mjs +++ b/java/copilot-native/scripts/fetch-native.test.mjs @@ -12,10 +12,10 @@ import { fileURLToPath } from 'node:url'; import test from 'node:test'; const version = '1.0.79'; -const integrity = 'sha512-test-integrity'; +const checksum = '0'.repeat(64); const runtimeContent = 'runtime content'; const wrapperContent = 'wrapper content'; -const stagingSchema = 'hostless-runtime-v2'; +const stagingSchema = 'hostless-runtime-v3'; const scriptPath = fileURLToPath(new URL('./fetch-native.mjs', import.meta.url)); for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64', 'darwin-arm64']) { @@ -26,7 +26,6 @@ for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64' assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /already staged/); - assert.equal(fs.existsSync(fixture.npmMarkerPath), false); }); test(`${classifier}: missing runtime wrapper does not use incremental fast path`, (t) => { @@ -38,10 +37,10 @@ for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64' assertRestagingAttempted(fixture, result); }); - test(`${classifier}: legacy staging schema does not use incremental fast path`, (t) => { + test(`${classifier}: v2 staging schema does not use incremental fast path`, (t) => { const fixture = createFixture(t, classifier); const stampPath = path.join(fixture.stagingDir, classifier, '.version'); - fs.writeFileSync(stampPath, fs.readFileSync(stampPath, 'utf8').replace(stagingSchema, 'hostless-runtime-v1')); + fs.writeFileSync(stampPath, fs.readFileSync(stampPath, 'utf8').replace(stagingSchema, 'hostless-runtime-v2')); const result = runScript(fixture); @@ -73,7 +72,6 @@ for (const classifier of ['linux-x64', 'linux-arm64', 'win32-x64', 'win32-arm64' assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /already staged/); - assert.equal(fs.existsSync(fixture.npmMarkerPath), false); }); } @@ -95,25 +93,17 @@ test('stages retained package assets and excludes CLI-only content', (t) => { fs.writeFileSync(path.join(packageRoot, 'README.md'), 'excluded'); const tarball = path.join(fixture.repoRoot, 'fixture.tgz'); execFileSync('tar', ['-czf', tarball, '-C', path.dirname(packageRoot), 'package']); - const packageIntegrity = digest(fs.readFileSync(tarball)); + const packageChecksum = createHash('sha256').update(fs.readFileSync(tarball)).digest('hex'); fs.writeFileSync( - path.join(fixture.repoRoot, 'nodejs', 'package-lock.json'), - JSON.stringify({ - packages: { - [`node_modules/@github/copilot-${classifier}`]: { version, integrity: packageIntegrity }, - }, - }), + path.join(fixture.repoRoot, 'nodejs', 'package.json'), + JSON.stringify({ copilotCliVersion: version }), ); - const fakeNpmPath = path.join(fixture.fakeBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'); - const fakeNpm = - process.platform === 'win32' - ? '@copy "%FETCH_NATIVE_TARBALL%" "%4\\fixture.tgz" >nul\r\n@echo fixture.tgz\r\n' - : '#!/bin/sh\ncp "$FETCH_NATIVE_TARBALL" "$4/fixture.tgz"\nprintf "fixture.tgz\\n"\n'; - fs.writeFileSync(fakeNpmPath, fakeNpm); - fs.chmodSync(fakeNpmPath, 0o755); fs.rmSync(path.join(fixture.stagingDir, classifier), { recursive: true, force: true }); - const result = runScript(fixture, { FETCH_NATIVE_TARBALL: tarball }); + const result = runScript(fixture, { + COPILOT_CLI_RELEASE_TARBALL: tarball, + COPILOT_CLI_RELEASE_SHA256: packageChecksum, + }); assert.equal(result.status, 0, result.stderr); const resourceDir = path.join(fixture.stagingDir, classifier, 'native', classifier); @@ -133,19 +123,12 @@ function createFixture(t, classifier) { const repoRoot = path.join(root, 'repo'); const stagingDir = path.join(root, 'staging'); const resourceDir = path.join(stagingDir, classifier, 'native', classifier); - const fakeBinDir = path.join(root, 'bin'); - const npmMarkerPath = path.join(root, 'npm-invoked'); fs.mkdirSync(path.join(repoRoot, 'nodejs'), { recursive: true }); fs.mkdirSync(resourceDir, { recursive: true }); - fs.mkdirSync(fakeBinDir); fs.writeFileSync( - path.join(repoRoot, 'nodejs', 'package-lock.json'), - JSON.stringify({ - packages: { - [`node_modules/@github/copilot-${classifier}`]: { version, integrity }, - }, - }), + path.join(repoRoot, 'nodejs', 'package.json'), + JSON.stringify({ copilotCliVersion: version }), ); const runtimePath = path.join(resourceDir, 'runtime.node'); @@ -167,23 +150,14 @@ function createFixture(t, classifier) { fs.writeFileSync(platformPropertiesPath, `classifier=${classifier}\nversion=${version}\n`); fs.writeFileSync( path.join(stagingDir, classifier, '.version'), - `${stagingSchema}\n${version}\n${integrity}\n${digestTree(resourceDir)}\n`, + `${stagingSchema}\n${version}\n${checksum}\n${digestTree(resourceDir)}\n`, ); - const fakeNpmPath = path.join(fakeBinDir, process.platform === 'win32' ? 'npm.cmd' : 'npm'); - if (process.platform === 'win32') { - fs.writeFileSync(fakeNpmPath, '@echo off\r\n> "%FETCH_NATIVE_NPM_MARKER%" echo invoked\r\nexit /b 42\r\n'); - } else { - fs.writeFileSync(fakeNpmPath, '#!/bin/sh\nprintf invoked > "$FETCH_NATIVE_NPM_MARKER"\nexit 42\n'); - fs.chmodSync(fakeNpmPath, 0o755); - } - return { + root, classifier, repoRoot, stagingDir, - fakeBinDir, - npmMarkerPath, runtimePath, wrapperPath, ripgrepPath, @@ -196,16 +170,15 @@ function runScript(fixture, extraEnv = {}) { encoding: 'utf8', env: { ...process.env, - PATH: `${fixture.fakeBinDir}${path.delimiter}${process.env.PATH}`, - FETCH_NATIVE_NPM_MARKER: fixture.npmMarkerPath, + COPILOT_CLI_RELEASE_TARBALL: path.join(fixture.root, 'missing.tgz'), + COPILOT_CLI_RELEASE_SHA256: checksum, ...extraEnv, }, }); } function assertRestagingAttempted(fixture, result) { - assert.notEqual(result.status, 0, 'The fake npm command should make restaging fail'); - assert.equal(fs.readFileSync(fixture.npmMarkerPath, 'utf8').trim(), 'invoked'); + assert.notEqual(result.status, 0, 'The unavailable release should make restaging fail'); } function digestTree(directory) { diff --git a/java/copilot-native/scripts/validate-native-artifact.mjs b/java/copilot-native/scripts/validate-native-artifact.mjs index 9af4ffd772..466fa260a4 100644 --- a/java/copilot-native/scripts/validate-native-artifact.mjs +++ b/java/copilot-native/scripts/validate-native-artifact.mjs @@ -119,22 +119,19 @@ export function validateSha256Manifest({ } export function readPinnedNativeVersion(repoRoot, classifier) { - const packageName = `@github/copilot-${classifier}`; - const lockPath = path.join(repoRoot, "nodejs", "package-lock.json"); - let lock; + const packagePath = path.join(repoRoot, "nodejs", "package.json"); + let packageJson; try { - lock = JSON.parse(fs.readFileSync(lockPath, "utf8")); + packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); } catch (error) { throw new Error( - `Could not read pinned ${packageName} version from ${lockPath}: ${error.message}`, + `Could not read pinned Copilot CLI version from ${packagePath}: ${error.message}`, ); } - const version = lock.packages?.[`node_modules/${packageName}`]?.version; + const version = packageJson.copilotCliVersion; if (!version) { - throw new Error( - `Could not find pinned ${packageName} version in ${lockPath}`, - ); + throw new Error(`Could not find copilotCliVersion in ${packagePath}`); } return version; } diff --git a/java/copilot-native/scripts/validate-native-artifact.test.mjs b/java/copilot-native/scripts/validate-native-artifact.test.mjs index 25851bb575..c07695a484 100644 --- a/java/copilot-native/scripts/validate-native-artifact.test.mjs +++ b/java/copilot-native/scripts/validate-native-artifact.test.mjs @@ -67,7 +67,7 @@ test("accepts a matching, complete Windows ARM64 classifier", (t) => { }), { classifier: windowsArm64Classifier, - nativeVersion: "9.8.10", + nativeVersion: "9.8.7", sha256: undefined, }, ); @@ -94,7 +94,7 @@ test("accepts a matching, complete Darwin classifier", (t) => { }), { classifier: darwinClassifier, - nativeVersion: "9.8.8", + nativeVersion: "9.8.7", sha256: undefined, }, ); @@ -121,7 +121,7 @@ test("accepts a matching, complete Linux ARM64 classifier", (t) => { }), { classifier: linuxArm64Classifier, - nativeVersion: "9.8.9", + nativeVersion: "9.8.7", sha256: undefined, }, ); @@ -248,7 +248,7 @@ test("rejects Windows resources in a Linux classifier", (t) => { ["native/linux-x64/copilot-runtime", "runtime wrapper"], [ "native/linux-x64/platform.properties", - "classifier=linux-x64\nversion=9.8.6\n", + "classifier=linux-x64\nversion=9.8.7\n", ], ["native/win32-x64/runtime.node", "wrong platform"], ]); @@ -462,7 +462,7 @@ test("local publication validation rejects cross-classifier contamination", (t) ["native/linux-x64/copilot-runtime", "runtime wrapper"], [ "native/linux-x64/platform.properties", - "classifier=linux-x64\nversion=9.8.6\n", + "classifier=linux-x64\nversion=9.8.7\n", ], ["native/win32-x64/runtime.node", "wrong platform"], ], @@ -537,16 +537,8 @@ function createFixture(t) { const repoRoot = path.join(root, "repo"); fs.mkdirSync(path.join(repoRoot, "nodejs"), { recursive: true }); fs.writeFileSync( - path.join(repoRoot, "nodejs", "package-lock.json"), - JSON.stringify({ - packages: { - "node_modules/@github/copilot-win32-x64": { version: "9.8.7" }, - "node_modules/@github/copilot-win32-arm64": { version: "9.8.10" }, - "node_modules/@github/copilot-linux-x64": { version: "9.8.6" }, - "node_modules/@github/copilot-linux-arm64": { version: "9.8.9" }, - "node_modules/@github/copilot-darwin-arm64": { version: "9.8.8" }, - }, - }), + path.join(repoRoot, "nodejs", "package.json"), + JSON.stringify({ copilotCliVersion: "9.8.7" }), ); return { diff --git a/java/docs/adr/adr-007-native-bundling-strategy.md b/java/docs/adr/adr-007-native-bundling-strategy.md index 1540829366..3c13d451cf 100644 --- a/java/docs/adr/adr-007-native-bundling-strategy.md +++ b/java/docs/adr/adr-007-native-bundling-strategy.md @@ -361,7 +361,7 @@ The pattern follows DJL's `LibUtils.loadLibrary()` approach: detect the platform 2. Locates the matching `runtime.node` binary on the classpath (via `getResourceAsStream` from the classifier JAR). 3. Extracts `runtime.node` and the transitional CLI entrypoint into `~/.copilot/runtime-cache/` if valid cached files are not already present. 4. Loads it via [JNA](#references) using the C ABI entry points, per the [binding technology decision](#binding-technology-jna-over-panama-ffm) above. The JNA-specific code is confined behind an internal binding interface to preserve a future FFM migration path. -* A validated supported-host profile fetches the pinned matching `@github/copilot-` npm package, verifies its SHA-512 integrity from `nodejs/package-lock.json`, and packages the version-matched runtime and CLI files. +* A validated supported-host profile fetches the matching platform tarball from the pinned `github/copilot-cli` release, verifies its release SHA-256, and packages the version-matched runtime files. * The current release work publishes the `linux-x64`, `linux-arm64`, `win32-x64`, `win32-arm64`, and `darwin-arm64` classifiers. The planned classifier set expands to the other detected platforms. * Adding an implemented platform requires validated host activation, a profile that supplies the classifier and platform CLI filename, and lifecycle bindings for the shared host validation, fetch, script test, package, and verification executions. * `cli-native.node` is not bundled. It provides terminal UI features that are irrelevant to the Java SDK's programmatic API surface. diff --git a/java/pom.xml b/java/pom.xml index 91a97aadff..b17cdbde7d 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.4-SNAPSHOT + 1.0.14-SNAPSHOT pom GitHub Copilot SDK :: Java :: Parent @@ -55,15 +55,6 @@ adjust for their directory depth (e.g. sdk/ overrides with ../../). --> ${project.basedir}/.. - - ^1.0.83-0 true @@ -139,7 +130,7 @@ com.github.spotbugs spotbugs-maven-plugin - 4.10.3.0 + 4.10.4.0 com.diffplug.spotless diff --git a/java/scripts/codegen/fetch-schemas.mjs b/java/scripts/codegen/fetch-schemas.mjs new file mode 100644 index 0000000000..d1bcb4c0b7 --- /dev/null +++ b/java/scripts/codegen/fetch-schemas.mjs @@ -0,0 +1,127 @@ +#!/usr/bin/env node + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { createHash } from 'node:crypto'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptDir, '../../..'); +const packagePath = path.join(repoRoot, 'nodejs', 'package.json'); +const outputDir = path.resolve( + process.env.COPILOT_CLI_SCHEMA_OUTPUT ?? path.join(scriptDir, 'target', 'schemas'), +); +// Schemas are platform-independent; use one asset consistently on every codegen host. +const platform = process.env.COPILOT_CLI_SCHEMA_PLATFORM ?? 'linux-x64'; +const version = + process.env.COPILOT_CLI_VERSION ?? + JSON.parse(fs.readFileSync(packagePath, 'utf8')).copilotCliVersion; + +if (!version) { + throw new Error(`Could not find copilotCliVersion in ${packagePath}`); +} + +const assetName = `github-copilot-${version}-${platform}.tgz`; +const releaseBase = ( + process.env.COPILOT_CLI_DOWNLOAD_BASE_URL ?? + 'https://github.com/github/copilot-cli/releases/download' +).replace(/\/+$/, ''); + +let archive; +let expectedHash; +if (process.env.COPILOT_CLI_RELEASE_TARBALL) { + archive = fs.readFileSync(process.env.COPILOT_CLI_RELEASE_TARBALL); + expectedHash = process.env.COPILOT_CLI_RELEASE_SHA256; +} else { + const releaseUrl = `${releaseBase}/v${version}`; + const checksums = (await download(`${releaseUrl}/SHA256SUMS.txt`)).toString('utf8'); + expectedHash = findChecksum(checksums, assetName); + archive = await download(`${releaseUrl}/${assetName}`); +} + +if (!expectedHash || !/^[a-fA-F0-9]{64}$/.test(expectedHash)) { + throw new Error(`Missing or invalid SHA-256 for ${assetName}`); +} +const actualHash = createHash('sha256').update(archive).digest('hex'); +if (actualHash !== expectedHash.toLowerCase()) { + throw new Error( + `Integrity verification failed for ${assetName}: expected ${expectedHash}, got ${actualHash}`, + ); +} + +const schemaNames = ['api.schema.json', 'session-events.schema.json']; +const members = execFileSync('tar', ['-tzf', '-'], { + encoding: 'utf8', + input: archive, + maxBuffer: 512 * 1024 * 1024, +}) + .split(/\r?\n/) + .filter(Boolean); +const outputParent = path.dirname(outputDir); +fs.mkdirSync(outputParent, { recursive: true }); +const stagingDir = fs.mkdtempSync(path.join(outputParent, '.schemas-')); + +try { + for (const schemaName of schemaNames) { + const member = `package/schemas/${schemaName}`; + if (members.filter((candidate) => candidate === member).length !== 1) { + throw new Error(`${assetName} must contain exactly one ${member}`); + } + const contents = execFileSync('tar', ['-xOzf', '-', member], { + encoding: null, + input: archive, + maxBuffer: 512 * 1024 * 1024, + }); + JSON.parse(contents.toString('utf8')); + fs.writeFileSync(path.join(stagingDir, schemaName), contents); + } + + fs.rmSync(outputDir, { recursive: true, force: true }); + fs.renameSync(stagingDir, outputDir); +} finally { + fs.rmSync(stagingDir, { recursive: true, force: true }); +} + +console.log(`Staged Copilot CLI ${version} schemas at ${outputDir}`); + +async function download(url) { + let lastError; + for (let attempt = 0; attempt < 3; attempt++) { + try { + // lgtm[js/file-access-to-http] The repository-pinned CLI version selects the release asset. + const response = await fetch(url, { signal: AbortSignal.timeout(600_000) }); + if (response.ok) { + return Buffer.from(await response.arrayBuffer()); + } + await response.body?.cancel(); + lastError = new Error(`${response.status} ${response.statusText}`); + if (response.status >= 400 && response.status < 500 && response.status !== 408 && response.status !== 429) { + break; + } + } catch (error) { + lastError = error; + } + if (attempt < 2) { + await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000)); + } + } + throw new Error(`Failed to download ${url}: ${lastError}`); +} + +function findChecksum(checksums, expectedAssetName) { + for (const line of checksums.split(/\r?\n/)) { + const [hash, name] = line.trim().split(/\s+/, 2); + if ( + name?.replace(/^\*/, '') === expectedAssetName && + /^[a-fA-F0-9]{64}$/.test(hash) + ) { + return hash.toLowerCase(); + } + } + throw new Error(`SHA256SUMS.txt does not contain ${expectedAssetName}`); +} diff --git a/java/scripts/codegen/fetch-schemas.test.mjs b/java/scripts/codegen/fetch-schemas.test.mjs new file mode 100644 index 0000000000..19d7ba6715 --- /dev/null +++ b/java/scripts/codegen/fetch-schemas.test.mjs @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { execFileSync, spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const scriptPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'fetch-schemas.mjs'); + +test('extracts schemas from a verified release archive', (t) => { + const fixture = createFixture(t); + const outputDir = path.join(fixture.root, 'output'); + const result = runFetch(fixture, outputDir); + + assert.equal(result.status, 0, result.stderr); + assert.deepEqual( + JSON.parse(fs.readFileSync(path.join(outputDir, 'api.schema.json'), 'utf8')), + { title: 'API' }, + ); + assert.deepEqual( + JSON.parse(fs.readFileSync(path.join(outputDir, 'session-events.schema.json'), 'utf8')), + { title: 'Events' }, + ); +}); + +test('rejects an archive with the wrong checksum', (t) => { + const fixture = createFixture(t); + const result = runFetch( + { ...fixture, hash: '0'.repeat(64) }, + path.join(fixture.root, 'output'), + ); + + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Integrity verification failed/); +}); + +test('requires both schema files', (t) => { + const fixture = createFixture(t, { includeEvents: false }); + const result = runFetch(fixture, path.join(fixture.root, 'output')); + + assert.notEqual(result.status, 0); + assert.match(result.stderr, /must contain exactly one package\/schemas\/session-events\.schema\.json/); +}); + +function createFixture(t, { includeEvents = true } = {}) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'copilot-java-schemas-')); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const packageDir = path.join(root, 'package'); + const schemasDir = path.join(packageDir, 'schemas'); + fs.mkdirSync(schemasDir, { recursive: true }); + fs.writeFileSync(path.join(schemasDir, 'api.schema.json'), '{"title":"API"}\n'); + if (includeEvents) { + fs.writeFileSync(path.join(schemasDir, 'session-events.schema.json'), '{"title":"Events"}\n'); + } + const archivePath = path.join(root, 'release.tgz'); + execFileSync('tar', ['-czf', archivePath, '-C', root, 'package']); + const hash = createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex'); + return { root, archivePath, hash }; +} + +function runFetch(fixture, outputDir) { + return spawnSync(process.execPath, [scriptPath], { + encoding: 'utf8', + env: { + ...process.env, + COPILOT_CLI_RELEASE_TARBALL: fixture.archivePath, + COPILOT_CLI_RELEASE_SHA256: fixture.hash, + COPILOT_CLI_SCHEMA_OUTPUT: outputDir, + }, + }); +} diff --git a/java/scripts/codegen/java.test.ts b/java/scripts/codegen/java.test.ts new file mode 100644 index 0000000000..8db96ee3d9 --- /dev/null +++ b/java/scripts/codegen/java.test.ts @@ -0,0 +1,118 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import type { JSONSchema7 } from "json-schema"; + +import { + collectNestedDiscriminatedUnionTypeNames, + schemaTypeToJava, +} from "./java.js"; + +test("nested discriminated array items use their named Java type", () => { + const definitions: Record = { + MessageResult: { + anyOf: [ + { $ref: "#/definitions/MessageDelivered" }, + { $ref: "#/definitions/MessageRejected" }, + ], + }, + MessageDelivered: { + type: "object", + additionalProperties: false, + properties: { + status: { const: "delivered" }, + actions: { + type: "array", + items: { $ref: "#/definitions/ActionChoice" }, + }, + }, + }, + MessageRejected: { + type: "object", + additionalProperties: false, + properties: { + status: { const: "rejected" }, + reason: { type: "string" }, + }, + }, + ActionChoice: { + anyOf: [ + { $ref: "#/definitions/PhoneAction" }, + { $ref: "#/definitions/EmailAction" }, + ], + }, + PhoneAction: { + type: "object", + title: "PhoneAction", + additionalProperties: false, + properties: { + kind: { const: "phone" }, + number: { type: "string" }, + sources: { + type: "object", + additionalProperties: { $ref: "#/definitions/ActionSource" }, + }, + }, + }, + EmailAction: { + type: "object", + title: "EmailAction", + additionalProperties: false, + properties: { + kind: { const: "email" }, + address: { type: "string" }, + sources: { + type: "object", + additionalProperties: { $ref: "#/definitions/ActionSource" }, + }, + }, + }, + ActionSource: { + anyOf: [ + { $ref: "#/definitions/LocalActionSource" }, + { $ref: "#/definitions/RemoteActionSource" }, + ], + }, + LocalActionSource: { + type: "object", + title: "LocalActionSource", + additionalProperties: false, + properties: { + location: { const: "local" }, + }, + }, + RemoteActionSource: { + type: "object", + title: "RemoteActionSource", + additionalProperties: false, + properties: { + location: { const: "remote" }, + url: { type: "string" }, + }, + }, + }; + const standaloneTypes = new Map(); + const promotedUnionTypes = collectNestedDiscriminatedUnionTypeNames( + { $ref: "#/definitions/MessageResult" }, + definitions + ); + + const result = schemaTypeToJava( + { + type: "array", + items: { $ref: "#/definitions/ActionChoice" }, + }, + false, + "MessageEnvelope", + "actions", + new Map(), + { + definitions, + standaloneTypes, + promotedUnionTypes, + } + ); + + assert.equal(result.javaType, "List"); + assert.deepEqual([...standaloneTypes.keys()], ["ActionChoice"]); + assert.deepEqual([...promotedUnionTypes], ["ActionChoice", "ActionSource"]); +}); diff --git a/java/scripts/codegen/java.ts b/java/scripts/codegen/java.ts index 785049afa1..71e556eda4 100644 --- a/java/scripts/codegen/java.ts +++ b/java/scripts/codegen/java.ts @@ -174,53 +174,15 @@ function toEnumConstant(value: string): string { // ── Schema path resolution ─────────────────────────────────────────────────── -/** - * Resolve a JSON schema shipped by the `@github/copilot` CLI package. - * - * The CLI package layout changed in 1.0.64-1: the umbrella `@github/copilot` - * package became a thin loader and its bundled assets (including the JSON - * schemas) moved into the platform-specific packages installed as optional - * dependencies, e.g. `@github/copilot-linux-x64` or `@github/copilot-win32-x64`. - * - * We search both the Java codegen install (`scripts/codegen/node_modules`) and - * the Node SDK install (`nodejs/node_modules`), checking the umbrella package - * first (older versions) and then whichever platform package is present. - */ +/** Resolve a JSON schema staged from the pinned GitHub Release artifact. */ async function resolveCopilotSchemaPath(fileName: string): Promise { - const nodeModulesDirs = [ - path.join(REPO_ROOT, "scripts/codegen/node_modules"), - path.join(REPO_ROOT, "nodejs/node_modules"), - ]; - - const candidates: string[] = []; - for (const nodeModulesDir of nodeModulesDirs) { - candidates.push(path.join(nodeModulesDir, "@github/copilot/schemas", fileName)); - const githubScopeDir = path.join(nodeModulesDir, "@github"); - try { - for (const entry of await fs.readdir(githubScopeDir)) { - if (entry.startsWith("copilot-")) { - candidates.push(path.join(githubScopeDir, entry, "schemas", fileName)); - } - } - } catch (err) { - const code = (err as NodeJS.ErrnoException).code; - if (code !== "ENOENT" && code !== "ENOTDIR") { - throw err; - } - // @github scope directory may not exist; try the next location. - } - } - - for (const candidate of candidates) { - try { - await fs.access(candidate); - return candidate; - } catch { - // Try the next candidate. - } + const schemaPath = path.join(REPO_ROOT, "scripts/codegen/target/schemas", fileName); + try { + await fs.access(schemaPath); + return schemaPath; + } catch { + throw new Error(`${fileName} not found. Run 'npm run fetch:schemas' in java/scripts/codegen.`); } - - throw new Error(`${fileName} not found. Run 'npm ci' in java/scripts/codegen or java/nodejs first.`); } async function getSessionEventsSchemaPath(): Promise { @@ -252,6 +214,7 @@ interface JavaTypeResult { // Set before each schema generation pass; used by schemaTypeToJava and helpers. let currentDefinitions: Record = {}; const pendingStandaloneTypes = new Map(); +const promotedNestedUnionTypes = new Set(); const generatedSessionEventTypeNames = new Set(); // Cross-schema definitions: keyed by schema filename (e.g. "session-events.schema.json"), @@ -365,18 +328,101 @@ function findDiscriminator(variants: JSONSchema7[]): DiscriminatorInfo | null { /** * Resolve anyOf variants, handling $ref to definitions. */ -function resolveAnyOfVariants(anyOf: JSONSchema7[]): JSONSchema7[] { +function resolveAnyOfVariants( + anyOf: JSONSchema7[], + definitions: Record = currentDefinitions +): JSONSchema7[] { return anyOf .map((v) => { if (v.$ref) { const name = v.$ref.replace(/^#\/definitions\//, ""); - return currentDefinitions[name] ?? v; + return definitions[name] ?? v; } return v; }) .filter((v) => v.type !== "null"); } +export function collectNestedDiscriminatedUnionTypeNames( + root: unknown, + definitions: Record +): Set { + const promotedTypes = new Set(); + const definitionName = (schema: JSONSchema7): string | null => { + return schema.$ref?.match(/^#\/definitions\/([^/]+)$/)?.[1] ?? null; + }; + const resolveLocal = (schema: JSONSchema7): JSONSchema7 | null => { + const name = definitionName(schema); + return name ? definitions[name] ?? null : schema; + }; + const closedDiscriminatedUnionVariants = (schema: JSONSchema7): JSONSchema7[] | null => { + const resolved = resolveLocal(schema); + if (!resolved?.anyOf || !Array.isArray(resolved.anyOf)) return null; + const variants = resolveAnyOfVariants(resolved.anyOf as JSONSchema7[], definitions); + return variants.length > 1 + && findDiscriminator(variants) + && variants.every((variant) => variant.additionalProperties === false) + ? variants + : null; + }; + + const rootSchema = typeof root === "object" && root !== null ? root as JSONSchema7 : null; + const rootVariants = rootSchema ? closedDiscriminatedUnionVariants(rootSchema) : null; + if (!rootVariants) return promotedTypes; + + const nestedUnionItems: JSONSchema7[] = []; + for (const variant of rootVariants) { + for (const property of Object.values(variant.properties ?? {})) { + if (!property || typeof property !== "object") continue; + const propertySchema = resolveLocal(property as JSONSchema7); + if ( + propertySchema?.type === "array" + && propertySchema.items + && !Array.isArray(propertySchema.items) + && closedDiscriminatedUnionVariants(propertySchema.items as JSONSchema7) + ) { + nestedUnionItems.push(propertySchema.items as JSONSchema7); + } + } + } + + const visitedDefinitions = new Set(); + const visit = (schema: JSONSchema7): void => { + const name = definitionName(schema); + if (name) { + if (visitedDefinitions.has(name)) return; + visitedDefinitions.add(name); + const resolved = definitions[name]; + if (!resolved) return; + if (closedDiscriminatedUnionVariants(schema)) { + promotedTypes.add(name); + } + visit(resolved); + return; + } + + for (const property of Object.values(schema.properties ?? {})) { + if (property && typeof property === "object") { + visit(property as JSONSchema7); + } + } + if (schema.items && !Array.isArray(schema.items)) { + visit(schema.items as JSONSchema7); + } + if (schema.additionalProperties && typeof schema.additionalProperties === "object") { + visit(schema.additionalProperties as JSONSchema7); + } + for (const branch of [...(schema.anyOf ?? []), ...(schema.allOf ?? [])]) { + if (branch && typeof branch === "object") { + visit(branch as JSONSchema7); + } + } + }; + + for (const items of nestedUnionItems) visit(items); + return promotedTypes; +} + /** * Generate a polymorphic base class and variant subclasses for a discriminated union result type. */ @@ -429,7 +475,10 @@ async function generatePolymorphicResultClass( baseLines.push(` * @since 1.0.0`); baseLines.push(` */`); } - baseLines.push(`@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "${discriminator.property}", visible = true)`); + const typeInfoInclude = promotedNestedUnionTypes.has(className) + ? `, include = JsonTypeInfo.As.EXISTING_PROPERTY` + : ""; + baseLines.push(`@JsonTypeInfo(use = JsonTypeInfo.Id.NAME${typeInfoInclude}, property = "${discriminator.property}", visible = true)`); baseLines.push(`@JsonSubTypes({`); for (let i = 0; i < variantInfos.length; i++) { const v = variantInfos[i]; @@ -578,12 +627,23 @@ async function generatePolymorphicVariantClass( await writeGeneratedFile(`${packageDir}/${className}.java`, lines.join("\n")); } -function schemaTypeToJava( +interface JavaTypeResolution { + definitions: Record; + standaloneTypes: Map; + promotedUnionTypes: Set; +} + +export function schemaTypeToJava( schema: JSONSchema7, required: boolean, context: string, propName: string, - nestedTypes: Map + nestedTypes: Map, + resolution: JavaTypeResolution = { + definitions: currentDefinitions, + standaloneTypes: pendingStandaloneTypes, + promotedUnionTypes: promotedNestedUnionTypes, + } ): JavaTypeResult { const imports = new Set(); @@ -606,16 +666,27 @@ function schemaTypeToJava( } const name = schema.$ref.replace(/^#\/definitions\//, ""); - const resolved = currentDefinitions[name]; + const resolved = resolution.definitions[name]; if (resolved) { + if ( + resolution.promotedUnionTypes.has(name) + && resolved.anyOf + && Array.isArray(resolved.anyOf) + ) { + const variants = resolveAnyOfVariants(resolved.anyOf as JSONSchema7[], resolution.definitions); + if (variants.length > 1 && findDiscriminator(variants)) { + resolution.standaloneTypes.set(name, resolved); + return { javaType: name, imports }; + } + } // Enum or object types → register for standalone generation, return ref name if ((resolved.type === "string" && resolved.enum) || (resolved.type === "object" && resolved.properties)) { - pendingStandaloneTypes.set(name, resolved); + resolution.standaloneTypes.set(name, resolved); return { javaType: name, imports }; } // Other types (primitives, arrays, maps, anyOf unions) → resolve and recurse - return schemaTypeToJava(resolved, required, context, propName, nestedTypes); + return schemaTypeToJava(resolved, required, context, propName, nestedTypes, resolution); } // Unresolved $ref — return name as-is console.warn(`[codegen] Unresolved $ref: ${schema.$ref}`); @@ -626,7 +697,8 @@ function schemaTypeToJava( const hasNull = schema.anyOf.some((s) => typeof s === "object" && (s as JSONSchema7).type === "null"); const nonNull = schema.anyOf.filter((s) => typeof s === "object" && (s as JSONSchema7).type !== "null"); if (nonNull.length === 1) { - const result = schemaTypeToJava(nonNull[0] as JSONSchema7, required && !hasNull, context, propName, nestedTypes); + const result = schemaTypeToJava(nonNull[0] as JSONSchema7, required && !hasNull, + context, propName, nestedTypes, resolution); return result; } // Multi-branch anyOf: fall through to Object, matching the C# generator's @@ -662,7 +734,8 @@ function schemaTypeToJava( const nonNullTypes = schema.type.filter((t) => t !== "null"); if (nonNullTypes.length === 1) { const baseSchema = { ...schema, type: nonNullTypes[0] }; - return schemaTypeToJava(baseSchema as JSONSchema7, required, context, propName, nestedTypes); + return schemaTypeToJava(baseSchema as JSONSchema7, required, context, propName, + nestedTypes, resolution); } } @@ -684,7 +757,8 @@ function schemaTypeToJava( const items = schema.items as JSONSchema7 | undefined; if (items) { // Always pass required=false so primitives are boxed (List, not List) - const itemResult = schemaTypeToJava(items, false, context, propName + "Item", nestedTypes); + const itemResult = schemaTypeToJava(items, false, context, propName + "Item", + nestedTypes, resolution); imports.add("java.util.List"); for (const imp of itemResult.imports) imports.add(imp); return { javaType: `List<${itemResult.javaType}>`, imports }; @@ -712,7 +786,8 @@ function schemaTypeToJava( ? schema.additionalProperties as JSONSchema7 : { type: "object" } as JSONSchema7; // Always pass required=false so primitives are boxed (Map, not Map) - const valueResult = schemaTypeToJava(valueSchema, false, context, propName + "Value", nestedTypes); + const valueResult = schemaTypeToJava(valueSchema, false, context, + propName + "Value", nestedTypes, resolution); imports.add("java.util.Map"); for (const imp of valueResult.imports) imports.add(imp); return { javaType: `Map`, imports }; @@ -1391,6 +1466,7 @@ async function generateRpcTypes(schemaPath: string): Promise { // Set module-level definitions for $ref resolution currentDefinitions = (schema.definitions ?? {}) as Record; pendingStandaloneTypes.clear(); + promotedNestedUnionTypes.clear(); crossSchemaDefinitions.clear(); // Load cross-schema definitions (session-events) so that cross-schema $ref values @@ -1415,6 +1491,14 @@ async function generateRpcTypes(schemaPath: string): Promise { if (schema.clientSession) sections.push(["clientSession", schema.clientSession]); if (schema.clientGlobal) sections.push(["clientGlobal", schema.clientGlobal]); + for (const [, sectionNode] of sections) { + for (const [, method] of collectRpcMethods(sectionNode)) { + for (const typeName of collectNestedDiscriminatedUnionTypeNames(method.result, currentDefinitions)) { + promotedNestedUnionTypes.add(typeName); + } + } + } + const generatedClasses = new Map(); const allFiles: string[] = []; @@ -2392,7 +2476,9 @@ async function main(): Promise { console.log("\n✅ Java code generation complete!"); } -main().catch((err) => { - console.error("❌ Code generation failed:", err); - process.exit(1); -}); +if (process.argv[1] && path.resolve(process.argv[1]) === __filename) { + main().catch((err) => { + console.error("❌ Code generation failed:", err); + process.exit(1); + }); +} diff --git a/java/scripts/codegen/package-lock.json b/java/scripts/codegen/package-lock.json index fd177ca1e8..a92322d340 100644 --- a/java/scripts/codegen/package-lock.json +++ b/java/scripts/codegen/package-lock.json @@ -6,9 +6,8 @@ "": { "name": "copilot-sdk-java-codegen", "dependencies": { - "@github/copilot": "^1.0.83-0", "json-schema": "^0.4.0", - "tsx": "^4.23.12" + "tsx": "^4.23.13" } }, "node_modules/@esbuild/aix-ppc64": { @@ -427,165 +426,6 @@ "node": ">=18" } }, - "node_modules/@github/copilot": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.83-0.tgz", - "integrity": "sha512-Nv4IsqsveMgghwaBhgvSBZyIyvsqNBZTqnbVnv69+9+Suyq20vJcv6aB74UcJ7VPCMxIGJJUaJkugEtkMNv6wA==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "detect-libc": "^2.1.2" - }, - "bin": { - "copilot": "npm-loader.js" - }, - "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-0", - "@github/copilot-darwin-x64": "1.0.83-0", - "@github/copilot-linux-arm64": "1.0.83-0", - "@github/copilot-linux-x64": "1.0.83-0", - "@github/copilot-linuxmusl-arm64": "1.0.83-0", - "@github/copilot-linuxmusl-x64": "1.0.83-0", - "@github/copilot-win32-arm64": "1.0.83-0", - "@github/copilot-win32-x64": "1.0.83-0" - } - }, - "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.83-0.tgz", - "integrity": "sha512-0KQjKS9vd4QGxLAbFJcvyv/zsC5kivrtDe0UZhHt/43nUGqoS61DFcsM596/kg75vNE6c9J4gmZ5fUPYef+0hw==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "copilot-darwin-arm64": "copilot" - } - }, - "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.83-0.tgz", - "integrity": "sha512-fiyW+hy4c8AI7ONxN623f9cmJGRpbqTztc0jSVXc9z9WwzcWi39X0nxUprRM2l2Dq6YQ3guPCqGl/g1T5bQfQg==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "copilot-darwin-x64": "copilot" - } - }, - "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.83-0.tgz", - "integrity": "sha512-RWbRU+KgEmtAdKp1GQVTqfdwg4Ti/OVmgZGkXq4lMYj3wnBBQcayFpSLHg5ShzDSS0RglD4b8Z27NjPrm7bXxA==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.83-0.tgz", - "integrity": "sha512-5COXUNT+jDfkeyqrymZMvhTogkBYUXt+wuRwKrK6ol5vaw5SoDP1DYbI2hIEfoUj4g7XTHLUCD1s3lw8eicqUA==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-x64": "copilot" - } - }, - "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-arm64/-/copilot-linuxmusl-arm64-1.0.83-0.tgz", - "integrity": "sha512-7sYf364iz6s97ClviBRQusTKz3S3TgoKniyYv8+aRi5f5w6TL8NTPnGX1bXMeU0VZmk5VKQTlxVRO2yA4uFwpg==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linuxmusl-x64/-/copilot-linuxmusl-x64-1.0.83-0.tgz", - "integrity": "sha512-jze/f6Yd3Y83kxUa88kXUiwHlZmHDwAqudswdHT6f6q+K1ZEELFGEzbB6Ku4i0L8M6wHXO1EF/zaiSFWQaM4Tw==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-x64": "copilot" - } - }, - "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.83-0.tgz", - "integrity": "sha512-93jln98UAJpslMQ7n+wAmCpoOWGEV5lXxV/DaEajySvYrCU33D2yj7d9kl8X2CgaUVBas6sNWSWKtqy+rKJxXQ==", - "cpu": [ - "arm64" - ], - "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.83-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.83-0.tgz", - "integrity": "sha512-+4Htk3CixO1qcOtYegjn33/8bSDdx8QXDpgVBak2D4Y5hzBWPO5IuQoICwvjaW5VOIW+I7Q62RK2pupSjxB38Q==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "copilot-win32-x64": "copilot.exe" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -648,9 +488,9 @@ "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/tsx": { - "version": "4.23.12", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", - "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", "license": "MIT", "dependencies": { "esbuild": "~0.28.0" diff --git a/java/scripts/codegen/package.json b/java/scripts/codegen/package.json index 8089a79133..3e7761fc20 100644 --- a/java/scripts/codegen/package.json +++ b/java/scripts/codegen/package.json @@ -3,12 +3,13 @@ "private": true, "type": "module", "scripts": { - "generate": "tsx java.ts", - "generate:java": "tsx java.ts" + "fetch:schemas": "node fetch-schemas.mjs", + "generate": "npm run fetch:schemas && tsx java.ts", + "generate:java": "npm run generate", + "test": "node --test fetch-schemas.test.mjs && tsx --test java.test.ts" }, "dependencies": { - "@github/copilot": "^1.0.83-0", "json-schema": "^0.4.0", - "tsx": "^4.23.12" + "tsx": "^4.23.13" } } diff --git a/java/sdk/jbang-example.java b/java/sdk/jbang-example.java index 0a1505f9c2..4df2583e8c 100644 --- a/java/sdk/jbang-example.java +++ b/java/sdk/jbang-example.java @@ -1,5 +1,5 @@ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS com.github:copilot-sdk-java:1.0.13-preview.4 +//DEPS com.github:copilot-sdk-java:1.0.13 import com.github.copilot.CopilotClient; import com.github.copilot.generated.AssistantMessageEvent; import com.github.copilot.generated.SessionUsageInfoEvent; diff --git a/java/sdk/pom.xml b/java/sdk/pom.xml index bb7cd2fa36..895c464911 100644 --- a/java/sdk/pom.xml +++ b/java/sdk/pom.xml @@ -8,7 +8,7 @@ com.github copilot-sdk-java-parent - 1.0.14-preview.4-SNAPSHOT + 1.0.14-SNAPSHOT ../pom.xml @@ -42,20 +42,18 @@ ${project.basedir}/../.. ${copilot.sdk.root}/test - ${copilot.sdk.root}/nodejs/node_modules/@github/copilot/npm-loader.js + false com.github.spotbugs spotbugs-annotations - 4.10.3 + 4.10.4 provided @@ -196,12 +194,7 @@ - + install-nodejs-cli-dependencies generate-test-resources @@ -249,9 +242,8 @@ ${copilot.cli.path} @@ -281,10 +273,9 @@ ${copilot.cli.path} @@ -632,6 +623,8 @@ did not produce the multi-release output. Re-build on JDK 25+ and verify the **/AskUserTest.java **/CompactionTest.java + + **/CopilotClientTest.java **/CopilotSessionTest.java **/ErrorHandlingTest.java **/EventFidelityTest.java @@ -782,57 +775,6 @@ did not produce the multi-release output. Re-build on JDK 25+ and verify the - - - update-schemas-from-npm-artifact - - - - org.codehaus.mojo - exec-maven-plugin - - - update-copilot-schema-version - generate-sources - - exec - - - npm - ${project.parent.basedir}/scripts/codegen - - install - @github/copilot@${copilot.schema.version} - - - - - - - org.apache.maven.plugins - maven-enforcer-plugin - - - require-schema-version - validate - - enforce - - - - - copilot.schema.version - You must specify -Dcopilot.schema.version=VERSION (e.g. 1.0.25) - - - - - - - - - diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AgentModelPolicy.java b/java/sdk/src/generated/java/com/github/copilot/generated/AgentModelPolicy.java new file mode 100644 index 0000000000..06c80ba22e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AgentModelPolicy.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Whether configured models are advisory preferences or required constraints + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AgentModelPolicy { + /** The {@code preferred} variant. */ + PREFERRED("preferred"), + /** The {@code required} variant. */ + REQUIRED("required"); + + private final String value; + AgentModelPolicy(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AgentModelPolicy fromValue(String value) { + for (AgentModelPolicy v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AgentModelPolicy value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseActivityEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseActivityEvent.java new file mode 100644 index 0000000000..64d8d58283 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantFusionPhaseActivityEvent.java @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.fusion_phase_activity". Experimental content-safe activity signal for a running HydraFusion phase. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantFusionPhaseActivityEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.fusion_phase_activity"; } + + @JsonProperty("data") + private AssistantFusionPhaseActivityEventData data; + + public AssistantFusionPhaseActivityEventData getData() { return data; } + public void setData(AssistantFusionPhaseActivityEventData data) { this.data = data; } + + /** Data payload for {@link AssistantFusionPhaseActivityEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantFusionPhaseActivityEventData( + /** Identifier of the HydraFusion turn containing the phase. */ + @JsonProperty("fusionId") String fusionId, + /** Stable identifier for the concrete phase. */ + @JsonProperty("phaseId") String phaseId, + /** Kind of phase currently executing. */ + @JsonProperty("phaseKind") FusionPhaseKind phaseKind, + /** HydraFusion orchestration pattern containing the phase. */ + @JsonProperty("pattern") FusionPattern pattern, + /** Semantic role assigned to the phase. */ + @JsonProperty("role") String role, + /** Conversation scope in which the phase executes. */ + @JsonProperty("conversationScope") FusionConversationScope conversationScope, + /** Kind of real activity observed. */ + @JsonProperty("activity") FusionPhaseActivityKind activity, + /** Cumulative private response bytes observed for this model call. The event never includes response text. */ + @JsonProperty("totalResponseSizeBytes") Long totalResponseSizeBytes, + /** Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. */ + @JsonProperty("toolCallId") String toolCallId + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java index d2ad87f7c4..ee9c5fd485 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AssistantMessageReasoningBlocks.java @@ -24,7 +24,7 @@ public record AssistantMessageReasoningBlocks( /** Model provider that produced these reasoning blocks. */ @JsonProperty("provider") String provider, - /** Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. */ + /** Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. */ @JsonProperty("blocks") List blocks ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/AutoTierSwitchFailureReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/AutoTierSwitchFailureReason.java new file mode 100644 index 0000000000..4180045ace --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/AutoTierSwitchFailureReason.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Terminal reason an Auto preference activation failed. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AutoTierSwitchFailureReason { + /** The {@code policy_rejected} variant. */ + POLICY_REJECTED("policy_rejected"), + /** The {@code request_failed} variant. */ + REQUEST_FAILED("request_failed"), + /** The {@code setup_failed} variant. */ + SETUP_FAILED("setup_failed"), + /** The {@code unsupported} variant. */ + UNSUPPORTED("unsupported"); + + private final String value; + AutoTierSwitchFailureReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AutoTierSwitchFailureReason fromValue(String value) { + for (AutoTierSwitchFailureReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AutoTierSwitchFailureReason value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptEventRange.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptEventRange.java new file mode 100644 index 0000000000..7b05d1c9cd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptEventRange.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Inclusive durable event range summarized by a completion receipt. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CompletionReceiptEventRange( + /** Identifier of the user message that starts the covered exchange. */ + @JsonProperty("startEventId") String startEventId, + /** Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. */ + @JsonProperty("endEventId") String endEventId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptFinalTool.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptFinalTool.java new file mode 100644 index 0000000000..74d78d7ee1 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptFinalTool.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Final structured tool completion in the covered event range. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CompletionReceiptFinalTool( + /** Unique identifier of the completed tool call. */ + @JsonProperty("toolCallId") String toolCallId, + /** Tool name from the matching tool execution start event, when available. */ + @JsonProperty("toolName") String toolName, + /** Structured success or failure status from the tool completion event. */ + @JsonProperty("status") CompletionReceiptToolStatus status, + /** Process exit code from a structured shell result, when available. */ + @JsonProperty("exitCode") Long exitCode +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptStopReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptStopReason.java new file mode 100644 index 0000000000..49874b0275 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptStopReason.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Runtime reason the completion decision was accepted. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum CompletionReceiptStopReason { + /** The {@code natural} variant. */ + NATURAL("natural"), + /** The {@code terminal_tool} variant. */ + TERMINAL_TOOL("terminal_tool"), + /** The {@code agent_stop_block_limit} variant. */ + AGENT_STOP_BLOCK_LIMIT("agent_stop_block_limit"); + + private final String value; + CompletionReceiptStopReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CompletionReceiptStopReason fromValue(String value) { + for (CompletionReceiptStopReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown CompletionReceiptStopReason value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptToolStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptToolStatus.java new file mode 100644 index 0000000000..e82a1e6986 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CompletionReceiptToolStatus.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Structured terminal status from a tool completion event. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum CompletionReceiptToolStatus { + /** The {@code success} variant. */ + SUCCESS("success"), + /** The {@code failure} variant. */ + FAILURE("failure"), + /** The {@code timeout} variant. */ + TIMEOUT("timeout"), + /** The {@code rejected} variant. */ + REJECTED("rejected"), + /** The {@code denied} variant. */ + DENIED("denied"); + + private final String value; + CompletionReceiptToolStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CompletionReceiptToolStatus fromValue(String value) { + for (CompletionReceiptToolStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown CompletionReceiptToolStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java b/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java index c2f195e486..762f0b1ac8 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/CustomAgentsUpdatedAgent.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. + * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. * * @since 1.0.0 */ @@ -37,6 +37,10 @@ public record CustomAgentsUpdatedAgent( /** Whether the agent can be selected by the user */ @JsonProperty("userInvocable") Boolean userInvocable, /** Model override for this agent, if set */ - @JsonProperty("model") String model + @JsonProperty("model") String model, + /** Authored model ids in priority order, if configured */ + @JsonProperty("models") List models, + /** Whether authored models are preferences or required constraints */ + @JsonProperty("modelPolicy") AgentModelPolicy modelPolicy ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java new file mode 100644 index 0000000000..a95a08cf91 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhaseActivityKind.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Content-safe activity observed while a HydraFusion phase is running. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum FusionPhaseActivityKind { + /** The {@code model_output} variant. */ + MODEL_OUTPUT("model_output"), + /** The {@code tool_started} variant. */ + TOOL_STARTED("tool_started"), + /** The {@code tool_completed} variant. */ + TOOL_COMPLETED("tool_completed"); + + private final String value; + FusionPhaseActivityKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static FusionPhaseActivityKind fromValue(String value) { + for (FusionPhaseActivityKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown FusionPhaseActivityKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java new file mode 100644 index 0000000000..cfb07a789a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/FusionPhasePlanStep.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Presentation-neutral phase planned for a HydraFusion turn. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record FusionPhasePlanStep( + /** Kind of phase that may execute. */ + @JsonProperty("kind") FusionPhaseKind kind, + /** Semantic role assigned to the phase. */ + @JsonProperty("role") String role, + /** Conversation scope in which the phase executes. */ + @JsonProperty("scope") FusionConversationScope scope, + /** Whether the phase executes only when an earlier phase requests it. */ + @JsonProperty("conditional") Boolean conditional +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java index 32386f898a..90ce9de668 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ManagedSettingsResolvedSource.java @@ -22,6 +22,8 @@ public enum ManagedSettingsResolvedSource { DEVICE("device"), /** The {@code client} variant. */ CLIENT("client"), + /** The {@code policyHelper} variant. */ + POLICYHELPER("policyHelper"), /** The {@code mixed} variant. */ MIXED("mixed"), /** The {@code none} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServerMetadata.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerMetadata.java new file mode 100644 index 0000000000..6657eb5d87 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServerMetadata.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Server-advertised metadata learned through modern discovery or legacy initialization. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpServerMetadata( + /** Non-empty natural-language guidance for using the server, or null when the server omitted instructions or advertised an empty string. */ + @JsonProperty("instructions") String instructions +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java index c4567f30fa..ca16f83ece 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/McpServersLoadedServer.java @@ -29,6 +29,8 @@ public record McpServersLoadedServer( @JsonProperty("source") McpServerSource source, /** Error message if the server failed to connect */ @JsonProperty("error") String error, + /** Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. */ + @JsonProperty("serverMetadata") McpServerMetadata serverMetadata, /** Transport mechanism: stdio, http, sse (deprecated), or memory (in-process MCP server) */ @JsonProperty("transport") McpServerTransport transport, /** Name of the plugin that supplied the effective MCP server config, only when source is plugin */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java index b7aae9ec80..a854d5ffa4 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/PermissionRequestedEvent.java @@ -40,6 +40,8 @@ public record PermissionRequestedEventData( @JsonProperty("permissionRequest") Object permissionRequest, /** Derived user-facing permission prompt details for UI consumers */ @JsonProperty("promptRequest") Object promptRequest, + /** Agent mode captured from the owning turn when permission evaluation began. */ + @JsonProperty("agentMode") SessionMode agentMode, /** Neutral risk metadata supplied by the tool host. Consumers may display this value but must not use it to bypass the permission decision. */ @JsonProperty("riskAssessment") Object riskAssessment, /** When true, this permission was already resolved by a permissionRequest hook and requires no client action */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/RemediationAction.java b/java/sdk/src/generated/java/com/github/copilot/generated/RemediationAction.java new file mode 100644 index 0000000000..5fba9dfc33 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/RemediationAction.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum RemediationAction { + /** The {@code sign_in} variant. */ + SIGN_IN("sign_in"), + /** The {@code switch_account} variant. */ + SWITCH_ACCOUNT("switch_account"), + /** The {@code show_account} variant. */ + SHOW_ACCOUNT("show_account"), + /** The {@code review_sandbox_policy} variant. */ + REVIEW_SANDBOX_POLICY("review_sandbox_policy"), + /** The {@code allow_sandbox_outbound} variant. */ + ALLOW_SANDBOX_OUTBOUND("allow_sandbox_outbound"); + + private final String value; + RemediationAction(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static RemediationAction fromValue(String value) { + for (RemediationAction v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown RemediationAction value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoTierSwitchFailedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoTierSwitchFailedEvent.java new file mode 100644 index 0000000000..7509bb678f --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionAutoTierSwitchFailedEvent.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.auto_tier_switch_failed". A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionAutoTierSwitchFailedEvent extends SessionEvent { + + @Override + public String getType() { return "session.auto_tier_switch_failed"; } + + @JsonProperty("data") + private SessionAutoTierSwitchFailedEventData data; + + public SessionAutoTierSwitchFailedEventData getData() { return data; } + public void setData(SessionAutoTierSwitchFailedEventData data) { this.data = data; } + + /** Data payload for {@link SessionAutoTierSwitchFailedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionAutoTierSwitchFailedEventData( + /** Auto preference that remains effective after the failed request. */ + @JsonProperty("effectiveAutoTier") AutoTier effectiveAutoTier, + /** Auto preference that failed to activate, or null when returning to provider-default routing failed. */ + @JsonProperty("requestedAutoTier") AutoTier requestedAutoTier, + /** Low-cardinality failure outcome reported by Auto resolution. */ + @JsonProperty("reason") AutoTierSwitchFailureReason reason + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompletionReceiptEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompletionReceiptEvent.java new file mode 100644 index 0000000000..15670c992a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionCompletionReceiptEvent.java @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.completion_receipt". Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionCompletionReceiptEvent extends SessionEvent { + + @Override + public String getType() { return "session.completion_receipt"; } + + @JsonProperty("data") + private SessionCompletionReceiptEventData data; + + public SessionCompletionReceiptEventData getData() { return data; } + public void setData(SessionCompletionReceiptEventData data) { this.data = data; } + + /** Data payload for {@link SessionCompletionReceiptEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionCompletionReceiptEventData( + /** Version of the completion receipt payload. */ + @JsonProperty("schemaVersion") Long schemaVersion, + /** One-based accepted completion receipt ordinal in the durable session history. */ + @JsonProperty("attempt") Long attempt, + /** Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. */ + @JsonProperty("sourceEventId") String sourceEventId, + /** Inclusive durable event range summarized by this receipt. */ + @JsonProperty("eventRange") CompletionReceiptEventRange eventRange, + /** Runtime reason the completion decision was accepted. */ + @JsonProperty("stopReason") CompletionReceiptStopReason stopReason, + /** Final structured tool completion in the covered range, when one exists. */ + @JsonProperty("finalTool") CompletionReceiptFinalTool finalTool, + /** Number of successful structured tool completions in the covered range. */ + @JsonProperty("successfulToolCount") Long successfulToolCount, + /** Number of failed structured tool completions in the covered range. */ + @JsonProperty("failedToolCount") Long failedToolCount + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionErrorEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionErrorEvent.java index cd7f34365f..3ef32dfd13 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionErrorEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionErrorEvent.java @@ -42,6 +42,8 @@ public record SessionErrorEventData( @JsonProperty("eligibleForAutoSwitch") Boolean eligibleForAutoSwitch, /** Human-readable error message */ @JsonProperty("message") String message, + /** What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. */ + @JsonProperty("remediation") RemediationAction remediation, /** Error stack trace, when available */ @JsonProperty("stack") String stack, /** HTTP status code from the upstream request, if applicable */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java index e22561b914..367fa120b5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -38,7 +38,9 @@ @JsonSubTypes.Type(value = SessionInfoEvent.class, name = "session.info"), @JsonSubTypes.Type(value = SessionWarningEvent.class, name = "session.warning"), @JsonSubTypes.Type(value = SessionModelChangeEvent.class, name = "session.model_change"), + @JsonSubTypes.Type(value = SessionAutoTierSwitchFailedEvent.class, name = "session.auto_tier_switch_failed"), @JsonSubTypes.Type(value = SessionModeChangedEvent.class, name = "session.mode_changed"), + @JsonSubTypes.Type(value = SessionModeNoticeDeliveredEvent.class, name = "session.mode_notice_delivered"), @JsonSubTypes.Type(value = SessionSessionLimitsChangedEvent.class, name = "session.session_limits_changed"), @JsonSubTypes.Type(value = SessionPermissionsChangedEvent.class, name = "session.permissions_changed"), @JsonSubTypes.Type(value = SessionPlanChangedEvent.class, name = "session.plan_changed"), @@ -55,6 +57,7 @@ @JsonSubTypes.Type(value = SessionCompactionStartEvent.class, name = "session.compaction_start"), @JsonSubTypes.Type(value = SessionCompactionCompleteEvent.class, name = "session.compaction_complete"), @JsonSubTypes.Type(value = SessionTaskCompleteEvent.class, name = "session.task_complete"), + @JsonSubTypes.Type(value = SessionCompletionReceiptEvent.class, name = "session.completion_receipt"), @JsonSubTypes.Type(value = SessionFusionRouteStartedEvent.class, name = "session.fusion_route_started"), @JsonSubTypes.Type(value = SessionFusionRouteFailedEvent.class, name = "session.fusion_route_failed"), @JsonSubTypes.Type(value = SessionFusionResolvedEvent.class, name = "session.fusion_resolved"), @@ -66,6 +69,7 @@ @JsonSubTypes.Type(value = AgentInterruptedEvent.class, name = "agent.interrupted"), @JsonSubTypes.Type(value = AssistantIntentEvent.class, name = "assistant.intent"), @JsonSubTypes.Type(value = AssistantFusionPhaseStartedEvent.class, name = "assistant.fusion_phase_started"), + @JsonSubTypes.Type(value = AssistantFusionPhaseActivityEvent.class, name = "assistant.fusion_phase_activity"), @JsonSubTypes.Type(value = AssistantFusionPhaseCompletedEvent.class, name = "assistant.fusion_phase_completed"), @JsonSubTypes.Type(value = AssistantFusionPhaseFailedEvent.class, name = "assistant.fusion_phase_failed"), @JsonSubTypes.Type(value = AssistantServerToolProgressEvent.class, name = "assistant.server_tool_progress"), @@ -143,6 +147,8 @@ @JsonSubTypes.Type(value = SessionCustomAgentsUpdatedEvent.class, name = "session.custom_agents_updated"), @JsonSubTypes.Type(value = SessionMcpServersLoadedEvent.class, name = "session.mcp_servers_loaded"), @JsonSubTypes.Type(value = SessionMcpServerStatusChangedEvent.class, name = "session.mcp_server_status_changed"), + @JsonSubTypes.Type(value = SessionMcpServerRemovedEvent.class, name = "session.mcp_server_removed"), + @JsonSubTypes.Type(value = SessionMcpServerNeedsReconnectEvent.class, name = "session.mcp_server_needs_reconnect"), @JsonSubTypes.Type(value = McpToolsListChangedEvent.class, name = "mcp.tools.list_changed"), @JsonSubTypes.Type(value = McpResourcesListChangedEvent.class, name = "mcp.resources.list_changed"), @JsonSubTypes.Type(value = McpPromptsListChangedEvent.class, name = "mcp.prompts.list_changed"), @@ -171,7 +177,9 @@ public abstract sealed class SessionEvent permits SessionInfoEvent, SessionWarningEvent, SessionModelChangeEvent, + SessionAutoTierSwitchFailedEvent, SessionModeChangedEvent, + SessionModeNoticeDeliveredEvent, SessionSessionLimitsChangedEvent, SessionPermissionsChangedEvent, SessionPlanChangedEvent, @@ -188,6 +196,7 @@ public abstract sealed class SessionEvent permits SessionCompactionStartEvent, SessionCompactionCompleteEvent, SessionTaskCompleteEvent, + SessionCompletionReceiptEvent, SessionFusionRouteStartedEvent, SessionFusionRouteFailedEvent, SessionFusionResolvedEvent, @@ -199,6 +208,7 @@ public abstract sealed class SessionEvent permits AgentInterruptedEvent, AssistantIntentEvent, AssistantFusionPhaseStartedEvent, + AssistantFusionPhaseActivityEvent, AssistantFusionPhaseCompletedEvent, AssistantFusionPhaseFailedEvent, AssistantServerToolProgressEvent, @@ -276,6 +286,8 @@ public abstract sealed class SessionEvent permits SessionCustomAgentsUpdatedEvent, SessionMcpServersLoadedEvent, SessionMcpServerStatusChangedEvent, + SessionMcpServerRemovedEvent, + SessionMcpServerNeedsReconnectEvent, McpToolsListChangedEvent, McpResourcesListChangedEvent, McpPromptsListChangedEvent, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java index 7553c6eb43..d63bee33b3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionFusionResolvedEvent.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; import javax.annotation.processing.Generated; /** @@ -62,6 +63,8 @@ public record SessionFusionResolvedEventData( @JsonProperty("scores") FusionScores scores, /** Validated orchestration pattern selected for the turn. */ @JsonProperty("pattern") FusionPattern pattern, + /** Presentation-neutral phase plan for clients that render workflow progress. */ + @JsonProperty("phasePlan") List phasePlan, /** Concrete model selected for the primary solver phase. */ @JsonProperty("primaryModel") String primaryModel, /** Concrete model selected for the review or judge phase, when required. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java index ac3763248a..e5428d26db 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionManagedSettingsResolvedEvent.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. * @since 1.0.0 */ @JsonIgnoreProperties(ignoreUnknown = true) @@ -35,7 +35,7 @@ public final class SessionManagedSettingsResolvedEvent extends SessionEvent { @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public record SessionManagedSettingsResolvedEventData( - /** Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. */ + /** Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. */ @JsonProperty("source") ManagedSettingsResolvedSource source, /** Whether the server (account/org) managed-settings layer was present */ @JsonProperty("serverManaged") Boolean serverManaged, @@ -43,6 +43,8 @@ public record SessionManagedSettingsResolvedEventData( @JsonProperty("deviceManaged") Boolean deviceManaged, /** Whether a session-local permissions layer injected by the SDK host was present */ @JsonProperty("clientManaged") Boolean clientManaged, + /** Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. */ + @JsonProperty("policyHelperManaged") Boolean policyHelperManaged, /** Whether managed policy could not be determined (e.g. a failed server fetch) and the session fell back to the fail-closed restriction. When true, restrictions such as disabling bypass-permissions are enforced even though `settings` may be absent. */ @JsonProperty("failClosed") Boolean failClosed, /** Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerNeedsReconnectEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerNeedsReconnectEvent.java new file mode 100644 index 0000000000..9b03a66b94 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerNeedsReconnectEvent.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.mcp_server_needs_reconnect". Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpServerNeedsReconnectEvent extends SessionEvent { + + @Override + public String getType() { return "session.mcp_server_needs_reconnect"; } + + @JsonProperty("data") + private SessionMcpServerNeedsReconnectEventData data; + + public SessionMcpServerNeedsReconnectEventData getData() { return data; } + public void setData(SessionMcpServerNeedsReconnectEventData data) { this.data = data; } + + /** Data payload for {@link SessionMcpServerNeedsReconnectEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionMcpServerNeedsReconnectEventData( + /** Name of the MCP server that needs to reconnect */ + @JsonProperty("serverName") String serverName + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerRemovedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerRemovedEvent.java new file mode 100644 index 0000000000..68b5107118 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionMcpServerRemovedEvent.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.mcp_server_removed". Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpServerRemovedEvent extends SessionEvent { + + @Override + public String getType() { return "session.mcp_server_removed"; } + + @JsonProperty("data") + private SessionMcpServerRemovedEventData data; + + public SessionMcpServerRemovedEventData getData() { return data; } + public void setData(SessionMcpServerRemovedEventData data) { this.data = data; } + + /** Data payload for {@link SessionMcpServerRemovedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionMcpServerRemovedEventData( + /** Name of the MCP server that was removed from the graph */ + @JsonProperty("serverName") String serverName + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModeNoticeDeliveredEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModeNoticeDeliveredEvent.java new file mode 100644 index 0000000000..9e24109f9a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModeNoticeDeliveredEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "session.mode_notice_delivered". Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionModeNoticeDeliveredEvent extends SessionEvent { + + @Override + public String getType() { return "session.mode_notice_delivered"; } + + @JsonProperty("data") + private SessionModeNoticeDeliveredEventData data; + + public SessionModeNoticeDeliveredEventData getData() { return data; } + public void setData(SessionModeNoticeDeliveredEventData data) { this.data = data; } + + /** Data payload for {@link SessionModeNoticeDeliveredEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record SessionModeNoticeDeliveredEventData( + /** Mode established by the delivered transition notice */ + @JsonProperty("mode") SessionMode mode, + /** Model-visible transition notice persisted for a mid-turn delivery */ + @JsonProperty("content") String content + ) { + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java index 868d78fe0d..4ea1dbd46d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionModelChangeEvent.java @@ -55,7 +55,11 @@ public record SessionModelChangeEventData( /** Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. */ @JsonProperty("cause") String cause, /** Origin of the effective model change, when known. */ - @JsonProperty("source") ModelChangeSource source + @JsonProperty("source") ModelChangeSource source, + /** Previously committed Auto preference, when one was explicitly selected. */ + @JsonProperty("previousAutoTier") AutoTier previousAutoTier, + /** Committed Auto preference after the model configuration change, when applicable. */ + @JsonProperty("autoTier") AutoTier autoTier ) { } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SessionWarningEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SessionWarningEvent.java index a253f246ed..5a76d8fa9e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SessionWarningEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SessionWarningEvent.java @@ -39,7 +39,9 @@ public record SessionWarningEventData( /** Human-readable warning message for display in the timeline */ @JsonProperty("message") String message, /** Optional URL associated with this warning that the user can open in a browser */ - @JsonProperty("url") String url + @JsonProperty("url") String url, + /** What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. */ + @JsonProperty("remediation") RemediationAction remediation ) { } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java index 6ad04f9699..e8675a6bbc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SkillInvokedEvent.java @@ -39,13 +39,15 @@ public record SkillInvokedEventData( @JsonProperty("name") String name, /** Model identifier active when the skill was invoked, when known */ @JsonProperty("model") String model, - /** File path to the SKILL.md definition */ + /** File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity */ @JsonProperty("path") String path, /** Full content of the skill file, injected into the conversation for the model */ @JsonProperty("content") String content, /** Tool names that should be auto-approved when this skill is active */ @JsonProperty("allowedTools") List allowedTools, - /** Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) */ + /** Whether model invocation is disabled for this skill */ + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, + /** Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) */ @JsonProperty("source") String source, /** Name of the plugin this skill originated from, when applicable */ @JsonProperty("pluginName") String pluginName, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java index b681faaae8..622dc5d88a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SkillSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Source location type (e.g., project, personal-copilot, plugin, builtin) + * Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) * * @since 1.0.0 */ @@ -29,7 +29,9 @@ public enum SkillSource { /** The {@code custom} variant. */ CUSTOM("custom"), /** The {@code builtin} variant. */ - BUILTIN("builtin"); + BUILTIN("builtin"), + /** The {@code sdk} variant. */ + SDK("sdk"); private final String value; SkillSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java b/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java index 932d9affe5..2335a9d9ea 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SkillsLoadedSkill.java @@ -27,7 +27,7 @@ public record SkillsLoadedSkill( @JsonProperty("commandName") String commandName, /** Description of what the skill does */ @JsonProperty("description") String description, - /** Source location type (e.g., project, personal-copilot, plugin, builtin) */ + /** Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk) */ @JsonProperty("source") SkillSource source, /** Whether the skill can be invoked by the user as a slash command */ @JsonProperty("userInvocable") Boolean userInvocable, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java index 62f6803652..26459bb955 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentCompletedEvent.java @@ -50,6 +50,8 @@ public record SubagentCompletedEventData( @JsonProperty("explicitModelOverride") String explicitModelOverride, /** Whether the explicit task-call model matched the user's configured preference */ @JsonProperty("explicitModelMatchesPreference") Boolean explicitModelMatchesPreference, + /** Why an explicit task-call model did not become the effective model */ + @JsonProperty("modelOverrideReason") String modelOverrideReason, /** Whether the first model actually dispatched matched the user's configured preference */ @JsonProperty("configuredModelMatchesActual") Boolean configuredModelMatchesActual, /** Total number of tool calls made by the sub-agent */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java index 1d1413c64d..54464f8ddf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/SubagentFailedEvent.java @@ -52,6 +52,8 @@ public record SubagentFailedEventData( @JsonProperty("explicitModelOverride") String explicitModelOverride, /** Whether the explicit task-call model matched the user's configured preference */ @JsonProperty("explicitModelMatchesPreference") Boolean explicitModelMatchesPreference, + /** Why an explicit task-call model did not become the effective model */ + @JsonProperty("modelOverrideReason") String modelOverrideReason, /** Whether the first model actually dispatched matched the user's configured preference */ @JsonProperty("configuredModelMatchesActual") Boolean configuredModelMatchesActual, /** Total number of tool calls made before the sub-agent failed */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteError.java b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteError.java index ac4c7d843d..8e183b78d1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteError.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/ToolExecutionCompleteError.java @@ -24,6 +24,8 @@ public record ToolExecutionCompleteError( /** Human-readable error message */ @JsonProperty("message") String message, /** Machine-readable error code */ - @JsonProperty("code") String code + @JsonProperty("code") String code, + /** What the user must do to recover, when the runtime knows of an action. Set on sandbox policy denials, where `message` names the rule that blocked the call but never the client affordance that relaxes it. */ + @JsonProperty("remediation") RemediationAction remediation ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java b/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java index 7d839087da..43a886ea01 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/UserMessageEvent.java @@ -37,6 +37,8 @@ public final class UserMessageEvent extends SessionEvent { public record UserMessageEventData( /** The user's message text as displayed in the timeline */ @JsonProperty("content") String content, + /** Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots */ + @JsonProperty("messageId") String messageId, /** Transformed version of the message sent to the model, with XML wrapping, timestamps, and other augmentations for prompt caching */ @JsonProperty("transformedContent") String transformedContent, /** Files, selections, or GitHub references attached to the message */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java index f239c82e61..3d9f9c2d7e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentInfo.java @@ -15,7 +15,7 @@ import javax.annotation.processing.Generated; /** - * Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. + * Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. * * @since 1.0.0 */ @@ -41,6 +41,10 @@ public record AgentInfo( @JsonProperty("tools") List tools, /** Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. */ @JsonProperty("model") String model, + /** Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. */ + @JsonProperty("models") List models, + /** Whether authored models are preferences or required constraints. */ + @JsonProperty("modelPolicy") AgentModelPolicy modelPolicy, /** MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. */ @JsonProperty("mcpServers") Map mcpServers, /** Skill names preloaded into this agent's context. Omitted means none. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentModelPolicy.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentModelPolicy.java new file mode 100644 index 0000000000..99158516f0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AgentModelPolicy.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Whether configured models are advisory preferences or required constraints + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AgentModelPolicy { + /** The {@code preferred} variant. */ + PREFERRED("preferred"), + /** The {@code required} variant. */ + REQUIRED("required"); + + private final String value; + AgentModelPolicy(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AgentModelPolicy fromValue(String value) { + for (AgentModelPolicy v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AgentModelPolicy value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java new file mode 100644 index 0000000000..d3f6a585b8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveCreditLimit.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Current per-window credit limit and consumption for an autopilot objective. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AutopilotObjectiveCreditLimit( + /** Configured AI-credit cap, when one is set. */ + @JsonProperty("credits") Double credits, + /** Window consumption in fractional AI credits, for display. */ + @JsonProperty("creditsUsed") Double creditsUsed, + /** Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. */ + @JsonProperty("creditsUsedNanoAiu") String creditsUsedNanoAiu +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java new file mode 100644 index 0000000000..657884c4f6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveState.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Public, persistence-independent projection of an autopilot objective. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record AutopilotObjectiveState( + /** Session-local objective identifier. */ + @JsonProperty("id") Long id, + /** User-provided objective text. */ + @JsonProperty("objective") String objective, + /** Current normalized lifecycle status. */ + @JsonProperty("status") AutopilotObjectiveStatus status, + /** Number of objective turns started. */ + @JsonProperty("turnCount") Long turnCount, + /** Optional reason the objective is paused. */ + @JsonProperty("pauseReason") String pauseReason, + /** Optional summary recorded when the objective completed. */ + @JsonProperty("completionSummary") String completionSummary, + /** Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. */ + @JsonProperty("creditCountNanoAiu") String creditCountNanoAiu, + /** Current per-window consumption and optional cap, when a credit-tracking window is present. */ + @JsonProperty("creditLimit") AutopilotObjectiveCreditLimit creditLimit +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java new file mode 100644 index 0000000000..b50cf2072c --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/AutopilotObjectiveStatus.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Current normalized autopilot objective lifecycle status. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum AutopilotObjectiveStatus { + /** The {@code active} variant. */ + ACTIVE("active"), + /** The {@code paused} variant. */ + PAUSED("paused"), + /** The {@code completed} variant. */ + COMPLETED("completed"); + + private final String value; + AutopilotObjectiveStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static AutopilotObjectiveStatus fromValue(String value) { + for (AutopilotObjectiveStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown AutopilotObjectiveStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java index e77117b2cb..4fd7a91ca2 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CapiSessionOptions.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record CapiSessionOptions( - /** Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. */ + /** Routing preference for sessions whose model is `auto`. On create or cold resume, this establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold resume, the runtime restores the last committed preference. On resident resume, a different value requests a safe switch after resume succeeds and cannot change an in-flight turn. Successful switches are persisted for later cold resume. When no preference is supplied or restored, CAPI default routing is used. */ @JsonProperty("autoTier") AutoTier autoTier, /** Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. */ @JsonProperty("enableWebSocketResponses") Boolean enableWebSocketResponses diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java new file mode 100644 index 0000000000..59e70f4935 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidate.java @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * An inert AI skill catalog result. AI skills are discovery-only and cannot be represented as installable through this surface. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CatalogAiSkillCandidate extends CatalogCandidate { + + @JsonProperty("kind") + private final String kind = "ai-skill"; + + @Override + public String getKind() { return kind; } + + /** Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. */ + @JsonProperty("handle") + private String handle; + + /** ISO 8601 timestamp after which the handle is stale and will be rejected. */ + @JsonProperty("handleExpiresAt") + private String handleExpiresAt; + + /** Media type of the underlying AI skill card */ + @JsonProperty("mediaType") + private String mediaType; + + /** AI skills are discovery-only and cannot be installed through this surface */ + @JsonProperty("installability") + private String installability; + + /** Display name taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("displayName") + private String displayName; + + /** Description taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("description") + private String description; + + /** Publisher taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("publisher") + private String publisher; + + /** Where the card came from: exactly one of a URL or embedded data, encoded as a tagged union so neither both nor neither can be represented. */ + @JsonProperty("source") + private CatalogCandidateSource source; + + /** Where the catalog reference was observed, without the card itself or any content digest. */ + @JsonProperty("provenance") + private CatalogAiSkillCandidateProvenance provenance; + + public String getHandle() { return handle; } + public void setHandle(String handle) { this.handle = handle; } + + public String getHandleExpiresAt() { return handleExpiresAt; } + public void setHandleExpiresAt(String handleExpiresAt) { this.handleExpiresAt = handleExpiresAt; } + + public String getMediaType() { return mediaType; } + public void setMediaType(String mediaType) { this.mediaType = mediaType; } + + public String getInstallability() { return installability; } + public void setInstallability(String installability) { this.installability = installability; } + + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public String getPublisher() { return publisher; } + public void setPublisher(String publisher) { this.publisher = publisher; } + + public CatalogCandidateSource getSource() { return source; } + public void setSource(CatalogCandidateSource source) { this.source = source; } + + public CatalogAiSkillCandidateProvenance getProvenance() { return provenance; } + public void setProvenance(CatalogAiSkillCandidateProvenance provenance) { this.provenance = provenance; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.java new file mode 100644 index 0000000000..0a2eff24e8 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogAiSkillCandidateProvenance.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Where and when an AI skill catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CatalogAiSkillCandidateProvenance( + /** Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. */ + @JsonProperty("authority") String authority, + /** ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. */ + @JsonProperty("observedAt") String observedAt, + /** Media type advertised for the referenced AI skill card */ + @JsonProperty("mediaType") String mediaType +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.java new file mode 100644 index 0000000000..a712236ddb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidate.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * One inert catalog result, represented as an MCP server or discovery-only AI skill variant so kind, media type, provenance, and installability cannot contradict each other. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = CatalogMcpServerCandidate.class, name = "mcp-server"), + @JsonSubTypes.Type(value = CatalogAiSkillCandidate.class, name = "ai-skill") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class CatalogCandidate { + + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + public abstract String getKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSource.java new file mode 100644 index 0000000000..798768739d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSource.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import javax.annotation.processing.Generated; + +/** + * Where a candidate's card came from. Exactly one of a URL or embedded data: the union has no variant carrying both, and no variant carrying neither, so the rule holds structurally rather than by validation. + * + * @since 1.0.0 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "kind", visible = true) +@JsonSubTypes({ + @JsonSubTypes.Type(value = CatalogCandidateSourceUrl.class, name = "url"), + @JsonSubTypes.Type(value = CatalogCandidateSourceEmbedded.class, name = "embedded") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public abstract class CatalogCandidateSource { + + /** + * Returns the discriminator value for this variant. + * + * @return the kind discriminator + */ + public abstract String getKind(); +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceEmbedded.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceEmbedded.java new file mode 100644 index 0000000000..6814a0c4f2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceEmbedded.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Candidate whose card reference arrived inline. The document and its content-derived properties stay behind the runtime boundary. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CatalogCandidateSourceEmbedded extends CatalogCandidateSource { + + @JsonProperty("kind") + private final String kind = "embedded"; + + @Override + public String getKind() { return kind; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.java new file mode 100644 index 0000000000..1c9b6d8905 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogCandidateSourceUrl.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Candidate whose card is retrieved from a URL through the runtime's hardened fetch boundary. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CatalogCandidateSourceUrl extends CatalogCandidateSource { + + @JsonProperty("kind") + private final String kind = "url"; + + @Override + public String getKind() { return kind; } + + /** Card URL as advertised. Inert untrusted data: the runtime retrieves it only through its own hardened boundary, and it is never logged. */ + @JsonProperty("url") + private String url; + + public String getUrl() { return url; } + public void setUrl(String url) { this.url = url; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidate.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidate.java new file mode 100644 index 0000000000..8183ca422a --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidate.java @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * An inert MCP server catalog result. Every free-text field is untrusted external data and must never be treated as an instruction, and the handle is the only way to refer to the candidate in a later operation. + * + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class CatalogMcpServerCandidate extends CatalogCandidate { + + @JsonProperty("kind") + private final String kind = "mcp-server"; + + @Override + public String getKind() { return kind; } + + /** Opaque, runtime-instance scoped, TTL-bound, single-use handle for this candidate. Carries no readable information and is rejected when stale, replayed, or presented to a different runtime instance. Never logged. */ + @JsonProperty("handle") + private String handle; + + /** ISO 8601 timestamp after which the handle is stale and will be rejected. */ + @JsonProperty("handleExpiresAt") + private String handleExpiresAt; + + /** JSON MCP media type of the underlying card. */ + @JsonProperty("mediaType") + private McpServerCardMediaType mediaType; + + /** Whether this MCP server can be planned for installation, and if policy prevents it. */ + @JsonProperty("installability") + private CatalogMcpServerInstallability installability; + + /** Display name taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("displayName") + private String displayName; + + /** Description taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("description") + private String description; + + /** Publisher taken verbatim from the card. Inert untrusted text. */ + @JsonProperty("publisher") + private String publisher; + + /** Where the card came from: exactly one of a URL or embedded data, encoded as a tagged union so neither both nor neither can be represented. */ + @JsonProperty("source") + private CatalogCandidateSource source; + + /** Where the catalog reference was observed, without the card itself or any content digest. */ + @JsonProperty("provenance") + private CatalogMcpServerCandidateProvenance provenance; + + public String getHandle() { return handle; } + public void setHandle(String handle) { this.handle = handle; } + + public String getHandleExpiresAt() { return handleExpiresAt; } + public void setHandleExpiresAt(String handleExpiresAt) { this.handleExpiresAt = handleExpiresAt; } + + public McpServerCardMediaType getMediaType() { return mediaType; } + public void setMediaType(McpServerCardMediaType mediaType) { this.mediaType = mediaType; } + + public CatalogMcpServerInstallability getInstallability() { return installability; } + public void setInstallability(CatalogMcpServerInstallability installability) { this.installability = installability; } + + public String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public String getPublisher() { return publisher; } + public void setPublisher(String publisher) { this.publisher = publisher; } + + public CatalogCandidateSource getSource() { return source; } + public void setSource(CatalogCandidateSource source) { this.source = source; } + + public CatalogMcpServerCandidateProvenance getProvenance() { return provenance; } + public void setProvenance(CatalogMcpServerCandidateProvenance provenance) { this.provenance = provenance; } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.java new file mode 100644 index 0000000000..3ef13704ff --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerCandidateProvenance.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Where and when an MCP server catalog reference was observed. Discovery provenance deliberately carries no content digest because search does not establish the exact validated content a later plan will bind. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CatalogMcpServerCandidateProvenance( + /** Host of the catalog authority that advertised the reference, without path, query, or credentials. Inert untrusted data. */ + @JsonProperty("authority") String authority, + /** ISO 8601 timestamp at which the runtime observed the catalog reference. This is not a retrieval or validation timestamp. */ + @JsonProperty("observedAt") String observedAt, + /** JSON MCP media type advertised for the referenced card. */ + @JsonProperty("mediaType") McpServerCardMediaType mediaType +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.java new file mode 100644 index 0000000000..4478a81bd3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogMcpServerInstallability.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Whether an MCP server candidate can be planned for installation + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum CatalogMcpServerInstallability { + /** The {@code installable} variant. */ + INSTALLABLE("installable"), + /** The {@code not-installable-policy} variant. */ + NOT_INSTALLABLE_POLICY("not-installable-policy"); + + private final String value; + CatalogMcpServerInstallability(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static CatalogMcpServerInstallability fromValue(String value) { + for (CatalogMcpServerInstallability v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown CatalogMcpServerInstallability value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java index d4afb32c3d..01d92e908a 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureError.java @@ -36,6 +36,10 @@ public final class CatalogNetworkFailureError extends CatalogSearchResult { @JsonProperty("statusCode") private Long statusCode; + /** Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. */ + @JsonProperty("retryAfterSeconds") + private Long retryAfterSeconds; + /** Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ @JsonProperty("message") private String message; @@ -46,6 +50,9 @@ public final class CatalogNetworkFailureError extends CatalogSearchResult { public Long getStatusCode() { return statusCode; } public void setStatusCode(Long statusCode) { this.statusCode = statusCode; } + public Long getRetryAfterSeconds() { return retryAfterSeconds; } + public void setRetryAfterSeconds(Long retryAfterSeconds) { this.retryAfterSeconds = retryAfterSeconds; } + public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java index c28d1cc4a7..ac821b062d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogNetworkFailureReason.java @@ -26,6 +26,12 @@ public enum CatalogNetworkFailureReason { TLS("tls"), /** The {@code connection-refused} variant. */ CONNECTION_REFUSED("connection-refused"), + /** The {@code proxy-authentication-required} variant. */ + PROXY_AUTHENTICATION_REQUIRED("proxy-authentication-required"), + /** The {@code rate-limited} variant. */ + RATE_LIMITED("rate-limited"), + /** The {@code service-unavailable} variant. */ + SERVICE_UNAVAILABLE("service-unavailable"), /** The {@code http-status} variant. */ HTTP_STATUS("http-status"), /** The {@code response-too-large} variant. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java index ee5e703621..68b90a19c9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchParams.java @@ -27,7 +27,7 @@ public record CatalogSearchParams( /** Protocol version and capabilities the caller requires. */ @JsonProperty("contract") CatalogClientContract contract, - /** Free-text search query. Never written to logs or telemetry. */ + /** Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. */ @JsonProperty("query") String query, /** Maximum number of candidates to return. Defaults to 10 when omitted. */ @JsonProperty("limit") Long limit, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java index 8ecd11788a..b49f78faff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CatalogSearchSucceeded.java @@ -35,7 +35,7 @@ public final class CatalogSearchSucceeded extends CatalogSearchResult { /** Matching candidates, never more than the requested limit. All text is inert untrusted data. */ @JsonProperty("candidates") - private List candidates; + private List candidates; /** Whether further matches existed beyond the requested limit. */ @JsonProperty("truncated") @@ -48,8 +48,8 @@ public final class CatalogSearchSucceeded extends CatalogSearchResult { public String getSearchId() { return searchId; } public void setSearchId(String searchId) { this.searchId = searchId; } - public List getCandidates() { return candidates; } - public void setCandidates(List candidates) { this.candidates = candidates; } + public List getCandidates() { return candidates; } + public void setCandidates(List candidates) { this.candidates = candidates; } public Boolean getTruncated() { return truncated; } public void setTruncated(Boolean truncated) { this.truncated = truncated; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ClientTaskCancelReason.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ClientTaskCancelReason.java new file mode 100644 index 0000000000..723e7e80b0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ClientTaskCancelReason.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Why the runtime requests client-task cancellation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ClientTaskCancelReason { + /** The {@code cancel_requested} variant. */ + CANCEL_REQUESTED("cancel_requested"), + /** The {@code session_shutdown} variant. */ + SESSION_SHUTDOWN("session_shutdown"); + + private final String value; + ClientTaskCancelReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ClientTaskCancelReason fromValue(String value) { + for (ClientTaskCancelReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ClientTaskCancelReason value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java index 05f2534970..0975b7bd87 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectParams.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import java.util.List; import javax.annotation.processing.Generated; /** @@ -28,6 +29,8 @@ public record ConnectParams( @JsonProperty("enableGitHubTelemetryForwarding") Boolean enableGitHubTelemetryForwarding, /** Identity of the integrating host. Optional; omit it to keep the default attribution. */ @JsonProperty("clientInfo") ConnectClientInfo clientInfo, + /** Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. */ + @JsonProperty("supportedTaskKinds") List supportedTaskKinds, /** Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN */ @JsonProperty("token") String token ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java index 8c12b57a80..41a200ff4b 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ConnectResult.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.copilot.CopilotExperimental; +import java.util.List; import javax.annotation.processing.Generated; /** @@ -29,6 +30,8 @@ public record ConnectResult( /** Server protocol version number */ @JsonProperty("protocolVersion") Long protocolVersion, /** Server package version */ - @JsonProperty("version") String version + @JsonProperty("version") String version, + /** Task kinds the server may return to this connection. */ + @JsonProperty("taskKinds") List taskKinds ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java new file mode 100644 index 0000000000..bf5f9a8b97 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/CurrentModel.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record CurrentModel( + /** Currently active model identifier */ + @JsonProperty("modelId") String modelId, + /** Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. */ + @JsonProperty("reasoningEffort") String reasoningEffort, + /** Context tier for models that support multiple context-window sizes. */ + @JsonProperty("contextTier") ContextTier contextTier, + /** Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. */ + @JsonProperty("autoTier") AutoTier autoTier, + /** Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. */ + @JsonProperty("pendingAutoTier") AutoTier pendingAutoTier, + /** Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. */ + @JsonProperty("activatingAutoTier") AutoTier activatingAutoTier +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredHook.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredHook.java new file mode 100644 index 0000000000..fce707243d --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/DiscoveredHook.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * One server-discovered hook action from user, repository, plugin, or managed-policy configuration. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record DiscoveredHook( + /** Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. */ + @JsonProperty("id") String id, + /** Hook event that invokes this action. */ + @JsonProperty("hookType") HookType hookType, + /** Configuration tier that contributed this hook action. */ + @JsonProperty("origin") HookOrigin origin, + /** Human-readable source label, such as a hook file path, settings source, or plugin name. */ + @JsonProperty("source") String source, + /** Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. */ + @JsonProperty("projectPath") String projectPath, + /** Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. */ + @JsonProperty("enabled") Boolean enabled, + /** Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. */ + @JsonProperty("disableKey") String disableKey +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java index bb28f40887..71ee3c49e6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/FactoryRunResult.java @@ -23,13 +23,15 @@ public record FactoryRunResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookOrigin.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookOrigin.java new file mode 100644 index 0000000000..bf3bfd14cd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookOrigin.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Configuration tier that contributed a discovered hook action. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum HookOrigin { + /** The {@code user} variant. */ + USER("user"), + /** The {@code repository} variant. */ + REPOSITORY("repository"), + /** The {@code plugin} variant. */ + PLUGIN("plugin"), + /** The {@code policy} variant. */ + POLICY("policy"); + + private final String value; + HookOrigin(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static HookOrigin fromValue(String value) { + for (HookOrigin v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown HookOrigin value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java index 8d7cd913c3..1a185958de 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HookType.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Hook event name dispatched through the SDK callback transport. + * Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. * * @since 1.0.0 */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverParams.java new file mode 100644 index 0000000000..a0e7610394 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverParams.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HooksDiscoverParams( + /** Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. */ + @JsonProperty("projectPaths") List projectPaths, + /** When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. */ + @JsonProperty("excludeHostHooks") Boolean excludeHostHooks +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverResult.java new file mode 100644 index 0000000000..f30d4837ae --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/HooksDiscoverResult.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record HooksDiscoverResult( + /** All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. */ + @JsonProperty("hooks") List hooks, + /** Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. */ + @JsonProperty("warnings") List warnings, + /** Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. */ + @JsonProperty("errors") List errors +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigRemoveParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigRemoveParams.java index 81a0aa0e21..74258dcfc5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigRemoveParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpConfigRemoveParams.java @@ -25,6 +25,8 @@ @JsonIgnoreProperties(ignoreUnknown = true) public record McpConfigRemoveParams( /** Name of the MCP server to remove */ - @JsonProperty("name") String name + @JsonProperty("name") String name, + /** OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. */ + @JsonProperty("authClientIdMetadataUrl") String authClientIdMetadataUrl ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java index 14a9118d0b..063385f6ff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServer.java @@ -32,6 +32,8 @@ public record McpServer( /** Plugin version that provided this server, when source is plugin. */ @JsonProperty("sourcePluginVersion") String sourcePluginVersion, /** Error message if the server failed to connect */ - @JsonProperty("error") String error + @JsonProperty("error") String error, + /** Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. */ + @JsonProperty("serverMetadata") McpServerMetadata serverMetadata ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerMetadata.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerMetadata.java new file mode 100644 index 0000000000..15ba7395d6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/McpServerMetadata.java @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Server-advertised metadata learned through modern discovery or legacy initialization. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record McpServerMetadata( + /** Non-empty natural-language guidance for using the server, or null when the server omitted instructions or advertised an empty string. */ + @JsonProperty("instructions") String instructions +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java index a652e8f4f6..3e9f0b6b87 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/Model.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; +import java.util.Map; import javax.annotation.processing.Generated; /** @@ -28,6 +29,8 @@ public record Model( @JsonProperty("name") String name, /** Model capabilities and limits */ @JsonProperty("capabilities") ModelCapabilities capabilities, + /** Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. */ + @JsonProperty("metadata") Map metadata, /** Policy state (if applicable) */ @JsonProperty("policy") ModelPolicy policy, /** Billing information */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java index 087ca1c15a..d44087dcff 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelBillingPromo.java @@ -28,6 +28,8 @@ public record ModelBillingPromo( /** UTC ISO 8601 timestamp marking when the promotion ends. Optional: an open-ended promotion omits this field. When present, the API only surfaces a promo whose expiry parses and is in the future, so consumers should treat a past value as expired. */ @JsonProperty("endsAt") String endsAt, /** Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. */ - @JsonProperty("message") String message + @JsonProperty("message") String message, + /** Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. */ + @JsonProperty("showBanner") Boolean showBanner ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java new file mode 100644 index 0000000000..e4a1baed5e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ModelSwitchAutoTierStatus.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Whether the requested preference was already effective or was accepted for later transactional activation. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum ModelSwitchAutoTierStatus { + /** The {@code unchanged} variant. */ + UNCHANGED("unchanged"), + /** The {@code pending} variant. */ + PENDING("pending"); + + private final String value; + ModelSwitchAutoTierStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static ModelSwitchAutoTierStatus fromValue(String value) { + for (ModelSwitchAutoTierStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown ModelSwitchAutoTierStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginInstallStagingMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginInstallStagingMode.java new file mode 100644 index 0000000000..e754f07bc9 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginInstallStagingMode.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Where completed plugin content was staged before atomic promotion. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum PluginInstallStagingMode { + /** The {@code external} variant. */ + EXTERNAL("external"), + /** The {@code destination_sibling} variant. */ + DESTINATION_SIBLING("destination_sibling"); + + private final String value; + PluginInstallStagingMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static PluginInstallStagingMode fromValue(String value) { + for (PluginInstallStagingMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown PluginInstallStagingMode value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginsInstallResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginsInstallResult.java index 82152cfe4c..a1e04692c6 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginsInstallResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/PluginsInstallResult.java @@ -28,6 +28,8 @@ public record PluginsInstallResult( @JsonProperty("plugin") InstalledPluginInfo plugin, /** Number of skills discovered and installed from the plugin */ @JsonProperty("skillsInstalled") Long skillsInstalled, + /** Where the completed plugin tree was staged before atomic promotion */ + @JsonProperty("stagingMode") PluginInstallStagingMode stagingMode, /** Optional post-install message provided by the plugin (e.g. setup instructions) */ @JsonProperty("postInstallMessage") String postInstallMessage, /** Set when the install path is deprecated (e.g. direct repo / URL / local installs). Callers should surface this to end users. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java index f3b2f99188..f815f565f5 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/QueuePendingItems.java @@ -23,6 +23,8 @@ public record QueuePendingItems( /** Stable opaque id for the canonical queued item. Batch rows share one id. */ @JsonProperty("id") String id, + /** Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. */ + @JsonProperty("messageId") String messageId, /** Whether this item is a queued user message or a queued slash command / model change */ @JsonProperty("kind") QueuePendingItemsKind kind, /** Human-readable text to display for this queue entry in the UI */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RemediationAction.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RemediationAction.java new file mode 100644 index 0000000000..50f6256352 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/RemediationAction.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum RemediationAction { + /** The {@code sign_in} variant. */ + SIGN_IN("sign_in"), + /** The {@code switch_account} variant. */ + SWITCH_ACCOUNT("switch_account"), + /** The {@code show_account} variant. */ + SHOW_ACCOUNT("show_account"), + /** The {@code review_sandbox_policy} variant. */ + REVIEW_SANDBOX_POLICY("review_sandbox_policy"), + /** The {@code allow_sandbox_outbound} variant. */ + ALLOW_SANDBOX_OUTBOUND("allow_sandbox_outbound"); + + private final String value; + RemediationAction(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static RemediationAction fromValue(String value) { + for (RemediationAction v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown RemediationAction value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java index beda6b20a2..dd522f7a34 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfig.java @@ -27,6 +27,16 @@ public record SandboxConfig( @JsonProperty("userPolicy") SandboxConfigUserPolicy userPolicy, /** Whether to auto-add the current working directory to readwritePaths. Default: true. */ @JsonProperty("addCurrentWorkingDirectory") Boolean addCurrentWorkingDirectory, + /** Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). */ + @JsonProperty("sandboxMcpServers") Boolean sandboxMcpServers, + /** Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). */ + @JsonProperty("sandboxLspServers") Boolean sandboxLspServers, + /** Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). */ + @JsonProperty("allowBypass") Boolean allowBypass, + /** Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. */ + @JsonProperty("managedMcpRoutingLocked") Boolean managedMcpRoutingLocked, + /** The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. */ + @JsonProperty("managedLspRoutingLocked") Boolean managedLspRoutingLocked, /** Credential-injection capability flags. */ @JsonProperty("auth") SandboxConfigAuth auth, /** Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java index 1e56acb53f..9c5e3e475f 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetwork.java @@ -25,7 +25,7 @@ public record SandboxConfigUserPolicyNetwork( @JsonProperty("allowOutbound") Boolean allowOutbound, /** Whether traffic to local/loopback addresses is allowed. */ @JsonProperty("allowLocalNetwork") Boolean allowLocalNetwork, - /** HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. */ + /** HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is accepted and routed through the IPv4 gateway), and does not support proxy credentials. macOS relies on applications honoring proxy environment variables. Windows also configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's networking stack. Configure supported credentials in the separate `username` and `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, while an https:// or authenticated loopback URL uses the URL form. */ @JsonProperty("proxy") SandboxConfigUserPolicyNetworkProxy proxy ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java index 74ff86919e..2946269ec1 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SandboxConfigUserPolicyNetworkProxy.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record SandboxConfigUserPolicyNetworkProxy( - /** Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ + /** Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ @JsonProperty("url") String url, /** Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. */ @JsonProperty("username") String username, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java new file mode 100644 index 0000000000..b6f69bf7dc --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerHooksApi.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code hooks} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class ServerHooksApi { + + private final RpcCaller caller; + + /** @param caller the RPC transport function */ + ServerHooksApi(RpcCaller caller) { + this.caller = caller; + } + + /** + * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture discover(HooksDiscoverParams params) { + return caller.invoke("hooks.discover", params, HooksDiscoverResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java index e85b7b987a..e6013c870d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerManagedSettingsApi.java @@ -37,4 +37,15 @@ public CompletableFuture read() { return caller.invoke("managedSettings.read", java.util.Map.of(), ManagedSettingsReadResult.class); } + /** + * Invokes {@code managedSettings.clearCache}. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture clearCache() { + return caller.invoke("managedSettings.clearCache", java.util.Map.of(), Void.class); + } + } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java index 111cee2560..caa7f92d07 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java @@ -25,6 +25,8 @@ public final class ServerRpc { private final RpcCaller caller; + /** API methods for the {@code hooks} namespace. */ + public final ServerHooksApi hooks; /** API methods for the {@code models} namespace. */ public final ServerModelsApi models; /** API methods for the {@code tools} namespace. */ @@ -71,6 +73,7 @@ public final class ServerRpc { */ public ServerRpc(RpcCaller caller) { this.caller = caller; + this.hooks = new ServerHooksApi(caller); this.models = new ServerModelsApi(caller); this.tools = new ServerToolsApi(caller); this.account = new ServerAccountApi(caller); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java index 52481a7d73..870f7ac3cf 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/ServerSessionsApi.java @@ -94,6 +94,17 @@ public CompletableFuture getMetadata(SessionsGetMetad return caller.invoke("sessions.getMetadata", params, SessionsGetMetadataResult.class); } + /** + * Pagination options for reading an inactive or active local session's persisted event journal. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture readPersistedEvents(SessionsReadPersistedEventsParams params) { + return caller.invoke("sessions.readPersistedEvents", params, SessionsReadPersistedEventsResult.class); + } + /** * Limit for non-empty local session IDs. * diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java new file mode 100644 index 0000000000..ab4058e6b6 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveApi.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code autopilotObjective} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionAutopilotObjectiveApi { + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionAutopilotObjectiveApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture getState() { + return caller.invoke("session.autopilotObjective.getState", java.util.Map.of("sessionId", this.sessionId), SessionAutopilotObjectiveGetStateResult.class); + } + +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateParams.java new file mode 100644 index 0000000000..94ca56d3b5 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAutopilotObjectiveGetStateParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateResult.java new file mode 100644 index 0000000000..d4d2185365 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionAutopilotObjectiveGetStateResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Canonical runtime state for the session's current autopilot objective. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionAutopilotObjectiveGetStateResult( + /** Current objective state, or `null` when the session has no objective. */ + @JsonProperty("state") AutopilotObjectiveState state +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java index 0cb66280c0..c9f9de2dcc 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryCancelResult.java @@ -26,13 +26,15 @@ public record SessionFactoryCancelResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java index 6742faf03e..2d6a5f52a9 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryGetRunResult.java @@ -26,13 +26,15 @@ public record SessionFactoryGetRunResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java index 71b12f94f1..1a9dee5926 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunFromToolResult.java @@ -26,13 +26,15 @@ public record SessionFactoryRunFromToolResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java index 46083f2284..d8ce481895 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionFactoryRunResult.java @@ -26,13 +26,15 @@ public record SessionFactoryRunResult( /** Factory run identifier. */ @JsonProperty("runId") String runId, + /** One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. */ + @JsonProperty("attempt") Long attempt, /** Current or terminal factory run status. */ @JsonProperty("status") FactoryRunStatus status, /** Completed factory result. */ @JsonProperty("result") Object result, /** Error message for an errored run. */ @JsonProperty("error") String error, - /** Machine-readable failure details for an errored run. */ + /** Machine-readable failure details for a halted or errored run. */ @JsonProperty("failure") Object failure, /** Reason for a halted or cancelled run. */ @JsonProperty("reason") String reason, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java index 12777c9a1f..b9adc34484 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApi.java @@ -57,6 +57,22 @@ public CompletableFuture switchTo(SessionModelSwitch return caller.invoke("session.model.switchTo", _p, SessionModelSwitchToResult.class); } + /** + * An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture switchAutoTier(SessionModelSwitchAutoTierParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.model.switchAutoTier", _p, SessionModelSwitchAutoTierResult.class); + } + /** * Managed, repository, and CLI model overrides to overlay onto the session at startup. *

diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java index a1d25ae7ec..dfb593fd21 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayParams.java @@ -30,6 +30,8 @@ public record SessionModelApplyStartupOverlayParams( @JsonProperty("deviceManagedModel") String deviceManagedModel, /** Model required by server-managed policy, when configured. */ @JsonProperty("serverManagedModel") String serverManagedModel, + /** Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. */ + @JsonProperty("policyHelperModel") String policyHelperModel, /** Model selected by repository settings, when configured. */ @JsonProperty("repoModel") String repoModel, /** Reasoning effort selected by repository settings, when configured. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java index f29e249c32..53ce443e08 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelApplyStartupOverlayResult.java @@ -40,6 +40,8 @@ public record SessionModelApplyStartupOverlayResult( /** User-facing warning produced while applying the model switch. */ @JsonProperty("warning") String warning, /** Deprecation warnings associated with the selected model or options. */ - @JsonProperty("deprecationWarnings") List deprecationWarnings + @JsonProperty("deprecationWarnings") List deprecationWarnings, + /** Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. */ + @JsonProperty("modelState") CurrentModel modelState ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java index 21afab2fa4..23a9788540 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelGetCurrentResult.java @@ -14,7 +14,7 @@ import javax.annotation.processing.Generated; /** - * The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + * The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. * * @apiNote This method is experimental and may change in a future version. * @since 1.0.0 @@ -29,6 +29,12 @@ public record SessionModelGetCurrentResult( /** Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Context tier for models that support multiple context-window sizes. */ - @JsonProperty("contextTier") ContextTier contextTier + @JsonProperty("contextTier") ContextTier contextTier, + /** Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. */ + @JsonProperty("autoTier") AutoTier autoTier, + /** Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. */ + @JsonProperty("pendingAutoTier") AutoTier pendingAutoTier, + /** Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. */ + @JsonProperty("activatingAutoTier") AutoTier activatingAutoTier ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierParams.java new file mode 100644 index 0000000000..576df55aa1 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModelSwitchAutoTierParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. */ + @JsonProperty("autoTier") AutoTier autoTier, + /** Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. */ + @JsonProperty("source") ModelChangeSource source +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierResult.java new file mode 100644 index 0000000000..7e95695492 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchAutoTierResult.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionModelSwitchAutoTierResult( + /** Immediate request status. `pending` means accepted but not committed. */ + @JsonProperty("status") ModelSwitchAutoTierStatus status, + /** Auto preference currently committed for the session. */ + @JsonProperty("effectiveAutoTier") AutoTier effectiveAutoTier, + /** Latest unclaimed Auto preference waiting for a future user turn. */ + @JsonProperty("pendingAutoTier") AutoTier pendingAutoTier, + /** Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. */ + @JsonProperty("activatingAutoTier") AutoTier activatingAutoTier, + /** Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. */ + @JsonProperty("supersededAutoTier") AutoTier supersededAutoTier +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java index a7c60d28cd..cfd59bcf36 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToParams.java @@ -28,6 +28,8 @@ public record SessionModelSwitchToParams( @JsonProperty("sessionId") String sessionId, /** Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. */ @JsonProperty("modelId") String modelId, + /** Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. */ + @JsonProperty("autoTier") AutoTier autoTier, /** Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. When omitted, no effort override is applied. */ @JsonProperty("reasoningEffort") String reasoningEffort, /** Reasoning summary mode to request for supported model clients */ @@ -38,7 +40,7 @@ public record SessionModelSwitchToParams( @JsonProperty("modelCapabilities") ModelCapabilitiesOverride modelCapabilities, /** Explicit context tier for the selected model. `"default"` / `"long_context"` apply the requested tier; omit this field to use normal model behavior with no explicit tier. */ @JsonProperty("contextTier") ContextTier contextTier, - /** Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. */ + /** Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. */ @JsonProperty("source") ModelChangeSource source, /** When true, defer this switch (enqueue it) if another model change is already queued, even when no turn is active — so it drains last (FIFO) and wins over the already-queued change. Intended for genuine user-initiated model selections; internal restore/reapply switches omit it and apply immediately when no turn is active. When no other model change is queued this has no effect (a switch still applies immediately unless a turn is active). */ @JsonProperty("deferIfModelChangeQueued") Boolean deferIfModelChangeQueued, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToResult.java index fb143abc30..47099e42ea 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionModelSwitchToResult.java @@ -40,6 +40,8 @@ public record SessionModelSwitchToResult( /** User-facing warning produced while applying the model switch. */ @JsonProperty("warning") String warning, /** Deprecation warnings associated with the selected model or options. */ - @JsonProperty("deprecationWarnings") List deprecationWarnings + @JsonProperty("deprecationWarnings") List deprecationWarnings, + /** Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. */ + @JsonProperty("modelState") CurrentModel modelState ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java index 34706a68e1..a1c234903e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOpenOptions.java @@ -37,6 +37,8 @@ public record SessionOpenOptions( @JsonProperty("verbosity") Verbosity verbosity, /** Identifier of the client driving the session. */ @JsonProperty("clientName") String clientName, + /** OAuth Client ID Metadata Document URL used by this host for MCP authorization. */ + @JsonProperty("authClientIdMetadataUrl") String authClientIdMetadataUrl, /** Structured client kind used for runtime behavior gates. */ @JsonProperty("clientKind") String clientKind, /** Identifier sent to LSP-style integrations. */ @@ -111,6 +113,10 @@ public record SessionOpenOptions( @JsonProperty("allowAllMcpServerInstructions") Boolean allowAllMcpServerInstructions, /** Additional directories to search for skills. */ @JsonProperty("skillDirectories") List skillDirectories, + /** Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by default. */ + @JsonProperty("enableSkills") Boolean enableSkills, + /** Whether the requesting SDK session has a skill provider. The provider remains ephemeral and is never persisted in session options or history. When enableSkills is false, it remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw sessions.open flows reject it because they cannot safely pre-register the callback handler. */ + @JsonProperty("hasSkillProvider") Boolean hasSkillProvider, /** Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. */ @JsonProperty("includedBuiltinSkills") List includedBuiltinSkills, /** Skill IDs disabled for this session. */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java index 2e8a069a4b..27a5c2db06 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java @@ -140,7 +140,7 @@ public record SessionOptionsUpdateParams( @JsonProperty("enableHostGitOperations") Boolean enableHostGitOperations, /** Whether to enable cross-session store writes and reads. */ @JsonProperty("enableSessionStore") Boolean enableSessionStore, - /** Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. */ + /** Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. */ @JsonProperty("enableSkills") Boolean enableSkills, /** Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. */ @JsonProperty("contextTier") OptionsUpdateContextTier contextTier, diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java index b9bd6b8226..cf89d37208 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java @@ -49,6 +49,8 @@ public final class SessionRpc { public final SessionPlanApi plan; /** API methods for the {@code workspaces} namespace. */ public final SessionWorkspacesApi workspaces; + /** API methods for the {@code autopilotObjective} namespace. */ + public final SessionAutopilotObjectiveApi autopilotObjective; /** API methods for the {@code completions} namespace. */ public final SessionCompletionsApi completions; /** API methods for the {@code instructions} namespace. */ @@ -127,6 +129,7 @@ public SessionRpc(RpcCaller caller, String sessionId) { this.name = new SessionNameApi(caller, sessionId); this.plan = new SessionPlanApi(caller, sessionId); this.workspaces = new SessionWorkspacesApi(caller, sessionId); + this.autopilotObjective = new SessionAutopilotObjectiveApi(caller, sessionId); this.completions = new SessionCompletionsApi(caller, sessionId); this.instructions = new SessionInstructionsApi(caller, sessionId); this.fleet = new SessionFleetApi(caller, sessionId); diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java index 68f038eb4b..c6ca1335de 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksApi.java @@ -57,6 +57,38 @@ public CompletableFuture list() { return caller.invoke("session.tasks.list", java.util.Map.of("sessionId", this.sessionId), SessionTasksListResult.class); } + /** + * Registers or reclaims a client-owned task. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture register(SessionTasksRegisterParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.register", _p, SessionTasksRegisterResult.class); + } + + /** + * Updates a client-owned task. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture update(SessionTasksUpdateParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.tasks.update", _p, SessionTasksUpdateResult.class); + } + /** * Identifies the target session. * diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterParams.java new file mode 100644 index 0000000000..0b14af08fd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterParams.java @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Registers or reclaims a client-owned task. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksRegisterParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Task kind */ + @JsonProperty("type") TaskClientType type, + /** Owner-scoped idempotency key used for registration and reclaim */ + @JsonProperty("clientTaskId") String clientTaskId, + /** Human-readable description of the external work */ + @JsonProperty("description") String description, + /** Optional short display name for the external work */ + @JsonProperty("displayName") String displayName, + /** Whether the owner supports runtime cancellation requests */ + @JsonProperty("cancellable") Boolean cancellable, + /** Expected current sequence for idempotent registration or orphan reclaim */ + @JsonProperty("expectedSequence") Long expectedSequence +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterResult.java new file mode 100644 index 0000000000..f7e25fe885 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksRegisterResult.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Result of registering or reclaiming a client-owned task. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksRegisterResult( + /** Authoritative registered or reclaimed task */ + @JsonProperty("task") TaskClientInfo task, + /** True only when this invocation created a new task */ + @JsonProperty("created") Boolean created, + /** True only when this invocation reclaimed an orphaned task */ + @JsonProperty("reclaimed") Boolean reclaimed +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateParams.java new file mode 100644 index 0000000000..ecd06a1b7e --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateParams.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Updates a client-owned task. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksUpdateParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Canonical runtime-generated task identifier */ + @JsonProperty("id") String id, + /** Owner update sequence to apply */ + @JsonProperty("sequence") Long sequence, + /** Progress or terminal update payload */ + @JsonProperty("update") Object update +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateResult.java new file mode 100644 index 0000000000..8a08e87859 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionTasksUpdateResult.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Result of publishing a client-owned task update. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionTasksUpdateResult( + /** Authoritative task after processing the update */ + @JsonProperty("task") TaskClientInfo task, + /** Whether this invocation changed task state */ + @JsonProperty("applied") Boolean applied, + /** Whether this invocation repeated the latest accepted update */ + @JsonProperty("duplicate") Boolean duplicate +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsParams.java new file mode 100644 index 0000000000..4da93409be --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsParams.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Pagination options for reading an inactive or active local session's persisted event journal. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsReadPersistedEventsParams( + /** Session ID whose persisted event journal should be read. */ + @JsonProperty("sessionId") String sessionId, + /** Opaque cursor returned by a previous persisted-event read. Omit on the first call. */ + @JsonProperty("cursor") String cursor, + /** Maximum number of events to return in this batch (1–1000, default 200). */ + @JsonProperty("max") Long max, + /** Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. */ + @JsonProperty("direction") EventsReadDirection direction +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsResult.java new file mode 100644 index 0000000000..f022df5ae2 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SessionsReadPersistedEventsResult.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import com.github.copilot.generated.SessionEvent; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Batch of session events returned by a read, with cursor and continuation metadata. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionsReadPersistedEventsResult( + /** Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. */ + @JsonProperty("events") List events, + /** Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). */ + @JsonProperty("cursor") String cursor, + /** True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. */ + @JsonProperty("hasMore") Boolean hasMore, + /** Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. */ + @JsonProperty("cursorStatus") EventsCursorStatus cursorStatus +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java new file mode 100644 index 0000000000..a678b95fa3 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderDescriptor.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillProviderDescriptor( + /** Invocation and display name. */ + @JsonProperty("name") String name, + /** Description used in skill catalogs without fetching content. */ + @JsonProperty("description") String description, + /** Whether users may invoke the skill directly. Defaults to true. */ + @JsonProperty("userInvocable") Boolean userInvocable, + /** Whether model invocation is disabled. Defaults to false. */ + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, + /** Optional freeform argument hint used by slash-command catalogs. */ + @JsonProperty("argumentHint") String argumentHint +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java new file mode 100644 index 0000000000..e11fcf0fbd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListParams.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies the target session. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillProviderListParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java new file mode 100644 index 0000000000..228ec9240b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderListResult.java @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate metadata. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillProviderListResult( + /** Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison. */ + @JsonProperty("skills") List skills +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java new file mode 100644 index 0000000000..895078384b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadParams.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Identifies one SDK-provided skill by invocation name. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillProviderReadParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Invocation name of the skill to read. */ + @JsonProperty("name") String name +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java new file mode 100644 index 0000000000..0f17696565 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillProviderReadResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SkillProviderReadResult( + /** Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. */ + @JsonProperty("markdown") String markdown +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java index 8d723548be..bef1995e6c 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillSource.java @@ -10,7 +10,7 @@ import javax.annotation.processing.Generated; /** - * Source location type (e.g., project, personal-copilot, plugin, builtin) + * Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) * * @since 1.0.0 */ @@ -29,7 +29,9 @@ public enum SkillSource { /** The {@code custom} variant. */ CUSTOM("custom"), /** The {@code builtin} variant. */ - BUILTIN("builtin"); + BUILTIN("builtin"), + /** The {@code sdk} variant. */ + SDK("sdk"); private final String value; SkillSource(String value) { this.value = value; } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java index a020c89ecf..5e1f6fd90e 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SkillsInvokedSkill.java @@ -24,12 +24,14 @@ public record SkillsInvokedSkill( /** Unique identifier for the skill */ @JsonProperty("name") String name, - /** Path to the SKILL.md file */ + /** Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity */ @JsonProperty("path") String path, /** Full content of the skill file */ @JsonProperty("content") String content, /** Tools that should be auto-approved when this skill is active, captured at invocation time */ @JsonProperty("allowedTools") List allowedTools, + /** Whether model invocation was disabled when this skill was invoked */ + @JsonProperty("disableModelInvocation") Boolean disableModelInvocation, /** Turn number when the skill was invoked */ @JsonProperty("invokedAtTurn") Long invokedAtTurn ) { diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java index b2a1970a36..c457181435 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandCompletedResult.java @@ -32,6 +32,10 @@ public final class SlashCommandCompletedResult extends SlashCommandInvocationRes @JsonProperty("message") private String message; + /** Optional target session mode applied without submitting an agent prompt */ + @JsonProperty("mode") + private SessionMode mode; + /** True when the invocation mutated user runtime settings; consumers caching settings should refresh */ @JsonProperty("runtimeSettingsChanged") private Boolean runtimeSettingsChanged; @@ -39,6 +43,9 @@ public final class SlashCommandCompletedResult extends SlashCommandInvocationRes public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } + public SessionMode getMode() { return mode; } + public void setMode(SessionMode mode) { this.mode = mode; } + public Boolean getRuntimeSettingsChanged() { return runtimeSettingsChanged; } public void setRuntimeSettingsChanged(Boolean runtimeSettingsChanged) { this.runtimeSettingsChanged = runtimeSettingsChanged; } } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandTimelineEntry.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandTimelineEntry.java index a238d7fb67..f8c832e05d 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandTimelineEntry.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SlashCommandTimelineEntry.java @@ -21,6 +21,8 @@ public record SlashCommandTimelineEntry( /** Text displayed for the timeline entry. */ @JsonProperty("text") String text, /** Optional URL associated with the timeline entry. */ - @JsonProperty("url") String url + @JsonProperty("url") String url, + /** What the user must do to recover, when the entry reports a failure the runtime knows an action for. The `text` never names a client affordance, so a client that offers one renders it from this value. */ + @JsonProperty("remediation") RemediationAction remediation ) { } diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java index 29426d931b..4a366864c3 100644 --- a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/SubagentSettingsEntry.java @@ -23,6 +23,8 @@ public record SubagentSettingsEntry( /** Model override for matching subagents */ @JsonProperty("model") String model, + /** Whether the configured model strategy is preferred or required */ + @JsonProperty("modelPolicy") AgentModelPolicy modelPolicy, /** Reasoning effort override for matching subagents */ @JsonProperty("effortLevel") String effortLevel, /** Context tier override for matching subagents */ diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientExecutionMode.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientExecutionMode.java new file mode 100644 index 0000000000..6c948f86e0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientExecutionMode.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Client-owned tasks always execute outside the runtime in background mode. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskClientExecutionMode { + /** The {@code background} variant. */ + BACKGROUND("background"); + + private final String value; + TaskClientExecutionMode(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskClientExecutionMode fromValue(String value) { + for (TaskClientExecutionMode v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskClientExecutionMode value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientInfo.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientInfo.java new file mode 100644 index 0000000000..6a221303eb --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientInfo.java @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Tracked client-owned task metadata. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record TaskClientInfo( + /** Task kind */ + @JsonProperty("type") TaskClientType type, + /** Canonical runtime-generated task identifier */ + @JsonProperty("id") String id, + /** Owner-scoped registration and reclaim key */ + @JsonProperty("clientTaskId") String clientTaskId, + /** Optional task display name */ + @JsonProperty("displayName") String displayName, + /** Task description */ + @JsonProperty("description") String description, + /** Client task lifecycle status */ + @JsonProperty("status") TaskClientStatus status, + /** Public attribution and presence for the task owner */ + @JsonProperty("owner") TaskClientOwner owner, + /** ISO 8601 timestamp when the task started */ + @JsonProperty("startedAt") OffsetDateTime startedAt, + /** ISO 8601 timestamp of the latest accepted lifecycle change */ + @JsonProperty("updatedAt") OffsetDateTime updatedAt, + /** ISO 8601 timestamp when the task reached a terminal status */ + @JsonProperty("completedAt") OffsetDateTime completedAt, + /** Accumulated active execution time in milliseconds */ + @JsonProperty("activeTimeMs") Long activeTimeMs, + /** ISO 8601 timestamp when the current active segment started */ + @JsonProperty("activeStartedAt") OffsetDateTime activeStartedAt, + /** ISO 8601 timestamp when the connected owner entered idle status */ + @JsonProperty("idleSince") OffsetDateTime idleSince, + /** ISO 8601 timestamp of the most recent orphan transition */ + @JsonProperty("orphanedAt") OffsetDateTime orphanedAt, + /** ISO 8601 timestamp of the most recent successful reclaim */ + @JsonProperty("reclaimedAt") OffsetDateTime reclaimedAt, + /** Execution mode, which is always background for client-owned tasks */ + @JsonProperty("executionMode") TaskClientExecutionMode executionMode, + /** Whether the currently bound owner can receive a cancellation request */ + @JsonProperty("canCancel") Boolean canCancel, + /** Sequence number of the latest accepted owner update */ + @JsonProperty("sequence") Long sequence, + /** Opaque successful terminal result supplied by the task owner */ + @JsonProperty("result") Object result, + /** Human-readable terminal failure message */ + @JsonProperty("error") String error, + /** Optional owner-supplied terminal failure code */ + @JsonProperty("errorCode") String errorCode, + /** Human-readable reason for terminal cancellation */ + @JsonProperty("cancellationReason") String cancellationReason +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwner.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwner.java new file mode 100644 index 0000000000..e2ce54019b --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwner.java @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.OffsetDateTime; +import javax.annotation.processing.Generated; + +/** + * Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record TaskClientOwner( + /** Opaque session-scoped participant identity */ + @JsonProperty("participantId") String participantId, + /** Opaque identity of the currently or most recently bound session join */ + @JsonProperty("joinId") String joinId, + /** Class of the task owner */ + @JsonProperty("kind") TaskClientOwnerKind kind, + /** Display-only owner name */ + @JsonProperty("displayName") String displayName, + /** Display-only owner source */ + @JsonProperty("source") String source, + /** Whether this task's bound join is currently connected */ + @JsonProperty("presence") TaskClientOwnerPresence presence, + /** ISO 8601 timestamp when the bound join disconnected */ + @JsonProperty("disconnectedAt") OffsetDateTime disconnectedAt +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerKind.java new file mode 100644 index 0000000000..92518264a5 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerKind.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Connection class owning a client task. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskClientOwnerKind { + /** The {@code extension} variant. */ + EXTENSION("extension"), + /** The {@code sdk} variant. */ + SDK("sdk"); + + private final String value; + TaskClientOwnerKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskClientOwnerKind fromValue(String value) { + for (TaskClientOwnerKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskClientOwnerKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerPresence.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerPresence.java new file mode 100644 index 0000000000..282cc69206 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientOwnerPresence.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Presence of the task's bound join. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskClientOwnerPresence { + /** The {@code connected} variant. */ + CONNECTED("connected"), + /** The {@code disconnected} variant. */ + DISCONNECTED("disconnected"); + + private final String value; + TaskClientOwnerPresence(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskClientOwnerPresence fromValue(String value) { + for (TaskClientOwnerPresence v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskClientOwnerPresence value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientStatus.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientStatus.java new file mode 100644 index 0000000000..a72daddc41 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientStatus.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Lifecycle status of a client-owned task. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskClientStatus { + /** The {@code running} variant. */ + RUNNING("running"), + /** The {@code idle} variant. */ + IDLE("idle"), + /** The {@code completed} variant. */ + COMPLETED("completed"), + /** The {@code failed} variant. */ + FAILED("failed"), + /** The {@code cancelled} variant. */ + CANCELLED("cancelled"), + /** The {@code orphaned} variant. */ + ORPHANED("orphaned"); + + private final String value; + TaskClientStatus(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskClientStatus fromValue(String value) { + for (TaskClientStatus v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskClientStatus value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientType.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientType.java new file mode 100644 index 0000000000..42f74cf3dd --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskClientType.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Discriminator for a client-owned task. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskClientType { + /** The {@code client} variant. */ + CLIENT("client"); + + private final String value; + TaskClientType(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskClientType fromValue(String value) { + for (TaskClientType v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskClientType value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java new file mode 100644 index 0000000000..413859db20 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TaskKind.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import javax.annotation.processing.Generated; + +/** + * Closed set of public task kinds a connection can negotiate. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum TaskKind { + /** The {@code agent} variant. */ + AGENT("agent"), + /** The {@code shell} variant. */ + SHELL("shell"), + /** The {@code client} variant. */ + CLIENT("client"); + + private final String value; + TaskKind(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static TaskKind fromValue(String value) { + for (TaskKind v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown TaskKind value: " + value); + } +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelParams.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelParams.java new file mode 100644 index 0000000000..57cbd5e6e0 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelParams.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Runtime-to-owner cancellation request for a client-owned task. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record TasksCancelParams( + /** Session that owns the client task */ + @JsonProperty("sessionId") String sessionId, + /** Canonical runtime-generated task identifier */ + @JsonProperty("id") String id, + /** Owner-scoped task key included for correlation */ + @JsonProperty("clientTaskId") String clientTaskId, + /** Opaque identifier shared by coalesced cancellation callers */ + @JsonProperty("cancellationId") String cancellationId, + /** Reason the runtime requests cancellation */ + @JsonProperty("reason") ClientTaskCancelReason reason +) { +} diff --git a/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelResult.java b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelResult.java new file mode 100644 index 0000000000..bd549d1df4 --- /dev/null +++ b/java/sdk/src/generated/java/com/github/copilot/generated/rpc/TasksCancelResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Whether the client authoritatively confirmed its external work stopped. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record TasksCancelResult( + /** True only when the owner confirms that external work stopped before responding */ + @JsonProperty("cancelled") Boolean cancelled +) { +} diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java index 661fe6dab4..9f7d8ebcf8 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotClient.java @@ -221,7 +221,10 @@ public CopilotClient(CopilotClientOptions options) { // Parse CliUrl if provided if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) { URI uri = CliServerManager.parseCliUrl(this.options.getCliUrl()); - this.optionsHost = uri.getHost(); + String host = uri.getHost(); + this.optionsHost = host != null && host.startsWith("[") && host.endsWith("]") + ? host.substring(1, host.length() - 1) + : host; this.optionsPort = uri.getPort(); } else { this.optionsHost = null; @@ -550,6 +553,7 @@ private Connection startCoreBody() { JsonRpcClient connectedRpc = rpc; Connection connection = new Connection(connectedRpc, process, new ServerRpc(connectedRpc::invoke), inProcessTransport == null ? null : inProcessTransport.host()); + connectedRpc.setCloseHandler(() -> sessions.values().forEach(CopilotSession::cancelPendingExternalTools)); // Register handlers for server-to-client calls RpcHandlerDispatcher dispatcher = new RpcHandlerDispatcher(sessions, lifecycleManager::dispatch, executor, @@ -635,6 +639,7 @@ private void verifyProtocolVersion(Connection connection) throws Exception { if (effectiveConnectionToken != null) { connectParams.put("token", effectiveConnectionToken); } + connectParams.put("supportedTaskKinds", List.of("agent", "client", "shell")); // Opt into GitHub telemetry forwarding at the connection level when a handler // is registered, so the runtime can forward the first session's un-replayable // start event. Also sent on session create/resume for backward compatibility @@ -642,6 +647,13 @@ private void verifyProtocolVersion(Connection connection) throws Exception { if (this.options.getOnGitHubTelemetry() != null) { connectParams.put("enableGitHubTelemetryForwarding", true); } + // Declare the integrating application's identity so the runtime attributes the + // telemetry it emits on this connection to a consistent surface instead of + // its own build. Omitted when the app didn't supply it (or supplied no fields). + var clientInfo = this.options.getClientInfo(); + if (clientInfo != null && !clientInfo.isEmpty()) { + connectParams.put("clientInfo", clientInfo); + } var connectResponse = connection.rpc.invoke("connect", connectParams, ConnectResult.class).get(30, TimeUnit.SECONDS); serverVersion = connectResponse.protocolVersion() != null @@ -735,7 +747,9 @@ public CompletableFuture stop() { */ public CompletableFuture forceStop() { disposed = true; + var activeSessions = new ArrayList<>(sessions.values()); sessions.clear(); + activeSessions.forEach(CopilotSession::cancelPendingExternalTools); gitHubTokenProviders.clear(); // Dispatch the blocking shutdownOwnedExecutor() on a dedicated thread: // cleanupConnection() is chained off async work running on the owned @@ -1007,6 +1021,7 @@ public CompletableFuture createSession(SessionConfig config) { CopilotSession session = preRegisteredSessionHolder[0] != null ? preRegisteredSessionHolder[0] : initializeSession.apply(returnedId); + preRegisteredSessionHolder[0] = session; if (tokenRegistration != null) { session.setGitHubTokenProviderRegistration(tokenRegistration); } @@ -1038,6 +1053,9 @@ public CompletableFuture createSession(SessionConfig config) { return session; }); }).exceptionally(ex -> { + if (preRegisteredSessionHolder[0] != null) { + preRegisteredSessionHolder[0].cancelPendingExternalTools(); + } if (registeredIdHolder[0] != null) { sessions.remove(registeredIdHolder[0]); } @@ -1218,6 +1236,7 @@ public CompletableFuture resumeSession(String sessionId, ResumeS return session; }); }).exceptionally(ex -> { + session.cancelPendingExternalTools(); sessions.remove(sessionId); // Also remove the re-keyed entry if the server returned a different ID String activeId = session.getSessionId(); diff --git a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java index f3a35967d3..c072e31ec1 100644 --- a/java/sdk/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/sdk/src/main/java/com/github/copilot/CopilotSession.java @@ -20,6 +20,7 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.logging.Level; @@ -29,7 +30,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.github.copilot.generated.AssistantMessageEvent; +import com.github.copilot.generated.ExternalToolCompletedEvent; import com.github.copilot.generated.rpc.SessionCommandsHandlePendingCommandParams; import com.github.copilot.generated.rpc.SessionLogParams; import com.github.copilot.generated.rpc.SessionLogLevel; @@ -37,6 +40,8 @@ import com.github.copilot.generated.rpc.ModelCapabilitiesOverride; import com.github.copilot.generated.rpc.ModelCapabilitiesOverrideLimits; import com.github.copilot.generated.rpc.ModelCapabilitiesOverrideSupports; +import com.github.copilot.generated.rpc.SessionModelSwitchAutoTierParams; +import com.github.copilot.generated.rpc.SessionModelSwitchAutoTierResult; import com.github.copilot.generated.rpc.SessionModelSwitchToParams; import com.github.copilot.generated.rpc.SessionPermissionsHandlePendingPermissionRequestParams; import com.github.copilot.generated.rpc.SessionRpc; @@ -184,6 +189,8 @@ public final class CopilotSession implements AutoCloseable { private volatile SessionRpc sessionRpc; private final Set> eventHandlers = ConcurrentHashMap.newKeySet(); private final Map toolHandlers = new ConcurrentHashMap<>(); + private final Map pendingExternalTools = new ConcurrentHashMap<>(); + private boolean externalToolsClosed; private final Map commandHandlers = new ConcurrentHashMap<>(); private final Map bearerTokenProviders = new ConcurrentHashMap<>(); private final AtomicReference permissionHandler = new AtomicReference<>(); @@ -204,6 +211,46 @@ public final class CopilotSession implements AutoCloseable { /** Tracks whether this session instance has been terminated via close(). */ private volatile boolean isTerminated = false; + private static final class PendingExternalTool { + private static final int WAITING = 0; + private static final int STARTED = 1; + private static final int CANCELLED = 2; + + private final AtomicInteger state = new AtomicInteger(WAITING); + private final AtomicReference> future = new AtomicReference<>(); + + T join(CompletableFuture operation) { + future.set(operation); + if (state.get() == CANCELLED) { + operation.cancel(true); + } + try { + return operation.join(); + } finally { + future.compareAndSet(operation, null); + } + } + + boolean tryStart() { + return state.compareAndSet(WAITING, STARTED); + } + + void attach(CompletableFuture toolFuture) { + future.set(toolFuture); + if (state.get() == CANCELLED) { + toolFuture.cancel(true); + } + } + + void cancel() { + state.set(CANCELLED); + CompletableFuture activeFuture = future.get(); + if (activeFuture != null) { + activeFuture.cancel(true); + } + } + } + /** * Creates a new session with the given ID and RPC client. *

@@ -857,6 +904,14 @@ private void handleBroadcastEventAsync(SessionEvent event) { } executeToolAndRespondAsync(data.requestId(), data.toolName(), data.toolCallId(), data.arguments(), tool); + } else if (event instanceof ExternalToolCompletedEvent completedEvent) { + var data = completedEvent.getData(); + if (data != null && data.requestId() != null) { + PendingExternalTool pending = pendingExternalTools.remove(data.requestId()); + if (pending != null) { + pending.cancel(); + } + } } else if (event instanceof PermissionRequestedEvent permEvent) { var data = permEvent.getData(); if (data == null || data.requestId() == null || data.permissionRequest() == null) { @@ -928,9 +983,9 @@ private void handleBroadcastEventAsync(SessionEvent event) { * built-in tool-search tool, so an override can filter the live catalog without * issuing its own RPC. The snapshot is fetched only for that tool to avoid a * round-trip on every ordinary tool call; a failed fetch leaves the snapshot - * {@code null} rather than failing the tool. Shared by both server-to-client - * tool dispatch paths ({@link RpcHandlerDispatcher} and - * {@link #executeToolAndRespondAsync}). + * {@code null} rather than failing the tool. Used by the direct RPC dispatch + * path; event-dispatched tools perform the same lookup with request-scoped + * cancellation. * * @param toolName * the name of the tool being invoked @@ -955,6 +1010,12 @@ void populateToolSearchMetadata(String toolName, com.github.copilot.rpc.ToolInvo */ private void executeToolAndRespondAsync(String requestId, String toolName, String toolCallId, Object arguments, ToolDefinition tool) { + var pending = new PendingExternalTool(); + synchronized (this) { + if (isTerminated || externalToolsClosed || pendingExternalTools.putIfAbsent(requestId, pending) != null) { + return; + } + } Runnable task = () -> { try { JsonNode argumentsNode = arguments instanceof JsonNode jn @@ -963,9 +1024,32 @@ private void executeToolAndRespondAsync(String requestId, String toolName, Strin var invocation = new com.github.copilot.rpc.ToolInvocation().setSessionId(sessionId) .setToolCallId(toolCallId).setToolName(toolName).setArguments(argumentsNode); - populateToolSearchMetadata(toolName, invocation); + if (TOOL_SEARCH_TOOL_NAME.equals(toolName)) { + try { + var metadata = pending.join(getRpc().tools.getCurrentMetadata()); + if (metadata != null) { + invocation.setAvailableTools(metadata.tools()); + } + } catch (RuntimeException e) { + if (pendingExternalTools.get(requestId) != pending) { + return; + } + LOG.log(Level.FINE, "Failed to fetch tool metadata for tool search", e); + } + if (pendingExternalTools.get(requestId) != pending) { + return; + } + } - tool.handler().invoke(invocation).thenAccept(result -> { + if (!pending.tryStart()) { + return; + } + CompletableFuture toolFuture = tool.handler().invoke(invocation); + pending.attach(toolFuture); + toolFuture.thenAccept(result -> { + if (!pendingExternalTools.remove(requestId, pending)) { + return; + } try { ToolResultObject toolResult; if (result instanceof ToolResultObject tr) { @@ -980,6 +1064,9 @@ private void executeToolAndRespondAsync(String requestId, String toolName, Strin LOG.log(Level.WARNING, "Error sending tool result for requestId=" + requestId, e); } }).exceptionally(ex -> { + if (!pendingExternalTools.remove(requestId, pending)) { + return null; + } try { getRpc().tools.handlePendingToolCall(new SessionToolsHandlePendingToolCallParams(sessionId, requestId, null, ex.getMessage() != null ? ex.getMessage() : ex.toString())); @@ -987,8 +1074,11 @@ private void executeToolAndRespondAsync(String requestId, String toolName, Strin LOG.log(Level.WARNING, "Error sending tool error for requestId=" + requestId, e); } return null; - }); + }).whenComplete((result, error) -> pendingExternalTools.remove(requestId, pending)); } catch (Exception e) { + if (!pendingExternalTools.remove(requestId, pending)) { + return; + } LOG.log(Level.WARNING, "Error executing tool for requestId=" + requestId, e); try { getRpc().tools.handlePendingToolCall(new SessionToolsHandlePendingToolCallParams(sessionId, @@ -1010,6 +1100,16 @@ private void executeToolAndRespondAsync(String requestId, String toolName, Strin } } + void cancelPendingExternalTools() { + List pending; + synchronized (this) { + externalToolsClosed = true; + pending = new ArrayList<>(pendingExternalTools.values()); + pendingExternalTools.clear(); + } + pending.forEach(PendingExternalTool::cancel); + } + /** * Builds a {@link SessionUiHandlePendingElicitationParams} carrying a * {@code cancel} action, used when an elicitation handler throws or the handler @@ -2010,8 +2110,8 @@ public CompletableFuture abort() { */ public CompletableFuture setModel(String model, String reasoningEffort) { ensureNotTerminated(); - return getRpc().model.switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, null, null, - null, null, null, null, null, null, null, null, null, null)).thenApply(r -> null); + return getRpc().model.switchTo(new SessionModelSwitchToParams(sessionId, model, null, reasoningEffort, null, + null, null, null, null, null, null, null, null, null, null, null)).thenApply(r -> null); } /** @@ -2073,30 +2173,141 @@ public CompletableFuture setModel(String model, String reasoningEffort, public CompletableFuture setModel(String model, String reasoningEffort, String reasoningSummary, com.github.copilot.rpc.ModelCapabilitiesOverride modelCapabilities) { ensureNotTerminated(); - ModelCapabilitiesOverride generatedCapabilities = null; - if (modelCapabilities != null) { - ModelCapabilitiesOverrideSupports supports = null; - if (modelCapabilities.getSupports() != null) { - var s = modelCapabilities.getSupports(); - supports = new ModelCapabilitiesOverrideSupports(s.getVision().orElse(null), - s.getReasoningEffort().orElse(null), null); - } - ModelCapabilitiesOverrideLimits limits = null; - if (modelCapabilities.getLimits() != null) { - limits = new ObjectMapper().convertValue(modelCapabilities.getLimits(), - ModelCapabilitiesOverrideLimits.class); - } - generatedCapabilities = new ModelCapabilitiesOverride(supports, limits); - } + ModelCapabilitiesOverride generatedCapabilities = toGeneratedCapabilities(modelCapabilities); var generatedReasoningSummary = reasoningSummary == null ? null : com.github.copilot.generated.rpc.ReasoningSummary.fromValue(reasoningSummary); - return getRpc().model - .switchTo(new SessionModelSwitchToParams(sessionId, model, reasoningEffort, generatedReasoningSummary, - null, generatedCapabilities, null, null, null, null, null, null, null, null, null)) + return getRpc().model.switchTo( + new SessionModelSwitchToParams(sessionId, model, null, reasoningEffort, generatedReasoningSummary, null, + generatedCapabilities, null, null, null, null, null, null, null, null, null)) .thenApply(r -> null); } + private static ModelCapabilitiesOverride toGeneratedCapabilities( + com.github.copilot.rpc.ModelCapabilitiesOverride modelCapabilities) { + if (modelCapabilities == null) { + return null; + } + ModelCapabilitiesOverrideSupports supports = null; + if (modelCapabilities.getSupports() != null) { + var s = modelCapabilities.getSupports(); + supports = new ModelCapabilitiesOverrideSupports(s.getVision().orElse(null), + s.getReasoningEffort().orElse(null), null); + } + ModelCapabilitiesOverrideLimits limits = null; + if (modelCapabilities.getLimits() != null) { + limits = MAPPER.convertValue(modelCapabilities.getLimits(), ModelCapabilitiesOverrideLimits.class); + } + return new ModelCapabilitiesOverride(supports, limits); + } + + private static com.github.copilot.generated.rpc.AutoTier toGeneratedAutoTier( + com.github.copilot.rpc.AutoTier autoTier) { + return autoTier == null ? null : com.github.copilot.generated.rpc.AutoTier.fromValue(autoTier.getValue()); + } + + /** + * Changes the model for this session using an options object. + *

+ * The new model takes effect for the next message. Conversation history is + * preserved. Use {@link com.github.copilot.rpc.SetModelOptions#setAutoTier} to + * request an Auto routing preference at the same time, which the runtime + * accepts only when the model is {@code "auto"}. + * + *

{@code
+     * session.setModel(new SetModelOptions().setModel("auto").setAutoTier(AutoTier.INTELLIGENCE)).get();
+     * session.setModel(new SetModelOptions().setModel("auto").setResetAutoTier(true)).get();
+     * }
+ * + * @param options + * the switch settings; the model ID is required + * @return a future that completes when the model switch is acknowledged + * @throws IllegalArgumentException + * if {@code options} is {@code null}, if it carries no model ID, or + * if it requests both an explicit Auto tier and a return to + * provider-default Auto routing + * @throws IllegalStateException + * if this session has been terminated + * @since 1.6.0 + */ + public CompletableFuture setModel(com.github.copilot.rpc.SetModelOptions options) { + ensureNotTerminated(); + if (options == null) { + throw new IllegalArgumentException("options must not be null"); + } + if (options.getModel() == null) { + throw new IllegalArgumentException("options must specify a model"); + } + if (options.getAutoTier() != null && options.isResetAutoTier()) { + throw new IllegalArgumentException( + "setModel cannot combine an explicit autoTier with resetAutoTier; choose one"); + } + var generatedReasoningSummary = options.getReasoningSummary() == null + ? null + : com.github.copilot.generated.rpc.ReasoningSummary.fromValue(options.getReasoningSummary()); + var params = new SessionModelSwitchToParams(sessionId, options.getModel(), + toGeneratedAutoTier(options.getAutoTier()), options.getReasoningEffort(), generatedReasoningSummary, + null, toGeneratedCapabilities(options.getModelCapabilities()), null, null, null, null, null, null, null, + null, null); + if (!options.isResetAutoTier()) { + return getRpc().model.switchTo(params).thenApply(r -> null); + } + // The generated params record omits null properties, but returning to + // provider-default Auto routing requires sending an explicit null tier, so + // build the payload directly and reinstate the null. + ObjectNode payload = MAPPER.valueToTree(params); + payload.putNull("autoTier"); + return rpc.invoke("session.model.switchTo", payload, Void.class); + } + + /** + * Changes the Auto routing preference without changing the selected model. + *

+ * The runtime does not apply the preference immediately. It records the request + * and commits it only when a later user turn using the {@code auto} model + * successfully obtains a usable model from the provider. A {@code pending} + * status therefore confirms that the request was accepted, not that it took + * effect. + *

+ * Watch for the outcome through the {@code session.model_change} event on + * success, or the ephemeral {@code session.auto_tier_switch_failed} event on + * failure. You can also read the committed and in-flight state at any time with + * {@code session.getRpc().model.getCurrent()}. + *

+ * Only the most recent request survives: a new request replaces any earlier one + * that no turn has claimed yet. + * + *

{@code
+     * var result = session.setAutoTier(AutoTier.INTELLIGENCE).get();
+     * if (result.status() == ModelSwitchAutoTierStatus.PENDING) {
+     * 	// Takes effect on a later turn that uses the `auto` model.
+     * }
+     * }
+ * + * @param autoTier + * the routing preference to activate, or {@code null} to return to + * the provider's default Auto routing + * @return a future completing with the runtime's immediate acknowledgement and + * Auto preference snapshot + * @throws IllegalStateException + * if this session has been terminated + * @since 1.6.0 + */ + @CopilotExperimental + public CompletableFuture setAutoTier(com.github.copilot.rpc.AutoTier autoTier) { + ensureNotTerminated(); + var params = new SessionModelSwitchAutoTierParams(sessionId, toGeneratedAutoTier(autoTier), null); + if (autoTier != null) { + return getRpc().model.switchAutoTier(params); + } + // The generated params record omits null properties, but the runtime + // distinguishes an explicit null tier (return to provider-default routing) + // from an absent one, so build the payload directly and reinstate the null. + ObjectNode payload = MAPPER.valueToTree(params); + payload.putNull("autoTier"); + return rpc.invoke("session.model.switchAutoTier", payload, SessionModelSwitchAutoTierResult.class); + } + /** * Changes the model for this session. *

@@ -2315,13 +2526,24 @@ public void close() { isTerminated = true; } + cancelPendingExternalTools(); timeoutScheduler.shutdownNow(); releaseGitHubTokenProviderRegistration(); + RuntimeException detachFailure = null; try { - rpc.invoke("session.destroy", Map.of("sessionId", sessionId), Void.class).get(5, TimeUnit.SECONDS); + SessionDetachResponse response = rpc + .invoke("session.detach", Map.of("sessionId", sessionId), SessionDetachResponse.class) + .get(5, TimeUnit.SECONDS); + if (response == null || !response.success()) { + String detail = response != null && response.error() != null ? response.error() : "unknown error"; + detachFailure = new IllegalStateException("Failed to detach session " + sessionId + ": " + detail); + } } catch (Exception e) { - LOG.log(Level.FINE, "Error destroying session", e); + if (e instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + detachFailure = new IllegalStateException("Failed to detach session " + sessionId, e); } eventHandlers.clear(); @@ -2333,10 +2555,18 @@ public void close() { exitPlanModeHandler.set(null); autoModeSwitchHandler.set(null); hooksHandler.set(null); + + if (detachFailure != null) { + throw detachFailure; + } } // ===== Internal response types for agent API ===== + @JsonIgnoreProperties(ignoreUnknown = true) + record SessionDetachResponse(@JsonProperty("success") boolean success, @JsonProperty("error") String error) { + } + @JsonIgnoreProperties(ignoreUnknown = true) private record AgentListResponse(@JsonProperty("agents") List agents) { } diff --git a/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java b/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java index 7eda069d23..f78ce00425 100644 --- a/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java +++ b/java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java @@ -53,7 +53,10 @@ class JsonRpcClient implements AutoCloseable { private final Map> pendingRequests = new ConcurrentHashMap<>(); private final Map> notificationHandlers = new ConcurrentHashMap<>(); private final ExecutorService readerExecutor; + private final Object closeHandlerLock = new Object(); private volatile boolean running = true; + private boolean closeNotified; + private Runnable closeHandler; private JsonRpcClient(InputStream inputStream, OutputStream outputStream, Socket socket, Process process) { this(inputStream, outputStream, socket, process, false); @@ -322,10 +325,41 @@ private void startReader() { if (running) { LOG.log(Level.SEVERE, "Error in JSON-RPC reader", e); } + } finally { + notifyClose(); } }); } + void setCloseHandler(Runnable handler) { + boolean runNow; + synchronized (closeHandlerLock) { + closeHandler = handler; + runNow = closeNotified; + } + if (runNow) { + handler.run(); + } + } + + private void notifyClose() { + Runnable handler; + synchronized (closeHandlerLock) { + if (closeNotified) { + return; + } + closeNotified = true; + handler = closeHandler; + } + if (handler != null) { + try { + handler.run(); + } catch (RuntimeException e) { + LOG.log(Level.WARNING, "Error handling JSON-RPC connection close", e); + } + } + } + private void handleMessage(String content) { try { JsonNode node = MAPPER.readTree(content); @@ -390,6 +424,7 @@ else if (node.has("method")) { public void close() { running = false; readerExecutor.shutdownNow(); + notifyClose(); // Cancel all pending requests pendingRequests.forEach((id, future) -> future.completeExceptionally(new IOException("Client closed"))); diff --git a/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java b/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java index bd4b185a07..3a1c6b35bc 100644 --- a/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java +++ b/java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java @@ -318,9 +318,8 @@ static Path resolve(String cliPathEnv, Path cacheBase, ClassLoader loader, Strin * *

* Checks, in order, the flat bundled layout ({@code runtime.node} directly next - * to the CLI) and the npm package layout - * ({@code prebuilds//runtime.node} next to the CLI), matching the - * two layouts the {@code @github/copilot-} packages may ship. + * to the CLI) and the release package layout + * ({@code prebuilds//runtime.node} next to the CLI). */ static Path resolveFromCliPath(String cliPathStr) throws IOException { if (cliPathStr == null || cliPathStr.isBlank()) { diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java new file mode 100644 index 0000000000..f9117abfb2 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/AutoTier.java @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * Routing tier for the {@code auto} model with Auto mode V2. + * + * @see CapiSessionOptions#setAutoTier(AutoTier) + */ +public enum AutoTier { + + /** Prioritize efficiency. */ + EFFICIENCY("efficiency"), + + /** Balance efficiency and intelligence. */ + BALANCE("balance"), + + /** Prioritize intelligence. */ + INTELLIGENCE("intelligence"); + + private final String value; + + AutoTier(String value) { + this.value = value; + } + + /** + * Returns the JSON value for this routing tier. + * + * @return the string value used in JSON serialization + */ + @JsonValue + public String getValue() { + return value; + } + + /** + * Deserializes a JSON string into its routing tier. + * + * @param value + * the JSON string value + * @return the matching tier, or {@code null} if value is {@code null} + * @throws IllegalArgumentException + * if the value does not match a known routing tier + */ + @JsonCreator + public static AutoTier fromValue(String value) { + if (value == null) { + return null; + } + for (AutoTier tier : values()) { + if (tier.value.equals(value)) { + return tier; + } + } + throw new IllegalArgumentException("Unknown AutoTier value: " + value); + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java index d94d59f67b..1743f572ed 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CapiSessionOptions.java @@ -29,9 +29,41 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class CapiSessionOptions { + @JsonProperty("autoTier") + private AutoTier autoTier; + @JsonProperty("enableWebSocketResponses") private Boolean enableWebSocketResponses; + /** + * Gets the routing tier for the {@code auto} model (Auto mode V2). + * + * @return the explicit tier, or {@code null} to leave tier selection to the + * runtime + */ + public AutoTier getAutoTier() { + return autoTier; + } + + /** + * Sets the routing tier, meaningful only with model {@code auto} (Auto mode + * V2). Requires a runtime version that supports {@code capi.autoTier}. + *

+ * When omitted, the runtime chooses its default on create and preserves the + * persisted or current tier on resume. An explicit tier overrides the persisted + * tier on cold resume. On resident resume, a different tier requests a safe + * switch that the runtime applies after the resume succeeds; it cannot change a + * turn that is already in flight. + * + * @param autoTier + * the routing tier, or {@code null} to omit it from the request + * @return this config for method chaining + */ + public CapiSessionOptions setAutoTier(AutoTier autoTier) { + this.autoTier = autoTier; + return this; + } + /** * Gets whether CAPI Responses API WebSocket transport is enabled. * diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java b/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java new file mode 100644 index 0000000000..ee9d97e1e9 --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/ClientInfo.java @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Identity of the integrating application, declared on the + * {@code server.connect} handshake. + *

+ * Declaring it lets the telemetry the runtime emits on the connection be + * attributed to a single, consistent surface (the application and its Copilot + * integration) instead of the runtime's own build. All fields are optional; an + * empty field is omitted from the handshake. + * + *

Example Usage

+ * + *
{@code
+ * var options = new CopilotClientOptions().setClientInfo(new ClientInfo().setApplicationName("acme-developer-portal")
+ * 		.setApplicationVersion("2.4.0").setIntegrationName("copilot-assistant").setIntegrationVersion("1.5.0"));
+ * }
+ * + * @see CopilotClientOptions#setClientInfo(ClientInfo) + * @since 1.6.0 + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class ClientInfo { + + private String applicationName; + + private String applicationVersion; + + private String integrationName; + + private String integrationVersion; + + /** + * Gets the name of the application using the SDK. + * + * @return the application name, or {@code null} + */ + @JsonProperty("editorName") + public String getApplicationName() { + return applicationName; + } + + /** + * Sets the name of the application using the SDK. + * + * @param applicationName + * the application name + * @return this client info for method chaining + */ + @JsonProperty("editorName") + public ClientInfo setApplicationName(String applicationName) { + this.applicationName = applicationName; + return this; + } + + /** + * Gets the version of the application using the SDK. + * + * @return the application version, or {@code null} + */ + @JsonProperty("editorVersion") + public String getApplicationVersion() { + return applicationVersion; + } + + /** + * Sets the version of the application using the SDK. + * + * @param applicationVersion + * the application version + * @return this client info for method chaining + */ + @JsonProperty("editorVersion") + public ClientInfo setApplicationVersion(String applicationVersion) { + this.applicationVersion = applicationVersion; + return this; + } + + /** + * Gets the optional name of a specific integration within the application, such + * as an extension or plugin. + * + * @return the integration name, or {@code null} + */ + @JsonProperty("extensionName") + public String getIntegrationName() { + return integrationName; + } + + /** + * Sets the optional name of a specific integration within the application, such + * as an extension or plugin. + * + * @param integrationName + * the integration name + * @return this client info for method chaining + */ + @JsonProperty("extensionName") + public ClientInfo setIntegrationName(String integrationName) { + this.integrationName = integrationName; + return this; + } + + /** + * Gets the optional version of the named integration. + * + * @return the integration version, or {@code null} + */ + @JsonProperty("extensionVersion") + public String getIntegrationVersion() { + return integrationVersion; + } + + /** + * Sets the optional version of the named integration. + * + * @param integrationVersion + * the integration version + * @return this client info for method chaining + */ + @JsonProperty("extensionVersion") + public ClientInfo setIntegrationVersion(String integrationVersion) { + this.integrationVersion = integrationVersion; + return this; + } + + /** + * Returns whether no field carries a non-empty value, in which case the SDK + * omits {@code clientInfo} from the handshake so the runtime keeps its default + * attribution. + * + * @return {@code true} when every field is {@code null} or empty + */ + @JsonIgnore + public boolean isEmpty() { + return isBlank(applicationName) && isBlank(applicationVersion) && isBlank(integrationName) + && isBlank(integrationVersion); + } + + private static boolean isBlank(String value) { + return value == null || value.isEmpty(); + } +} diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java index a9c9cbcda7..c67947bf48 100644 --- a/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java +++ b/java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java @@ -55,6 +55,7 @@ public class CopilotClientOptions { private String cliPath; private String cliUrl; private RuntimeConnection connection; + private ClientInfo clientInfo; private String copilotHome; private String cwd; private Map environment; @@ -678,6 +679,36 @@ public CopilotClientOptions setTelemetry(TelemetryConfig telemetry) { return this; } + /** + * Gets the integrating application's declared identity. + * + * @return the client info, or {@code null} + * @since 1.6.0 + */ + public ClientInfo getClientInfo() { + return clientInfo; + } + + /** + * Declares the integrating application's identity, forwarded to the runtime on + * the {@code server.connect} handshake. + *

+ * Declaring it lets the telemetry the runtime emits on this connection be + * attributed to a consistent surface (the application and its Copilot + * integration) instead of the runtime's own build. All fields on + * {@link ClientInfo} are optional; leave this unset to keep the runtime's + * default attribution. + * + * @param clientInfo + * the application identity to declare + * @return this options instance for method chaining + * @since 1.6.0 + */ + public CopilotClientOptions setClientInfo(ClientInfo clientInfo) { + this.clientInfo = Objects.requireNonNull(clientInfo, "clientInfo must not be null"); + return this; + } + /** * Gets the server-wide idle timeout for sessions in seconds. * @@ -830,6 +861,7 @@ public CopilotClientOptions clone() { copy.cliPath = this.cliPath; copy.cliUrl = this.cliUrl; copy.connection = this.connection; + copy.clientInfo = this.clientInfo; copy.copilotHome = this.copilotHome; copy.cwd = this.cwd; copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null; diff --git a/java/sdk/src/main/java/com/github/copilot/rpc/SetModelOptions.java b/java/sdk/src/main/java/com/github/copilot/rpc/SetModelOptions.java new file mode 100644 index 0000000000..9ebfbf199b --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/rpc/SetModelOptions.java @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.github.copilot.CopilotExperimental; + +/** + * Optional settings for a model switch. + *

+ * All setter methods return {@code this} for method chaining. {@code model} is + * required. Every other option is optional; an unset option leaves the + * corresponding session state unchanged. + * + *

{@code
+ * session.setModel(new SetModelOptions().setModel("auto").setAutoTier(AutoTier.INTELLIGENCE)).get();
+ * }
+ * + * @since 1.6.0 + */ +public class SetModelOptions { + + private String model; + + private String reasoningEffort; + + private String reasoningSummary; + + private ModelCapabilitiesOverride modelCapabilities; + + private AutoTier autoTier; + + private boolean resetAutoTier; + + /** + * Gets the target model ID. + * + * @return the model ID, or {@code null} when none has been set + */ + public String getModel() { + return model; + } + + /** + * Sets the model to switch to. This option is required. + * + * @param model + * the model ID (e.g., {@code "gpt-5.4"} or {@code "auto"}) + * @return this options object for method chaining + */ + public SetModelOptions setModel(String model) { + this.model = model; + return this; + } + + /** + * Gets the reasoning effort level. + * + * @return the reasoning effort level, or {@code null} to use the default + */ + public String getReasoningEffort() { + return reasoningEffort; + } + + /** + * Sets the reasoning effort level. + * + * @param reasoningEffort + * reasoning effort level (e.g., {@code "low"}, {@code "medium"}, + * {@code "high"}, {@code "xhigh"}, {@code "max"}); {@code null} to + * use the default + * @return this options object for method chaining + */ + public SetModelOptions setReasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + return this; + } + + /** + * Gets the reasoning summary mode. + * + * @return the reasoning summary mode, or {@code null} to use the default + */ + public String getReasoningSummary() { + return reasoningSummary; + } + + /** + * Sets the reasoning summary mode. + * + * @param reasoningSummary + * reasoning summary mode ({@code "none"}, {@code "concise"}, or + * {@code "detailed"}); {@code null} to use the default + * @return this options object for method chaining + */ + public SetModelOptions setReasoningSummary(String reasoningSummary) { + this.reasoningSummary = reasoningSummary; + return this; + } + + /** + * Gets the model capability overrides. + * + * @return the capability overrides, or {@code null} to use runtime defaults + */ + public ModelCapabilitiesOverride getModelCapabilities() { + return modelCapabilities; + } + + /** + * Sets per-property overrides for model capabilities. + * + * @param modelCapabilities + * the capability overrides; {@code null} to use runtime defaults + * @return this options object for method chaining + */ + public SetModelOptions setModelCapabilities(ModelCapabilitiesOverride modelCapabilities) { + this.modelCapabilities = modelCapabilities; + return this; + } + + /** + * Gets the requested Auto routing preference. + * + * @return the requested tier, or {@code null} when no tier was requested + */ + @CopilotExperimental + public AutoTier getAutoTier() { + return autoTier; + } + + /** + * Requests an Auto routing preference alongside the model switch. + *

+ * The runtime records the request and commits it only when a later user turn + * using the {@code auto} model successfully obtains a usable model from the + * provider. Use {@link #setResetAutoTier(boolean)} to return to the provider's + * default Auto routing instead. + * + * @param autoTier + * the routing preference to request; {@code null} to leave the + * current preference unchanged + * @return this options object for method chaining + */ + @CopilotExperimental + public SetModelOptions setAutoTier(AutoTier autoTier) { + this.autoTier = autoTier; + return this; + } + + /** + * Gets whether the request returns to provider-default Auto routing. + * + * @return {@code true} when the request clears the Auto routing preference + */ + @CopilotExperimental + public boolean isResetAutoTier() { + return resetAutoTier; + } + + /** + * Requests a return to the provider's default Auto routing. + *

+ * This differs from leaving {@link #setAutoTier(AutoTier)} unset, which keeps + * the current preference. It cannot be combined with an explicit tier. + * + * @param resetAutoTier + * {@code true} to return to provider-default Auto routing + * @return this options object for method chaining + */ + @CopilotExperimental + public SetModelOptions setResetAutoTier(boolean resetAutoTier) { + this.resetAutoTier = resetAutoTier; + return this; + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/AutoTierIT.java b/java/sdk/src/test/java/com/github/copilot/AutoTierIT.java new file mode 100644 index 0000000000..d486ce1c2c --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/AutoTierIT.java @@ -0,0 +1,120 @@ +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.generated.rpc.ModelSwitchAutoTierStatus; +import com.github.copilot.rpc.AutoTier; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.SetModelOptions; + +/** + * End-to-end coverage for Auto tier switching, mirroring + * {@code nodejs/test/e2e/auto_tier.e2e.test.ts}. + *

+ * The runtime stages an Auto routing preference rather than applying it + * immediately: a request stays unclaimed until a later turn using the + * {@code auto} model mints a usable model and token pair. These tests read the + * staged state back through {@code model.getCurrent()}, so they assert what the + * runtime actually recorded rather than what the SDK serialized. + */ +class AutoTierIT { + + private static final String MODEL_ID = "auto"; + + private static E2ETestContext ctx; + + @BeforeAll + static void setUp() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void tearDown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + private static com.github.copilot.generated.rpc.AutoTier pendingAutoTier(CopilotSession session) throws Exception { + return session.getRpc().model.getCurrent().get(30, TimeUnit.SECONDS).pendingAutoTier(); + } + + private static CopilotSession createAutoSession(CopilotClient client) throws Exception { + return client + .createSession( + new SessionConfig().setModel(MODEL_ID).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + } + + @Test + void shouldStageAndResetAutoTierPreference() throws Exception { + ctx.configureForTest("auto_tier", "should_stage_and_reset_auto_tier_preference"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = createAutoSession(client); + try { + assertNull(pendingAutoTier(session)); + + var staged = session.setAutoTier(AutoTier.EFFICIENCY).get(30, TimeUnit.SECONDS); + assertEquals(ModelSwitchAutoTierStatus.PENDING, staged.status()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.EFFICIENCY, staged.pendingAutoTier()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.EFFICIENCY, pendingAutoTier(session)); + + // A second request replaces the first and reports the one it displaced. + var superseded = session.setAutoTier(AutoTier.INTELLIGENCE).get(30, TimeUnit.SECONDS); + assertEquals(ModelSwitchAutoTierStatus.PENDING, superseded.status()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.INTELLIGENCE, superseded.pendingAutoTier()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.EFFICIENCY, superseded.supersededAutoTier()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.INTELLIGENCE, pendingAutoTier(session)); + + // A null tier returns the session to provider-default routing. The status + // is `unchanged` because provider-default was already the committed + // preference; the request's effect is cancelling the staged one. + var reset = session.setAutoTier(null).get(30, TimeUnit.SECONDS); + assertEquals(ModelSwitchAutoTierStatus.UNCHANGED, reset.status()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.INTELLIGENCE, reset.supersededAutoTier()); + assertNull(pendingAutoTier(session)); + } finally { + session.close(); + } + } + } + + @Test + void shouldPreserveAutoTierWhenSetModelOmitsIt() throws Exception { + ctx.configureForTest("auto_tier", "should_preserve_auto_tier_when_set_model_omits_it"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = createAutoSession(client); + try { + session.setAutoTier(AutoTier.BALANCE).get(30, TimeUnit.SECONDS); + assertEquals(com.github.copilot.generated.rpc.AutoTier.BALANCE, pendingAutoTier(session)); + + // Omitting the preference leaves the staged one alone. + session.setModel(new SetModelOptions().setModel(MODEL_ID)).get(30, TimeUnit.SECONDS); + assertEquals(com.github.copilot.generated.rpc.AutoTier.BALANCE, pendingAutoTier(session)); + + // Supplying a tier replaces it. + session.setModel(new SetModelOptions().setModel(MODEL_ID).setAutoTier(AutoTier.INTELLIGENCE)).get(30, + TimeUnit.SECONDS); + assertEquals(com.github.copilot.generated.rpc.AutoTier.INTELLIGENCE, pendingAutoTier(session)); + + // Requesting a reset clears it. Omission, an explicit tier, and a reset + // are three distinct outcomes. + session.setModel(new SetModelOptions().setModel(MODEL_ID).setResetAutoTier(true)).get(30, + TimeUnit.SECONDS); + assertNull(pendingAutoTier(session)); + } finally { + session.close(); + } + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java index 17e8f131f7..dccb4e9add 100644 --- a/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CapiSessionOptionsTest.java @@ -9,12 +9,16 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import com.fasterxml.jackson.databind.JsonNode; +import com.github.copilot.rpc.AutoTier; import com.github.copilot.rpc.CapiSessionOptions; import com.github.copilot.rpc.ResumeSessionConfig; import com.github.copilot.rpc.SessionConfig; @@ -29,6 +33,7 @@ void defaultsAreNull() { var capi = new CapiSessionOptions(); assertNull(capi.getEnableWebSocketResponses()); + assertNull(capi.getAutoTier()); } @Test @@ -37,6 +42,8 @@ void fluentSetterReturnsSameInstance() { assertSame(capi, capi.setEnableWebSocketResponses(true)); assertEquals(Boolean.TRUE, capi.getEnableWebSocketResponses()); + assertSame(capi, capi.setAutoTier(AutoTier.BALANCE)); + assertEquals(AutoTier.BALANCE, capi.getAutoTier()); } @Test @@ -46,6 +53,7 @@ void serializesEnableWebSocketResponses() { JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); assertTrue(json.get("enableWebSocketResponses").asBoolean()); + assertTrue(json.path("autoTier").isMissingNode()); } @Test @@ -55,6 +63,44 @@ void omitsUnsetEnableWebSocketResponses() { JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); assertTrue(json.path("enableWebSocketResponses").isMissingNode()); + assertTrue(json.path("autoTier").isMissingNode()); + assertEquals(0, json.size()); + } + + @ParameterizedTest + @CsvSource({"EFFICIENCY,efficiency", "BALANCE,balance", "INTELLIGENCE,intelligence"}) + void autoTierCanonicalValuesRoundTripAndForward(AutoTier tier, String value) throws Exception { + var mapper = JsonRpcClient.getObjectMapper(); + var capi = new CapiSessionOptions().setAutoTier(tier); + JsonNode json = mapper.valueToTree(capi); + assertEquals(value, json.get("autoTier").asText()); + assertEquals(1, json.size()); + assertEquals(tier, mapper.treeToValue(json, CapiSessionOptions.class).getAutoTier()); + + capi.setEnableWebSocketResponses(false); + var create = SessionRequestBuilder.buildCreateRequest(new SessionConfig().setModel("auto").setCapi(capi), + "session-1"); + var resume = SessionRequestBuilder.buildResumeRequest("session-1", new ResumeSessionConfig().setCapi(capi)); + for (Object request : new Object[]{create, resume}) { + JsonNode requestJson = mapper.valueToTree(request); + assertEquals(value, requestJson.get("capi").get("autoTier").asText()); + assertFalse(requestJson.get("capi").get("enableWebSocketResponses").asBoolean()); + assertEquals(2, requestJson.get("capi").size()); + } + } + + @Test + void autoTierRejectsNoncanonicalValues() { + for (String value : new String[]{"balanced", "Balance", "unknown"}) { + assertThrows(IllegalArgumentException.class, () -> AutoTier.fromValue(value)); + } + assertNull(AutoTier.fromValue(null)); + } + + @Test + void clearingAutoTierOmitsIt() { + var capi = new CapiSessionOptions().setAutoTier(AutoTier.BALANCE).setAutoTier(null); + JsonNode json = JsonRpcClient.getObjectMapper().valueToTree(capi); assertEquals(0, json.size()); } @@ -67,6 +113,7 @@ void createRequestIncludesCapiWhenSet() { assertNotNull(request.getCapi()); assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean()); + assertTrue(json.get("capi").path("autoTier").isMissingNode()); } @Test @@ -89,6 +136,7 @@ void resumeRequestIncludesCapiWhenSet() { assertNotNull(request.getCapi()); assertTrue(json.get("capi").get("enableWebSocketResponses").asBoolean()); + assertTrue(json.get("capi").path("autoTier").isMissingNode()); } @Test diff --git a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java index a227be04b9..b41f99adb7 100644 --- a/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CliServerManagerTest.java @@ -93,6 +93,13 @@ void parseCliUrlWithHttpsPrefix() { assertEquals("https://secure.host:443", uri.toString()); } + @Test + void parseCliUrlWithBracketedIpv6() { + URI uri = CliServerManager.parseCliUrl("[::1]:4321"); + assertNotNull(uri.getHost()); + assertEquals(4321, uri.getPort()); + } + @Test void parseCliUrlWithHostOnly() { URI uri = CliServerManager.parseCliUrl("copilot.example.com"); diff --git a/java/sdk/src/test/java/com/github/copilot/ClientOptionsE2ETest.java b/java/sdk/src/test/java/com/github/copilot/ClientOptionsE2ETest.java index 45056afdb4..a2561434d8 100644 --- a/java/sdk/src/test/java/com/github/copilot/ClientOptionsE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/ClientOptionsE2ETest.java @@ -263,6 +263,8 @@ function resultFor(message) { return { sessionId: message.params?.sessionId ?? 'fake-session', openCanvases: [] }; case 'session.resume': return { sessionId: message.params?.sessionId ?? 'fake-session', openCanvases: [] }; + case 'session.detach': + return { success: true }; case 'session.options.update': return { success: true }; default: diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java index 4cfd7f4c48..6b8e861e88 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import com.github.copilot.generated.ExternalToolRequestedEvent; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.DeleteSessionResponse; import com.github.copilot.rpc.GitHubTokenProviderResult; @@ -15,14 +16,19 @@ import com.github.copilot.rpc.SessionConfig; import com.github.copilot.rpc.SessionLifecycleEvent; import com.github.copilot.rpc.SessionLifecycleEventTypes; +import com.github.copilot.rpc.ToolDefinition; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; @@ -113,6 +119,49 @@ void testForceStopAndExternalStopDoNotRequestRuntimeShutdown() throws Exception verify(externalRpc, never()).invoke(eq("runtime.shutdown"), any(), eq(Void.class)); } + @Test + @SuppressWarnings("unchecked") + void testForceStopCancelsPendingExternalTools() throws Exception { + var client = new CopilotClient(new CopilotClientOptions().setAutoStart(false)); + var rpc = mock(JsonRpcClient.class); + setConnectionFuture(client, rpc, null); + var session = new CopilotSession("force-stop-session", rpc); + var toolFuture = new CompletableFuture(); + var started = new CountDownLatch(1); + var lateStarted = new CountDownLatch(1); + var invocations = new AtomicInteger(); + session.registerTools(List.of(ToolDefinition.create("blocked_tool", "Blocks", Map.of(), invocation -> { + if (invocations.incrementAndGet() == 1) { + started.countDown(); + } else { + lateStarted.countDown(); + } + return toolFuture; + }))); + Field sessionsField = CopilotClient.class.getDeclaredField("sessions"); + sessionsField.setAccessible(true); + var sessions = (Map) sessionsField.get(client); + sessions.put(session.getSessionId(), session); + + var requested = new ExternalToolRequestedEvent(); + requested.setData(new ExternalToolRequestedEvent.ExternalToolRequestedEventData("request-force-stop", + session.getSessionId(), "tool-call-force-stop", "blocked_tool", null, Map.of(), null, null, null)); + session.dispatchEvent(requested); + assertTrue(started.await(1, TimeUnit.SECONDS)); + + client.forceStop().get(); + + assertThrows(CancellationException.class, () -> toolFuture.get(1, TimeUnit.SECONDS)); + assertTrue(sessions.isEmpty()); + + var lateRequest = new ExternalToolRequestedEvent(); + lateRequest.setData(new ExternalToolRequestedEvent.ExternalToolRequestedEventData("request-after-force-stop", + session.getSessionId(), "tool-call-after-force-stop", "blocked_tool", null, Map.of(), null, null, + null)); + session.dispatchEvent(lateRequest); + assertFalse(lateStarted.await(100, TimeUnit.MILLISECONDS)); + } + @Test @SuppressWarnings("unchecked") void testDeleteSessionReleasesGitHubTokenProvider() throws Exception { @@ -120,8 +169,8 @@ void testDeleteSessionReleasesGitHubTokenProvider() throws Exception { var rpc = mock(JsonRpcClient.class); when(rpc.invoke(eq("session.delete"), any(), eq(DeleteSessionResponse.class))) .thenReturn(CompletableFuture.completedFuture(new DeleteSessionResponse(true, null))); - when(rpc.invoke(eq("session.destroy"), any(), eq(Void.class))) - .thenReturn(CompletableFuture.completedFuture(null)); + when(rpc.invoke(eq("session.detach"), any(), eq(CopilotSession.SessionDetachResponse.class))) + .thenReturn(CompletableFuture.completedFuture(new CopilotSession.SessionDetachResponse(true, null))); setConnectionFuture(client, rpc, null); var registry = new GitHubTokenProviderRegistry(); @@ -179,6 +228,15 @@ void testCliUrlOnlyConstruction() { client.close(); } + @Test + void testBracketedIpv6CliUrlNormalizesHost() throws Exception { + try (var client = new CopilotClient(new CopilotClientOptions().setCliUrl("[::1]:4321"))) { + Field hostField = CopilotClient.class.getDeclaredField("optionsHost"); + hostField.setAccessible(true); + assertEquals("::1", hostField.get(client)); + } + } + @Test void testCliUrlMutualExclusionWithCliPath() { var options = new CopilotClientOptions().setCliUrl("localhost:3000").setCliPath("/path/to/cli"); diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java b/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java index 3025c64c39..b4159d839c 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotRequestSessionIdE2ETest.java @@ -77,11 +77,11 @@ void threadsSessionIdForCapiAndByok() throws Exception { // BYOK session. int before = handler.inferenceRequests().size(); ProviderConfig provider = new ProviderConfig().setType("openai").setWireApi("responses") - .setBaseUrl("https://byok.invalid/v1").setApiKey("byok-secret").setModelId("claude-sonnet-4.5") - .setWireModel("claude-sonnet-4.5"); + .setBaseUrl("https://byok.invalid/v1").setApiKey("byok-secret").setModelId("claude-sonnet-5") + .setWireModel("claude-sonnet-5"); CopilotSession byokSession = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setModel("claude-sonnet-4.5").setProvider(provider)) + .setModel("claude-sonnet-5").setProvider(provider)) .get(); String byokSessionId = byokSession.getSessionId(); diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java b/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java index aa173ef30e..fa2a6354be 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java @@ -134,7 +134,7 @@ static String anthropicMessageSseBody(String text) { startMessage.put("id", "msg_stub_1"); startMessage.put("type", "message"); startMessage.put("role", "assistant"); - startMessage.put("model", "claude-sonnet-4.5"); + startMessage.put("model", "claude-sonnet-5"); startMessage.put("content", List.of()); startMessage.put("stop_reason", null); startMessage.put("stop_sequence", null); @@ -251,7 +251,7 @@ static HttpResponse buildInferenceResponse(String url, String bodyT body.put("id", "msg_stub_1"); body.put("type", "message"); body.put("role", "assistant"); - body.put("model", "claude-sonnet-4.5"); + body.put("model", "claude-sonnet-5"); body.put("content", List.of(Map.of("type", "text", "text", text))); body.put("stop_reason", "end_turn"); body.put("stop_sequence", null); @@ -301,14 +301,14 @@ static String modelCatalog(List supportedEndpoints) { Map capabilities = new LinkedHashMap<>(); capabilities.put("type", "chat"); - capabilities.put("family", "claude-sonnet-4.5"); + capabilities.put("family", "claude-sonnet-5"); capabilities.put("tokenizer", "o200k_base"); capabilities.put("limits", limits); capabilities.put("supports", supports); Map model = new LinkedHashMap<>(); - model.put("id", "claude-sonnet-4.5"); - model.put("name", "Claude Sonnet 4.5"); + model.put("id", "claude-sonnet-5"); + model.put("name", "Claude Sonnet 5"); model.put("object", "model"); model.put("vendor", "Anthropic"); model.put("version", "1"); @@ -416,7 +416,7 @@ private static Map chatChunkBase() { base.put("id", "chatcmpl-stub-1"); base.put("object", "chat.completion.chunk"); base.put("created", 1); - base.put("model", "claude-sonnet-4.5"); + base.put("model", "claude-sonnet-5"); return base; } @@ -459,7 +459,7 @@ private static Map chatCompletion(String text) { root.put("id", "chatcmpl-stub-1"); root.put("object", "chat.completion"); root.put("created", 1); - root.put("model", "claude-sonnet-4.5"); + root.put("model", "claude-sonnet-5"); root.put("choices", List.of(choice)); root.put("usage", chatUsage()); return root; diff --git a/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java b/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java index eb061b029d..667769fab2 100644 --- a/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java +++ b/java/sdk/src/test/java/com/github/copilot/CopilotSessionTest.java @@ -352,6 +352,51 @@ void testShouldResumeSessionUsingNewClient() throws Exception { } } + @Test + @Tag("isolated-resume") + void testShouldRecoverMarkerAfterColdResumeWithExplicitSessionId() throws Exception { + final String snapshot = "should_recover_marker_after_cold_resume_with_explicit_session_id"; + ctx.configureForTest("session", snapshot); + + String sessionId = "e2e-cold-resume-" + java.util.UUID.randomUUID(); + + try (CopilotClient client1 = ctx.createClient()) { + CopilotSession session1 = client1.createSession( + new SessionConfig().setSessionId(sessionId).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + assertEquals(sessionId, session1.getSessionId()); + + AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions() + .setPrompt("Please remember this exact secret marker for later - MARKER-7f3ac21e. " + + "Reply with only the single word \"Acknowledged\".")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer); + assertTrue(answer.getData().content().contains("Acknowledged"), + "Response should contain Acknowledged: " + answer.getData().content()); + + session1.close(); + client1.forceStop().get(30, TimeUnit.SECONDS); + } + + try (CopilotClient client2 = ctx.createClient()) { + CopilotSession session2 = client2 + .resumeSession(sessionId, + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + .get(30, TimeUnit.SECONDS); + assertEquals(sessionId, session2.getSessionId()); + + AssistantMessageEvent answer2 = session2.sendAndWait( + new MessageOptions().setPrompt("What was the exact secret marker I asked you to remember earlier? " + + "Reply with only that marker value and nothing else.")) + .get(60, TimeUnit.SECONDS); + assertNotNull(answer2); + assertTrue(answer2.getData().content().contains("MARKER-7f3ac21e"), + "Resumed response should contain marker: " + answer2.getData().content()); + + session2.close(); + } + } + /** * Verifies that sessions work with appended system message configuration. * diff --git a/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java b/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java index cb302a8cd2..a6bec65d2c 100644 --- a/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java +++ b/java/sdk/src/test/java/com/github/copilot/E2ETestContext.java @@ -596,53 +596,15 @@ private static String getCliPath(Path repoRoot) throws IOException { return envPath; } - // Try test harness platform-specific binary (preferred as it has correct - // version) - String os = System.getProperty("os.name").toLowerCase(); - String arch = System.getProperty("os.arch").toLowerCase(); - String platform = os.contains("mac") ? "darwin" : os.contains("win") ? "win32" : "linux"; - String cpuArch = arch.contains("aarch64") || arch.contains("arm64") ? "arm64" : "x64"; - Path platformBinary = repoRoot - .resolve("test/harness/node_modules/@github/copilot-" + platform + "-" + cpuArch + "/copilot"); - if (os.contains("win")) { - platformBinary = repoRoot - .resolve("test/harness/node_modules/@github/copilot-" + platform + "-" + cpuArch + "/copilot.exe"); - } - if (Files.exists(platformBinary)) { - return platformBinary.toString(); - } - - // Try test harness npm-loader.js - Path harnessCliPath = repoRoot.resolve("test/harness/node_modules/@github/copilot/npm-loader.js"); - if (Files.exists(harnessCliPath)) { - return harnessCliPath.toString(); - } - - // Try nodejs installation. As of CLI 1.0.64-1 the @github/copilot package - // is a thin loader; the runnable index.js ships in the installed - // platform-specific package (e.g. @github/copilot-linux-x64). Exactly one - // is installed. Running index.js under Node.js is the documented preferred - // entry point and matches the Go, Python, Rust, and .NET test harnesses. - Path githubModules = repoRoot.resolve("nodejs/node_modules/@github"); - if (Files.isDirectory(githubModules)) { - try (var modules = Files.newDirectoryStream(githubModules, "copilot-*")) { - for (Path module : modules) { - Path indexJs = module.resolve("index.js"); - if (Files.exists(indexJs)) { - return indexJs.toString(); - } - } + try { + return TestUtil.preparePinnedCli(repoRoot); + } catch (Exception e) { + String copilotInPath = findCopilotInPath(); + if (copilotInPath != null) { + return copilotInPath; } + throw new IOException("The pinned CLI could not be prepared and no CLI was found on PATH.", e); } - - // Fallback: try to find 'copilot' in PATH - String copilotInPath = findCopilotInPath(); - if (copilotInPath != null) { - return copilotInPath; - } - - throw new IOException("CLI not found. Either install 'copilot' globally, set COPILOT_CLI_PATH, " - + "or run 'npm install' in the nodejs directory or test/harness directory."); } private static String findCopilotInPath() { diff --git a/java/sdk/src/test/java/com/github/copilot/ExternalToolCancellationE2ETest.java b/java/sdk/src/test/java/com/github/copilot/ExternalToolCancellationE2ETest.java new file mode 100644 index 0000000000..d5ae322e32 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/ExternalToolCancellationE2ETest.java @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BooleanSupplier; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.rpc.MessageOptions; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; +import com.github.copilot.rpc.ToolDefinition; + +public class ExternalToolCancellationE2ETest { + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void shouldCancelToolHandlerWhenSessionDisconnects() throws Exception { + ctx.configureForTest("external_tool_cancellation", "should_cancel_tool_handler_when_session_disconnects"); + + var pendingTool = new AtomicReference>(); + ToolDefinition slowTool = ToolDefinition.create("slow_analysis", + "A slow analysis tool that blocks until released", slowAnalysisSchema(), invocation -> { + CompletableFuture pending = new CompletableFuture<>(); + pendingTool.set(pending); + return pending; + }); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setTools(List.of(slowTool))) + .get(60, TimeUnit.SECONDS); + try { + session.send(new MessageOptions() + .setPrompt("Use slow_analysis with value 'test_abort'. Wait for the result.")) + .get(60, TimeUnit.SECONDS); + + waitFor(() -> pendingTool.get() != null, 60_000); + session.close(); + waitFor(() -> pendingTool.get() != null && pendingTool.get().isCancelled(), 60_000); + } finally { + if (session != null) { + session.close(); + } + } + } + } + + private static Map slowAnalysisSchema() { + Map props = new HashMap<>(); + props.put("value", Map.of("type", "string", "description", "Value to analyze")); + Map schema = new HashMap<>(); + schema.put("type", "object"); + schema.put("properties", props); + schema.put("required", List.of("value")); + return schema; + } + + private static void waitFor(BooleanSupplier predicate, long timeoutMillis) throws InterruptedException { + long deadline = System.currentTimeMillis() + timeoutMillis; + while (!predicate.getAsBoolean()) { + if (System.currentTimeMillis() > deadline) { + throw new AssertionError("waitFor timed out"); + } + Thread.sleep(50); + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java index 7b0deb9977..10c47eee86 100644 --- a/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java +++ b/java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.github.copilot.generated.rpc.GitHubTelemetryNotification; +import com.github.copilot.rpc.ClientInfo; import com.github.copilot.rpc.CopilotClientOptions; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.ResumeSessionConfig; @@ -188,6 +189,7 @@ void clientOmitsForwardingWhenNoHandler() throws Exception { JsonNode connectParams = server.awaitConnect(); assertFalse(connectParams.has("enableGitHubTelemetryForwarding"), "connect request should omit the flag when no handler is registered"); + assertEquals("[\"agent\",\"client\",\"shell\"]", connectParams.path("supportedTaskKinds").toString()); client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(15, TimeUnit.SECONDS); @@ -204,6 +206,96 @@ void clientOmitsForwardingWhenNoHandler() throws Exception { } } + @Test + void connectForwardsDeclaredClientInfo() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setApplicationName("acme-developer-portal") + .setApplicationVersion("2.4.0").setIntegrationName("copilot-assistant") + .setIntegrationVersion("1.5.0")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + JsonNode clientInfo = connectParams.path("clientInfo"); + assertEquals(4, clientInfo.size(), "clientInfo should carry only the four declared fields"); + assertEquals("acme-developer-portal", clientInfo.path("editorName").asText()); + assertEquals("2.4.0", clientInfo.path("editorVersion").asText()); + assertEquals("copilot-assistant", clientInfo.path("extensionName").asText()); + assertEquals("1.5.0", clientInfo.path("extensionVersion").asText()); + } + } + + @Test + void connectOmitsClientInfoWhenUnset() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + assertFalse(connectParams.has("clientInfo"), + "connect request should omit clientInfo when none was declared"); + } + } + + @Test + void connectOmitsEmptyClientInfoFields() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setApplicationName("example-app")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + JsonNode clientInfo = connectParams.path("clientInfo"); + assertEquals("example-app", clientInfo.path("editorName").asText()); + assertFalse(clientInfo.has("editorVersion"), "unset editorVersion should be omitted"); + assertFalse(clientInfo.has("extensionName"), "unset extensionName should be omitted"); + assertFalse(clientInfo.has("extensionVersion"), "unset extensionVersion should be omitted"); + } + } + + @Test + void connectDropsEmptyClientInfoFields() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setApplicationName("example-app").setApplicationVersion("")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + JsonNode clientInfo = connectParams.path("clientInfo"); + assertEquals(1, clientInfo.size(), "clientInfo should carry only the non-empty field"); + assertEquals("example-app", clientInfo.path("editorName").asText()); + assertFalse(clientInfo.has("editorVersion"), "empty editorVersion should be dropped"); + } + } + + @Test + void connectOmitsAllEmptyClientInfo() throws Exception { + try (var server = new FakeRuntimeServer(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()) + .setClientInfo(new ClientInfo().setApplicationName("").setApplicationVersion("") + .setIntegrationName("").setIntegrationVersion("")))) { + + client.start().get(15, TimeUnit.SECONDS); + + JsonNode connectParams = server.awaitConnect(); + assertFalse(connectParams.has("clientInfo"), "connect request should omit an all-empty clientInfo"); + } + } + + @Test + void optionsRetainAndCloneClientInfo() { + var info = new ClientInfo().setApplicationName("example-app"); + var options = new CopilotClientOptions().setClientInfo(info); + assertSame(info, options.getClientInfo()); + + var copy = options.clone(); + assertSame(info, copy.getClientInfo()); + } + @Test void optionsRetainAndCloneTelemetryHandler() { Function> handler = n -> CompletableFuture @@ -274,7 +366,8 @@ private void acceptLoop() { respond(rpc, id, Map.of("sessionId", params.path("sessionId").asText("resume-1"), "workspacePath", "/workspace")); }); - rpc.registerMethodHandler("session.destroy", (id, params) -> respond(rpc, id, Map.of())); + rpc.registerMethodHandler("session.detach", + (id, params) -> respond(rpc, id, Map.of("success", true))); rpc.registerMethodHandler("runtime.shutdown", (id, params) -> respond(rpc, id, Map.of())); }); ready.complete(server); diff --git a/java/sdk/src/test/java/com/github/copilot/JsonRpcClientTest.java b/java/sdk/src/test/java/com/github/copilot/JsonRpcClientTest.java index 009f15c200..13668cd97f 100644 --- a/java/sdk/src/test/java/com/github/copilot/JsonRpcClientTest.java +++ b/java/sdk/src/test/java/com/github/copilot/JsonRpcClientTest.java @@ -16,6 +16,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test; @@ -184,6 +185,36 @@ void testGetProcessNullForSocket() throws Exception { } } + @Test + void testCloseHandlerRunsOnceOnRemoteAndExplicitClose() throws Exception { + try (var pair = createSocketPair()) { + var closeCount = new AtomicInteger(); + var closed = new CompletableFuture(); + pair.client.setCloseHandler(() -> { + closeCount.incrementAndGet(); + closed.complete(null); + }); + + pair.serverSide.close(); + closed.get(5, TimeUnit.SECONDS); + pair.client.close(); + + assertEquals(1, closeCount.get()); + } + } + + @Test + void testCloseHandlerRunsWhenRegisteredAfterClose() throws Exception { + try (var pair = createSocketPair()) { + pair.client.close(); + var closed = new CompletableFuture(); + + pair.client.setCloseHandler(() -> closed.complete(null)); + + closed.get(5, TimeUnit.SECONDS); + } + } + // ---- invoke() edge cases ---- @Test diff --git a/java/sdk/src/test/java/com/github/copilot/McpAndAgentsTest.java b/java/sdk/src/test/java/com/github/copilot/McpAndAgentsTest.java index 06d9dca39e..018204d030 100644 --- a/java/sdk/src/test/java/com/github/copilot/McpAndAgentsTest.java +++ b/java/sdk/src/test/java/com/github/copilot/McpAndAgentsTest.java @@ -451,7 +451,7 @@ void testShouldAcceptDefaultAgentConfigurationOnSessionResume() throws Exception assertNotNull(session.getSessionId()); String sessionId = session.getSessionId(); - // Do not call session.close() here — that invokes session.destroy on the + // Do not call session.close() here — that invokes session.detach on the // server, // which removes the session and causes the subsequent resumeSession to fail // with "Session not found". The session handle is simply abandoned and the diff --git a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java index 06ac08a2a4..b83cedc70a 100644 --- a/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java +++ b/java/sdk/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java @@ -255,7 +255,10 @@ private static JsonNode resultFor(String method, JsonNode params) { } case "session.eventLog.registerInterest" -> result.put("id", "interest-1"); case "session.options.update" -> result.put("success", true); - case "session.skills.reload", "session.destroy" -> { + case "session.skills.reload" -> { + } + case "session.detach" -> { + result.put("success", true); } default -> throw new IllegalStateException("Unexpected RPC method " + method); } diff --git a/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java index 2393f334b2..6c9753025a 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcServerE2ETest.java @@ -135,7 +135,7 @@ void testShouldCallRpcModelsListWithTypedResult() throws Exception { var result = client.getRpc().models.list().get(TIMEOUT_SECONDS, TimeUnit.SECONDS); assertNotNull(result.models()); - assertTrue(result.models().stream().anyMatch(model -> "claude-sonnet-4.5".equals(model.id()))); + assertTrue(result.models().stream().anyMatch(model -> "claude-sonnet-5".equals(model.id()))); result.models().forEach(model -> { assertFalse(model.id().isBlank()); assertFalse(model.name().isBlank()); diff --git a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java index 18045f3e86..235b1d5720 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcSessionStateExtrasE2ETest.java @@ -65,7 +65,7 @@ void testShouldAddByokProviderAndModelAtRuntime() throws Exception { var selectionId = "java-e2e-provider/small"; session.getRpc().model.switchTo(new SessionModelSwitchToParams(null, selectionId, null, null, null, - null, null, null, null, null, null, null, null, null, null)).get(30, TimeUnit.SECONDS); + null, null, null, null, null, null, null, null, null, null, null)).get(30, TimeUnit.SECONDS); var current = session.getRpc().model.getCurrent().get(30, TimeUnit.SECONDS); assertEquals(selectionId, current.modelId()); } @@ -142,7 +142,7 @@ void testShouldUpdateAndClearLiveSubagentSettings() throws Exception { session.getRpc().tools.updateSubagentSettings(new SessionToolsUpdateSubagentSettingsParams(null, new SessionToolsUpdateSubagentSettingsParams.SessionToolsUpdateSubagentSettingsParamsSubagents( Map.of("general-purpose", - new SubagentSettingsEntry("gpt-5-mini", "low", + new SubagentSettingsEntry("gpt-5-mini", null, "low", SubagentSettingsEntryContextTier.LONG_CONTEXT)), List.of("legacy-agent"), null, null))) .get(30, TimeUnit.SECONDS); diff --git a/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java b/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java index 3bb9001f35..7b55748bb8 100644 --- a/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java +++ b/java/sdk/src/test/java/com/github/copilot/RpcWrappersTest.java @@ -206,7 +206,7 @@ void sessionRpc_model_switchTo_merges_sessionId_with_extra_params() { // switchTo takes extra params beyond sessionId var switchParams = new SessionModelSwitchToParams(null, "gpt-5", null, null, null, null, null, null, null, null, - null, null, null, null, null); + null, null, null, null, null, null); session.model.switchTo(switchParams); assertEquals(1, stub.calls.size()); diff --git a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java new file mode 100644 index 0000000000..25e356a8d1 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierEventTest.java @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.github.copilot.generated.AutoTier; +import com.github.copilot.generated.AutoTierSwitchFailureReason; +import com.github.copilot.generated.SessionAutoTierSwitchFailedEvent; +import com.github.copilot.generated.SessionEvent; +import com.github.copilot.generated.SessionResumeEvent; +import com.github.copilot.generated.SessionStartEvent; + +/** + * Verifies auto routing preferences on generated session lifecycle events. + */ +class SessionAutoTierEventTest { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + @ParameterizedTest + @CsvSource({"session.start,EFFICIENCY,efficiency", "session.start,BALANCE,balance", + "session.start,INTELLIGENCE,intelligence", "session.resume,EFFICIENCY,efficiency", + "session.resume,BALANCE,balance", "session.resume,INTELLIGENCE,intelligence"}) + void canonicalAutoTierRoundTrips(String type, AutoTier tier, String value) throws Exception { + String json = """ + {"type":"%s","data":{"selectedModel":"auto","autoTier":"%s"}} + """.formatted(type, value); + + var event = MAPPER.readValue(json, SessionEvent.class); + assertEquals(tier, autoTier(event, type)); + String serialized = MAPPER.writeValueAsString(event); + assertEquals(value, MAPPER.readTree(serialized).path("data").path("autoTier").asText()); + assertEquals(tier, autoTier(MAPPER.readValue(serialized, SessionEvent.class), type)); + } + + @ParameterizedTest + @ValueSource(strings = {"session.start", "session.resume"}) + void missingOrNullAutoTierRemainsOptional(String type) throws Exception { + for (String data : new String[]{"{}", "{\"autoTier\":null}"}) { + String json = """ + {"type":"%s","data":%s} + """.formatted(type, data); + + var event = MAPPER.readValue(json, SessionEvent.class); + assertNull(autoTier(event, type)); + var serialized = MAPPER.readTree(MAPPER.writeValueAsString(event)); + assertFalse(serialized.path("data").has("autoTier")); + } + } + + private static AutoTier autoTier(SessionEvent event, String type) { + if ("session.start".equals(type)) { + return assertInstanceOf(SessionStartEvent.class, event).getData().autoTier(); + } + return assertInstanceOf(SessionResumeEvent.class, event).getData().autoTier(); + } + + @ParameterizedTest + @CsvSource({"policy_rejected,POLICY_REJECTED", "request_failed,REQUEST_FAILED", "setup_failed,SETUP_FAILED", + "unsupported,UNSUPPORTED"}) + void autoTierSwitchFailedEventDecodesEveryReason(String value, AutoTierSwitchFailureReason reason) + throws Exception { + String json = """ + {"type":"session.auto_tier_switch_failed","data":{"effectiveAutoTier":"balance", + "requestedAutoTier":"intelligence","reason":"%s"}} + """.formatted(value); + + var event = MAPPER.readValue(json, SessionEvent.class); + + var data = assertInstanceOf(SessionAutoTierSwitchFailedEvent.class, event).getData(); + assertEquals(AutoTier.BALANCE, data.effectiveAutoTier()); + assertEquals(AutoTier.INTELLIGENCE, data.requestedAutoTier()); + assertEquals(reason, data.reason()); + } + + @org.junit.jupiter.api.Test + void autoTierSwitchFailedEventAllowsNullRequestedTier() throws Exception { + // A null requested tier means the attempt to return to provider-default + // Auto routing is what failed. + String json = """ + {"type":"session.auto_tier_switch_failed","data":{"effectiveAutoTier":"efficiency", + "requestedAutoTier":null,"reason":"unsupported"}} + """; + + var event = MAPPER.readValue(json, SessionEvent.class); + + var data = assertInstanceOf(SessionAutoTierSwitchFailedEvent.class, event).getData(); + assertEquals(AutoTier.EFFICIENCY, data.effectiveAutoTier()); + assertNull(data.requestedAutoTier()); + assertEquals(AutoTierSwitchFailureReason.UNSUPPORTED, data.reason()); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java new file mode 100644 index 0000000000..adeeca2c6b --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/SessionAutoTierSwitchTest.java @@ -0,0 +1,228 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.rpc.ModelSwitchAutoTierStatus; +import com.github.copilot.generated.rpc.SessionModelSwitchAutoTierResult; +import com.github.copilot.rpc.AutoTier; +import com.github.copilot.rpc.SetModelOptions; +import java.io.InputStream; +import java.net.ServerSocket; +import java.net.Socket; +import org.junit.jupiter.api.Test; + +/** + * Verifies the wire payloads produced by Auto routing preference switches. + *

+ * The runtime treats an explicit {@code null} {@code autoTier} (return to + * provider-default routing) differently from an absent one (leave the + * preference unchanged), so these tests assert on the raw JSON rather than on + * the generated params records, which drop null properties. + */ +@AllowCopilotExperimental +class SessionAutoTierSwitchTest { + + @Test + void setModel_omits_autoTier_when_no_preference_is_requested() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-1", sockets.client()); + var stub = sockets.stubServer(); + + session.setModel(new SetModelOptions().setModel("auto")); + + var params = stub.readOneMessage().get("params"); + assertEquals("auto", params.get("modelId").asText()); + assertFalse(params.has("autoTier"), "an unset preference must not appear on the wire"); + } + } + + @Test + void setModel_sends_requested_autoTier() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-2", sockets.client()); + var stub = sockets.stubServer(); + + session.setModel(new SetModelOptions().setModel("auto").setAutoTier(AutoTier.INTELLIGENCE) + .setReasoningEffort("high")); + + var sent = stub.readOneMessage(); + assertEquals("session.model.switchTo", sent.get("method").asText()); + var params = sent.get("params"); + assertEquals("intelligence", params.get("autoTier").asText()); + assertEquals("high", params.get("reasoningEffort").asText()); + assertEquals("sess-2", params.get("sessionId").asText()); + } + } + + @Test + void setModel_sends_explicit_null_autoTier_when_clearing() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-3", sockets.client()); + var stub = sockets.stubServer(); + + session.setModel(new SetModelOptions().setModel("auto").setResetAutoTier(true)); + + var sent = stub.readOneMessage(); + assertEquals("session.model.switchTo", sent.get("method").asText()); + var params = sent.get("params"); + assertTrue(params.has("autoTier"), "clearing must send the property"); + assertTrue(params.get("autoTier").isNull(), "clearing must send an explicit null"); + assertEquals("sess-3", params.get("sessionId").asText()); + } + } + + @Test + void setModel_rejects_a_tier_combined_with_clearing() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-4", sockets.client()); + + var options = new SetModelOptions().setModel("auto").setAutoTier(AutoTier.BALANCE).setResetAutoTier(true); + + assertThrows(IllegalArgumentException.class, () -> session.setModel(options)); + } + } + + @Test + void setModel_requires_a_model() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-5", sockets.client()); + + assertThrows(IllegalArgumentException.class, () -> session.setModel(new SetModelOptions())); + assertThrows(IllegalArgumentException.class, () -> session.setModel((SetModelOptions) null)); + } + } + + @Test + void setAutoTier_sends_the_requested_tier() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-6", sockets.client()); + var stub = sockets.stubServer(); + + session.setAutoTier(AutoTier.EFFICIENCY); + + var sent = stub.readOneMessage(); + assertEquals("session.model.switchAutoTier", sent.get("method").asText()); + var params = sent.get("params"); + assertEquals("efficiency", params.get("autoTier").asText()); + assertEquals("sess-6", params.get("sessionId").asText()); + } + } + + @Test + void setAutoTier_sends_explicit_null_for_provider_default_routing() throws Exception { + try (var sockets = new SocketPair()) { + var session = new CopilotSession("sess-7", sockets.client()); + var stub = sockets.stubServer(); + + session.setAutoTier(null); + + var sent = stub.readOneMessage(); + assertEquals("session.model.switchAutoTier", sent.get("method").asText()); + var params = sent.get("params"); + assertTrue(params.has("autoTier"), "returning to provider-default routing must send the property"); + assertTrue(params.get("autoTier").isNull(), "returning to provider-default routing must send null"); + assertEquals("sess-7", params.get("sessionId").asText()); + } + } + + @Test + void switchAutoTier_result_deserializes_every_field() throws Exception { + var json = """ + { + "status": "pending", + "effectiveAutoTier": "balance", + "pendingAutoTier": "intelligence", + "activatingAutoTier": null, + "supersededAutoTier": "efficiency" + } + """; + + var result = new ObjectMapper().readValue(json, SessionModelSwitchAutoTierResult.class); + + assertEquals(ModelSwitchAutoTierStatus.PENDING, result.status()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.BALANCE, result.effectiveAutoTier()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.INTELLIGENCE, result.pendingAutoTier()); + assertNull(result.activatingAutoTier()); + assertEquals(com.github.copilot.generated.rpc.AutoTier.EFFICIENCY, result.supersededAutoTier()); + } + + /** + * Loopback socket pair; the client side backs a real {@link JsonRpcClient} and + * the server side exposes the raw outbound messages. + */ + private static final class SocketPair implements AutoCloseable { + + private final Socket clientSocket; + private final Socket serverSocket; + private final JsonRpcClient rpcClient; + + SocketPair() throws Exception { + try (var ss = new ServerSocket(0)) { + clientSocket = new Socket("localhost", ss.getLocalPort()); + serverSocket = ss.accept(); + } + serverSocket.setSoTimeout(3000); + rpcClient = JsonRpcClient.fromSocket(clientSocket); + } + + JsonRpcClient client() { + return rpcClient; + } + + StubServer stubServer() { + return new StubServer(serverSocket); + } + + @Override + public void close() throws Exception { + rpcClient.close(); + clientSocket.close(); + serverSocket.close(); + } + } + + /** Reads Content-Length framed JSON-RPC messages from the server socket. */ + private static final class StubServer { + + private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper(); + + private final InputStream in; + + StubServer(Socket socket) { + try { + this.in = socket.getInputStream(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + JsonNode readOneMessage() throws Exception { + var header = new StringBuilder(); + int b; + while ((b = in.read()) != -1) { + if (b == '\n' && header.toString().endsWith("\r")) { + break; + } + header.append((char) b); + } + in.read(); + in.read(); + + String hdr = header.toString().trim(); + int colon = hdr.indexOf(':'); + int len = Integer.parseInt(hdr.substring(colon + 1).trim()); + byte[] body = in.readNBytes(len); + return MAPPER.readTree(body); + } + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java b/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java index 925fd6d873..e786bda994 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionConfigE2ETest.java @@ -125,7 +125,7 @@ void testShouldForwardProviderWireModel() throws Exception { try (CopilotClient client = ctx.createClient()) { CopilotSession session = client - .createSession(new SessionConfig().setModel("claude-sonnet-4.5") + .createSession(new SessionConfig().setModel("claude-sonnet-5") .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) .setApiKey("test-provider-key").setWireModel("test-wire-model") .setMaxOutputTokens(1024)) @@ -149,7 +149,7 @@ void testShouldUseProviderModelIdAsWireModel() throws Exception { try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(new SessionConfig() .setProvider(new ProviderConfig().setType("openai").setBaseUrl(ctx.getProxyUrl()) - .setApiKey("test-provider-key").setModelId("claude-sonnet-4.5")) + .setApiKey("test-provider-key").setModelId("claude-sonnet-5")) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(30, TimeUnit.SECONDS); @@ -158,7 +158,7 @@ void testShouldUseProviderModelIdAsWireModel() throws Exception { assertFalse(exchanges.isEmpty(), "Should have at least one exchange"); @SuppressWarnings("unchecked") Map request = (Map) exchanges.get(0).get("request"); - assertEquals("claude-sonnet-4.5", request.get("model")); + assertEquals("claude-sonnet-5", request.get("model")); } } @@ -272,7 +272,7 @@ void testShouldEnableCitationsForAnthropicFileAttachmentsOnCreate() throws Excep var handler = new CopilotRequestTestSupport.RecordingRequestHandler(SYNTHETIC_TEXT); try (CopilotClient client = newLlmClient(ctx, handler)) { - CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-4.5") + CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-5") .setEnableCitations(true).setProvider(createAnthropicProvider()) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); @@ -296,7 +296,7 @@ void testShouldEnableCitationsForAnthropicFileAttachmentsOnResume() throws Excep CopilotSession session1 = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); CopilotSession session2 = client.resumeSession(session1.getSessionId(), - new ResumeSessionConfig().setModel("claude-sonnet-4.5").setEnableCitations(true) + new ResumeSessionConfig().setModel("claude-sonnet-5").setEnableCitations(true) .setProvider(createAnthropicProvider()) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) .get(); @@ -388,7 +388,7 @@ private static BlobAttachment createPdfAttachment() { private static ProviderConfig createAnthropicProvider() { return new ProviderConfig().setType("anthropic").setBaseUrl("https://anthropic-citations.invalid/v1") - .setApiKey("test-provider-key").setModelId("claude-sonnet-4.5").setWireModel("claude-sonnet-4.5"); + .setApiKey("test-provider-key").setModelId("claude-sonnet-5").setWireModel("claude-sonnet-5"); } private static String singleInferenceRequestBody(CopilotRequestTestSupport.RecordingRequestHandler handler) { diff --git a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java index b75e710720..83913e82b1 100644 --- a/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java +++ b/java/sdk/src/test/java/com/github/copilot/SessionEventHandlingTest.java @@ -10,9 +10,12 @@ import java.io.Closeable; import java.lang.reflect.Method; +import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -26,11 +29,15 @@ import com.github.copilot.generated.SessionEvent; import com.github.copilot.generated.AssistantMessageEvent; +import com.github.copilot.generated.ExternalToolCompletedEvent; +import com.github.copilot.generated.ExternalToolRequestedEvent; import com.github.copilot.generated.SessionIdleEvent; import com.github.copilot.generated.SessionMode; import com.github.copilot.generated.SessionStartEvent; +import com.github.copilot.generated.rpc.SessionToolsGetCurrentMetadataResult; import com.github.copilot.rpc.MessageOptions; import com.github.copilot.rpc.SendMessageResponse; +import com.github.copilot.rpc.ToolDefinition; /** * Unit tests for session event handling API. @@ -50,10 +57,14 @@ void setup() throws Exception { } private CopilotSession createTestSession() throws Exception { + return createTestSession(null); + } + + private CopilotSession createTestSession(JsonRpcClient rpc) throws Exception { // Use the package-private constructor via reflection for testing var constructor = CopilotSession.class.getDeclaredConstructor(String.class, JsonRpcClient.class, String.class); constructor.setAccessible(true); - return constructor.newInstance("test-session-id", null, null); + return constructor.newInstance("test-session-id", rpc, null); } @Test @@ -73,6 +84,103 @@ void testGenericEventHandler() { assertInstanceOf(SessionIdleEvent.class, receivedEvents.get(2)); } + @Test + void testExternalToolCompletedCancelsBlockedHandler() throws Exception { + var toolFuture = new CompletableFuture(); + var started = new CountDownLatch(1); + session.registerTools(List.of(ToolDefinition.create("blocked_tool", "Blocks", Map.of(), invocation -> { + started.countDown(); + return toolFuture; + }))); + + var requested = new ExternalToolRequestedEvent(); + requested.setData(new ExternalToolRequestedEvent.ExternalToolRequestedEventData("request-1", "test-session-id", + "tool-call-1", "blocked_tool", null, Map.of(), null, null, null)); + dispatchEvent(requested); + assertTrue(started.await(1, TimeUnit.SECONDS)); + + var completed = new ExternalToolCompletedEvent(); + completed.setData(new ExternalToolCompletedEvent.ExternalToolCompletedEventData("request-1")); + dispatchEvent(completed); + + assertThrows(CancellationException.class, () -> toolFuture.get(1, TimeUnit.SECONDS)); + } + + @Test + void testCloseCancelsBlockedExternalTool() throws Exception { + var rpc = mock(JsonRpcClient.class); + when(rpc.invoke(eq("session.detach"), any(), eq(CopilotSession.SessionDetachResponse.class))) + .thenReturn(CompletableFuture.completedFuture(new CopilotSession.SessionDetachResponse(true, null))); + session = createTestSession(rpc); + var toolFuture = new CompletableFuture(); + var started = new CountDownLatch(1); + session.registerTools(List.of(ToolDefinition.create("blocked_tool", "Blocks", Map.of(), invocation -> { + started.countDown(); + return toolFuture; + }))); + + var requested = new ExternalToolRequestedEvent(); + requested.setData(new ExternalToolRequestedEvent.ExternalToolRequestedEventData("request-close", + "test-session-id", "tool-call-close", "blocked_tool", null, Map.of(), null, null, null)); + dispatchEvent(requested); + assertTrue(started.await(1, TimeUnit.SECONDS)); + + session.close(); + + assertThrows(CancellationException.class, () -> toolFuture.get(1, TimeUnit.SECONDS)); + } + + @Test + void testExternalToolCompletedDoesNotBlockOnSynchronousHandler() throws Exception { + var started = new CountDownLatch(1); + var release = new CountDownLatch(1); + session.registerTools( + List.of(ToolDefinition.create("blocked_tool", "Blocks synchronously", Map.of(), invocation -> { + started.countDown(); + try { + release.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + return CompletableFuture.completedFuture("done"); + }))); + + var requested = new ExternalToolRequestedEvent(); + requested.setData(new ExternalToolRequestedEvent.ExternalToolRequestedEventData("request-sync", + "test-session-id", "tool-call-sync", "blocked_tool", null, Map.of(), null, null, null)); + dispatchEvent(requested); + assertTrue(started.await(1, TimeUnit.SECONDS)); + + var completed = new ExternalToolCompletedEvent(); + completed.setData(new ExternalToolCompletedEvent.ExternalToolCompletedEventData("request-sync")); + try { + assertTimeoutPreemptively(Duration.ofSeconds(1), () -> dispatchEvent(completed)); + } finally { + release.countDown(); + } + } + + @Test + void testToolSearchRunsWhenMetadataResultIsNull() throws Exception { + var rpc = mock(JsonRpcClient.class); + CompletableFuture metadata = CompletableFuture.completedFuture(null); + when(rpc.invoke(eq("session.tools.getCurrentMetadata"), any(), eq(SessionToolsGetCurrentMetadataResult.class))) + .thenReturn(metadata); + session = createTestSession(rpc); + var invoked = new CountDownLatch(1); + session.registerTools(List.of(ToolDefinition.create("tool_search_tool", "Searches", Map.of(), invocation -> { + invoked.countDown(); + return new CompletableFuture<>(); + }))); + + var requested = new ExternalToolRequestedEvent(); + requested.setData(new ExternalToolRequestedEvent.ExternalToolRequestedEventData("request-search", + "test-session-id", "tool-call-search", "tool_search_tool", null, Map.of(), null, null, null)); + dispatchEvent(requested); + + assertTrue(invoked.await(1, TimeUnit.SECONDS)); + } + @Test void testTypedEventHandler() { var receivedMessages = new ArrayList(); @@ -96,8 +204,8 @@ void testSendAndWaitSkipsAutopilotContinuationIdle() throws Exception { var rpc = mock(JsonRpcClient.class); when(rpc.invoke(eq("session.send"), any(), eq(SendMessageResponse.class))) .thenReturn(CompletableFuture.completedFuture(new SendMessageResponse("message-1"))); - when(rpc.invoke(eq("session.destroy"), any(), eq(Void.class))) - .thenReturn(CompletableFuture.completedFuture(null)); + when(rpc.invoke(eq("session.detach"), any(), eq(CopilotSession.SessionDetachResponse.class))) + .thenReturn(CompletableFuture.completedFuture(new CopilotSession.SessionDetachResponse(true, null))); session = new CopilotSession("test-session-id", rpc); try { diff --git a/java/sdk/src/test/java/com/github/copilot/TestUtil.java b/java/sdk/src/test/java/com/github/copilot/TestUtil.java index 23bb53e493..8e90c2f48f 100644 --- a/java/sdk/src/test/java/com/github/copilot/TestUtil.java +++ b/java/sdk/src/test/java/com/github/copilot/TestUtil.java @@ -5,9 +5,12 @@ package com.github.copilot; import java.io.BufferedReader; +import java.io.File; import java.io.InputStreamReader; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.regex.Pattern; /** * Shared test utilities for locating the Copilot CLI binary and other @@ -37,10 +40,9 @@ public static String tempPath(String filename) { * Resolution order: *
    *
  1. Use the {@code COPILOT_CLI_PATH} environment variable when set.
  2. + *
  3. Prepare the release pinned by {@code nodejs/package.json}.
  4. *
  5. Otherwise search the system PATH using {@code where.exe} (Windows) or * {@code which} (Linux/macOS).
  6. - *
  7. Walk parent directories looking for - * {@code nodejs/node_modules/@github/copilot/npm-loader.js}.
  8. *
* *

@@ -60,43 +62,54 @@ static String findCliPath() { return envPath; } - String copilotInPath = findCopilotInPath(); - if (copilotInPath != null) { - return copilotInPath; - } - - // Walk parent directories looking for the CLI in the test harness or nodejs - // installation. Mirrors the resolution order in E2ETestContext.getCliPath(). - String os = System.getProperty("os.name").toLowerCase(); - String arch = System.getProperty("os.arch").toLowerCase(); - String platform = os.contains("mac") ? "darwin" : os.contains("win") ? "win32" : "linux"; - String cpuArch = arch.contains("aarch64") || arch.contains("arm64") ? "arm64" : "x64"; - String binaryName = os.contains("win") ? "copilot.exe" : "copilot"; - Path current = Paths.get(System.getProperty("user.dir")); while (current != null) { - // Test harness platform-specific binary - Path platformBinary = current.resolve( - "test/harness/node_modules/@github/copilot-" + platform + "-" + cpuArch + "/" + binaryName); - if (platformBinary.toFile().exists()) { - return platformBinary.toString(); + if (current.resolve("nodejs/package.json").toFile().exists()) { + try { + return preparePinnedCli(current); + } catch (Exception preparationFailed) { + break; + } } + current = current.getParent(); + } - // Test harness npm-loader.js - Path npmLoader = current.resolve("test/harness/node_modules/@github/copilot/npm-loader.js"); - if (npmLoader.toFile().exists()) { - return npmLoader.toString(); - } + return findCopilotInPath(); + } - // nodejs installation (thin loader; resolves the platform-specific - // CLI package internally) - Path cliPath = current.resolve("nodejs/node_modules/@github/copilot/npm-loader.js"); - if (cliPath.toFile().exists()) { - return cliPath.toString(); - } - current = current.getParent(); + static String preparePinnedCli(Path repoRoot) throws Exception { + var nodePath = findExecutableInPath("node"); + if (nodePath == null) { + throw new IllegalStateException("Node.js was not found in PATH"); + } + var process = new ProcessBuilder(nodePath, "node_modules/tsx/dist/cli.mjs", "scripts/prepare-runtime.ts", + "--print-path").directory(repoRoot.resolve("nodejs").toFile()).redirectErrorStream(true).start(); + String output; + try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + output = reader.lines().reduce((first, second) -> second).orElse("").trim(); + } + int exitCode = process.waitFor(); + if (exitCode != 0 || output.isEmpty()) { + throw new IllegalStateException("Failed to prepare the pinned Copilot CLI: " + output); + } + return output; + } + + private static String findExecutableInPath(String name) { + var pathValue = System.getenv("PATH"); + if (pathValue == null || pathValue.isEmpty()) { + return null; } + var windows = System.getProperty("os.name").toLowerCase().contains("win"); + var fileName = windows ? name + ".exe" : name; + for (var directory : pathValue.split(Pattern.quote(File.pathSeparator))) { + var unquotedDirectory = directory.replaceAll("^\"|\"$", ""); + var candidate = Paths.get(unquotedDirectory, fileName).toAbsolutePath().normalize(); + if (Files.isRegularFile(candidate) && (windows || Files.isExecutable(candidate))) { + return candidate.toString(); + } + } return null; } diff --git a/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java b/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java index 17e1851bb4..01530bc25f 100644 --- a/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java +++ b/java/sdk/src/test/java/com/github/copilot/TimeoutEdgeCaseTest.java @@ -10,7 +10,11 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; import java.net.Socket; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; @@ -34,29 +38,46 @@ public class TimeoutEdgeCaseTest { /** - * Creates a {@link JsonRpcClient} whose {@code invoke()} returns futures that - * never complete. The reader thread blocks forever on the input stream, and - * writes go to a no-op output stream. + * Creates a {@link JsonRpcClient} whose prompt requests never complete but + * whose cleanup detach request succeeds. */ private JsonRpcClient createHangingRpcClient() throws Exception { - InputStream blockingInput = new InputStream() { + PipedInputStream input = new PipedInputStream(); + PipedOutputStream responseWriter = new PipedOutputStream(input); + OutputStream sinkOutput = new ByteArrayOutputStream() { @Override - public int read() throws IOException { + public synchronized void flush() throws IOException { + super.flush(); + // Each JsonRpcClient.sendMessage() call writes exactly one + // complete message before calling flush(), so the buffer must + // be cleared after every flush; otherwise a later request's + // "id" lookup can match a stale, already-processed message + // still sitting in the buffer. try { - Thread.sleep(Long.MAX_VALUE); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - return -1; + String request = toString(StandardCharsets.UTF_8); + if (!request.contains("\"method\":\"session.detach\"")) { + return; + } + + int idIndex = request.indexOf("\"id\":"); + int idEnd = request.indexOf(",", idIndex); + String id = request.substring(idIndex + "\"id\":".length(), idEnd); + String response = "{\"jsonrpc\":\"2.0\",\"id\":" + id + ",\"result\":{\"success\":true}}"; + byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8); + responseWriter.write( + ("Content-Length: " + responseBytes.length + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); + responseWriter.write(responseBytes); + responseWriter.flush(); + } finally { + reset(); } - return -1; } }; - ByteArrayOutputStream sinkOutput = new ByteArrayOutputStream(); var ctor = JsonRpcClient.class.getDeclaredConstructor(InputStream.class, java.io.OutputStream.class, Socket.class, Process.class); ctor.setAccessible(true); - return (JsonRpcClient) ctor.newInstance(blockingInput, sinkOutput, null, null); + return (JsonRpcClient) ctor.newInstance(input, sinkOutput, null, null); } /** @@ -64,7 +85,7 @@ public int read() throws IOException { * completed by a stale timeout. *

* Contract: {@code close()} shuts down the timeout scheduler before the - * blocking {@code session.destroy} RPC call, so any pending timeout task is + * blocking {@code session.detach} RPC call, so any pending timeout task is * cancelled and the future remains incomplete (not exceptionally completed with * {@code TimeoutException}). */ @@ -79,7 +100,7 @@ void testTimeoutDoesNotFireAfterSessionClose() throws Exception { assertFalse(result.isDone(), "Future should be pending before timeout fires"); - // close() blocks up to 5s on session.destroy RPC. The 2s timeout + // close() blocks up to 5s on session.detach RPC. The 2s timeout // fires during that window with the current per-call scheduler. session.close(); diff --git a/java/sdk/src/test/java/com/github/copilot/ZeroTimeoutContractTest.java b/java/sdk/src/test/java/com/github/copilot/ZeroTimeoutContractTest.java index 3d986566dc..25a0e892cc 100644 --- a/java/sdk/src/test/java/com/github/copilot/ZeroTimeoutContractTest.java +++ b/java/sdk/src/test/java/com/github/copilot/ZeroTimeoutContractTest.java @@ -31,9 +31,9 @@ void sendAndWaitWithZeroTimeoutShouldNotTimeOut() throws Exception { var mockRpc = mock(JsonRpcClient.class); when(mockRpc.invoke(any(), any(), any())).thenAnswer(invocation -> { Object method = invocation.getArgument(0); - if ("session.destroy".equals(method)) { - // Make session.close() non-blocking by completing destroy immediately - return CompletableFuture.completedFuture(null); + if ("session.detach".equals(method)) { + // Make session.close() non-blocking by completing detach immediately + return CompletableFuture.completedFuture(new CopilotSession.SessionDetachResponse(true, null)); } // For other calls (e.g., message send), return an incomplete future so the // sendAndWait result does not complete due to a mock response. diff --git a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java index 64eff21726..fd3f92100d 100644 --- a/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java +++ b/java/sdk/src/test/java/com/github/copilot/e2e/RewindIT.java @@ -60,10 +60,8 @@ void shouldRestoreTrackedFileAndConversation() throws Exception { Files.writeString(filePath, ORIGINAL_FILE_CONTENT); try (CopilotClient client = ctx.createClient(); - CopilotSession session = client - .createSession( - new SessionConfig().setModel("claude-sonnet-4.5").setEnableFileChangeTracking(true) - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) + CopilotSession session = client.createSession(new SessionConfig().setModel("claude-sonnet-5") + .setEnableFileChangeTracking(true).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)) .get(30, TimeUnit.SECONDS)) { AssistantMessageEvent ready = session .sendAndWait(new MessageOptions().setPrompt("Use the edit tool to replace the exact contents of " diff --git a/java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java b/java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java new file mode 100644 index 0000000000..3ef4770562 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/MessageIdentitySerializationTest.java @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.UserMessageEvent.UserMessageEventData; +import com.github.copilot.generated.rpc.QueuePendingItems; + +class MessageIdentitySerializationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void testQueuePendingMessageIdUsesCamelCaseAndIsOptional() throws Exception { + var item = MAPPER.readValue(""" + { + "id": "queue-1", + "messageId": "message-1", + "kind": "message", + "displayText": "hello", + "agentMode": "interactive" + } + """, QueuePendingItems.class); + + assertEquals("message-1", item.messageId()); + assertEquals("message-1", MAPPER.valueToTree(item).get("messageId").textValue()); + + var olderItem = MAPPER.readValue(""" + { + "id": "queue-2", + "kind": "command", + "displayText": "/help", + "agentMode": "interactive" + } + """, QueuePendingItems.class); + + assertNull(olderItem.messageId()); + assertFalse(MAPPER.valueToTree(olderItem).has("messageId")); + } + + @Test + void testUserMessageIdUsesCamelCaseAndIsOptional() throws Exception { + var message = MAPPER.readValue(""" + {"content": "hello", "messageId": "message-1"} + """, UserMessageEventData.class); + + assertEquals("message-1", message.messageId()); + assertEquals("message-1", MAPPER.valueToTree(message).get("messageId").textValue()); + + var olderMessage = MAPPER.readValue(""" + {"content": "hello"} + """, UserMessageEventData.class); + + assertNull(olderMessage.messageId()); + assertFalse(MAPPER.valueToTree(olderMessage).has("messageId")); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java new file mode 100644 index 0000000000..22af474624 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/CatalogCandidateJacksonTest.java @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated.rpc; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +class CatalogCandidateJacksonTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void searchResult_deserializesTypedCandidatesAndSources() throws Exception { + var json = """ + { + "kind": "succeeded", + "searchId": "search-1", + "candidates": [ + { + "handle": "mcp-handle", + "handleExpiresAt": "2026-09-04T14:00:00Z", + "kind": "mcp-server", + "mediaType": "application/mcp-server-card+json", + "installability": "installable", + "displayName": "Example MCP server", + "source": { + "kind": "url", + "url": "https://example.test/server.json" + }, + "provenance": { + "authority": "example.test", + "observedAt": "2026-09-04T13:00:00Z", + "mediaType": "application/mcp-server-card+json" + } + }, + { + "handle": "skill-handle", + "handleExpiresAt": "2026-09-04T14:00:00Z", + "kind": "ai-skill", + "mediaType": "application/ai-skill", + "installability": "not-installable-kind", + "displayName": "Example skill", + "source": { + "kind": "embedded" + }, + "provenance": { + "authority": "example.test", + "observedAt": "2026-09-04T13:00:00Z", + "mediaType": "application/ai-skill" + } + } + ], + "truncated": false, + "negotiated": { + "runtimeProtocolVersion": 1, + "grantedCapabilities": ["mcp-server-card", "ai-skill-discovery"] + } + } + """; + + var result = MAPPER.readValue(json, CatalogSearchResult.class); + var succeeded = assertInstanceOf(CatalogSearchSucceeded.class, result); + assertEquals(2, succeeded.getCandidates().size()); + + var mcp = assertInstanceOf(CatalogMcpServerCandidate.class, succeeded.getCandidates().get(0)); + var urlSource = assertInstanceOf(CatalogCandidateSourceUrl.class, mcp.getSource()); + assertEquals("https://example.test/server.json", urlSource.getUrl()); + + var skill = assertInstanceOf(CatalogAiSkillCandidate.class, succeeded.getCandidates().get(1)); + assertInstanceOf(CatalogCandidateSourceEmbedded.class, skill.getSource()); + + var serializedJson = MAPPER.writeValueAsString(result); + assertEquals(6, serializedJson.split("\"kind\"", -1).length - 1); + + var serializedTree = MAPPER.readTree(serializedJson); + assertEquals("mcp-server", serializedTree.at("/candidates/0/kind").asText()); + assertEquals("url", serializedTree.at("/candidates/0/source/kind").asText()); + assertEquals("ai-skill", serializedTree.at("/candidates/1/kind").asText()); + assertEquals("embedded", serializedTree.at("/candidates/1/source/kind").asText()); + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java index fd0fdaaea1..191c290026 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcApiCoverageTest.java @@ -97,7 +97,7 @@ void serverRpc_mcp_config_remove_invokes_correct_method() { var stub = new StubCaller(); var server = new ServerRpc(stub); - var params = new McpConfigRemoveParams("myServer"); + var params = new McpConfigRemoveParams("myServer", null); server.mcp.config.remove(params); assertEquals(1, stub.calls.size()); diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index 602089d012..938e0609f6 100644 --- a/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -48,7 +48,7 @@ void mcpDiscoverParams_record() { @Test void mcpConfigRemoveParams_record() { - var params = new McpConfigRemoveParams("old-server"); + var params = new McpConfigRemoveParams("old-server", null); assertEquals("old-server", params.name()); } @@ -326,10 +326,10 @@ void sessionModelGetCurrentParams_record() { @Test void sessionModelSwitchToParams_record() { - var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-4.5", "high", null, null, null, null, + var params = new SessionModelSwitchToParams("sess-32", "claude-sonnet-5", null, "high", null, null, null, null, null, null, null, null, null, null, null, null); assertEquals("sess-32", params.sessionId()); - assertEquals("claude-sonnet-4.5", params.modelId()); + assertEquals("claude-sonnet-5", params.modelId()); assertEquals("high", params.reasoningEffort()); assertNull(params.reasoningSummary()); assertNull(params.verbosity()); @@ -470,7 +470,7 @@ void pingResult_fields() { @Test void sessionAgentListResult_with_items() { var item = new AgentInfo("name1", "Name One", "Desc 1", "/path/to/agent1", null, null, null, null, null, null, - null, null); + null, null, null, null); var result = new SessionAgentListResult(List.of(item)); assertEquals(1, result.agents().size()); assertEquals("name1", result.agents().get(0).name()); @@ -482,7 +482,7 @@ void sessionAgentListResult_with_items() { @Test void sessionAgentGetCurrentResult_nested() { var agent = new AgentInfo("agent-1", "Agent One", "Does things", null, null, null, null, null, null, null, null, - null); + null, null, null); var result = new SessionAgentGetCurrentResult(agent); assertEquals("agent-1", result.agent().name()); assertEquals("Agent One", result.agent().displayName()); @@ -498,7 +498,8 @@ void sessionAgentGetCurrentResult_null_agent() { @Test void sessionAgentReloadResult_with_items() { - var item = new AgentInfo("a", "A", "Desc", "/path/to/a", null, null, null, null, null, null, null, null); + var item = new AgentInfo("a", "A", "Desc", "/path/to/a", null, null, null, null, null, null, null, null, null, + null); var result = new SessionAgentReloadResult(List.of(item)); assertEquals(1, result.agents().size()); assertEquals("a", result.agents().get(0).name()); @@ -507,7 +508,7 @@ void sessionAgentReloadResult_with_items() { @Test void sessionAgentSelectResult_nested() { var agent = new AgentInfo("selected", "Selected", "The selected agent", "/path/to/selected", null, null, null, - null, null, null, null, null); + null, null, null, null, null, null, null); var result = new SessionAgentSelectResult(agent); assertEquals("selected", result.agent().name()); } @@ -637,12 +638,16 @@ void sessionLogResult_record() { @Test void sessionMcpListResult_nested() { - var server = new McpServer("my-mcp", McpServerStatus.CONNECTED, McpServerSource.USER, null, null, null); + var metadata = new McpServerMetadata("Use this server for repository operations."); + var server = new McpServer("my-mcp", McpServerStatus.CONNECTED, McpServerSource.USER, null, null, null, + metadata); var result = new SessionMcpListResult(List.of(server), null); assertEquals(1, result.servers().size()); assertEquals("my-mcp", result.servers().get(0).name()); assertEquals(McpServerStatus.CONNECTED, result.servers().get(0).status()); assertEquals(McpServerSource.USER, result.servers().get(0).source()); + assertEquals("Use this server for repository operations.", + result.servers().get(0).serverMetadata().instructions()); } @Test @@ -656,13 +661,13 @@ void sessionMcpListResult_status_enum_all_values() { @Test void sessionModelGetCurrentResult_record() { - var result = new SessionModelGetCurrentResult("claude-sonnet-4.5", null, null); - assertEquals("claude-sonnet-4.5", result.modelId()); + var result = new SessionModelGetCurrentResult("claude-sonnet-5", null, null, null, null, null); + assertEquals("claude-sonnet-5", result.modelId()); } @Test void sessionModelSwitchToResult_record() { - var result = new SessionModelSwitchToResult("gpt-5", true, null, null, null, null, null, null); + var result = new SessionModelSwitchToResult("gpt-5", true, null, null, null, null, null, null, null); assertEquals("gpt-5", result.modelId()); assertEquals(true, result.deferred()); } @@ -816,10 +821,10 @@ void modelsListResult_nested() { var limits = new ModelCapabilitiesLimits(100000L, 8192L, 128000L, null); var capabilities = new ModelCapabilities(supports, limits); var policy = new ModelPolicy(ModelPolicyState.ENABLED, null); - var promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount"); + var promo = new ModelBillingPromo("summer-2026", 25.0, "2026-08-01T00:00:00Z", "Summer discount", true); var billing = new ModelBilling(1.0, null, null, promo); - var modelItem = new Model("gpt-5", "GPT-5", capabilities, policy, billing, null, null, null, null, null, null, - null, null); + var modelItem = new Model("gpt-5", "GPT-5", capabilities, null, policy, billing, null, null, null, null, null, + null, null, null); var result = new ModelsListResult(List.of(modelItem)); assertEquals(1, result.models().size()); @@ -834,6 +839,7 @@ void modelsListResult_nested() { assertEquals(Double.valueOf(25.0), result.models().get(0).billing().promo().discountPercent()); assertEquals("2026-08-01T00:00:00Z", result.models().get(0).billing().promo().endsAt()); assertEquals("Summer discount", result.models().get(0).billing().promo().message()); + assertTrue(result.models().get(0).billing().promo().showBanner()); } @Test @@ -855,8 +861,8 @@ void sessionModelSwitchToParams_nested_records() { var limits = new ModelCapabilitiesOverrideLimits(100000L, 8192L, 128000L, limitsVision); var supports = new ModelCapabilitiesOverrideSupports(true, true, null); var capabilities = new ModelCapabilitiesOverride(supports, limits); - var params = new SessionModelSwitchToParams("sess-m", "gpt-5", null, null, null, capabilities, null, null, null, - null, null, null, null, null, null); + var params = new SessionModelSwitchToParams("sess-m", "gpt-5", null, null, null, null, capabilities, null, null, + null, null, null, null, null, null, null); assertEquals("gpt-5", params.modelId()); assertNotNull(params.modelCapabilities()); diff --git a/java/sdk/src/test/java/com/github/copilot/generated/rpc/SandboxConfigSerializationTest.java b/java/sdk/src/test/java/com/github/copilot/generated/rpc/SandboxConfigSerializationTest.java new file mode 100644 index 0000000000..c34734c9a6 --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/generated/rpc/SandboxConfigSerializationTest.java @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.generated.rpc; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; + +class SandboxConfigSerializationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void allowBypassRoundTripsAndIsOmittedWhenAbsent() throws Exception { + var configured = MAPPER.readValue(""" + {"enabled":true,"allowBypass":true} + """, SandboxConfig.class); + + assertEquals(Boolean.TRUE, configured.allowBypass()); + var configuredJson = MAPPER.readTree(MAPPER.writeValueAsString(configured)); + assertTrue(configuredJson.path("allowBypass").asBoolean()); + + var omitted = MAPPER.readValue(""" + {"enabled":true} + """, SandboxConfig.class); + var omittedJson = MAPPER.readTree(MAPPER.writeValueAsString(omitted)); + assertTrue(omittedJson.path("allowBypass").isMissingNode()); + } +} diff --git a/justfile b/justfile index c84166862f..69666bc00a 100644 --- a/justfile +++ b/justfile @@ -9,7 +9,7 @@ format: format-go format-python format-nodejs format-dotnet format-rust lint: lint-go lint-python lint-nodejs lint-dotnet lint-rust # Run tests for all languages -test: test-go test-python test-nodejs test-dotnet test-rust test-corrections +test: test-go test-python test-nodejs test-dotnet test-rust test-harness test-corrections # Format Go code format-go: @@ -66,6 +66,11 @@ test-nodejs: @echo "=== Testing Node.js code ===" @cd nodejs && npm test +# Run test harness tests +test-harness: + @echo "=== Testing test harness ===" + @cd test/harness && npm test + # Test .NET code test-dotnet: @echo "=== Testing .NET code ===" @@ -168,4 +173,3 @@ validate-docs-go: validate-docs-cs: @echo "=== Validating C# documentation ===" @cd scripts/docs-validation && npm run validate:cs - diff --git a/nodejs/README.md b/nodejs/README.md index 5886df234f..e3d76ba6e4 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -8,6 +8,20 @@ To use the SDK, you'll need: - Node.js ^20.19.0 or >=22.12.0 +The SDK uses an optional `@github/copilot-sdk-` package containing the +Copilot CLI runtime for the host platform. These packages are built from +verified `github/copilot-cli` release assets when the SDK is published, so +starting the SDK performs no runtime download. Set `COPILOT_CLI_PATH` to use an +existing installation instead. + +The checked-in release pin is `copilotCliVersion` in `package.json`. Run +`npm run set:cli-version -- ` to update it and regenerate the compiled +metadata in `src/cliVersion.ts`. Packaging verifies release assets against the +release's `SHA256SUMS.txt`. + +`npm run pack:release` builds the main package and all platform packages. Set +`COPILOT_CLI_DOWNLOAD_BASE_URL` to use a release mirror while packaging. + ## Installation ```bash @@ -22,6 +36,7 @@ Try the interactive chat sample (from the repo root): cd nodejs npm ci npm run build +export COPILOT_CLI_PATH="$(npm run --silent prepare:runtime -- --print-path)" cd samples npm install npm start @@ -132,6 +147,7 @@ Create a new conversation session. - `sessionId?: string` - Custom session ID. - `model?: string` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.** +- `capi?: CapiSessionOptions` - Copilot API options. With `model: "auto"`, set `autoTier` to `"efficiency"`, `"balance"`, or `"intelligence"` to choose a routing preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics. - `reasoningEffort?: "low" | "medium" | "high" | "xhigh" | "max"` - Reasoning effort level for models that support it. Use `listModels()` to check which models support this option. - `tools?: Tool[]` - Custom tools exposed to the CLI. Tools without `handler` are declaration-only and must be resolved via pending tool-call RPCs. - `systemMessage?: SystemMessageConfig` - System message customization (see below) @@ -303,6 +319,32 @@ const unsubscribe = session.on((event) => { unsubscribe(); ``` +##### `setModel(model: string, options?): Promise` + +Change the model for this session. The new model takes effect for the next message; conversation history is preserved. + +**Options:** + +- `reasoningEffort?: string` - Reasoning effort level +- `autoTier?: AutoTier | null` - Auto routing preference to stage together with selecting `auto`. Pass `null` to return to the provider's default Auto routing; omit it to leave the current preference unchanged. + +##### `setAutoTier(autoTier: AutoTier | null): Promise` + +Change the Auto routing preference without changing the selected model. Pass `null` to return to the provider's default Auto routing. + +The runtime does not apply the preference immediately. It records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure, and read the authoritative state at any time with `session.rpc.model.getCurrent()`. + +```typescript +const result = await session.setAutoTier("intelligence"); +if (result.status === "pending") { + // Accepted, but not yet in effect. +} +``` + +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. + ##### `abort(): Promise` Abort the currently processing message in this session. @@ -935,15 +977,15 @@ const session = await client.createSession({ The handler must return one of the `PermissionDecision` shapes (or `{ kind: "no-result" }`). Approval scopes are present-tense — they describe the decision to apply, not the outcome reported back on session events: -| Kind | Meaning | Extra fields | -| ------------------------ | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -| `"approve-once"` | Allow this single request | — | -| `"approve-for-session"` | Allow this request and remember the approval for the rest of the session | `approval?` (rule to remember), `domain?` (for URL approvals) | -| `"approve-for-location"` | Allow this request and persist the approval for this project location (git root or cwd) | `approval` (rule to persist), `locationKey` (location to persist under) | -| `"approve-permanently"` | Allow this request and persist the approval across sessions (currently used for URL domains) | `domain` (URL domain to approve) | -| `"reject"` | Deny the request | `feedback?` (optional string surfaced to the agent) | -| `"user-not-available"` | Deny the request because no user is available to confirm it | — | -| `"no-result"` | Suppress this SDK client's response so another connected client can answer the pending request | — | +| Kind | Meaning | Extra fields | +| ------------------------ | ---------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| `"approve-once"` | Allow this single request | — | +| `"approve-for-session"` | Allow this request and remember the approval for the rest of the session | `approval?` (rule to remember), `domain?` (for URL approvals) | +| `"approve-for-location"` | Allow this request and persist the approval for this project location (git root or cwd) | `approval` (rule to persist), `locationKey` (location to persist under) | +| `"approve-permanently"` | Allow this request and persist the approval across sessions (currently used for URL domains) | `domain` (URL domain to approve) | +| `"reject"` | Deny the request | `feedback?` (optional string surfaced to the agent) | +| `"user-not-available"` | Deny the request because no user is available to confirm it | — | +| `"no-result"` | Suppress this SDK client's response so another connected client can answer the pending request | — | ### Resuming Sessions diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 6480e85e29..a10153a1d3 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-0", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -29,6 +28,7 @@ "quicktype-core": "^23.2.6", "rimraf": "^6.1.2", "semver": "^7.7.3", + "tar": "^7.5.22", "tsx": "^4.20.6", "typescript": "^5.0.0", "vitest": "^4.0.18", @@ -57,7 +57,7 @@ }, "node_modules/@emnapi/core": { "version": "1.10.0", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "integrity": "sha1-OAzMjyQS6iLR2XLff47iOjucdGc=", "dev": true, "license": "MIT", "optional": true, @@ -68,7 +68,7 @@ }, "node_modules/@emnapi/runtime": { "version": "1.10.0", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "integrity": "sha1-SyYMDTU0IE6YxhELjbGph9JuyHw=", "dev": true, "license": "MIT", "optional": true, @@ -78,7 +78,7 @@ }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "integrity": "sha1-KP7SGhuhznl8RKBwq8lNQvOuhUg=", "dev": true, "license": "MIT", "optional": true, @@ -88,7 +88,7 @@ }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "integrity": "sha1-egGo0uwvuy2seK2tCbD6eB5Agr4=", "cpu": [ "ppc64" ], @@ -104,7 +104,7 @@ }, "node_modules/@esbuild/android-arm": { "version": "0.28.1", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "integrity": "sha1-cEvSl95tdi3lTqu+r79V9nVqvi8=", "cpu": [ "arm" ], @@ -120,7 +120,7 @@ }, "node_modules/@esbuild/android-arm64": { "version": "0.28.1", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "integrity": "sha1-tUCifRTkr9BYSWpNvsTT9BTbEQo=", "cpu": [ "arm64" ], @@ -136,7 +136,7 @@ }, "node_modules/@esbuild/android-x64": { "version": "0.28.1", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "integrity": "sha1-0csWbTSw+/D+irRgpVlPJKN4cB4=", "cpu": [ "x64" ], @@ -168,7 +168,7 @@ }, "node_modules/@esbuild/darwin-x64": { "version": "0.28.1", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "integrity": "sha1-ZVVqQyoeTXIDLYIYwZMvzKGkl3I=", "cpu": [ "x64" ], @@ -184,7 +184,7 @@ }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.28.1", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "integrity": "sha1-LmHgWS+QMNfj2uGO4l68U1kYrvY=", "cpu": [ "arm64" ], @@ -200,7 +200,7 @@ }, "node_modules/@esbuild/freebsd-x64": { "version": "0.28.1", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "integrity": "sha1-yV7CiZWe+AecTcqBeh4sS+Zrm9M=", "cpu": [ "x64" ], @@ -216,7 +216,7 @@ }, "node_modules/@esbuild/linux-arm": { "version": "0.28.1", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "integrity": "sha1-wJoPZ5F1kqwN6JKpvk04FN69Kmw=", "cpu": [ "arm" ], @@ -232,7 +232,7 @@ }, "node_modules/@esbuild/linux-arm64": { "version": "0.28.1", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "integrity": "sha1-QLIhdd2gYYLz7oFBGGxf8wTEpxc=", "cpu": [ "arm64" ], @@ -248,7 +248,7 @@ }, "node_modules/@esbuild/linux-ia32": { "version": "0.28.1", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "integrity": "sha1-pYD5xnZ5eDOJHlGfx6EzfIr9jbM=", "cpu": [ "ia32" ], @@ -264,7 +264,7 @@ }, "node_modules/@esbuild/linux-loong64": { "version": "0.28.1", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "integrity": "sha1-RkUs8yHcf56Rwvp4Cla7Vuec1os=", "cpu": [ "loong64" ], @@ -280,7 +280,7 @@ }, "node_modules/@esbuild/linux-mips64el": { "version": "0.28.1", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "integrity": "sha1-QhGzGE3WYI9T3LIuOfXTTuCIUsg=", "cpu": [ "mips64el" ], @@ -296,7 +296,7 @@ }, "node_modules/@esbuild/linux-ppc64": { "version": "0.28.1", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "integrity": "sha1-aXhXwqYcubC2u2ZS5AwdxeHKjl0=", "cpu": [ "ppc64" ], @@ -312,7 +312,7 @@ }, "node_modules/@esbuild/linux-riscv64": { "version": "0.28.1", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "integrity": "sha1-0ZKUPrFGpArExkl9DPe+NbmGvwg=", "cpu": [ "riscv64" ], @@ -328,7 +328,7 @@ }, "node_modules/@esbuild/linux-s390x": { "version": "0.28.1", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "integrity": "sha1-rOoDVtoODrwI+Xz3ucLkAeHmSNw=", "cpu": [ "s390x" ], @@ -344,7 +344,7 @@ }, "node_modules/@esbuild/linux-x64": { "version": "0.28.1", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "integrity": "sha1-bww84MtkxTS3DExF7LLBbTTjXf0=", "cpu": [ "x64" ], @@ -360,7 +360,7 @@ }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.28.1", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "integrity": "sha1-i813B3oNzjN4tXT+2ybSolO3PTY=", "cpu": [ "arm64" ], @@ -376,7 +376,7 @@ }, "node_modules/@esbuild/netbsd-x64": { "version": "0.28.1", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "integrity": "sha1-5/sqAemcgwyU5mI82f77TI+1g0c=", "cpu": [ "x64" ], @@ -392,7 +392,7 @@ }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.28.1", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "integrity": "sha1-xSkJNy24uG4sVeBaiUADO1Zgo7I=", "cpu": [ "arm64" ], @@ -408,7 +408,7 @@ }, "node_modules/@esbuild/openbsd-x64": { "version": "0.28.1", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "integrity": "sha1-xCe5vlpkwmL/mn63C1+7qt9EbGw=", "cpu": [ "x64" ], @@ -424,7 +424,7 @@ }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.28.1", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "integrity": "sha1-3JsUe6yi5sSzyFVxdB70hgpIkJc=", "cpu": [ "arm64" ], @@ -440,7 +440,7 @@ }, "node_modules/@esbuild/sunos-x64": { "version": "0.28.1", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "integrity": "sha1-zoZtEt8TwV5MmfBzo9Rm9uBkmzo=", "cpu": [ "x64" ], @@ -456,7 +456,7 @@ }, "node_modules/@esbuild/win32-arm64": { "version": "0.28.1", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "integrity": "sha1-dGjjaS0B1inVlB5dg4F7uA+eObQ=", "cpu": [ "arm64" ], @@ -472,7 +472,7 @@ }, "node_modules/@esbuild/win32-ia32": { "version": "0.28.1", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "integrity": "sha1-pbwAY/sryrbQ7WPyoVN5WLwmnsY=", "cpu": [ "ia32" ], @@ -488,7 +488,7 @@ }, "node_modules/@esbuild/win32-x64": { "version": "0.28.1", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "integrity": "sha1-EAZO5E9DR7kMmgK0Rrv4CpFjKxI=", "cpu": [ "x64" ], @@ -657,147 +657,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@github/copilot": { - "version": "1.0.83-0", - "integrity": "sha512-Nv4IsqsveMgghwaBhgvSBZyIyvsqNBZTqnbVnv69+9+Suyq20vJcv6aB74UcJ7VPCMxIGJJUaJkugEtkMNv6wA==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "detect-libc": "^2.1.2" - }, - "bin": { - "copilot": "npm-loader.js" - }, - "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.83-0", - "@github/copilot-darwin-x64": "1.0.83-0", - "@github/copilot-linux-arm64": "1.0.83-0", - "@github/copilot-linux-x64": "1.0.83-0", - "@github/copilot-linuxmusl-arm64": "1.0.83-0", - "@github/copilot-linuxmusl-x64": "1.0.83-0", - "@github/copilot-win32-arm64": "1.0.83-0", - "@github/copilot-win32-x64": "1.0.83-0" - } - }, - "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-0KQjKS9vd4QGxLAbFJcvyv/zsC5kivrtDe0UZhHt/43nUGqoS61DFcsM596/kg75vNE6c9J4gmZ5fUPYef+0hw==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "copilot-darwin-arm64": "copilot" - } - }, - "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.83-0", - "integrity": "sha512-fiyW+hy4c8AI7ONxN623f9cmJGRpbqTztc0jSVXc9z9WwzcWi39X0nxUprRM2l2Dq6YQ3guPCqGl/g1T5bQfQg==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "darwin" - ], - "bin": { - "copilot-darwin-x64": "copilot" - } - }, - "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-RWbRU+KgEmtAdKp1GQVTqfdwg4Ti/OVmgZGkXq4lMYj3wnBBQcayFpSLHg5ShzDSS0RglD4b8Z27NjPrm7bXxA==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-0", - "integrity": "sha512-5COXUNT+jDfkeyqrymZMvhTogkBYUXt+wuRwKrK6ol5vaw5SoDP1DYbI2hIEfoUj4g7XTHLUCD1s3lw8eicqUA==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-x64": "copilot" - } - }, - "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-7sYf364iz6s97ClviBRQusTKz3S3TgoKniyYv8+aRi5f5w6TL8NTPnGX1bXMeU0VZmk5VKQTlxVRO2yA4uFwpg==", - "cpu": [ - "arm64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-0", - "integrity": "sha512-jze/f6Yd3Y83kxUa88kXUiwHlZmHDwAqudswdHT6f6q+K1ZEELFGEzbB6Ku4i0L8M6wHXO1EF/zaiSFWQaM4Tw==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-x64": "copilot" - } - }, - "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-93jln98UAJpslMQ7n+wAmCpoOWGEV5lXxV/DaEajySvYrCU33D2yj7d9kl8X2CgaUVBas6sNWSWKtqy+rKJxXQ==", - "cpu": [ - "arm64" - ], - "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.83-0", - "integrity": "sha512-+4Htk3CixO1qcOtYegjn33/8bSDdx8QXDpgVBak2D4Y5hzBWPO5IuQoICwvjaW5VOIW+I7Q62RK2pupSjxB38Q==", - "cpu": [ - "x64" - ], - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "copilot-win32-x64": "copilot.exe" - } - }, "node_modules/@glideapps/ts-necessities": { "version": "2.2.3", "integrity": "sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==", @@ -852,6 +711,18 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "integrity": "sha1-LVmuOrSzj7QnC/oj0w+OLobH/jI=", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", @@ -881,7 +752,7 @@ }, "node_modules/@koromix/koffi-darwin-x64": { "version": "3.1.0", - "integrity": "sha512-n/tVRB9xIzdXT5H3zZt8ueThgWTSDL+yU7PWnU8wbZPBSawP/otx3swQyd6nMOqj1bmHgSHopiKSBXRS9pllmg==", + "integrity": "sha1-r1z9mNiPAynDUcq8q6sPqKFY+xs=", "cpu": [ "x64" ], @@ -896,7 +767,7 @@ }, "node_modules/@koromix/koffi-freebsd-arm64": { "version": "3.1.0", - "integrity": "sha512-vazoPYIhOAlXZksVIqDRMIID4VeUZKx8F3dR90hOobT2ATyOkqNS5dv5UCV7Q7DSq22lQTrdbvENBAhROzCp0w==", + "integrity": "sha1-rOu888/eXKu5jj4KgZb0gCzt+xE=", "cpu": [ "arm64" ], @@ -911,7 +782,7 @@ }, "node_modules/@koromix/koffi-freebsd-ia32": { "version": "3.1.0", - "integrity": "sha512-Vm7Uc97ru6RTSVmae2zCZZQeaizqVZ8WoU4+gG4H03Qe+WOj7kbKt/MxT7VBzdbPYIU5ZJeG/ZED1YlZyab6eQ==", + "integrity": "sha1-0mky/r//V0JaXd+q9IRmdsZrkPI=", "cpu": [ "ia32" ], @@ -926,7 +797,7 @@ }, "node_modules/@koromix/koffi-freebsd-x64": { "version": "3.1.0", - "integrity": "sha512-N+VuVWjoiYPy1Go5mRadZ3B6RM5Qz+eCLhj2LXrMlefbUJ+O4gg7teCUGvPGfBEHDgmSN4yYUrfQmdJC10vOYw==", + "integrity": "sha1-2INR65Jz3bqyIPXHyqZ+PRGVkCs=", "cpu": [ "x64" ], @@ -941,7 +812,7 @@ }, "node_modules/@koromix/koffi-linux-arm64": { "version": "3.1.0", - "integrity": "sha512-Wx5iOkeALe2ympLdiYwRpIg5qUkyQIv8N2foZ9rRker0uE7ZtXew2RRkbEgMir4b0yDYR1zyXd6B62GUzLtZ/g==", + "integrity": "sha1-9h8pjdDbtKufG4JXoxDFRNzAbnI=", "cpu": [ "arm64" ], @@ -956,7 +827,7 @@ }, "node_modules/@koromix/koffi-linux-ia32": { "version": "3.1.0", - "integrity": "sha512-1DjYm1QehXU0dgn0uE+FGYOb3Of7GiTMqLS+ZI2gbl1b+h76sz4LRBvDVrQyAmSMVVU8/7696S21YgE/iBhBVg==", + "integrity": "sha1-8AbgHpYQoyx0uSoDgGpHwc1sGvM=", "cpu": [ "ia32" ], @@ -971,7 +842,7 @@ }, "node_modules/@koromix/koffi-linux-loong64": { "version": "3.1.0", - "integrity": "sha512-NOa0LdyltdESz3oeTqUH6MErHVoJOHoeXIsEp6xIMTUh4eKXEtlDQeoK6EYqo0DnBt83Xud95qLvi4Aw12pG4Q==", + "integrity": "sha1-bQvI/dvGGdARiMeDkZg0R+POkPI=", "cpu": [ "loong64" ], @@ -986,7 +857,7 @@ }, "node_modules/@koromix/koffi-linux-riscv64": { "version": "3.1.0", - "integrity": "sha512-Ye6kiXZCGxGtAIXSly6XuOP5tJZNYOZ2eVg33k1MilKrzimAy9Mpw4d6e9+Sfsc1jesgeNYs1sb5iaI8HS3ncA==", + "integrity": "sha1-V5aXxWe4DH2j2r1dnJcMH9w44co=", "cpu": [ "riscv64" ], @@ -1001,7 +872,7 @@ }, "node_modules/@koromix/koffi-linux-x64": { "version": "3.1.0", - "integrity": "sha512-3yQTOkQrMna4VX+yeyfYImBjLlGrItMpsWyfaW1uSiz/A6GRydqdwYH7DWnp4Z+RSGYZpsewkf7byMc8pOOQKA==", + "integrity": "sha1-eJX/wAVe0HJASLRTgubjJrHppuo=", "cpu": [ "x64" ], @@ -1016,7 +887,7 @@ }, "node_modules/@koromix/koffi-openbsd-ia32": { "version": "3.1.0", - "integrity": "sha512-/cDoFHb9yx4+yoT3GUpnKnfi3W2drG+/Ewo0TTZaQHb4PsxnYYyT6V8+t4cL5XXbQcTTcOsZxpmBRrn0NBa3dA==", + "integrity": "sha1-LwhKcVohSKoVCgY945O/4/gyoSk=", "cpu": [ "ia32" ], @@ -1031,7 +902,7 @@ }, "node_modules/@koromix/koffi-openbsd-x64": { "version": "3.1.0", - "integrity": "sha512-CoQdqgnKvWgTXXZlUst8cBRQEov7QsxlTN2WAsu9wez01Xe6gEcH/zYePANualzzCbnaELfe5P0rA80QkoDuPA==", + "integrity": "sha1-wWm472ijVY45okrdH/8EIAVtzAo=", "cpu": [ "x64" ], @@ -1046,7 +917,7 @@ }, "node_modules/@koromix/koffi-win32-ia32": { "version": "3.1.0", - "integrity": "sha512-WjrA+DEkpy0xEHu48+NSOboHhTnzkIfsFuq3d/WrSs+T9WflWRng3jC7mdJxmR4eHb6i6BqjW3k/U0mNUTjFPA==", + "integrity": "sha1-nDF+wS4sK934tD6A6e4I036PANM=", "cpu": [ "ia32" ], @@ -1061,7 +932,7 @@ }, "node_modules/@koromix/koffi-win32-x64": { "version": "3.1.0", - "integrity": "sha512-tnK5+IkzQBauQAQSzuyjso8OOIQRlaTZS39xIWpfqVYDLVDIuLDQk/WwHcOrR5yxlDrZq9ygiebBTOfcJFia7w==", + "integrity": "sha1-FyJ7F9SaNAIYddhe6gJ533eKOI4=", "cpu": [ "x64" ], @@ -1075,21 +946,24 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.2.3", + "integrity": "sha1-l+PUXXQk3F2h1OMvO/OykvbBtEw=", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@oxc-project/types": { @@ -1112,7 +986,7 @@ }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "integrity": "sha1-VM6Pg4IhP0oxSgwve6g/gf/q5ZI=", "cpu": [ "arm64" ], @@ -1144,7 +1018,7 @@ }, "node_modules/@rolldown/binding-darwin-x64": { "version": "1.0.3", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "integrity": "sha1-U/V94fWZ7PHbE4I8/IjBj7gJVK0=", "cpu": [ "x64" ], @@ -1160,7 +1034,7 @@ }, "node_modules/@rolldown/binding-freebsd-x64": { "version": "1.0.3", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "integrity": "sha1-bz/dobeuqsnSaKUmgEtPuW5ONfE=", "cpu": [ "x64" ], @@ -1176,7 +1050,7 @@ }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { "version": "1.0.3", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "integrity": "sha1-2HpFS/WFzJZ2hJN36R1uN1KXMm8=", "cpu": [ "arm" ], @@ -1192,7 +1066,7 @@ }, "node_modules/@rolldown/binding-linux-arm64-gnu": { "version": "1.0.3", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "integrity": "sha1-QZ/Wv2Es80jxBSjLzZTrq5YH2NE=", "cpu": [ "arm64" ], @@ -1208,7 +1082,7 @@ }, "node_modules/@rolldown/binding-linux-arm64-musl": { "version": "1.0.3", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "integrity": "sha1-/MaRhpa7doRId+HkkwoY/Q03QGk=", "cpu": [ "arm64" ], @@ -1224,7 +1098,7 @@ }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { "version": "1.0.3", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "integrity": "sha1-Mq7LfI2uXU8qjN5XoFjshpkVQvg=", "cpu": [ "ppc64" ], @@ -1240,7 +1114,7 @@ }, "node_modules/@rolldown/binding-linux-s390x-gnu": { "version": "1.0.3", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "integrity": "sha1-vtk0bqgea7i5PPEfXYi3fbiQt2M=", "cpu": [ "s390x" ], @@ -1256,7 +1130,7 @@ }, "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.0.3", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "integrity": "sha1-ZMLSb3Xf/ZtaH5dVegCudyUMjLc=", "cpu": [ "x64" ], @@ -1272,7 +1146,7 @@ }, "node_modules/@rolldown/binding-linux-x64-musl": { "version": "1.0.3", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "integrity": "sha1-WkUTLopHZZ7qrztUDClUqXyGD/M=", "cpu": [ "x64" ], @@ -1288,7 +1162,7 @@ }, "node_modules/@rolldown/binding-openharmony-arm64": { "version": "1.0.3", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "integrity": "sha1-KQUTBoxV6EnchFejKv7h17Csswk=", "cpu": [ "arm64" ], @@ -1304,7 +1178,7 @@ }, "node_modules/@rolldown/binding-wasm32-wasi": { "version": "1.0.3", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "integrity": "sha1-PZly2/GpU9PHr6pKDyDvKy458xs=", "cpu": [ "wasm32" ], @@ -1322,7 +1196,7 @@ }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.3", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "integrity": "sha1-oASrYHoW1vA7y1VXKP+IivdXc60=", "cpu": [ "arm64" ], @@ -1338,7 +1212,7 @@ }, "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.0.3", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "integrity": "sha1-4qJbNGkaHMihIJ195wkGMCbdDNs=", "cpu": [ "x64" ], @@ -1365,8 +1239,8 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "integrity": "sha1-AVy6np3UfOFNA9KoxdVHv7FpZl0=", "dev": true, "license": "MIT", "optional": true, @@ -1933,6 +1807,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/chownr": { + "version": "3.0.0", + "integrity": "sha1-mFXmTs0kCpzEJnzopKpdJKHaFeQ=", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/collection-utils": { "version": "1.0.1", "integrity": "sha512-LA2YTIlR7biSpXkKYwwuzGjwL5rjWEZVOSnvdUc7gObvWe4WkjxOpfrdhoP7Hs09YWDVfg0Mal9BpAqLfVEzQg==", @@ -2018,6 +1901,7 @@ "node_modules/detect-libc": { "version": "2.1.2", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -2549,8 +2433,8 @@ "license": "BSD-3-Clause" }, "node_modules/js-yaml": { - "version": "4.2.0", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.2", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -2695,7 +2579,7 @@ }, "node_modules/lightningcss-android-arm64": { "version": "1.32.0", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "integrity": "sha1-8DOIURbf79nG9UeHUj41FLYeGWg=", "cpu": [ "arm64" ], @@ -2735,7 +2619,7 @@ }, "node_modules/lightningcss-darwin-x64": { "version": "1.32.0", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "integrity": "sha1-NfPpczLRMLnKGB4RtWje1q68bV4=", "cpu": [ "x64" ], @@ -2755,7 +2639,7 @@ }, "node_modules/lightningcss-freebsd-x64": { "version": "1.32.0", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "integrity": "sha1-l3enZHK2Ttb/lDQq1kx7r9eUpXU=", "cpu": [ "x64" ], @@ -2775,7 +2659,7 @@ }, "node_modules/lightningcss-linux-arm-gnueabihf": { "version": "1.32.0", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "integrity": "sha1-E65lLhq3O5E117faFy9mbEEK1T0=", "cpu": [ "arm" ], @@ -2795,7 +2679,7 @@ }, "node_modules/lightningcss-linux-arm64-gnu": { "version": "1.32.0", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "integrity": "sha1-QXhYeVqUWS9oASOhsfnaig4e8zU=", "cpu": [ "arm64" ], @@ -2815,7 +2699,7 @@ }, "node_modules/lightningcss-linux-arm64-musl": { "version": "1.32.0", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "integrity": "sha1-a+NmkugQtxgECAL9gJYjz/5zITM=", "cpu": [ "arm64" ], @@ -2835,7 +2719,7 @@ }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.32.0", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "integrity": "sha1-C3gDr06yHP043Tn+Kru1PH3QkfY=", "cpu": [ "x64" ], @@ -2855,7 +2739,7 @@ }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.32.0", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "integrity": "sha1-iNyLqGXd3bGsXvBLDxYYBEGMFjs=", "cpu": [ "x64" ], @@ -2875,7 +2759,7 @@ }, "node_modules/lightningcss-win32-arm64-msvc": { "version": "1.32.0", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "integrity": "sha1-TzC6P6XpJfW3n5RejMDRdsOxqzg=", "cpu": [ "arm64" ], @@ -2895,7 +2779,7 @@ }, "node_modules/lightningcss-win32-x64-msvc": { "version": "1.32.0", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "integrity": "sha1-FBqlYFZFBkkokCu0rwRfp9n0Igo=", "cpu": [ "x64" ], @@ -3012,6 +2896,18 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minizlib": { + "version": "3.1.0", + "integrity": "sha1-atdsOo8QInybUdHJrI4wsn9aJRw=", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/ms": { "version": "2.1.3", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", @@ -3492,6 +3388,22 @@ "node": ">=8" } }, + "node_modules/tar": { + "version": "7.5.22", + "integrity": "sha1-ppb5mBNucUh9w/hpqFu6LGeXG6k=", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tiny-inflate": { "version": "1.0.3", "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", @@ -3558,7 +3470,7 @@ }, "node_modules/tslib": { "version": "2.8.1", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", "dev": true, "license": "0BSD", "optional": true @@ -3910,6 +3822,15 @@ } } }, + "node_modules/yallist": { + "version": "5.0.0", + "integrity": "sha1-AOLeRDY57Q14/YfeDSdGn7z/tTM=", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/yaml": { "version": "2.9.0", "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", diff --git a/nodejs/package.json b/nodejs/package.json index e1744c4fdc..783c4d5390 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -5,6 +5,7 @@ "url": "https://github.com/github/copilot-sdk.git" }, "version": "0.0.0-dev", + "copilotCliVersion": "1.0.83", "description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC", "main": "./dist/cjs/index.js", "types": "./dist/index.d.ts", @@ -34,6 +35,9 @@ "scripts": { "clean": "rimraf --glob dist *.tgz", "build": "tsx esbuild-copilotsdk-nodejs.ts", + "pack:release": "tsx scripts/package-sdk.ts", + "verify:release-packages": "tsx scripts/verify-release-packages.ts", + "prepare:runtime": "tsx scripts/prepare-runtime.ts", "test": "vitest run", "test:watch": "vitest", "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" --ignore-path .prettierignore", @@ -42,9 +46,10 @@ "lint:fix": "eslint --fix \"src/**/*.ts\" \"test/**/*.ts\"", "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json", "generate": "cd ../scripts/codegen && npm run generate", + "set:cli-version": "node scripts/set-cli-version.js", "update:protocol-version": "tsx scripts/update-protocol-version.ts", "prepublishOnly": "npm run build", - "package": "npm run clean && npm run build && node scripts/set-version.js && npm pack && npm version 0.0.0-dev --no-git-tag-version --allow-same-version" + "package": "npm run clean && npm run build && node scripts/set-version.js && npm run pack:release && npm version 0.0.0-dev --no-git-tag-version --allow-same-version" }, "keywords": [ "github", @@ -56,7 +61,6 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-0", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -76,6 +80,7 @@ "quicktype-core": "^23.2.6", "rimraf": "^6.1.2", "semver": "^7.7.3", + "tar": "^7.5.22", "tsx": "^4.20.6", "typescript": "^5.0.0", "vitest": "^4.0.18", diff --git a/nodejs/samples/package-lock.json b/nodejs/samples/package-lock.json index 0ef1a077b2..e2a9ab8a00 100644 --- a/nodejs/samples/package-lock.json +++ b/nodejs/samples/package-lock.json @@ -18,7 +18,6 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.83-0", "koffi": "^3.1.0", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" @@ -38,6 +37,7 @@ "quicktype-core": "^23.2.6", "rimraf": "^6.1.2", "semver": "^7.7.3", + "tar": "^7.5.22", "tsx": "^4.20.6", "typescript": "^5.0.0", "vitest": "^4.0.18", @@ -47,10 +47,2535 @@ "node": "^20.19.0 || >=22.12.0" } }, + "../node_modules/@apidevtools/json-schema-ref-parser": { + "version": "11.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "../node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "../node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "../node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "../node_modules/@eslint/config-array": { + "version": "0.21.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../node_modules/@eslint/core": { + "version": "0.17.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "../node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../node_modules/@eslint/js": { + "version": "9.39.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "../node_modules/@eslint/object-schema": { + "version": "2.1.7", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "../node_modules/@glideapps/ts-necessities": { + "version": "2.2.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/@humanfs/core": { + "version": "0.19.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "../node_modules/@humanfs/node": { + "version": "0.16.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "../node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "../node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "../node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "../node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/@koromix/koffi-darwin-arm64": { + "version": "3.1.0", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "../node_modules/@oxc-project/types": { + "version": "0.133.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "../node_modules/@platformatic/vfs": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 22" + } + }, + "../node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "../node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/@standard-schema/spec": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "../node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "../node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "../node_modules/@types/json-schema": { + "version": "7.0.15", + "dev": true, + "license": "MIT" + }, + "../node_modules/@types/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "../node_modules/@types/node": { + "version": "25.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "../node_modules/@types/ws": { + "version": "8.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "../node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/parser": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/project-service": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/type-utils": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/types": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/utils": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "../node_modules/@typescript-eslint/visitor-keys": { + "version": "8.56.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "../node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/@vitest/expect": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/@vitest/mocker": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.8", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "../node_modules/@vitest/pretty-format": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/@vitest/runner": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/@vitest/snapshot": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/@vitest/spy": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/@vitest/utils": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../node_modules/abort-controller": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "../node_modules/acorn": { + "version": "8.15.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "../node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "../node_modules/ajv": { + "version": "6.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "../node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "../node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "../node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "../node_modules/base64-js": { + "version": "1.5.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "../node_modules/brace-expansion": { + "version": "1.1.16", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "../node_modules/browser-or-node": { + "version": "3.0.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/buffer": { + "version": "6.0.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "../node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../node_modules/chai": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "../node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "../node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../node_modules/chownr": { + "version": "3.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "../node_modules/collection-utils": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0" + }, + "../node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "../node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "../node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/cross-fetch": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "../node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "../node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "../node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "../node_modules/detect-libc": { + "version": "2.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "../node_modules/es-module-lexer": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/esbuild": { + "version": "0.28.1", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "../node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/eslint": { + "version": "9.39.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "../node_modules/eslint-scope": { + "version": "8.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "../node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "../node_modules/espree": { + "version": "10.4.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "../node_modules/esquery": { + "version": "1.6.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "../node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "../node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "../node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "../node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../node_modules/event-target-shim": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../node_modules/events": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "../node_modules/expect-type": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "../node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "../node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "../node_modules/file-entry-cache": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "../node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/flat-cache": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "../node_modules/flatted": { + "version": "3.4.2", + "dev": true, + "license": "ISC" + }, + "../node_modules/fsevents": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "../node_modules/glob": { + "version": "13.0.6", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "../node_modules/globals": { + "version": "14.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../node_modules/ieee754": { + "version": "1.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "../node_modules/ignore": { + "version": "7.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "../node_modules/import-fresh": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "../node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "../node_modules/is-url": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "../node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../node_modules/js-base64": { + "version": "3.7.8", + "dev": true, + "license": "BSD-3-Clause" + }, + "../node_modules/js-yaml": { + "version": "4.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "../node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/json-schema": { + "version": "0.4.0", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "../node_modules/json-schema-to-typescript": { + "version": "15.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^11.5.5", + "@types/json-schema": "^7.0.15", + "@types/lodash": "^4.17.7", + "is-glob": "^4.0.3", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "prettier": "^3.2.5", + "tinyglobby": "^0.2.9" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "../node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "../node_modules/koffi": { + "version": "3.1.0", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://liberapay.com/Koromix" + }, + "optionalDependencies": { + "@koromix/koffi-darwin-arm64": "3.1.0", + "@koromix/koffi-darwin-x64": "3.1.0", + "@koromix/koffi-freebsd-arm64": "3.1.0", + "@koromix/koffi-freebsd-ia32": "3.1.0", + "@koromix/koffi-freebsd-x64": "3.1.0", + "@koromix/koffi-linux-arm64": "3.1.0", + "@koromix/koffi-linux-ia32": "3.1.0", + "@koromix/koffi-linux-loong64": "3.1.0", + "@koromix/koffi-linux-riscv64": "3.1.0", + "@koromix/koffi-linux-x64": "3.1.0", + "@koromix/koffi-openbsd-ia32": "3.1.0", + "@koromix/koffi-openbsd-x64": "3.1.0", + "@koromix/koffi-win32-ia32": "3.1.0", + "@koromix/koffi-win32-x64": "3.1.0" + } + }, + "../node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../node_modules/lightningcss": { + "version": "1.32.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "../node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "../node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/lodash": { + "version": "4.18.1", + "dev": true, + "license": "MIT" + }, + "../node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "../node_modules/lru-cache": { + "version": "11.2.6", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "../node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "../node_modules/minimatch": { + "version": "10.2.4", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../node_modules/minimatch/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "../node_modules/minimatch/node_modules/brace-expansion": { + "version": "5.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "../node_modules/minimist": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "../node_modules/minipass": { + "version": "7.1.3", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "../node_modules/minizlib": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "../node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/nanoid": { + "version": "3.3.17", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "../node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/node-fetch": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "../node_modules/obug": { + "version": "2.1.1", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "../node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "../node_modules/pako": { + "version": "1.0.11", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "../node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "../node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../node_modules/path-scurry": { + "version": "2.0.2", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "../node_modules/picomatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "../node_modules/pluralize": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "../node_modules/postcss": { + "version": "8.5.25", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.16", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "../node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "../node_modules/prettier": { + "version": "3.8.1", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "../node_modules/process": { + "version": "0.11.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "../node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../node_modules/quicktype-core": { + "version": "23.2.6", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@glideapps/ts-necessities": "2.2.3", + "browser-or-node": "^3.0.0", + "collection-utils": "^1.0.1", + "cross-fetch": "^4.0.0", + "is-url": "^1.2.4", + "js-base64": "^3.7.7", + "lodash": "^4.17.21", + "pako": "^1.0.6", + "pluralize": "^8.0.0", + "readable-stream": "4.5.2", + "unicode-properties": "^1.4.1", + "urijs": "^1.19.1", + "wordwrap": "^1.0.0", + "yaml": "^2.4.1" + } + }, + "../node_modules/readable-stream": { + "version": "4.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "../node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "../node_modules/rimraf": { + "version": "6.1.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../node_modules/rolldown": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "../node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "../node_modules/semver": { + "version": "7.7.3", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "../node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "../node_modules/std-env": { + "version": "4.1.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "../node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../node_modules/tar": { + "version": "7.5.22", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "../node_modules/tiny-inflate": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/tinyexec": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "../node_modules/tinyglobby": { + "version": "0.2.17", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "../node_modules/tinyrainbow": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../node_modules/tr46": { + "version": "0.0.3", + "dev": true, + "license": "MIT" + }, + "../node_modules/ts-api-utils": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "../node_modules/tsx": { + "version": "4.22.4", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "../node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "../node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "../node_modules/undici-types": { + "version": "7.18.2", + "dev": true, + "license": "MIT" + }, + "../node_modules/unicode-properties": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + } + }, + "../node_modules/unicode-trie": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "../node_modules/unicode-trie/node_modules/pako": { + "version": "0.2.9", + "dev": true, + "license": "MIT" + }, + "../node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "../node_modules/urijs": { + "version": "1.19.11", + "dev": true, + "license": "MIT" + }, + "../node_modules/vite": { + "version": "8.0.16", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "../node_modules/vitest": { + "version": "4.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "../node_modules/vscode-jsonrpc": { + "version": "8.2.1", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../node_modules/webidl-conversions": { + "version": "3.0.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "../node_modules/whatwg-url": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "../node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "../node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "../node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../node_modules/wordwrap": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "../node_modules/ws": { + "version": "8.21.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "../node_modules/yallist": { + "version": "5.0.0", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "../node_modules/yaml": { + "version": "2.9.0", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "../node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../node_modules/zod": { + "version": "4.3.6", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha1-v24QMDvPLnxoaXX6Uvk37Cco2Lw=", "cpu": [ "ppc64" ], @@ -65,9 +2590,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha1-LYTs5qTiaE2SvibuE9QnV9gxw4E=", "cpu": [ "arm" ], @@ -82,9 +2607,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha1-DGJGvI0sTRcqrC2z+xGQ1yvWVQQ=", "cpu": [ "arm64" ], @@ -99,9 +2624,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha1-/DjU1jWNjcHPU/CfdYn+Q262SAE=", "cpu": [ "x64" ], @@ -116,9 +2641,7 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "version": "0.28.2", "cpu": [ "arm64" ], @@ -133,9 +2656,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha1-UQFHwFWnlViNu+FP1rG4rQovMN4=", "cpu": [ "x64" ], @@ -150,9 +2673,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "version": "0.28.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha1-CTuSAOzwsRW6Tl4kinSFycX4vV4=", "cpu": [ "arm64" ], @@ -167,9 +2690,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha1-C+Irbfkl0hPoQeqHEjr134Cw+vc=", "cpu": [ "x64" ], @@ -184,9 +2707,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha1-vrEq1yuE9y0oSIzBuO6ffrFB11M=", "cpu": [ "arm" ], @@ -201,9 +2724,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha1-G9vGUc2pupmVxT7ZxxzqplCUdi0=", "cpu": [ "arm64" ], @@ -218,9 +2741,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "version": "0.28.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha1-uB+dVVKbRcIGpGoTghSxqmh5aWs=", "cpu": [ "ia32" ], @@ -235,9 +2758,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha1-WYZnJBoEyZt27W75QKxQA4xBn5g=", "cpu": [ "loong64" ], @@ -252,9 +2775,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha1-HFHrnOqQP1PZe1rzsYQdtw9Vlso=", "cpu": [ "mips64el" ], @@ -269,9 +2792,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha1-Y91h8XzrMagSJ/QT/qyKcbwsUfI=", "cpu": [ "ppc64" ], @@ -286,9 +2809,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha1-N2Owj95c8lqx+suOd1Lt/kX7/Cc=", "cpu": [ "riscv64" ], @@ -303,9 +2826,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "version": "0.28.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha1-GhN/8pOoKQbrMXY4W9fo4OXPt8s=", "cpu": [ "s390x" ], @@ -320,9 +2843,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "version": "0.28.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha1-Jos2IRwUbKVPj+EsV4qNbviXlIU=", "cpu": [ "x64" ], @@ -337,9 +2860,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha1-Ilca2VHWK7aszILY0frVyMGsC6E=", "cpu": [ "arm64" ], @@ -354,9 +2877,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "version": "0.28.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha1-QvzFcpfrCgyj9fxHUpH0waP3wN4=", "cpu": [ "x64" ], @@ -371,9 +2894,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha1-nrMq8QSsPaz07coB9ZZmSqsMc+8=", "cpu": [ "arm64" ], @@ -388,9 +2911,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "version": "0.28.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha1-/r7SQC1giCJekfIPtM4lIq0KTv0=", "cpu": [ "x64" ], @@ -405,9 +2928,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha1-hWQcPUZkKL+8zqXyHCaDZmP+9c4=", "cpu": [ "arm64" ], @@ -422,9 +2945,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "version": "0.28.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha1-pzb52JYkgQRfxMPlT1R58iyHD7Q=", "cpu": [ "x64" ], @@ -439,9 +2962,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "version": "0.28.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha1-7lq0D60YYgG2UqM/il6xSenkJTI=", "cpu": [ "arm64" ], @@ -456,9 +2979,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "version": "0.28.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha1-xA0optmaEn2mcR8q/XSxHLY7Bqc=", "cpu": [ "ia32" ], @@ -473,9 +2996,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "version": "0.28.2", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha1-shr/uATMFnwTPZX0WzodwTI7moc=", "cpu": [ "x64" ], @@ -494,9 +3017,7 @@ "link": true }, "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "version": "22.20.1", "dev": true, "license": "MIT", "dependencies": { @@ -504,9 +3025,7 @@ } }, "node_modules/esbuild": { - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "version": "0.28.2", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -517,40 +3036,37 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/fsevents": { "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -561,9 +3077,7 @@ } }, "node_modules/tsx": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "version": "4.23.12", "dev": true, "license": "MIT", "dependencies": { @@ -581,8 +3095,6 @@ }, "node_modules/undici-types": { "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" } diff --git a/nodejs/scripts/package-sdk.ts b/nodejs/scripts/package-sdk.ts new file mode 100644 index 0000000000..cc7f2f8464 --- /dev/null +++ b/nodejs/scripts/package-sdk.ts @@ -0,0 +1,74 @@ +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { COPILOT_CLI_VERSION } from "../src/cliVersion.js"; +import { + getRuntimePackageName, + materializeRuntimeBundle, + RUNTIME_PLATFORMS, +} from "../src/runtimeArtifacts.js"; +import { ensureCopilotPackage } from "./releaseArtifacts.js"; + +const nodeRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const packagePath = join(nodeRoot, "package.json"); +const originalPackage = readFileSync(packagePath, "utf8"); +const packageJson = JSON.parse(originalPackage); +const sdkVersion = packageJson.version; +const npmCliPath = process.env.npm_execpath; +if (!npmCliPath) { + throw new Error("package-sdk.ts must be run through an npm script"); +} +const requestedPlatforms = process.env.COPILOT_SDK_RUNTIME_PLATFORMS?.split(",").filter(Boolean); +const platforms = requestedPlatforms ?? [...RUNTIME_PLATFORMS]; +const stagingRoot = mkdtempSync(join(tmpdir(), "copilot-sdk-platform-packages-")); + +try { + const optionalDependencies: Record = {}; + for (const platform of platforms) { + if (!(RUNTIME_PLATFORMS as readonly string[]).includes(platform)) { + throw new Error(`Unsupported runtime platform: ${platform}`); + } + const releasePackage = await ensureCopilotPackage(COPILOT_CLI_VERSION, { platform }); + const runtimeWrapper = materializeRuntimeBundle( + { packageRoot: releasePackage, platform }, + stagingRoot, + platform + ); + const runtimeRoot = resolve(dirname(runtimeWrapper), "..", ".."); + const packageName = getRuntimePackageName(platform); + const [osName, cpu] = platform.replace("linuxmusl", "linux").split("-"); + const runtimePackage = { + name: packageName, + version: sdkVersion, + description: `Platform runtime for @github/copilot-sdk (${platform})`, + repository: packageJson.repository, + license: "MIT", + os: [osName], + cpu: [cpu], + ...(platform.startsWith("linux") + ? { libc: [platform.startsWith("linuxmusl") ? "musl" : "glibc"] } + : {}), + }; + writeFileSync( + join(runtimeRoot, "package.json"), + `${JSON.stringify(runtimePackage, null, 4)}\n` + ); + execFileSync(process.execPath, [npmCliPath, "pack", runtimeRoot, "--pack-destination", nodeRoot], { + stdio: "inherit", + }); + optionalDependencies[packageName] = sdkVersion; + } + + packageJson.optionalDependencies = optionalDependencies; + writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 4)}\n`); + execFileSync( + process.execPath, + [npmCliPath, "pack", nodeRoot, "--pack-destination", nodeRoot], + { stdio: "inherit" } + ); +} finally { + writeFileSync(packagePath, originalPackage); + rmSync(stagingRoot, { recursive: true, force: true }); +} diff --git a/nodejs/scripts/prepare-runtime.ts b/nodejs/scripts/prepare-runtime.ts new file mode 100644 index 0000000000..1cdfc0a6e7 --- /dev/null +++ b/nodejs/scripts/prepare-runtime.ts @@ -0,0 +1,16 @@ +import { join } from "node:path"; +import { getRuntimePlatform, materializeRuntimeBundle } from "../src/runtimeArtifacts.js"; +import { COPILOT_CLI_VERSION } from "../src/cliVersion.js"; +import { ensureCopilotPackage } from "./releaseArtifacts.js"; + +const [option] = process.argv.slice(2); +const platform = getRuntimePlatform(); +const packageRoot = await ensureCopilotPackage(COPILOT_CLI_VERSION, { platform }); +if (option === "--print-legacy-path") { + process.stdout.write(`${join(packageRoot, "app.js")}\n`); +} else if (option === "--print-path" || option === undefined) { + const runtimePath = materializeRuntimeBundle({ packageRoot, platform }); + process.stdout.write(`${runtimePath}\n`); +} else { + throw new Error(`Unknown option: ${option}`); +} diff --git a/nodejs/scripts/releaseArtifacts.ts b/nodejs/scripts/releaseArtifacts.ts new file mode 100644 index 0000000000..2731d878c5 --- /dev/null +++ b/nodejs/scripts/releaseArtifacts.ts @@ -0,0 +1,233 @@ +import { createHash } from "node:crypto"; +import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { x as extractTar } from "tar"; +import { + defaultRuntimeCacheRoot, + getRuntimePlatform, + getRuntimeReleaseAssetName, + resolvePackageRoot, + validateFile, +} from "../src/runtimeArtifacts.js"; +import { COPILOT_CLI_USE_NPM_PACKAGE, COPILOT_CLI_VERSION } from "../src/cliVersion.js"; + +export interface EnsureCopilotPackageOptions { + cacheRoot?: string; + environment?: NodeJS.ProcessEnv; + fetch?: typeof globalThis.fetch; + fetchTimeoutMs?: number; + platform?: string; +} + +const packageDownloads = new Map>(); +const checksumDownloads = new Map>>(); +const DEFAULT_FETCH_TIMEOUT_MS = 60_000; + +async function fetchWithRetry( + fetcher: typeof globalThis.fetch, + url: string, + readResponse: (response: Response) => Promise, + timeoutMs: number +): Promise { + let lastError: unknown; + for (let attempt = 0; attempt < 3; attempt++) { + try { + const response = await fetcher(url, { + signal: AbortSignal.timeout(timeoutMs), + }); + if (response.ok) { + return await readResponse(response); + } + await response.body?.cancel(); + lastError = new Error(`${response.status} ${response.statusText}`); + if ( + response.status >= 400 && + response.status < 500 && + response.status !== 408 && + response.status !== 429 + ) { + break; + } + } catch (error) { + lastError = error; + } + if (attempt < 2) { + await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000)); + } + } + throw new Error(`Failed to download ${url}: ${String(lastError)}`); +} + +async function getReleaseChecksum( + version: string, + assetName: string, + baseUrl: string, + fetcher: typeof globalThis.fetch, + fetchTimeoutMs: number +): Promise { + const key = `${baseUrl}\0${version}`; + let checksums = checksumDownloads.get(key); + if (!checksums) { + checksums = downloadReleaseChecksums(version, baseUrl, fetcher, fetchTimeoutMs); + checksumDownloads.set(key, checksums); + try { + return (await checksums).get(assetName); + } catch (error) { + checksumDownloads.delete(key); + throw error; + } + } + return (await checksums).get(assetName); +} + +async function downloadReleaseChecksums( + version: string, + baseUrl: string, + fetcher: typeof globalThis.fetch, + fetchTimeoutMs: number +): Promise> { + const contents = await fetchWithRetry( + fetcher, + `${baseUrl}/v${version}/SHA256SUMS.txt`, + (response) => response.text(), + fetchTimeoutMs + ); + const checksums = new Map(); + for (const line of contents.split(/\r?\n/)) { + const [hash, name] = line.trim().split(/\s+/, 2); + if (/^[a-fA-F0-9]{64}$/.test(hash) && name) { + checksums.set(name.replace(/^\*/, ""), hash.toLowerCase()); + } + } + return checksums; +} + +export async function ensureCopilotPackage( + version = COPILOT_CLI_VERSION, + options: EnsureCopilotPackageOptions = {} +): Promise { + const platform = options.platform ?? getRuntimePlatform(); + // lgtm[js/trivial-conditional] This generated constant is true for internal canary builds. + if (version === COPILOT_CLI_VERSION && COPILOT_CLI_USE_NPM_PACKAGE) { + const packageName = `@github/copilot-${platform}`; + const packageRoot = resolvePackageRoot(packageName); + if (!packageRoot) { + throw new Error(`Could not resolve ${packageName} for Copilot CLI ${version}.`); + } + validateFile( + join(packageRoot, "prebuilds", platform, "runtime.node"), + "Copilot runtime.node" + ); + return packageRoot; + } + + const cacheRoot = options.cacheRoot ?? defaultRuntimeCacheRoot(); + const cachedPackageRoot = join(cacheRoot, version, "packages", platform); + const cachedRuntimeNode = join(cachedPackageRoot, "prebuilds", platform, "runtime.node"); + if (existsSync(cachedRuntimeNode)) { + validateFile(cachedRuntimeNode, "Copilot runtime.node"); + return cachedPackageRoot; + } + + const baseUrl = ( + (options.environment ?? process.env).COPILOT_CLI_DOWNLOAD_BASE_URL ?? + "https://github.com/github/copilot-cli/releases/download" + ).replace(/\/+$/, ""); + const fetchTimeoutMs = options.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS; + const key = `${cacheRoot}\0${version}\0${platform}\0${baseUrl}`; + if (!options.fetch) { + const existing = packageDownloads.get(key); + if (existing) { + return existing; + } + const download = downloadCopilotPackage( + version, + platform, + cacheRoot, + baseUrl, + globalThis.fetch, + fetchTimeoutMs + ); + packageDownloads.set(key, download); + try { + return await download; + } finally { + packageDownloads.delete(key); + } + } + return downloadCopilotPackage( + version, + platform, + cacheRoot, + baseUrl, + options.fetch, + fetchTimeoutMs + ); +} + +async function downloadCopilotPackage( + version: string, + platform: string, + cacheRoot: string, + baseUrl: string, + fetcher: typeof globalThis.fetch, + fetchTimeoutMs: number +): Promise { + if (!fetcher) { + throw new Error("This Node.js runtime does not provide fetch()."); + } + const assetName = getRuntimeReleaseAssetName(version, platform); + const expectedChecksum = await getReleaseChecksum( + version, + assetName, + baseUrl, + fetcher, + fetchTimeoutMs + ); + if (!expectedChecksum) { + throw new Error(`SHA256SUMS.txt does not contain ${assetName}.`); + } + const archive = await fetchWithRetry( + fetcher, + `${baseUrl}/v${version}/${assetName}`, + async (response) => Buffer.from(await response.arrayBuffer()), + fetchTimeoutMs + ); + const actualChecksum = createHash("sha256").update(archive).digest("hex"); + if (actualChecksum !== expectedChecksum) { + throw new Error( + `Checksum mismatch for ${assetName}: expected ${expectedChecksum}, got ${actualChecksum}.` + ); + } + + mkdirSync(cacheRoot, { recursive: true }); + const stagingRoot = mkdtempSync(join(cacheRoot, ".download-")); + const archivePath = join(stagingRoot, assetName); + const packageRoot = join(stagingRoot, "package"); + const cachedPackageRoot = join(cacheRoot, version, "packages", platform); + writeFileSync(archivePath, archive); + try { + await extractTar({ + cwd: stagingRoot, + file: archivePath, + gzip: true, + preservePaths: false, + strict: true, + }); + validateFile( + join(packageRoot, "prebuilds", platform, "runtime.node"), + "Copilot runtime.node" + ); + mkdirSync(dirname(cachedPackageRoot), { recursive: true }); + try { + renameSync(packageRoot, cachedPackageRoot); + } catch (error) { + if (!existsSync(cachedPackageRoot)) { + throw error; + } + } + return cachedPackageRoot; + } finally { + rmSync(stagingRoot, { recursive: true, force: true }); + } +} diff --git a/nodejs/scripts/set-cli-version.js b/nodejs/scripts/set-cli-version.js new file mode 100644 index 0000000000..ea45f90ada --- /dev/null +++ b/nodejs/scripts/set-cli-version.js @@ -0,0 +1,71 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const [version, mode] = process.argv.slice(2); +if (!version || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z._-]+)?$/.test(version)) { + throw new Error("Usage: set-cli-version.js [--npm-package]"); +} +if (mode !== undefined && mode !== "--npm-package") { + throw new Error(`Unknown option: ${mode}`); +} + +const nodeRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const runtimePlatforms = [ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "linuxmusl-arm64", + "linuxmusl-x64", + "win32-arm64", + "win32-x64", +]; +const cliAssets = [ + "copilot-darwin-arm64.tar.gz", + "copilot-darwin-x64.tar.gz", + "copilot-linux-arm64.tar.gz", + "copilot-linux-x64.tar.gz", + "copilot-win32-arm64.zip", + "copilot-win32-x64.zip", +]; +const useNpmPackage = mode === "--npm-package"; +if (!useNpmPackage) { + const checksumsUrl = `https://github.com/github/copilot-cli/releases/download/v${version}/SHA256SUMS.txt`; + const response = await fetch(checksumsUrl); + if (!response.ok) { + throw new Error( + `Failed to download ${checksumsUrl}: ${response.status} ${response.statusText}` + ); + } + const checksums = new Map( + (await response.text()) + .split(/\r?\n/) + .map((line) => line.trim().split(/\s+/, 2)) + .filter(([hash, name]) => /^[a-fA-F0-9]{64}$/.test(hash) && name) + .map(([hash, name]) => [name.replace(/^\*/, ""), hash.toLowerCase()]) + ); + for (const assetName of [ + ...runtimePlatforms.map((platform) => `github-copilot-${version}-${platform}.tgz`), + ...cliAssets, + ]) { + if (!checksums.has(assetName)) { + throw new Error(`SHA256SUMS.txt does not contain ${assetName}`); + } + } +} +const packagePath = join(nodeRoot, "package.json"); +const packageJson = JSON.parse(readFileSync(packagePath, "utf8")); +packageJson.copilotCliVersion = version; +writeFileSync(packagePath, `${JSON.stringify(packageJson, null, 4)}\n`); + +const sourcePath = join(nodeRoot, "src", "cliVersion.ts"); +writeFileSync( + sourcePath, + [ + `export const COPILOT_CLI_VERSION = ${JSON.stringify(version)};`, + "", + `export const COPILOT_CLI_USE_NPM_PACKAGE = ${useNpmPackage};`, + "", + ].join("\n") +); diff --git a/nodejs/scripts/verify-release-packages.ts b/nodejs/scripts/verify-release-packages.ts new file mode 100644 index 0000000000..0d2f4589d9 --- /dev/null +++ b/nodejs/scripts/verify-release-packages.ts @@ -0,0 +1,112 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { globSync } from "glob"; +import { t as listTar, x as extractTar } from "tar"; +import { getRuntimePackageName, RUNTIME_PLATFORMS } from "../src/runtimeArtifacts.js"; + +interface PackedPackage { + manifest: { + name: string; + version: string; + repository?: string | { type?: string; url?: string }; + optionalDependencies?: Record; + }; + entries: Set; +} + +const nodeRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const sourceManifest = JSON.parse( + readFileSync(join(nodeRoot, "package.json"), "utf8") +) as PackedPackage["manifest"]; +assert( + typeof sourceManifest.repository === "string" + ? sourceManifest.repository.trim() + : sourceManifest.repository?.url?.trim(), + "Main package is missing repository metadata" +); +const expectedRuntimePackages = Object.fromEntries( + RUNTIME_PLATFORMS.map((platform) => [getRuntimePackageName(platform), sourceManifest.version]) +); +const expectedPackageNames = new Set([ + sourceManifest.name, + ...Object.keys(expectedRuntimePackages), +]); +const packages = new Map(); + +for (const archive of globSync("*.tgz", { cwd: nodeRoot, absolute: true })) { + const entries = new Set(); + await listTar({ + file: archive, + onReadEntry(entry) { + entries.add(entry.path); + entry.resume(); + }, + }); + + const manifestRoot = mkdtempSync(join(tmpdir(), "copilot-sdk-package-manifest-")); + let manifest: PackedPackage["manifest"]; + try { + await extractTar({ + cwd: manifestRoot, + file: archive, + strict: true, + filter: (entryPath) => entryPath === "package/package.json", + }); + manifest = JSON.parse( + readFileSync(join(manifestRoot, "package", "package.json"), "utf8") + ) as PackedPackage["manifest"]; + } finally { + rmSync(manifestRoot, { recursive: true, force: true }); + } + + if (manifest.version !== sourceManifest.version || !expectedPackageNames.has(manifest.name)) { + continue; + } + assert(!packages.has(manifest.name), `Duplicate tarball for ${manifest.name}`); + packages.set(manifest.name, { manifest, entries }); +} + +assert.deepEqual( + [...packages.keys()].sort(), + [...expectedPackageNames].sort(), + "Release packaging did not produce the expected main and platform packages" +); + +const mainPackage = packages.get(sourceManifest.name); +assert(mainPackage, `Missing ${sourceManifest.name} tarball`); +assert.deepEqual( + mainPackage.manifest.optionalDependencies, + expectedRuntimePackages, + "Main package optional dependencies do not match the platform packages" +); +assert(mainPackage.entries.has("package/dist/index.js"), "Main package is missing dist/index.js"); +assert( + mainPackage.entries.has("package/dist/cjs/index.js"), + "Main package is missing dist/cjs/index.js" +); + +for (const platform of RUNTIME_PLATFORMS) { + const packageName = getRuntimePackageName(platform); + const packed = packages.get(packageName); + assert(packed, `Missing ${packageName} tarball`); + assert.deepEqual( + packed.manifest.repository, + sourceManifest.repository, + `${packageName} repository metadata does not match the main package` + ); + const runtimeName = platform.startsWith("win32") ? "copilot-runtime.exe" : "copilot-runtime"; + for (const requiredPath of [ + `package/prebuilds/${platform}/${runtimeName}`, + `package/prebuilds/${platform}/runtime.node`, + "package/copilot-sdk/extension.js", + "package/preloads/extension_bootstrap.mjs", + "package/sdk/index.js", + ]) { + assert(packed.entries.has(requiredPath), `${packageName} is missing ${requiredPath}`); + } +} + +console.log(`Verified ${packages.size} release package tarballs.`); diff --git a/nodejs/src/cliVersion.ts b/nodejs/src/cliVersion.ts new file mode 100644 index 0000000000..b15767e821 --- /dev/null +++ b/nodejs/src/cliVersion.ts @@ -0,0 +1,3 @@ +export const COPILOT_CLI_VERSION = "1.0.83"; + +export const COPILOT_CLI_USE_NPM_PACKAGE = false; diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index acaf67ba85..eb92cf0bed 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -14,10 +14,8 @@ import { spawn, type ChildProcess } from "node:child_process"; import { randomUUID } from "node:crypto"; import { existsSync } from "node:fs"; -import { createRequire } from "node:module"; -import { Socket } from "node:net"; +import { isIPv6, Socket } from "node:net"; import { dirname, isAbsolute, join, resolve } from "node:path"; -import { fileURLToPath } from "node:url"; import { createMessageConnection, ErrorCodes, @@ -34,16 +32,19 @@ import { registerClientSessionApiHandlers, } from "./generated/rpc.js"; import type { + ConnectClientInfo, GitHubTelemetryNotification, GitHubTokenAcquireRequest, GitHubTokenAcquireResult, OpenCanvasInstance, SessionUpdateOptionsParams, + TaskKind, } from "./generated/rpc.js"; import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; import { CopilotSession } from "./session.js"; import type { FfiRuntimeHost } from "./ffiRuntimeHost.js"; -import { materializeRuntimeBundle } from "./runtimeArtifacts.js"; +import { ensureRuntimeBundle } from "./runtimeArtifacts.js"; +import { COPILOT_CLI_VERSION } from "./cliVersion.js"; import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js"; import { createCopilotRequestAdapter } from "./copilotRequestHandler.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; @@ -52,6 +53,7 @@ import { ToolSet } from "./toolSet.js"; import type { AutoModeSwitchRequest, AutoModeSwitchResponse, + CopilotClientInfo, CopilotClientMode, CopilotClientOptions, CustomAgentConfig, @@ -259,6 +261,22 @@ function toWireCustomAgents(agents: CustomAgentConfig[] | undefined): unknown[] }); } +/** + * Map the public {@link CopilotClientInfo} onto the generated connect wire + * shape, dropping empty fields. Returns `undefined` when no field carries a + * non-empty value so the caller omits `clientInfo` from the handshake and keeps + * the runtime's default attribution. + */ +function clientInfoToWire(info: CopilotClientInfo | undefined): ConnectClientInfo | undefined { + if (info == null) return undefined; + const wire: ConnectClientInfo = {}; + if (info.applicationName) wire.editorName = info.applicationName; + if (info.applicationVersion) wire.editorVersion = info.applicationVersion; + if (info.integrationName) wire.extensionName = info.integrationName; + if (info.integrationVersion) wire.extensionVersion = info.integrationVersion; + return Object.keys(wire).length > 0 ? wire : undefined; +} + /** * Convert a {@link LargeToolOutputConfig} from the public API shape * (`outputDirectory`) to the wire shape (`outputDir`). @@ -353,83 +371,8 @@ function getNodeExecPath(): string { return process.execPath; } -/** - * Computes the candidate platform-specific CLI package names for the current - * platform/arch, mirroring @github/copilot's npm-loader. As of CLI 1.0.64-1 the - * @github/copilot package is a thin loader and the actual CLI ships in a - * platform package (e.g. @github/copilot-darwin-arm64). For Linux we try both - * the glibc and musl variants since only the matching one is installed. - */ -function getCliPlatformPackageNames(): string[] { - const arch = process.arch; - const variants = process.platform === "linux" ? ["linux", "linuxmusl"] : [process.platform]; - return variants.map((variant) => `@github/copilot-${variant}-${arch}`); -} - -interface BundledCliPackage { - root: string; - platform: string; -} - -/** - * Resolves the current platform package and its npm prebuilds folder. - * - * In ESM, uses import.meta.resolve directly. In CJS (e.g., VS Code extensions - * bundled with esbuild format:"cjs"), import.meta is empty so we fall back to - * walking node_modules to find the package. - */ -function getBundledCliPackage(): BundledCliPackage { - const packageNames = getCliPlatformPackageNames(); - - if (typeof import.meta.resolve === "function") { - // ESM: resolve via import.meta.resolve - for (const packageName of packageNames) { - try { - const packageEntryUrl = import.meta.resolve(packageName); - const packageEntryPath = fileURLToPath(packageEntryUrl); - return { - root: dirname(packageEntryPath), - platform: packageName.slice("@github/copilot-".length), - }; - } catch { - // Try the next candidate platform package. - } - } - throw new Error( - `Could not resolve a @github/copilot platform package (tried ${packageNames.join(", ")}). ` + - `Ensure @github/copilot is installed, or pass cliPath/cliUrl to CopilotClient.` - ); - } - - // CJS fallback: the platform packages have ESM-only exports so - // require.resolve cannot reach them. Walk the module search paths instead. - const req = createRequire(__filename); - const searchPaths = req.resolve.paths("@github/copilot") ?? []; - for (const base of searchPaths) { - for (const packageName of packageNames) { - const root = join(base, ...packageName.split("/")); - const candidate = join(root, "index.js"); - if (existsSync(candidate)) { - return { - root, - platform: packageName.slice("@github/copilot-".length), - }; - } - } - } - throw new Error( - `Could not find a @github/copilot platform package (tried ${packageNames.join(", ")}). ` + - `Searched ${searchPaths.length} paths. ` + - `Ensure @github/copilot is installed, or pass cliPath/cliUrl to CopilotClient.` - ); -} - -function getBundledRuntimePath(): string { - const bundled = getBundledCliPackage(); - return materializeRuntimeBundle({ - packageRoot: bundled.root, - platform: bundled.platform, - }); +function getBundledRuntimePath(): Promise { + return ensureRuntimeBundle(COPILOT_CLI_VERSION); } /** @@ -500,6 +443,7 @@ export class CopilotClient { private ffiHost: FfiRuntimeHost | null = null; private connection: MessageConnection | null = null; private messageWriter: TeardownResilientStreamMessageWriter | null = null; + private connectionClosed: boolean = false; private socket: Socket | null = null; private runtimePort: number | null = null; private actualHost: string = "localhost"; @@ -522,6 +466,7 @@ export class CopilotClient { sessionIdleTimeoutSeconds: number; enableRemoteSessions: boolean; mode: CopilotClientMode; + clientInfo?: CopilotClientInfo; }; private isExternalServer: boolean = false; private forceStopping: boolean = false; @@ -757,8 +702,6 @@ export class CopilotClient { const explicitCliPath = conn.path ?? effectiveEnv.COPILOT_CLI_PATH; if (explicitCliPath) { this.resolvedCliPath = explicitCliPath; - } else { - this.resolvedCliPath = getBundledRuntimePath(); } } @@ -778,6 +721,7 @@ export class CopilotClient { sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0, enableRemoteSessions: options.enableRemoteSessions ?? false, mode: options.mode ?? "copilot-cli", + clientInfo: options.clientInfo, }; // Empty mode: validate at construction time that the app supplied a @@ -806,22 +750,38 @@ export class CopilotClient { /** * Parse CLI URL into host and port - * Supports formats: "host:port", "http://host:port", "https://host:port", or just "port" + * Supports formats: "host:port", "[ipv6]:port", "http://host:port", "https://host:port", or just "port" */ private parseCliUrl(url: string): { host: string; port: number } { // Remove protocol if present - let cleanUrl = url.replace(/^https?:\/\//, ""); + const cleanUrl = url.replace(/^https?:\/\//, ""); // Check if it's just a port number if (/^\d+$/.test(cleanUrl)) { return { host: "localhost", port: parseInt(cleanUrl, 10) }; } + // Handle the canonical bracketed IPv6 host:port form without changing + // the existing parser behavior for other inputs. + const ipv6Match = cleanUrl.match(/^\[([^\]]+)\]:(\d+)$/); + if (ipv6Match) { + const host = ipv6Match[1]; + if (!isIPv6(host)) { + throw new Error(`Invalid cliUrl format: ${url}`); + } + + const port = parseInt(ipv6Match[2], 10); + if (isNaN(port) || port <= 0 || port > 65535) { + throw new Error(`Invalid port in cliUrl: ${url}`); + } + return { host, port }; + } + // Parse host:port format const parts = cleanUrl.split(":"); if (parts.length !== 2) { throw new Error( - `Invalid cliUrl format: ${url}. Expected "host:port", "http://host:port", or "port"` + `Invalid cliUrl format: ${url}. Expected "host:port", "[ipv6]:port", "http://host:port", or "port"` ); } @@ -976,6 +936,7 @@ export class CopilotClient { } this.forceStopping = false; + this.connectionClosed = false; this.processTransportError = null; this.state = "connecting"; @@ -1111,7 +1072,12 @@ export class CopilotClient { // Ask SDK-owned runtimes to flush and clean up before we tear down // their transport/process. External runtimes may be shared, so only // close our connection to them. - if (this.connection && (this.cliProcess || this.ffiHost) && !this.isExternalServer) { + if ( + this.connection && + !this.connectionClosed && + (this.cliProcess || this.ffiHost) && + !this.isExternalServer + ) { const runtimeShutdownStart = Date.now(); const shutdownPromise = this.rpc.runtime.shutdown(); void shutdownPromise.catch(() => undefined); @@ -1791,6 +1757,7 @@ export class CopilotClient { await this.updateSessionOptionsForMode(session, config); this.commitGitHubTokenProvider(returnedSessionId, gitHubTokenProviderRegistrationId); } catch (e) { + session?._markDisconnected(); if (registeredId !== undefined) { this.sessions.delete(registeredId); } @@ -2069,6 +2036,7 @@ export class CopilotClient { await this.updateSessionOptionsForMode(session, config); this.commitGitHubTokenProvider(sessionId, gitHubTokenProviderRegistrationId); } catch (e) { + session._markDisconnected(); this.sessions.delete(sessionId); if (gitHubTokenProviderRegistrationId !== undefined) { this.githubTokenProviders.delete(gitHubTokenProviderRegistrationId); @@ -2218,7 +2186,12 @@ export class CopilotClient { const connectParams: { token?: string; enableGitHubTelemetryForwarding?: boolean; - } = { token: this.effectiveConnectionToken }; + clientInfo?: ConnectClientInfo; + supportedTaskKinds?: TaskKind[]; + } = { + token: this.effectiveConnectionToken, + supportedTaskKinds: ["agent", "client", "shell"], + }; // Opt in to GitHub telemetry forwarding at the connection level when a // handler is registered (mirrors the runtime, which reads this flag on the // `connect` handshake so the first session's un-replayable `session.start` @@ -2226,6 +2199,14 @@ export class CopilotClient { if (this.onGitHubTelemetry != null) { connectParams.enableGitHubTelemetryForwarding = true; } + // Declare the integrating application's identity so the runtime attributes + // the telemetry it emits on this connection to a consistent surface + // instead of its own build. Empty fields are dropped, and an + // all-empty identity is omitted entirely. + const clientInfo = clientInfoToWire(this.options.clientInfo); + if (clientInfo != null) { + connectParams.clientInfo = clientInfo; + } const result = await raceAgainstExit(this.internalRpc.connect(connectParams)); serverVersion = result.protocolVersion; } catch (err) { @@ -2610,6 +2591,7 @@ export class CopilotClient { * Start the CLI server process */ private async startCLIServer(): Promise { + this.resolvedCliPath ??= await getBundledRuntimePath(); return new Promise((resolve, reject) => { // Clear stderr buffer for fresh capture this.stderrBuffer = ""; @@ -2669,7 +2651,7 @@ export class CopilotClient { // Verify CLI exists before attempting to spawn if (!existsSync(this.resolvedCliPath)) { throw new Error( - `Copilot CLI not found at ${this.resolvedCliPath}. Ensure @github/copilot is installed.` + `Copilot CLI not found at ${this.resolvedCliPath}. Set COPILOT_CLI_PATH to use a custom installation.` ); } @@ -2813,14 +2795,21 @@ export class CopilotClient { /** Starts the in-process FFI runtime with SDK-managed typed options. */ private async startInProcessFfi(): Promise { const explicitEntrypoint = this.resolvedEnv.COPILOT_CLI_PATH; - const runtimeLibrary = explicitEntrypoint - ? join( - dirname(resolve(explicitEntrypoint)), - "prebuilds", - CopilotClient.getNapiPrebuildsFolder(explicitEntrypoint), - "runtime.node" - ) - : join(dirname(getBundledRuntimePath()), "runtime.node"); + let runtimeLibrary: string; + if (explicitEntrypoint) { + const entrypointDirectory = dirname(resolve(explicitEntrypoint)); + const adjacentRuntime = join(entrypointDirectory, "runtime.node"); + runtimeLibrary = existsSync(adjacentRuntime) + ? adjacentRuntime + : join( + entrypointDirectory, + "prebuilds", + CopilotClient.getNapiPrebuildsFolder(explicitEntrypoint), + "runtime.node" + ); + } else { + runtimeLibrary = join(dirname(await getBundledRuntimePath()), "runtime.node"); + } // Load the FFI host lazily so the native `koffi` addon (and its // platform-specific `koffi.node`) is only loaded on the in-process path; // out-of-process (stdio/tcp) consumers never touch the native dependency. @@ -3072,13 +3061,24 @@ export class CopilotClient { } ); - this.connection.onClose(() => { + const connection = this.connection; + const markDisconnected = () => { + if (this.connection !== connection) { + return; + } + this.connectionClosed = true; this.state = "disconnected"; + for (const session of this.sessions.values()) { + session._markDisconnected(); + } + this.sessions.clear(); this.githubTokenProviders.clear(); - }); - - this.connection.onError((_error) => { - this.state = "disconnected"; + }; + this.connection.onClose(markDisconnected); + this.connection.onError(() => { + if (this.connection === connection) { + this.state = "disconnected"; + } }); } diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 32a002dd01..f4978de1ff 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -5,7 +5,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; -import type { AbortReason, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; +import type { AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompleteData, TaskCompletionOutcome, UserToolSessionApproval, Verbosity } from "./session-events.js"; /** A value that can be represented losslessly on the SDK JSON wire. */ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }; @@ -276,6 +276,20 @@ export type AuthInfoType = */ /** @experimental */ export type AuthValidationErrors = AuthValidationError[]; +/** + * Current normalized autopilot objective lifecycle status. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AutopilotObjectiveStatus". + */ +/** @experimental */ +export type AutopilotObjectiveStatus = + /** The objective is actively running. */ + | "active" + /** The objective is paused and may be resumed. */ + | "paused" + /** The objective completed. */ + | "completed"; /** * Root JSON Schema type for a built-in tool input. * @@ -552,7 +566,13 @@ export type CatalogNetworkFailureReason = | "tls" /** The connection was refused or reset. */ | "connection-refused" - /** The authority returned a status the runtime treats as a failure. */ + /** The configured proxy returned 407 and requires authentication. */ + | "proxy-authentication-required" + /** The authority rate-limited requests and supplied or implied a bounded cooldown. */ + | "rate-limited" + /** The authority returned a transient 5xx response. */ + | "service-unavailable" + /** The authority returned another status the runtime treats as a failure. */ | "http-status" /** The response exceeded the permitted size. */ | "response-too-large" @@ -657,6 +677,18 @@ export type CatalogUnavailableTransportReason = | "transport-not-supported" /** Eligible remotes could not be enumerated, so no explicit choice can be offered. */ | "remote-enumeration-unavailable"; +/** + * Why the runtime requests client-task cancellation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ClientTaskCancelReason". + */ +/** @experimental */ +export type ClientTaskCancelReason = + /** A caller requested task cancellation. */ + | "cancel_requested" + /** The session is shutting down. */ + | "session_shutdown"; /** * Coarse command category for grouping and behavior: runtime built-in, skill-backed command, or SDK/client-owned command * @@ -739,6 +771,20 @@ export type ConnectedRemoteSessionMetadataKind = | "remote-session" /** GitHub Copilot coding agent session. */ | "coding-agent"; +/** + * Closed set of public task kinds a connection can negotiate. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskKind". + */ +/** @experimental */ +export type TaskKind = + /** Runtime-owned background agent task. */ + | "agent" + /** Runtime-owned shell task. */ + | "shell" + /** Client-owned externally executed task. */ + | "client"; /** * Controls how MCP tool result content is filtered: none leaves content unchanged, markdown sanitizes HTML while preserving Markdown-friendly output, and hidden_characters removes characters that can hide directives. * @@ -863,6 +909,64 @@ export type DiscoveredExtensionMode = | "load_only" /** Extensions are loaded and the agent can create, reload, and manage them. */ | "load_and_augment"; +/** + * Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HookType". + */ +/** @experimental */ +export type HookType = + /** Runs before a tool is invoked. */ + | "preToolUse" + /** Runs before an MCP tool is invoked. */ + | "preMcpToolCall" + /** Runs after a tool completes successfully. */ + | "postToolUse" + /** Runs after a tool fails. */ + | "postToolUseFailure" + /** Runs after the user submits a prompt. */ + | "userPromptSubmitted" + /** Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. */ + | "userPromptTransformed" + /** Runs when a session starts. */ + | "sessionStart" + /** Runs when a session ends. */ + | "sessionEnd" + /** Runs after an agent result is produced. */ + | "postResult" + /** Runs before a pull request description is generated. */ + | "prePRDescription" + /** Runs when the agent encounters an error. */ + | "errorOccurred" + /** Runs when the agent stops. */ + | "agentStop" + /** Runs when a subagent starts. */ + | "subagentStart" + /** Runs when a subagent stops. */ + | "subagentStop" + /** Runs before conversation context is compacted. */ + | "preCompact" + /** Runs when the agent requests permission. */ + | "permissionRequest" + /** Runs when the agent emits a notification. */ + | "notification"; +/** + * Configuration tier that contributed a discovered hook action. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HookOrigin". + */ +/** @experimental */ +export type HookOrigin = + /** Hook loaded from user settings or the user's hook directory. */ + | "user" + /** Hook loaded from repository settings or the repository hook directory. */ + | "repository" + /** Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. */ + | "plugin" + /** Hook enforced by centrally managed policy. */ + | "policy"; /** * Server transport type: stdio, http, sse (deprecated), or memory * @@ -1127,6 +1231,16 @@ export type FactoryRunFailure = * Factory failure variant discriminator. */ type: "factory_accounting_incomplete"; + } + | { + /** + * Factory run identifier. + */ + runId: string; + /** + * Factory failure variant discriminator. + */ + type: "factory_provider_disconnected"; }; /** * Cumulative resource ceiling that stopped a factory run. @@ -1332,49 +1446,6 @@ export type HistoryRewindOutcome = | "checkpoint-cleanup-failed" /** Files and conversation were rewound, but obsolete file snapshots could not be removed; only conversation-and-files rewinds produce this. */ | "snapshot-prune-failed"; -/** - * Hook event name dispatched through the SDK callback transport. - * - * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema - * via the `definition` "HookType". - */ -/** @experimental */ -/** @internal */ -export type HookType = - /** Runs before a tool is invoked. */ - | "preToolUse" - /** Runs before an MCP tool is invoked. */ - | "preMcpToolCall" - /** Runs after a tool completes successfully. */ - | "postToolUse" - /** Runs after a tool fails. */ - | "postToolUseFailure" - /** Runs after the user submits a prompt. */ - | "userPromptSubmitted" - /** Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. */ - | "userPromptTransformed" - /** Runs when a session starts. */ - | "sessionStart" - /** Runs when a session ends. */ - | "sessionEnd" - /** Runs after an agent result is produced. */ - | "postResult" - /** Runs before a pull request description is generated. */ - | "prePRDescription" - /** Runs when the agent encounters an error. */ - | "errorOccurred" - /** Runs when the agent stops. */ - | "agentStop" - /** Runs when a subagent starts. */ - | "subagentStart" - /** Runs when a subagent stops. */ - | "subagentStop" - /** Runs before conversation context is compacted. */ - | "preCompact" - /** Runs when the agent requests permission. */ - | "permissionRequest" - /** Runs when the agent emits a notification. */ - | "notification"; /** * Source for direct repo installs (when marketplace is empty) * @@ -2343,6 +2414,18 @@ export type ModelListRequest = */ skipCache?: boolean; }; +/** + * Whether the requested preference was already effective or was accepted for later transactional activation. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelSwitchAutoTierStatus". + */ +/** @experimental */ +export type ModelSwitchAutoTierStatus = + /** The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. */ + | "unchanged" + /** The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. */ + | "pending"; /** * Provider type. Defaults to "openai" for generic OpenAI-compatible APIs. * @@ -2657,6 +2740,18 @@ export type PermissionsSetApproveAllSource = | "user_setting" /** Allow-all was enabled through an RPC caller. */ | "rpc"; +/** + * Where completed plugin content was staged before atomic promotion. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "PluginInstallStagingMode". + */ +/** @experimental */ +export type PluginInstallStagingMode = + /** A sibling of the installed-plugins root, outside the recursively watched tree. */ + | "external" + /** A sibling of the destination plugin directory, used when external staging is unavailable. */ + | "destination_sibling"; /** * Optional flags controlling which side effects the reload performs. * @@ -3529,13 +3624,158 @@ export type TaskExecutionMode = /** The task is managed in the background. */ | "background"; /** - * Tracked task union returned by task APIs, containing either an agent task or a shell task. + * Active status a client owner may publish with a progress update. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientActiveStatus". + */ +/** @experimental */ +export type TaskClientActiveStatus = + /** The external owner is actively working. */ + | "running" + /** The external owner is connected but waiting. */ + | "idle"; +/** + * Client-owned tasks always execute outside the runtime in background mode. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientExecutionMode". + */ +/** @experimental */ +export type TaskClientExecutionMode = "background"; +/** + * Discriminator for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientType". + */ +/** @experimental */ +export type TaskClientType = "client"; +/** + * Lifecycle status of a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientStatus". + */ +/** @experimental */ +export type TaskClientStatus = + /** The external owner is actively working. */ + | "running" + /** The external owner is connected but waiting. */ + | "idle" + /** The owner reported successful completion. */ + | "completed" + /** The owner reported failure. */ + | "failed" + /** The owner reported or confirmed cancellation. */ + | "cancelled" + /** The bound owner join disappeared; external executor state is unknown. */ + | "orphaned"; +/** + * Connection class owning a client task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientOwnerKind". + */ +/** @experimental */ +export type TaskClientOwnerKind = + /** A discovered extension connection owns the task. */ + | "extension" + /** A generic SDK connection owns the task. */ + | "sdk"; +/** + * Presence of the task's bound join. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientOwnerPresence". + */ +/** @experimental */ +export type TaskClientOwnerPresence = + /** The bound session join is connected. */ + | "connected" + /** The bound session join is disconnected. */ + | "disconnected"; +/** + * Progress or terminal update for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientUpdate". + */ +/** @experimental */ +export type TaskClientUpdate = + | { + status?: TaskClientActiveStatus; + /** + * Optional progress message appended to recent activity when nonempty + */ + message?: string; + /** + * Optional progress phase; null clears the current phase + */ + phase?: string | null; + /** + * Optional completion percentage; null clears the current percentage + */ + percentage?: number | null; + /** + * Client task update variant discriminator. + */ + kind: "progress"; + } + | { + /** + * Optional final progress message + */ + message?: string; + /** + * Optional opaque successful terminal result + */ + result?: JsonValue; + /** + * Client task update variant discriminator. + */ + kind: "completed"; + } + | { + /** + * Optional final progress message + */ + message?: string; + /** + * Human-readable terminal failure message + */ + error: string; + /** + * Optional owner-supplied terminal failure code + */ + code?: string; + /** + * Client task update variant discriminator. + */ + kind: "failed"; + } + | { + /** + * Optional final progress message + */ + message?: string; + /** + * Optional human-readable cancellation reason + */ + reason?: string; + /** + * Client task update variant discriminator. + */ + kind: "cancelled"; + }; +/** + * Tracked task union returned by task APIs, containing an agent, client, or shell task. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "TaskInfo". */ /** @experimental */ -export type TaskInfo = TaskAgentInfo | TaskShellInfo; +export type TaskInfo = TaskAgentInfo | TaskClientInfo | TaskShellInfo; /** * Whether the shell runs inside a managed PTY session or as an independent background process * @@ -3555,7 +3795,7 @@ export type TaskShellInfoAttachmentMode = * via the `definition` "TaskProgress". */ /** @experimental */ -export type TaskProgress = (TaskAgentProgress | TaskShellProgress) | null; +export type TaskProgress = TaskAgentProgress | TaskClientProgress | TaskShellProgress | null; /** * Canonical result returned by a session tool. * @@ -4573,7 +4813,7 @@ export interface AgentGetCurrentResult { agent?: AgentInfo | null; } /** - * Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. + * Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "AgentInfo". @@ -4613,6 +4853,11 @@ export interface AgentInfo { * Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. */ model?: string; + /** + * Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + */ + models?: string[]; + modelPolicy?: AgentModelPolicy; /** * MCP server configurations attached to this agent, keyed by server name. Server config shape mirrors the MCP `mcpServers` schema. * @@ -4978,6 +5223,75 @@ export interface AuthValidationError { */ githubMessage?: string; } +/** + * Current per-window credit limit and consumption for an autopilot objective. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AutopilotObjectiveCreditLimit". + */ +/** @experimental */ +export interface AutopilotObjectiveCreditLimit { + /** + * Configured AI-credit cap, when one is set. + */ + credits?: number; + /** + * Window consumption in fractional AI credits, for display. + */ + creditsUsed: number; + /** + * Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. + */ + creditsUsedNanoAiu: string; +} +/** + * Canonical runtime state for the session's current autopilot objective. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AutopilotObjectiveGetStateResult". + */ +/** @experimental */ +export interface AutopilotObjectiveGetStateResult { + /** + * Current objective state, or `null` when the session has no objective. + */ + state: AutopilotObjectiveState | null; +} +/** + * Public, persistence-independent projection of an autopilot objective. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AutopilotObjectiveState". + */ +/** @experimental */ +export interface AutopilotObjectiveState { + /** + * Session-local objective identifier. + */ + id: number; + /** + * User-provided objective text. + */ + objective: string; + status: AutopilotObjectiveStatus; + /** + * Number of objective turns started. + */ + turnCount: number; + /** + * Optional reason the objective is paused. + */ + pauseReason?: string; + /** + * Optional summary recorded when the objective completed. + */ + completionSummary?: string; + /** + * Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. + */ + creditCountNanoAiu: string; + creditLimit?: AutopilotObjectiveCreditLimit; +} /** * The running runtime's complete catalog of well-known built-in model IDs, including supported models and additional IDs with built-in metadata. * @@ -5834,6 +6148,10 @@ export interface CatalogNetworkFailureError { * HTTP status code, when the failure was a rejected response. */ statusCode?: number; + /** + * Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + */ + retryAfterSeconds?: number; /** * Human-readable explanation, safe to surface. Never contains a query, URL, handle, or secret. */ @@ -5885,7 +6203,7 @@ export interface CatalogPolicyRejectedError { export interface CatalogSearchRequest { contract: CatalogClientContract; /** - * Free-text search query. Never written to logs or telemetry. + * Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. */ query: string; /** @@ -6007,6 +6325,45 @@ export interface CatalogUnavailableTransportError { */ message: string; } +/** + * Runtime-to-owner cancellation request for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ClientTaskCancelRequest". + */ +/** @experimental */ +export interface ClientTaskCancelRequest { + /** + * Session that owns the client task + */ + sessionId: string; + /** + * Canonical runtime-generated task identifier + */ + id: string; + /** + * Owner-scoped task key included for correlation + */ + clientTaskId: string; + /** + * Opaque identifier shared by coalesced cancellation callers + */ + cancellationId: string; + reason: ClientTaskCancelReason; +} +/** + * Whether the client authoritatively confirmed its external work stopped. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ClientTaskCancelResult". + */ +/** @experimental */ +export interface ClientTaskCancelResult { + /** + * True only when the owner confirms that external work stopped before responding + */ + cancelled: boolean; +} /** * Slash commands available in the session, after applying any include/exclude filters. * @@ -6449,6 +6806,10 @@ export interface ConnectRequest { */ enableGitHubTelemetryForwarding?: boolean; clientInfo?: ConnectClientInfo; + /** + * Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. + */ + supportedTaskKinds?: TaskKind[]; /** * Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN */ @@ -6475,6 +6836,10 @@ export interface ConnectResult { * Server package version */ version: string; + /** + * Task kinds the server may return to this connection. + */ + taskKinds?: TaskKind[]; } /** * Local file system absolute paths within the session working directory to check against its content-exclusion policy. @@ -6549,7 +6914,7 @@ export interface ContextHeaviestMessage { tokens: number; } /** - * The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + * The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "CurrentModel". @@ -6565,6 +6930,15 @@ export interface CurrentModel { */ reasoningEffort?: string; contextTier?: ContextTier; + autoTier?: AutoTier; + /** + * Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + */ + pendingAutoTier?: AutoTier | null; + /** + * Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + */ + activatingAutoTier?: AutoTier | null; } /** * Lightweight metadata for a currently initialized session tool @@ -6821,6 +7195,37 @@ export interface DiscoveredExtensionsEnableRequest { */ ids: string[]; } +/** + * One server-discovered hook action from user, repository, plugin, or managed-policy configuration. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "DiscoveredHook". + */ +/** @experimental */ +export interface DiscoveredHook { + /** + * Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. + */ + id: string; + hookType: HookType; + origin: HookOrigin; + /** + * Human-readable source label, such as a hook file path, settings source, or plugin name. + */ + source?: string; + /** + * Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. + */ + projectPath?: string; + /** + * Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. + */ + enabled: boolean; + /** + * Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. + */ + disableKey?: string; +} /** * MCP server discovered by `mcp.discover`, with config source, optional plugin source, transport type, and enabled state. * @@ -8156,6 +8561,10 @@ export interface FactoryRunResult { * Factory run identifier. */ runId: string; + /** + * One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + */ + attempt?: number; status: FactoryRunStatus; /** * Completed factory result. @@ -8958,6 +9367,44 @@ export interface HookInvokeRequest { export interface HookInvokeResponse { output?: JsonValue; } +/** + * Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HooksDiscoverRequest". + */ +/** @experimental */ +export interface HooksDiscoverRequest { + /** + * Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + */ + projectPaths?: string[]; + /** + * When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + */ + excludeHostHooks?: boolean; +} +/** + * Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "HooksDiscoverResult". + */ +/** @experimental */ +export interface HooksDiscoverResult { + /** + * All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + */ + hooks: DiscoveredHook[]; + /** + * Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + */ + warnings: string[]; + /** + * Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + */ + errors: string[]; +} /** * Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. * @@ -10344,6 +10791,10 @@ export interface McpConfigRemoveRequest { * Name of the MCP server to remove */ name: string; + /** + * OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. + */ + authClientIdMetadataUrl?: string; } /** * MCP server name and replacement configuration to write to user configuration. @@ -11646,6 +12097,7 @@ export interface McpServer { * Error message if the server failed to connect */ error?: string; + serverMetadata?: McpServerMetadata; } /** * In-process MCP server configuration used by embedded SDK clients. @@ -12110,6 +12562,12 @@ export interface Model { */ name: string; capabilities: ModelCapabilities; + /** + * Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. + */ + metadata?: { + [k: string]: JsonValue | undefined; + }; policy?: ModelPolicy; billing?: ModelBilling; /** @@ -12356,6 +12814,10 @@ export interface ModelBillingPromo { * Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. */ message?: string; + /** + * Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + */ + showBanner?: boolean; } /** * Service-published warning text that hosts should display when presenting a model. @@ -12403,6 +12865,10 @@ export interface ModelApplyStartupOverlayRequest { * Model required by server-managed policy, when configured. */ serverManagedModel?: string; + /** + * Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. + */ + policyHelperModel?: string; /** * Model selected by repository settings, when configured. */ @@ -12581,6 +13047,43 @@ export interface ModelsListRequest { */ gitHubToken?: string; } +/** + * An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelSwitchAutoTierRequest". + */ +/** @experimental */ +export interface ModelSwitchAutoTierRequest { + /** + * Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. + */ + autoTier: AutoTier | null; + source?: ModelChangeSource; +} +/** + * Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "ModelSwitchAutoTierResult". + */ +/** @experimental */ +export interface ModelSwitchAutoTierResult { + status: ModelSwitchAutoTierStatus; + effectiveAutoTier?: AutoTier; + /** + * Latest unclaimed Auto preference waiting for a future user turn. + */ + pendingAutoTier?: AutoTier | null; + /** + * Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + */ + activatingAutoTier?: AutoTier | null; + /** + * Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + */ + supersededAutoTier?: AutoTier | null; +} /** @experimental */ export interface ModelSwitchConfirmation { @@ -12609,6 +13112,10 @@ export interface ModelSwitchToRequest { * Model selection id to switch to, as returned by `list`. A bare id (e.g. `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. */ modelId: string; + /** + * Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + */ + autoTier?: AutoTier | null; /** * Reasoning effort level to use for the model. CAPI values are model-defined and validated against the selected model; BYOK providers may define additional values. "none" disables reasoning. When omitted, no effort override is applied. */ @@ -12681,6 +13188,7 @@ export interface ModelSwitchToResult { * Deprecation warnings associated with the selected model or options. */ deprecationWarnings?: string[]; + modelState?: CurrentModel; } /** * Agent interaction mode to apply to the session. @@ -14614,6 +15122,7 @@ export interface PluginInstallResult { * Number of skills discovered and installed from the plugin */ skillsInstalled: number; + stagingMode?: PluginInstallStagingMode; /** * Optional post-install message provided by the plugin (e.g. setup instructions) */ @@ -15842,6 +16351,10 @@ export interface QueuePendingItems { * Stable opaque id for the canonical queued item. Batch rows share one id. */ id: string; + /** + * Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. + */ + messageId?: string; kind: QueuePendingItemsKind; /** * Human-readable text to display for this queue entry in the UI @@ -16457,6 +16970,30 @@ export interface SandboxConfig { * Whether to auto-add the current working directory to readwritePaths. Default: true. */ addCurrentWorkingDirectory?: boolean; + /** + * Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + */ + sandboxMcpServers?: boolean; + /** + * Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + */ + sandboxLspServers?: boolean; + /** + * Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). + */ + allowBypass?: boolean; + /** + * Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. + * + * @internal + */ + managedMcpRoutingLocked?: boolean; + /** + * The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + * + * @internal + */ + managedLspRoutingLocked?: boolean; auth?: SandboxConfigAuth; /** * Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). @@ -16528,7 +17065,7 @@ export interface SandboxConfigUserPolicyNetwork { /** @experimental */ export interface SandboxConfigUserPolicyNetworkProxy { /** - * Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + * Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. */ url: string; /** @@ -18166,6 +18703,10 @@ export interface SessionOpenOptions { * Identifier of the client driving the session. */ clientName?: string; + /** + * OAuth Client ID Metadata Document URL used by this host for MCP authorization. + */ + authClientIdMetadataUrl?: string; /** * Structured client kind used for runtime behavior gates. */ @@ -18301,6 +18842,17 @@ export interface SessionOpenOptions { * Additional directories to search for skills. */ skillDirectories?: string[]; + /** + * Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by default. + */ + enableSkills?: boolean; + /** + * Whether the requesting SDK session has a skill provider. The provider remains ephemeral and is never persisted in session options or history. When enableSkills is false, it remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw sessions.open flows reject it because they cannot safely pre-register the callback handler. + * + * @internal + * @experimental + */ + hasSkillProvider?: boolean; /** * Built-in skill names to include in this session. When specified, only these runtime-bundled skills are available. Skills from other sources with the same name remain available. */ @@ -19452,17 +20004,39 @@ export interface SessionsPruneOldRequest { */ olderThanDays: number; /** - * When true, only report what would be deleted without performing any deletion + * When true, only report what would be deleted without performing any deletion + */ + dryRun?: boolean; + /** + * When true, named sessions (set via /rename) are also eligible for pruning + */ + includeNamed?: boolean; + /** + * Session IDs that should never be considered for pruning + */ + excludeSessionIds?: string[]; +} +/** + * Pagination options for reading an inactive or active local session's persisted event journal. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SessionsReadPersistedEventsRequest". + */ +/** @experimental */ +export interface SessionsReadPersistedEventsRequest { + /** + * Session ID whose persisted event journal should be read. */ - dryRun?: boolean; + sessionId: string; /** - * When true, named sessions (set via /rename) are also eligible for pruning + * Opaque cursor returned by a previous persisted-event read. Omit on the first call. */ - includeNamed?: boolean; + cursor?: string; /** - * Session IDs that should never be considered for pruning + * Maximum number of events to return in this batch (1–1000, default 200). */ - excludeSessionIds?: string[]; + max?: number; + direction?: EventsReadDirection; } /** * Session ID whose in-use lock should be released. @@ -19834,7 +20408,7 @@ export interface SessionUpdateOptionsParams { */ enableSessionStore?: boolean; /** - * Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + * Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. */ enableSkills?: boolean; contextTier?: OptionsUpdateContextTier; @@ -20055,6 +20629,83 @@ export interface SkillList { */ skills: Skill[]; } +/** + * Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderDescriptor". + */ +/** @experimental */ +export interface SkillProviderDescriptor { + /** + * Invocation and display name. + */ + name: string; + /** + * Description used in skill catalogs without fetching content. + */ + description: string; + /** + * Whether users may invoke the skill directly. Defaults to true. + */ + userInvocable?: boolean; + /** + * Whether model invocation is disabled. Defaults to false. + */ + disableModelInvocation?: boolean; + /** + * Optional freeform argument hint used by slash-command catalogs. + */ + argumentHint?: string; +} +/** + * Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate metadata. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderListResult". + */ +/** @experimental */ +/** @internal */ +export interface SkillProviderListResult { + /** + * Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison. + * + * @maxItems 1024 + */ + skills: SkillProviderDescriptor[]; +} +/** + * Identifies one SDK-provided skill by invocation name. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderReadRequest". + */ +/** @experimental */ +/** @internal */ +export interface SkillProviderReadRequest { + /** + * Target session identifier + */ + sessionId: string; + /** + * Invocation name of the skill to read. + */ + name: string; +} +/** + * Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderReadResult". + */ +/** @experimental */ +/** @internal */ +export interface SkillProviderReadResult { + /** + * Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. + */ + markdown: string; +} /** * Skill names to mark as disabled in global configuration, replacing any previous list. * @@ -20175,7 +20826,7 @@ export interface SkillsInvokedSkill { */ name: string; /** - * Path to the SKILL.md file + * Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity */ path: string; /** @@ -20186,6 +20837,10 @@ export interface SkillsInvokedSkill { * Tools that should be auto-approved when this skill is active, captured at invocation time */ allowedTools?: string[]; + /** + * Whether model invocation was disabled when this skill was invoked + */ + disableModelInvocation?: boolean; /** * Turn number when the skill was invoked */ @@ -20240,6 +20895,7 @@ export interface SlashCommandTimelineEntry { * Optional URL associated with the timeline entry. */ url?: string; + remediation?: RemediationAction; } /** * Slash-command invocation result that submits an agent prompt, with display prompt, optional mode, optional user-facing notice, and settings-change flag. @@ -20287,6 +20943,7 @@ export interface SlashCommandCompletedResult { * Optional user-facing message describing the completed command */ message?: string; + mode?: SessionMode; /** * True when the invocation mutated user runtime settings; consumers caching settings should refresh */ @@ -20472,6 +21129,7 @@ export interface SubagentSettingsEntry { * Model override for matching subagents */ model?: string; + modelPolicy?: AgentModelPolicy; /** * Reasoning effort override for matching subagents */ @@ -20599,6 +21257,157 @@ export interface TaskProgressLine { */ timestamp: string; } +/** + * Tracked client-owned task metadata. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientInfo". + */ +/** @experimental */ +export interface TaskClientInfo { + type: TaskClientType; + /** + * Canonical runtime-generated task identifier + */ + id: string; + /** + * Owner-scoped registration and reclaim key + */ + clientTaskId: string; + /** + * Optional task display name + */ + displayName?: string; + /** + * Task description + */ + description: string; + status: TaskClientStatus; + owner: TaskClientOwner; + /** + * ISO 8601 timestamp when the task started + */ + startedAt: string; + /** + * ISO 8601 timestamp of the latest accepted lifecycle change + */ + updatedAt: string; + /** + * ISO 8601 timestamp when the task reached a terminal status + */ + completedAt?: string; + /** + * Accumulated active execution time in milliseconds + */ + activeTimeMs: number; + /** + * ISO 8601 timestamp when the current active segment started + */ + activeStartedAt?: string; + /** + * ISO 8601 timestamp when the connected owner entered idle status + */ + idleSince?: string; + /** + * ISO 8601 timestamp of the most recent orphan transition + */ + orphanedAt?: string; + /** + * ISO 8601 timestamp of the most recent successful reclaim + */ + reclaimedAt?: string; + executionMode: TaskClientExecutionMode; + /** + * Whether the currently bound owner can receive a cancellation request + */ + canCancel: boolean; + /** + * Sequence number of the latest accepted owner update + */ + sequence: number; + /** + * Opaque successful terminal result supplied by the task owner + */ + result?: JsonValue; + /** + * Human-readable terminal failure message + */ + error?: string; + /** + * Optional owner-supplied terminal failure code + */ + errorCode?: string; + /** + * Human-readable reason for terminal cancellation + */ + cancellationReason?: string; +} +/** + * Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientOwner". + */ +/** @experimental */ +export interface TaskClientOwner { + /** + * Opaque session-scoped participant identity + */ + participantId: string; + /** + * Opaque identity of the currently or most recently bound session join + */ + joinId: string; + kind: TaskClientOwnerKind; + /** + * Display-only owner name + */ + displayName?: string; + /** + * Display-only owner source + */ + source?: string; + presence: TaskClientOwnerPresence; + /** + * ISO 8601 timestamp when the bound join disconnected + */ + disconnectedAt?: string; +} +/** + * Generic progress for a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TaskClientProgress". + */ +/** @experimental */ +export interface TaskClientProgress { + type: TaskClientType; + status: TaskClientStatus; + /** + * Sequence number of the latest accepted owner update + */ + sequence: number; + /** + * ISO 8601 timestamp of the latest accepted lifecycle change + */ + updatedAt: string; + /** + * Current owner-defined progress phase + */ + phase?: string; + /** + * Current completion percentage from zero through one hundred + */ + percentage?: number; + /** + * Most recent nonempty progress message + */ + lastMessage?: string; + /** + * Recent server-timestamped progress messages + */ + recentActivity: TaskProgressLine[]; +} /** @experimental */ export interface TaskCompletionDecision { @@ -20816,6 +21625,54 @@ export interface TasksPromoteToBackgroundResult { */ /** @experimental */ export interface TasksRefreshResult {} +/** + * Registers or reclaims a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TasksRegisterRequest". + */ +/** @experimental */ +export interface TasksRegisterRequest { + type: TaskClientType; + /** + * Owner-scoped idempotency key used for registration and reclaim + */ + clientTaskId: string; + /** + * Human-readable description of the external work + */ + description: string; + /** + * Optional short display name for the external work + */ + displayName?: string; + /** + * Whether the owner supports runtime cancellation requests + */ + cancellable: boolean; + /** + * Expected current sequence for idempotent registration or orphan reclaim + */ + expectedSequence?: number; +} +/** + * Result of registering or reclaiming a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TasksRegisterResult". + */ +/** @experimental */ +export interface TasksRegisterResult { + task: TaskClientInfo; + /** + * True only when this invocation created a new task + */ + created: boolean; + /** + * True only when this invocation reclaimed an orphaned task + */ + reclaimed: boolean; +} /** * Identifier of the completed or cancelled task to remove from tracking. * @@ -20922,6 +21779,42 @@ export interface TasksStartAgentResult { */ agentId: string; } +/** + * Updates a client-owned task. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TasksUpdateRequest". + */ +/** @experimental */ +export interface TasksUpdateRequest { + /** + * Canonical runtime-generated task identifier + */ + id: string; + /** + * Owner update sequence to apply + */ + sequence: number; + update: TaskClientUpdate; +} +/** + * Result of publishing a client-owned task update. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "TasksUpdateResult". + */ +/** @experimental */ +export interface TasksUpdateResult { + task: TaskClientInfo; + /** + * Whether this invocation changed task state + */ + applied: boolean; + /** + * Whether this invocation repeated the latest accepted update + */ + duplicate: boolean; +} /** * Wait until all in-flight background tasks (agents + shells) and any follow-up turns scheduled by their completions have settled. Returns when the runtime is fully drained or after an internal timeout (default 10 minutes; configurable via COPILOT_TASK_WAIT_TIMEOUT_SECONDS). * @@ -22790,6 +23683,19 @@ export interface SessionLimitPredictionPredictRequest { modelId?: string; clientType?: SessionLimitPredictionClientType; } +/** + * Identifies the target session. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "SkillProviderListRequest". + */ +/** @experimental */ +export interface SkillProviderListRequest { + /** + * Target session identifier + */ + sessionId: string; +} /** * Identifies the target session. * @@ -22819,6 +23725,18 @@ export function createServerRpc(connection: MessageConnection) { ping: async (params: PingRequest): Promise => connection.sendRequest("ping", params), /** @experimental */ + hooks: { + /** + * Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. + * + * @param params Optional project paths and host-exclusion behavior for server-scoped hook discovery. + * + * @returns Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + */ + discover: async (params: HooksDiscoverRequest): Promise => + connection.sendRequest("hooks.discover", params), + }, + /** @experimental */ models: { /** * Lists Copilot models available to the authenticated user. @@ -23257,6 +24175,11 @@ export function createServerRpc(connection: MessageConnection) { */ read: async (): Promise => connection.sendRequest("managedSettings.read", {}), + /** + * Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. + */ + clearCache: async (): Promise => + connection.sendRequest("managedSettings.clearCache", {}), }, /** @experimental */ runtime: { @@ -23344,6 +24267,15 @@ export function createServerRpc(connection: MessageConnection) { */ list: async (params: SessionsListRequest): Promise => connection.sendRequest("sessions.list", params), + /** + * Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session. + * + * @param params Pagination options for reading an inactive or active local session's persisted event journal. + * + * @returns Batch of session events returned by a read, with cursor and continuation metadata. + */ + readPersistedEvents: async (params: SessionsReadPersistedEventsRequest): Promise => + connection.sendRequest("sessions.readPersistedEvents", params), /** * Finds the local session bound to a GitHub task ID, if any. * @@ -23885,9 +24817,9 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin /** @experimental */ model: { /** - * Gets the currently selected model for the session. + * Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. * - * @returns The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + * @returns The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. */ getCurrent: async (): Promise => connection.sendRequest("session.model.getCurrent", { sessionId }), @@ -23900,6 +24832,15 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ switchTo: async (params: ModelSwitchToRequest): Promise => connection.sendRequest("session.model.switchTo", { sessionId, ...params }), + /** + * Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. + * + * @param params An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + * + * @returns Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + */ + switchAutoTier: async (params: ModelSwitchAutoTierRequest): Promise => + connection.sendRequest("session.model.switchAutoTier", { sessionId, ...params }), /** * Updates the session's reasoning effort without changing the selected model. * @@ -24134,6 +25075,16 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.workspaces.diff", { sessionId, ...params }), }, /** @experimental */ + autopilotObjective: { + /** + * Reads the current canonical autopilot objective state for this session. + * + * @returns Canonical runtime state for the session's current autopilot objective. + */ + getState: async (): Promise => + connection.sendRequest("session.autopilotObjective.getState", { sessionId }), + }, + /** @experimental */ completions: { /** * Gets the characters that should trigger host-driven completions for the session. Empty disables host-driven completions (e.g. local sessions, or a relay host that does not advertise them). @@ -24239,6 +25190,24 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin */ list: async (): Promise => connection.sendRequest("session.tasks.list", { sessionId }), + /** + * Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. + * + * @param params Registers or reclaims a client-owned task. + * + * @returns Result of registering or reclaiming a client-owned task. + */ + register: async (params: TasksRegisterRequest): Promise => + connection.sendRequest("session.tasks.register", { sessionId, ...params }), + /** + * Publishes generic progress or a terminal outcome for a client-owned task. + * + * @param params Updates a client-owned task. + * + * @returns Result of publishing a client-owned task update. + */ + update: async (params: TasksUpdateRequest): Promise => + connection.sendRequest("session.tasks.update", { sessionId, ...params }), /** * Refreshes metadata for any detached background shells the runtime knows about. * @@ -25917,6 +26886,19 @@ export interface FactoryHandler { abort(params: FactoryAbortRequest): Promise; } +/** Handler for `tasks` client session API methods. */ +/** @experimental */ +export interface TasksHandler { + /** + * Asks the client currently bound to a client-owned session task to confirm that its external work stopped. + * + * @param params Runtime-to-owner cancellation request for a client-owned task. + * + * @returns Whether the client authoritatively confirmed its external work stopped. + */ + cancel(params: ClientTaskCancelRequest): Promise; +} + /** Handler for `sessionFs` client session API methods. */ /** @experimental */ export interface SessionFsHandler { @@ -26057,6 +27039,7 @@ export interface CanvasHandler { export interface ClientSessionApiHandlers { providerToken?: ProviderTokenHandler; factory?: FactoryHandler; + tasks?: TasksHandler; sessionFs?: SessionFsHandler; canvas?: CanvasHandler; } @@ -26086,6 +27069,11 @@ export function registerClientSessionApiHandlers( if (!handler) throw new Error(`No factory handler registered for session: ${params.sessionId}`); return handler.abort(params); }); + connection.onRequest("tasks.cancel", async (params: ClientTaskCancelRequest) => { + const handler = getHandlers(params.sessionId).tasks; + if (!handler) throw new Error(`No tasks handler registered for session: ${params.sessionId}`); + return handler.cancel(params); + }); connection.onRequest("sessionFs.readFile", async (params: SessionFsReadFileRequest) => { const handler = getHandlers(params.sessionId).sessionFs; if (!handler) throw new Error(`No sessionFs handler registered for session: ${params.sessionId}`); diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index c7a610abef..02fbad6c5c 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -23,7 +23,9 @@ export type SessionEvent = | InfoEvent | WarningEvent | ModelChangeEvent + | AutoTierSwitchFailedEvent | ModeChangedEvent + | ModeNoticeDeliveredEvent | SessionLimitsChangedEvent | PermissionsChangedEvent | PlanChangedEvent @@ -40,6 +42,7 @@ export type SessionEvent = | CompactionStartEvent | CompactionCompleteEvent | TaskCompleteEvent + | CompletionReceiptEvent | FusionRouteStartedEvent | FusionRouteFailedEvent | FusionResolvedEvent @@ -49,6 +52,7 @@ export type SessionEvent = | AssistantTurnStartEvent | AssistantIntentEvent | AssistantFusionPhaseStartedEvent + | AssistantFusionPhaseActivityEvent | AssistantFusionPhaseCompletedEvent | AssistantFusionPhaseFailedEvent | AssistantServerToolProgressEvent @@ -123,6 +127,8 @@ export type SessionEvent = | CustomAgentsUpdatedEvent | McpServersLoadedEvent | McpServerStatusChangedEvent + | McpServerRemovedEvent + | McpServerNeedsReconnectEvent | McpToolsListChangedEvent | McpResourcesListChangedEvent | McpPromptsListChangedEvent @@ -181,6 +187,20 @@ export type Verbosity = | "medium" /** A more detailed response was requested. */ | "high"; +/** + * What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take. + */ +export type RemediationAction = + /** Authenticate again with the Copilot backend. The current credential is absent, expired, or rejected. */ + | "sign_in" + /** Authenticate as a different account. The current account exists but lacks access to the requested resource. */ + | "switch_account" + /** Inspect which account is currently authenticated before deciding what to change. */ + | "show_account" + /** Review or widen the sandbox policy. The blocked path or host is named by the accompanying message or by the tool result the action arrived with. */ + | "review_sandbox_policy" + /** Permit outbound network access in the sandbox policy. */ + | "allow_sandbox_outbound"; /** * The session mode the agent is operating in */ @@ -247,6 +267,18 @@ export type ModelChangeSource = | "automatic" /** An SDK or RPC caller selected the model. */ | "sdk"; +/** + * Terminal reason an Auto preference activation failed. + */ +export type AutoTierSwitchFailureReason = + /** The candidate model was rejected by model policy. */ + | "policy_rejected" + /** The Auto routing request failed or returned an unusable response. */ + | "request_failed" + /** The runtime could not prepare the Auto routing request. */ + | "setup_failed" + /** The provider does not support Auto routing. */ + | "unsupported"; /** * Permission mode for the session. */ @@ -316,6 +348,30 @@ export type TaskCompletionOutcome = | "continue" /** Completion cannot proceed without intervention; the active objective is paused when one is identified. */ | "blocked"; +/** + * Structured terminal status from a tool completion event. + */ +export type CompletionReceiptToolStatus = + /** The tool completed successfully. */ + | "success" + /** The tool failed without a more specific structured status. */ + | "failure" + /** The tool exceeded its time budget. */ + | "timeout" + /** The user rejected the tool call. */ + | "rejected" + /** The permissions service denied the tool call. */ + | "denied"; +/** + * Runtime reason the completion decision was accepted. + */ +export type CompletionReceiptStopReason = + /** The model reached a natural terminal response. */ + | "natural" + /** A terminal tool ended the interaction. */ + | "terminal_tool" + /** The configured agentStop continuation limit was reached. */ + | "agent_stop_block_limit"; /** * Kind of turn for which HydraFusion routing is running. */ @@ -345,6 +401,34 @@ export type FusionPattern = | "cascade" /** Run a primary draft, a read-only critique, and a revision. */ | "critique"; +/** + * HydraFusion phase kind. + */ +/** @experimental */ +export type FusionPhaseKind = + /** Primary solver phase. */ + | "primary" + /** Read-only cascade judge phase. */ + | "judge" + /** Cascade repair phase. */ + | "repair" + /** Initial critique-pattern draft phase. */ + | "draft" + /** Read-only critique phase. */ + | "critic" + /** Critique-pattern revision phase. */ + | "revision" + /** Follow-up phase continuing from the resolved model. */ + | "follow_up"; +/** + * Conversation scope in which a HydraFusion phase executes. + */ +/** @experimental */ +export type FusionConversationScope = + /** Canonical root conversation history. */ + | "root" + /** Isolated read-only review history that does not enter the root conversation. */ + | "review"; /** * The agent mode that was active when this message was sent */ @@ -405,33 +489,16 @@ export type UserMessageDelivery = /** Enqueued while the agent was busy; processed as its own run afterward. */ | "queued"; /** - * Conversation scope in which a HydraFusion phase executes. + * Content-safe activity observed while a HydraFusion phase is running. */ /** @experimental */ -export type FusionConversationScope = - /** Canonical root conversation history. */ - | "root" - /** Isolated read-only review history that does not enter the root conversation. */ - | "review"; -/** - * HydraFusion phase kind. - */ -/** @experimental */ -export type FusionPhaseKind = - /** Primary solver phase. */ - | "primary" - /** Read-only cascade judge phase. */ - | "judge" - /** Cascade repair phase. */ - | "repair" - /** Initial critique-pattern draft phase. */ - | "draft" - /** Read-only critique phase. */ - | "critic" - /** Critique-pattern revision phase. */ - | "revision" - /** Follow-up phase continuing from the resolved model. */ - | "follow_up"; +export type FusionPhaseActivityKind = + /** The provider produced additional private output bytes. */ + | "model_output" + /** A tool began executing inside the phase. */ + | "tool_started" + /** A tool finished executing inside the phase. */ + | "tool_completed"; /** * How a durable phase checkpoint contributes its exact message to canonical root history. */ @@ -947,7 +1014,9 @@ export type ManagedSettingsResolvedSource = | "device" /** Only session-local SDK-host injection contributed. */ | "client" - /** More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. */ + /** A policy helper registered by device or server policy contributed. Device registration takes priority when present. */ + | "policyHelper" + /** More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. */ | "mixed" /** No managed policy is in force (no channel contributed). */ | "none"; @@ -998,7 +1067,7 @@ export type FactoryRunSettledStatus = /** The run failed, with `failureType` carrying the class when it has one. */ | "error"; /** - * Source location type (e.g., project, personal-copilot, plugin, builtin) + * Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) */ export type SkillSource = /** Skill defined in the current project's skill directories. */ @@ -1014,7 +1083,17 @@ export type SkillSource = /** Skill loaded from a configured custom skill directory. */ | "custom" /** Skill bundled with the runtime. */ - | "builtin"; + | "builtin" + /** Pathless skill supplied lazily by an SDK skill provider. */ + | "sdk"; +/** + * Whether configured models are advisory preferences or required constraints + */ +export type AgentModelPolicy = + /** Treat the authored models as advisory preferences that callers may override. */ + | "preferred" + /** Require subagent execution to use one of the authored models. */ + | "required"; /** * Configuration source: user, workspace, plugin, or builtin */ @@ -1411,6 +1490,7 @@ export interface ErrorData { * GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs */ providerCallId?: string; + remediation?: RemediationAction; /** * Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation */ @@ -1791,6 +1871,7 @@ export interface WarningData { * Human-readable warning message for display in the timeline */ message: string; + remediation?: RemediationAction; /** * Optional URL associated with this warning that the user can open in a browser */ @@ -1834,6 +1915,10 @@ export interface ModelChangeEvent { * Model change details including previous and new model identifiers */ export interface ModelChangeData { + /** + * Committed Auto preference after the model configuration change, when applicable. + */ + autoTier?: AutoTier | null; /** * Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. */ @@ -1846,6 +1931,7 @@ export interface ModelChangeData { * Newly selected model identifier */ newModel: string; + previousAutoTier?: AutoTier; /** * Model that was previously selected, if any */ @@ -1864,6 +1950,47 @@ export interface ModelChangeData { source?: ModelChangeSource; verbosity?: Verbosity; } +/** + * Session event "session.auto_tier_switch_failed". A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. + */ +export interface AutoTierSwitchFailedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: AutoTierSwitchFailedData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.auto_tier_switch_failed". + */ + type: "session.auto_tier_switch_failed"; +} +/** + * A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. + */ +export interface AutoTierSwitchFailedData { + effectiveAutoTier?: AutoTier; + reason: AutoTierSwitchFailureReason; + /** + * Auto preference that failed to activate, or null when returning to provider-default routing failed. + */ + requestedAutoTier: AutoTier | null; +} /** * Session event "session.mode_changed". Agent mode change details including previous and new modes */ @@ -1901,6 +2028,46 @@ export interface ModeChangedData { newMode: SessionMode; previousMode: SessionMode; } +/** + * Session event "session.mode_notice_delivered". Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. + */ +export interface ModeNoticeDeliveredEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: ModeNoticeDeliveredData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.mode_notice_delivered". + */ + type: "session.mode_notice_delivered"; +} +/** + * Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. + */ +export interface ModeNoticeDeliveredData { + /** + * Model-visible transition notice persisted for a mid-turn delivery + */ + content?: string; + mode: SessionMode; +} /** * Session event "session.session_limits_changed". Session limits update details. Null clears the limits. */ @@ -3025,6 +3192,97 @@ export interface TaskCompleteData { */ summary?: string; } +/** + * Session event "session.completion_receipt". Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. + */ +/** @experimental */ +export interface CompletionReceiptEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: CompletionReceiptData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.completion_receipt". + */ + type: "session.completion_receipt"; +} +/** + * Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. + */ +/** @experimental */ +export interface CompletionReceiptData { + /** + * One-based accepted completion receipt ordinal in the durable session history. + */ + attempt: number; + eventRange: CompletionReceiptEventRange; + /** + * Number of failed structured tool completions in the covered range. + */ + failedToolCount: number; + finalTool?: CompletionReceiptFinalTool; + /** + * Version of the completion receipt payload. + */ + schemaVersion: number; + /** + * Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + */ + sourceEventId: string; + stopReason: CompletionReceiptStopReason; + /** + * Number of successful structured tool completions in the covered range. + */ + successfulToolCount: number; +} +/** + * Inclusive durable event range summarized by a completion receipt. + */ +export interface CompletionReceiptEventRange { + /** + * Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + */ + endEventId: string; + /** + * Identifier of the user message that starts the covered exchange. + */ + startEventId: string; +} +/** + * Final structured tool completion in the covered event range. + */ +export interface CompletionReceiptFinalTool { + /** + * Process exit code from a structured shell result, when available. + */ + exitCode?: number; + status: CompletionReceiptToolStatus; + /** + * Unique identifier of the completed tool call. + */ + toolCallId: string; + /** + * Tool name from the matching tool execution start event, when available. + */ + toolName?: string; +} /** * Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn. */ @@ -3198,6 +3456,12 @@ export interface FusionResolvedData { */ modelUniverseVersion?: string; pattern: FusionPattern; + /** + * Presentation-neutral phase plan for clients that render workflow progress. + * + * @experimental + */ + phasePlan?: FusionPhasePlanStep[]; /** * Version of the validated execution-plan format. */ @@ -3256,6 +3520,22 @@ export interface FusionFollowUpRecommendation { compactionTurn: FusionFollowUpAction; userTurn: FusionFollowUpAction; } +/** + * Presentation-neutral phase planned for a HydraFusion turn. + */ +/** @experimental */ +export interface FusionPhasePlanStep { + /** + * Whether the phase executes only when an earlier phase requests it. + */ + conditional: boolean; + kind: FusionPhaseKind; + /** + * Semantic role assigned to the phase. + */ + role: string; + scope: FusionConversationScope; +} /** * Validated HydraFusion routing capability scores. */ @@ -3436,6 +3716,10 @@ export interface UserMessageData { * True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. */ isAutopilotContinuation?: boolean; + /** + * Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots + */ + messageId?: string; /** * Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit */ @@ -4075,6 +4359,67 @@ export interface FusionPhaseStartedData { */ role: string; } +/** + * Session event "assistant.fusion_phase_activity". Experimental content-safe activity signal for a running HydraFusion phase. + */ +/** @experimental */ +export interface AssistantFusionPhaseActivityEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: FusionPhaseActivityData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "assistant.fusion_phase_activity". + */ + type: "assistant.fusion_phase_activity"; +} +/** + * Experimental content-safe activity signal for a running HydraFusion phase. + */ +/** @experimental */ +export interface FusionPhaseActivityData { + activity: FusionPhaseActivityKind; + conversationScope: FusionConversationScope; + /** + * Identifier of the HydraFusion turn containing the phase. + */ + fusionId: string; + pattern: FusionPattern; + /** + * Stable identifier for the concrete phase. + */ + phaseId: string; + phaseKind: FusionPhaseKind; + /** + * Semantic role assigned to the phase. + */ + role: string; + /** + * Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + */ + toolCallId?: string; + /** + * Cumulative private response bytes observed for this model call. The event never includes response text. + */ + totalResponseSizeBytes?: number; +} /** * Session event "assistant.fusion_phase_completed". Experimental durable HydraFusion phase output and lossless replay checkpoint. */ @@ -4815,7 +5160,7 @@ export interface FusionAttribution { /** @experimental */ export interface AssistantMessageReasoningBlocks { /** - * Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + * Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. */ blocks?: JsonValue[]; /** @@ -6011,6 +6356,7 @@ export interface ToolExecutionCompleteError { * Human-readable error message */ message: string; + remediation?: RemediationAction; } /** * Tool execution result on success @@ -6569,6 +6915,10 @@ export interface SkillInvokedData { * Description of the skill from its SKILL.md frontmatter */ description?: string; + /** + * Whether model invocation is disabled for this skill + */ + disableModelInvocation?: boolean; /** * Model identifier active when the skill was invoked, when known */ @@ -6578,7 +6928,7 @@ export interface SkillInvokedData { */ name: string; /** - * File path to the SKILL.md definition + * File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity */ path: string; /** @@ -6590,7 +6940,7 @@ export interface SkillInvokedData { */ pluginVersion?: string; /** - * Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + * Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) */ source?: string; trigger?: SkillInvokedTrigger; @@ -6795,6 +7145,10 @@ export interface SubagentCompletedData { * Model used by the sub-agent */ model?: string; + /** + * Why an explicit task-call model did not become the effective model + */ + modelOverrideReason?: string; /** * Tool call ID of the parent tool invocation that spawned this sub-agent */ @@ -6882,6 +7236,10 @@ export interface SubagentFailedData { * Model selected for the sub-agent, when known */ model?: string; + /** + * Why an explicit task-call model did not become the effective model + */ + modelOverrideReason?: string; /** * Tool call ID of the parent tool invocation that spawned this sub-agent */ @@ -7546,6 +7904,7 @@ export interface PermissionRequestedEvent { * Permission request notification requiring client approval with request details */ export interface PermissionRequestedData { + agentMode?: SessionMode; permissionRequest: PermissionRequest; promptRequest?: PermissionPromptRequest; /** @@ -7606,11 +7965,11 @@ export interface PermissionRequestShell { */ possibleUrls: PermissionRequestShellPossibleUrl[]; /** - * True when the model has requested to run this command outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + * True when the tool is asking to run this command outside the sandbox, either because the command detaches and cannot be sandboxed at all, or because a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. */ requestSandboxBypass?: boolean; /** - * Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + * What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. */ requestSandboxBypassReason?: string; /** @@ -7723,11 +8082,11 @@ export interface PermissionRequestRead { */ path: string; /** - * True when the model has requested to run this search outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + * True when the tool is asking to re-run this search outside the sandbox, after a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. */ requestSandboxBypass?: boolean; /** - * Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + * What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. */ requestSandboxBypassReason?: string; /** @@ -7795,11 +8154,11 @@ export interface PermissionRequestUrl { */ redirectedFrom?: string; /** - * True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + * True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. */ requestSandboxBypass?: boolean; /** - * Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + * What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. */ requestSandboxBypassReason?: string; /** @@ -8256,11 +8615,11 @@ export interface PermissionPromptRequestUrl { */ redirectedFrom?: string; /** - * True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + * True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. */ requestSandboxBypass?: boolean; /** - * Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + * What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. */ requestSandboxBypassReason?: string; /** @@ -10068,7 +10427,7 @@ export interface AutoModeResolvedData { stickyOverride?: boolean; } /** - * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. */ /** @experimental */ export interface ManagedSettingsResolvedEvent { @@ -10099,7 +10458,7 @@ export interface ManagedSettingsResolvedEvent { type: "session.managed_settings_resolved"; } /** - * Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. + * Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. */ /** @experimental */ export interface ManagedSettingsResolvedData { @@ -10127,6 +10486,10 @@ export interface ManagedSettingsResolvedData { * Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. */ permissionsAllowIntersected?: boolean; + /** + * Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + */ + policyHelperManaged?: boolean; /** * Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. */ @@ -10751,7 +11114,7 @@ export interface CustomAgentsUpdatedData { warnings: string[]; } /** - * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. + * A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. */ export interface CustomAgentsUpdatedAgent { /** @@ -10770,6 +11133,11 @@ export interface CustomAgentsUpdatedAgent { * Model override for this agent, if set */ model?: string; + modelPolicy?: AgentModelPolicy; + /** + * Authored model ids in priority order, if configured + */ + models?: string[]; /** * Internal name of the agent */ @@ -10846,10 +11214,20 @@ export interface McpServersLoadedServer { * Version of the plugin that supplied the effective MCP server config, only when source is plugin */ pluginVersion?: string; + serverMetadata?: McpServerMetadata; source?: McpServerSource; status: McpServerStatus; transport?: McpServerTransport; } +/** + * Server-advertised metadata learned through modern discovery or legacy initialization. + */ +export interface McpServerMetadata { + /** + * Non-empty natural-language guidance for using the server, or null when the server omitted instructions or advertised an empty string. + */ + instructions: string | null; +} /** * Session event "session.mcp_server_status_changed". Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error. */ @@ -10894,6 +11272,84 @@ export interface McpServerStatusChangedData { serverName: string; status: McpServerStatus; } +/** + * Session event "session.mcp_server_removed". Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. + */ +export interface McpServerRemovedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: McpServerRemovedData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.mcp_server_removed". + */ + type: "session.mcp_server_removed"; +} +/** + * Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. + */ +export interface McpServerRemovedData { + /** + * Name of the MCP server that was removed from the graph + */ + serverName: string; +} +/** + * Session event "session.mcp_server_needs_reconnect". Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. + */ +export interface McpServerNeedsReconnectEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: McpServerNeedsReconnectData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.mcp_server_needs_reconnect". + */ + type: "session.mcp_server_needs_reconnect"; +} +/** + * Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. + */ +export interface McpServerNeedsReconnectData { + /** + * Name of the MCP server that needs to reconnect + */ + serverName: string; +} /** * Session event "mcp.tools.list_changed". Payload identifying the MCP server associated with a list change. */ diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 8b9095e0ba..2007679d61 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -69,6 +69,7 @@ export type { UserPromptTransformedHandler, UserPromptTransformedHookInput, UserPromptTransformedHookOutput, + CopilotClientInfo, CopilotClientMode, CopilotClientOptions, CopilotExpAssignmentResponse, @@ -120,7 +121,11 @@ export type { ModelBilling, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, + AutoTier, CapiSessionOptions, + CurrentModel, + ModelSwitchAutoTierResult, + ModelSwitchAutoTierStatus, ModelCapabilities, ModelCapabilitiesOverride, ModelInfo, diff --git a/nodejs/src/runtimeArtifacts.ts b/nodejs/src/runtimeArtifacts.ts index 19d9926250..d33b72c9da 100644 --- a/nodejs/src/runtimeArtifacts.ts +++ b/nodejs/src/runtimeArtifacts.ts @@ -1,4 +1,3 @@ -import { createHash } from "node:crypto"; import { chmodSync, copyFileSync, @@ -11,32 +10,50 @@ import { rmSync, statSync, } from "node:fs"; +import { createHash } from "node:crypto"; +import { createRequire } from "node:module"; import { homedir } from "node:os"; import { dirname, join, relative, sep } from "node:path"; +import { COPILOT_CLI_USE_NPM_PACKAGE } from "./cliVersion.js"; export interface RuntimeArtifactSources { packageRoot: string; platform: string; } +export interface EnsureRuntimeBundleOptions { + cacheRoot?: string; + packageSearchPaths?: string[]; + platform?: string; +} + +const require = createRequire(typeof __filename === "string" ? __filename : import.meta.url); +export const RUNTIME_PLATFORMS = [ + "darwin-arm64", + "darwin-x64", + "linux-arm64", + "linux-x64", + "linuxmusl-arm64", + "linuxmusl-x64", + "win32-arm64", + "win32-x64", +] as const; + const EXCLUDED_TOP_LEVEL = new Set([ "app.js", "assets", "changelog.json", "copilot", "copilot.exe", - "copilot-sdk", "foundry-local-sdk", "index.js", "LICENSE.md", "napi-oop-runtime", "npm-loader.js", "package.json", - "preloads", "pvrecorder", "queries", "README.md", - "sdk", "sea-loader.js", "webview", ]); @@ -46,7 +63,7 @@ interface RuntimeAsset { relativePath: string; } -function validateFile(path: string, label: string): void { +export function validateFile(path: string, label: string): void { if (!existsSync(path)) { throw new Error(`${label} not found at ${path}.`); } @@ -92,14 +109,12 @@ function collectRuntimeAssets(sources: RuntimeArtifactSources): RuntimeAsset[] { } const parts = sourceRelative.split(sep); - let relativePath = sourceRelative; if (parts[0] === "prebuilds") { if (parts[1] !== sources.platform || parts.length < 3) { continue; } - relativePath = parts.slice(2).join(sep); } - assets.push({ source, relativePath }); + assets.push({ source, relativePath: sourceRelative }); } }; visit(sources.packageRoot); @@ -142,17 +157,25 @@ export function defaultRuntimeCacheRoot( export function materializeRuntimeBundle( sources: RuntimeArtifactSources, - cacheRoot = defaultRuntimeCacheRoot() + cacheRoot = defaultRuntimeCacheRoot(), + cacheKey = `${sources.platform}-${sourceFingerprint(collectRuntimeAssets(sources))}` ): string { const assets = collectRuntimeAssets(sources); - const wrapperName = process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; - const sourceWrapper = assets.find((asset) => asset.relativePath === wrapperName)?.source; - const sourceRuntimeNode = assets.find((asset) => asset.relativePath === "runtime.node")?.source; + const wrapperName = sources.platform.startsWith("win32") + ? "copilot-runtime.exe" + : "copilot-runtime"; + const prebuildDir = join("prebuilds", sources.platform); + const wrapperRelative = join(prebuildDir, wrapperName); + const runtimeNodeRelative = join(prebuildDir, "runtime.node"); + const sourceWrapper = assets.find((asset) => asset.relativePath === wrapperRelative)?.source; + const sourceRuntimeNode = assets.find( + (asset) => asset.relativePath === runtimeNodeRelative + )?.source; validateRuntimeBundle(sourceWrapper ?? "", sourceRuntimeNode ?? ""); - const installDir = join(cacheRoot, `${sources.platform}-${sourceFingerprint(assets)}`); - const installedWrapper = join(installDir, wrapperName); - const installedRuntimeNode = join(installDir, "runtime.node"); + const installDir = join(cacheRoot, cacheKey); + const installedWrapper = join(installDir, wrapperRelative); + const installedRuntimeNode = join(installDir, runtimeNodeRelative); if (existsSync(installDir)) { validateRuntimeBundle(installedWrapper, installedRuntimeNode); makeExecutable(installedWrapper); @@ -167,7 +190,7 @@ export function materializeRuntimeBundle( mkdirSync(dirname(destination), { recursive: true }); copyFileSync(asset.source, destination); } - const stagedWrapper = join(stagingDir, wrapperName); + const stagedWrapper = join(stagingDir, wrapperRelative); makeExecutable(stagedWrapper); renameSync(stagingDir, installDir); } catch (error) { @@ -181,3 +204,92 @@ export function materializeRuntimeBundle( return installedWrapper; } + +function isMusl(): boolean { + if (process.platform !== "linux") { + return false; + } + const report = process.report?.getReport() as + | { header?: { glibcVersionRuntime?: string } } + | undefined; + return report?.header?.glibcVersionRuntime === undefined; +} + +export function getRuntimePlatform( + platform = process.platform, + arch = process.arch, + musl = isMusl() +): string { + if (arch !== "x64" && arch !== "arm64") { + throw new Error(`Unsupported Copilot CLI architecture: ${arch}.`); + } + if (platform === "linux") { + return `${musl ? "linuxmusl" : "linux"}-${arch}`; + } + if (platform === "darwin" || platform === "win32") { + return `${platform}-${arch}`; + } + throw new Error(`Unsupported Copilot CLI platform: ${platform}-${arch}.`); +} + +export function getRuntimeReleaseAssetName(version: string, platform: string): string { + return `github-copilot-${version}-${platform}.tgz`; +} + +export function getRuntimePackageName(platform: string): string { + return `@github/copilot-sdk-${platform}`; +} + +export function resolvePackageRoot( + packageName: string, + searchPaths = require.resolve.paths(packageName) ?? [] +): string | undefined { + return searchPaths + .map((base) => join(base, ...packageName.split("/"))) + .find((candidate) => existsSync(join(candidate, "package.json"))); +} + +export async function ensureRuntimeBundle( + version: string, + options: EnsureRuntimeBundleOptions = {} +): Promise { + const platform = options.platform ?? getRuntimePlatform(); + // lgtm[js/trivial-conditional] This generated constant is true for internal canary builds. + if (COPILOT_CLI_USE_NPM_PACKAGE) { + const packageName = `@github/copilot-${platform}`; + const packageRoot = resolvePackageRoot(packageName, options.packageSearchPaths); + if (!packageRoot) { + throw new Error(`Could not resolve ${packageName} for Copilot CLI ${version}.`); + } + validateFile( + join(packageRoot, "prebuilds", platform, "runtime.node"), + "Copilot runtime.node" + ); + return materializeRuntimeBundle( + { packageRoot, platform }, + options.cacheRoot, + `${version}-${platform}` + ); + } + + const packageName = getRuntimePackageName(platform); + const packageRoot = resolvePackageRoot(packageName, options.packageSearchPaths); + if (!packageRoot) { + throw new Error( + `Could not resolve ${packageName}. Reinstall @github/copilot-sdk so its platform package is installed.` + ); + } + const wrapperName = platform.startsWith("win32") ? "copilot-runtime.exe" : "copilot-runtime"; + const prebuildDir = join(packageRoot, "prebuilds", platform); + const wrapper = join(prebuildDir, wrapperName); + try { + validateRuntimeBundle(wrapper, join(prebuildDir, "runtime.node")); + } catch (error) { + throw new Error( + `${packageName} is missing required Copilot CLI runtime files. Reinstall @github/copilot-sdk.`, + { cause: error } + ); + } + makeExecutable(wrapper); + return wrapper; +} diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index eb4c6b7561..b7fc7837a5 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -18,6 +18,7 @@ import type { McpOauthPendingRequestResponse, FactoryLogLine, FactoryRunResult as WireFactoryRunResult, + ModelSwitchAutoTierResult, } from "./generated/rpc.js"; import { type Canvas, CanvasError } from "./canvas.js"; import type { OpenCanvasInstance } from "./generated/rpc.js"; @@ -46,6 +47,7 @@ import type { ContextTier, ReasoningEffort, ReasoningSummary, + AutoTier, ModelCapabilitiesOverride, SectionTransformFn, SessionCapabilities, @@ -420,6 +422,7 @@ export class CopilotSession { private typedEventHandlers: Map void>> = new Map(); private toolHandlers: Map = new Map(); + private pendingExternalTools: Map = new Map(); private canvases: Map = new Map(); private bearerTokenProviders: Map = new Map(); private commandHandlers: Map = new Map(); @@ -439,6 +442,7 @@ export class CopilotSession { private _capabilities: SessionCapabilities = {}; private openCanvasInstances: OpenCanvasInstance[] = []; private disconnected = false; + private disconnecting = false; private onDisconnected?: () => void; /** @internal Client session API handlers, populated by CopilotClient during create/resume. */ @@ -818,6 +822,10 @@ export class CopilotSession { return; } this.disconnected = true; + for (const controller of this.pendingExternalTools.values()) { + controller.abort(); + } + this.pendingExternalTools.clear(); this._runOnDisconnected(); this.eventHandlers.clear(); this.typedEventHandlers.clear(); @@ -994,6 +1002,15 @@ export class CopilotSession { tracestate ); } + } else if (event.type === "external_tool.completed") { + const { requestId } = event.data as { requestId?: string }; + if (requestId) { + const controller = this.pendingExternalTools.get(requestId); + if (controller) { + this.pendingExternalTools.delete(requestId); + controller.abort(); + } + } } else if (event.type === "permission.requested") { const { requestId, permissionRequest, resolvedByHook } = event.data as { requestId: string; @@ -1103,6 +1120,12 @@ export class CopilotSession { traceparent?: string, tracestate?: string ): Promise { + const controller = new AbortController(); + if (this.disconnected || this.pendingExternalTools.has(requestId)) { + return; + } + this.pendingExternalTools.set(requestId, controller); + try { // The built-in tool-search tool receives a snapshot of the session's // currently initialized tools so an override can filter the live @@ -1111,13 +1134,27 @@ export class CopilotSession { // leaves the snapshot undefined rather than failing the tool. let availableTools: CurrentToolMetadata[] | undefined; if (toolName === TOOL_SEARCH_TOOL_NAME) { + if (controller.signal.aborted) { + return; + } + const aborted = new Promise((resolve) => { + controller.signal.addEventListener("abort", () => resolve(undefined), { + once: true, + }); + }); try { - const metadata = await this.rpc.tools.getCurrentMetadata(); - availableTools = metadata.tools ?? undefined; + const metadata = await Promise.race([ + this.rpc.tools.getCurrentMetadata(), + aborted, + ]); + availableTools = metadata?.tools ?? undefined; } catch { availableTools = undefined; } } + if (controller.signal.aborted) { + return; + } const rawResult = await handler(args, { sessionId: this.sessionId, toolCallId, @@ -1126,6 +1163,7 @@ export class CopilotSession { availableTools, traceparent, tracestate, + signal: controller.signal, }); let result: ToolResult; if (rawResult == null) { @@ -1137,12 +1175,12 @@ export class CopilotSession { } else { result = JSON.stringify(rawResult); } - if (this.disconnected) { + if (!this._claimExternalTool(requestId, controller)) { return; } await this.rpc.tools.handlePendingToolCall({ requestId, result }); } catch (error) { - if (this.disconnected) { + if (!this._claimExternalTool(requestId, controller)) { return; } const message = error instanceof Error ? error.message : String(error); @@ -1154,9 +1192,22 @@ export class CopilotSession { } // Connection lost or RPC error — nothing we can do } + } finally { + if (this.pendingExternalTools.get(requestId) === controller) { + this.pendingExternalTools.delete(requestId); + } + controller.abort(); } } + private _claimExternalTool(requestId: string, controller: AbortController): boolean { + if (this.disconnected || this.pendingExternalTools.get(requestId) !== controller) { + return false; + } + this.pendingExternalTools.delete(requestId); + return true; + } + /** * Executes a permission handler and sends the result back via RPC. * @internal @@ -2014,13 +2065,27 @@ export class CopilotSession { * ``` */ async disconnect(): Promise { - if (this.disconnected) { + if (this.disconnected || this.disconnecting) { return; } - await this.connection.sendRequest("session.destroy", { - sessionId: this.sessionId, - }); - this._markDisconnected(); + this.disconnecting = true; + try { + let response: { success: boolean; error?: string } = { success: false }; + for (let attempt = 0; attempt < 2 && !response.success; attempt++) { + response = (await this.connection.sendRequest("session.detach", { + sessionId: this.sessionId, + })) as { success: boolean; error?: string }; + } + if (!response.success) { + throw new Error( + `Failed to disconnect session ${this.sessionId}: ${response.error || "Unknown error"}` + ); + } + this._markDisconnected(); + } catch (error) { + this.disconnecting = false; + throw error; + } } /** Enables `await using session = ...` syntax for automatic cleanup. */ @@ -2065,6 +2130,9 @@ export class CopilotSession { * ```typescript * await session.setModel("gpt-5.4"); * await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" }); + * + * // Select the Auto model and its routing preference in one call. + * await session.setModel("auto", { autoTier: "intelligence" }); * ``` */ async setModel( @@ -2074,11 +2142,58 @@ export class CopilotSession { reasoningSummary?: ReasoningSummary; contextTier?: ContextTier; modelCapabilities?: ModelCapabilitiesOverride; + /** + * Routing preference to apply when `model` is `auto`. + * + * Pass `null` to return to the provider's default Auto routing. The + * runtime rejects this option when `model` is anything other than + * `auto`; use {@link setAutoTier} to change the preference without + * changing the selected model. + * + * @experimental Part of an experimental Auto routing surface and may + * change or be removed in a future release. + */ + autoTier?: AutoTier | null; } ): Promise { await this.rpc.model.switchTo({ modelId: model, ...options }); } + /** + * Change the Auto routing preference without changing the selected model. + * + * The runtime does not apply the preference immediately. It records the + * request and commits it only when a later user turn using the `auto` model + * successfully obtains a usable model from the provider. A `pending` status + * therefore confirms that the request was accepted, not that it took effect. + * + * Watch for the outcome through the `session.model_change` event on success, + * or the ephemeral `session.auto_tier_switch_failed` event on failure. You + * can also read the current committed and in-flight state at any time with + * `session.rpc.model.getCurrent()`. + * + * Only the most recent request survives: issuing a new request replaces any + * earlier one that has not yet been claimed by a turn. + * + * @param autoTier - Routing preference to activate, or `null` to return to + * the provider's default Auto routing + * @returns The runtime's immediate acknowledgement and Auto preference snapshot + * + * @experimental Part of an experimental Auto routing surface and may change + * or be removed in a future release. + * + * @example + * ```typescript + * const result = await session.setAutoTier("intelligence"); + * if (result.status === "pending") { + * // Takes effect on a later turn that uses the `auto` model. + * } + * ``` + */ + async setAutoTier(autoTier: AutoTier | null): Promise { + return await this.rpc.model.switchAutoTier({ autoTier }); + } + /** * Log a message to the session timeline. * The message appears in the session event stream and is visible to SDK consumers diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 85c77f8340..0f15749f71 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -11,6 +11,7 @@ import type { Canvas } from "./canvas.js"; import type { SessionFsProvider } from "./sessionFsProvider.js"; import type { CopilotRequestHandler } from "./copilotRequestHandler.js"; import type { + AutoTier, PermissionRequest as GeneratedPermissionRequest, PermissionRequestedData as GeneratedPermissionRequestedData, PermissionRequestedEvent as GeneratedPermissionRequestedEvent, @@ -72,7 +73,12 @@ export type { export type SessionEvent = | Exclude | PermissionRequestedEvent; -export type { ReasoningSummary } from "./generated/session-events.js"; +export type { AutoTier, ReasoningSummary } from "./generated/session-events.js"; +export type { + CurrentModel, + ModelSwitchAutoTierResult, + ModelSwitchAutoTierStatus, +} from "./generated/rpc.js"; export type { SessionFsProvider } from "./sessionFsProvider.js"; export { createSessionFsAdapter } from "./sessionFsProvider.js"; export type { SessionFsFileInfo } from "./sessionFsProvider.js"; @@ -306,6 +312,37 @@ export type InternalRuntimeConnection = RuntimeConnection | ParentProcessRuntime */ export type CopilotClientMode = "empty" | "copilot-cli"; +/** + * Identity of the integrating application, declared once on the `server.connect` + * handshake so the telemetry the runtime emits on this connection is attributed + * to a single, consistent surface rather than to the runtime's own build. + * + * All fields are optional; omit any of them (or the whole object) to keep the + * runtime's default attribution. Version fields are ignored by the runtime + * unless they look like a version string. + */ +export interface CopilotClientInfo { + /** + * Name of the application using the SDK, e.g. `"acme-developer-portal"`. + */ + applicationName?: string; + + /** + * Version of the application using the SDK, e.g. `"2.4.0"`. + */ + applicationVersion?: string; + + /** + * Optional name of a specific integration within the application, such as an extension or plugin. + */ + integrationName?: string; + + /** + * Optional version of the integration identified by `integrationName`. + */ + integrationVersion?: string; +} + export interface CopilotClientOptions { /** * How to connect to the Copilot runtime. When omitted, defaults to @@ -477,6 +514,16 @@ export interface CopilotClientOptions { */ enableRemoteSessions?: boolean; + /** + * Identity of the integrating application, forwarded to the runtime on the + * `server.connect` handshake. Declaring it lets the telemetry the runtime + * emits on this connection be attributed to a single, consistent surface + * (e.g. the application and its Copilot integration) instead of the + * runtime's own build. All fields are optional; omit it to keep the default + * attribution. + */ + clientInfo?: CopilotClientInfo; + /** * @internal Hook used by `joinSession()` to construct a client that talks * to its parent process over stdio. Not part of the public API. @@ -645,6 +692,8 @@ export interface ToolInvocation { traceparent?: string; /** W3C Trace Context tracestate from the CLI's execute_tool span. */ tracestate?: string; + /** Aborted when the runtime completes this request or the session disconnects. */ + signal?: AbortSignal; } export type ToolHandler = ( @@ -2129,6 +2178,21 @@ export interface FactoryMeta { * provider-level choices are conceptually per-provider rather than global. */ export interface CapiSessionOptions { + /** + * Routing preference used when the session model is `auto`. + * Requires a runtime with Auto tier support and V2 Auto routing. + * + * When omitted on create, the runtime uses its default routing behavior. + * The runtime persists this preference across cold resume; when omitted on + * cold resume, it restores the last committed preference. On resident + * resume, a different tier requests a safe switch that takes effect after + * resume succeeds, and never disturbs a turn that is already running. + * + * To change the preference on a live session, call + * {@link CopilotSession.setAutoTier} instead. + */ + autoTier?: AutoTier; + /** * Whether to use the WebSocket transport for the CAPI Responses API. * diff --git a/nodejs/test/client-api-codegen.test.ts b/nodejs/test/client-api-codegen.test.ts new file mode 100644 index 0000000000..9331ad7689 --- /dev/null +++ b/nodejs/test/client-api-codegen.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from "vitest"; + +import { emitClientSessionApiRegistration as emitGoClientSessionApiRegistration } from "../../scripts/codegen/go.ts"; +import { emitClientSessionApiRegistration as emitPythonClientSessionApiRegistration } from "../../scripts/codegen/python.ts"; +import { emitClientSessionApiRegistration as emitTypeScriptClientSessionApiRegistration } from "../../scripts/codegen/typescript.ts"; + +const clientSessionSchema: Record = { + mixed: { + visible: { + rpcMethod: "mixed.visible", + params: { + type: "object", + title: "VisibleRequest", + properties: { + sessionId: { type: "string" }, + }, + required: ["sessionId"], + }, + result: { + type: "object", + title: "VisibleResult", + properties: {}, + }, + }, + secret: { + rpcMethod: "mixed.secret", + visibility: "internal", + params: { + $ref: "#/definitions/InternalRequest", + }, + result: { + $ref: "#/definitions/InternalResult", + }, + }, + }, + internalOnly: { + hidden: { + rpcMethod: "internalOnly.hidden", + visibility: "internal", + params: { + $ref: "#/definitions/InternalRequest", + }, + result: { + $ref: "#/definitions/InternalResult", + }, + }, + }, +}; + +const allInternalClientSessionSchema: Record = { + internalOnly: clientSessionSchema.internalOnly, +}; + +function expectOnlyPublicClientSessionHandlers(code: string): void { + expect(code).toContain("mixed.visible"); + expect(code).not.toContain("mixed.secret"); + expect(code).not.toContain("internalOnly.hidden"); + expect(code).not.toContain("InternalRequest"); + expect(code).not.toContain("InternalResult"); +} + +describe("client-session API codegen", () => { + it("excludes internal methods from TypeScript handlers", () => { + const code = emitTypeScriptClientSessionApiRegistration(clientSessionSchema).join("\n"); + const allInternalCode = emitTypeScriptClientSessionApiRegistration( + allInternalClientSessionSchema + ).join("\n"); + + expectOnlyPublicClientSessionHandlers(code); + expect(code).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).toContain("export interface ClientSessionApiHandlers {"); + expect(allInternalCode).toContain("export function registerClientSessionApiHandlers("); + expect(allInternalCode).not.toContain("InternalOnlyHandler"); + }); + + it("excludes internal methods from Go handlers", () => { + const lines: string[] = []; + emitGoClientSessionApiRegistration(lines, clientSessionSchema, (name) => name, new Map()); + const code = lines.join("\n"); + const allInternalLines: string[] = []; + emitGoClientSessionApiRegistration( + allInternalLines, + allInternalClientSessionSchema, + (name) => name, + new Map() + ); + const allInternalCode = allInternalLines.join("\n"); + + expectOnlyPublicClientSessionHandlers(code); + expect(code).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).toContain("type ClientSessionAPIHandlers struct {"); + expect(allInternalCode).toContain("func RegisterClientSessionAPIHandlers("); + expect(allInternalCode).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).not.toContain("clientSessionHandlerError"); + }); + + it("excludes internal methods from Python handlers", () => { + const lines: string[] = []; + emitPythonClientSessionApiRegistration(lines, clientSessionSchema, (name) => name); + const code = lines.join("\n"); + const allInternalLines: string[] = []; + emitPythonClientSessionApiRegistration( + allInternalLines, + allInternalClientSessionSchema, + (name) => name + ); + const allInternalCode = allInternalLines.join("\n"); + + expectOnlyPublicClientSessionHandlers(code); + expect(code).not.toContain("InternalOnlyHandler"); + expect(allInternalCode).toContain("class ClientSessionApiHandlers:"); + expect(allInternalCode).toContain("def register_client_session_api_handlers("); + expect(allInternalCode).not.toContain("InternalOnlyHandler"); + }); +}); diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index a08e4a6e5a..3db96ea47b 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -12,6 +12,8 @@ import { createCanvas, DisableBypassPermissionsModes, RuntimeConnection, + type CapiSessionOptions, + type CopilotClientOptions, type GitHubTelemetryNotification, type ManagedSettings, type ModelInfo, @@ -1364,37 +1366,50 @@ describe("CopilotClient", () => { expect(resumePayload.featureFlags).toEqual(featureFlags); }); - it("forwards capi options in session.create and session.resume", async () => { - const client = new CopilotClient(); - await client.start(); - onTestFinished(() => stopClient(client)); + it.each([ + undefined, + {}, + { enableWebSocketResponses: false }, + { enableWebSocketResponses: true }, + { autoTier: "efficiency" }, + { autoTier: "balance" }, + { autoTier: "intelligence" }, + { autoTier: "balance", enableWebSocketResponses: false }, + ] satisfies (CapiSessionOptions | undefined)[])( + "forwards capi options %j in session.create and session.resume", + async (capi) => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); - const spy = vi - .spyOn((client as any).connection!, "sendRequest") - .mockImplementation(async (method: string, params: any) => { - if (method === "session.create") return { sessionId: params.sessionId }; - if (method === "session.resume") return { sessionId: params.sessionId }; - throw new Error(`Unexpected method: ${method}`); - }); + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); - const session = await client.createSession({ - onPermissionRequest: approveAll, - capi: { enableWebSocketResponses: false }, - }); - await client.resumeSession(session.sessionId, { - onPermissionRequest: approveAll, - capi: { enableWebSocketResponses: false }, - }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: "auto", + capi, + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + capi, + }); - const createPayload = spy.mock.calls.find( - ([method]) => method === "session.create" - )![1] as any; - const resumePayload = spy.mock.calls.find( - ([method]) => method === "session.resume" - )![1] as any; - expect(createPayload.capi).toEqual({ enableWebSocketResponses: false }); - expect(resumePayload.capi).toEqual({ enableWebSocketResponses: false }); - }); + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(JSON.parse(JSON.stringify(createPayload)).capi).toEqual(capi); + expect(JSON.parse(JSON.stringify(resumePayload)).capi).toEqual(capi); + } + ); it("forwards pluginDirectories and largeOutput in session.create and session.resume", async () => { const client = new CopilotClient(); @@ -2589,6 +2604,117 @@ describe("CopilotClient", () => { spy.mockRestore(); }); + it("sends the auto tier with session.model.switchTo when selecting the auto model", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, _params: any) => { + if (method === "session.model.switchTo") return {}; + throw new Error(`Unexpected method: ${method}`); + }); + + await session.setModel("auto", { autoTier: "intelligence" }); + + expect(spy).toHaveBeenCalledWith("session.model.switchTo", { + sessionId: session.sessionId, + modelId: "auto", + autoTier: "intelligence", + }); + + spy.mockRestore(); + }); + + it("sends a null auto tier with session.model.switchTo to restore default routing", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, _params: any) => { + if (method === "session.model.switchTo") return {}; + throw new Error(`Unexpected method: ${method}`); + }); + + await session.setModel("auto", { autoTier: null }); + + expect(spy).toHaveBeenCalledWith("session.model.switchTo", { + sessionId: session.sessionId, + modelId: "auto", + autoTier: null, + }); + + spy.mockRestore(); + }); + + it("sends session.model.switchAutoTier RPC and returns the runtime snapshot", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, _params: any) => { + if (method === "session.model.switchAutoTier") { + return { + status: "pending", + effectiveAutoTier: "balance", + pendingAutoTier: "intelligence", + activatingAutoTier: null, + supersededAutoTier: null, + }; + } + throw new Error(`Unexpected method: ${method}`); + }); + + const result = await session.setAutoTier("intelligence"); + + expect(spy).toHaveBeenCalledWith("session.model.switchAutoTier", { + sessionId: session.sessionId, + autoTier: "intelligence", + }); + expect(result.status).toBe("pending"); + expect(result.effectiveAutoTier).toBe("balance"); + expect(result.pendingAutoTier).toBe("intelligence"); + expect(result.activatingAutoTier).toBeNull(); + + spy.mockRestore(); + }); + + it("sends a null auto tier with session.model.switchAutoTier to restore default routing", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => stopClient(client)); + + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, _params: any) => { + if (method === "session.model.switchAutoTier") return { status: "unchanged" }; + throw new Error(`Unexpected method: ${method}`); + }); + + const result = await session.setAutoTier(null); + + expect(spy).toHaveBeenCalledWith("session.model.switchAutoTier", { + sessionId: session.sessionId, + autoTier: null, + }); + expect(result.status).toBe("unchanged"); + + spy.mockRestore(); + }); + describe("URL parsing", () => { it("should parse port-only URL format", () => { const client = new CopilotClient({ @@ -2612,6 +2738,17 @@ describe("CopilotClient", () => { expect((client as any).isExternalServer).toBe(true); }); + it("should parse bracketed IPv6 host:port URL format", () => { + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("[::1]:9000"), + logLevel: "error", + }); + + expect((client as any).runtimePort).toBe(9000); + expect((client as any).actualHost).toBe("::1"); + expect((client as any).isExternalServer).toBe(true); + }); + it("should parse http://host:port URL format", () => { const client = new CopilotClient({ connection: RuntimeConnection.forUri("http://localhost:7000"), @@ -2623,6 +2760,26 @@ describe("CopilotClient", () => { expect((client as any).isExternalServer).toBe(true); }); + it("should parse http://[ipv6]:port URL format", () => { + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("http://[::1]:7000"), + logLevel: "error", + }); + + expect((client as any).runtimePort).toBe(7000); + expect((client as any).actualHost).toBe("::1"); + expect((client as any).isExternalServer).toBe(true); + }); + + it("should reject a bracketed non-IPv6 host", () => { + expect(() => { + new CopilotClient({ + connection: RuntimeConnection.forUri("[not-ipv6]:1234"), + logLevel: "error", + }); + }).toThrow(/Invalid cliUrl format/); + }); + it("should parse https://host:port URL format", () => { const client = new CopilotClient({ connection: RuntimeConnection.forUri("https://example.com:443"), @@ -3186,6 +3343,42 @@ describe("CopilotClient", () => { const client = new CopilotClient(); await client.start(); onTestFinished(() => stopClient(client)); + let invocationSignal: AbortSignal | undefined; + let toolStarted!: () => void; + const started = new Promise((resolve) => { + toolStarted = resolve; + }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [ + { + name: "blocked_tool", + description: "blocks until cancelled", + handler: async (_args, invocation) => { + invocationSignal = invocation.signal; + toolStarted(); + await new Promise((_, reject) => + invocation.signal?.addEventListener( + "abort", + () => reject(invocation.signal?.reason), + { once: true } + ) + ); + }, + }, + ], + }); + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-connection-close", + sessionId: session.sessionId, + toolCallId: "tool-call-connection-close", + toolName: "blocked_tool", + arguments: {}, + }, + }); + await started; expect((client as any).state).toBe("connected"); @@ -3197,6 +3390,7 @@ describe("CopilotClient", () => { // Wait for the connection.onClose handler to fire await vi.waitFor(() => { expect((client as any).state).toBe("disconnected"); + expect(invocationSignal?.aborted).toBe(true); }); } ); @@ -4230,3 +4424,82 @@ describe("managedSettings serialization", () => { }); }); }); + +describe("connect handshake clientInfo", () => { + // Drives verifyProtocolVersion() against a stubbed connection so we can + // observe the `connect` params without spawning a runtime. `connect` maps to + // connection.sendRequest("connect", params) in the generated internal RPC. + async function captureConnectParams( + options: Partial> = {} + ): Promise> { + const client = new CopilotClient({ + connection: RuntimeConnection.forUri("localhost:1234"), + ...options, + }); + const sendRequest = vi.fn(async (method: string, _params?: unknown) => { + if (method === "connect") return { protocolVersion: 3 }; + throw new Error(`Unexpected method: ${method}`); + }); + (client as any).connection = { sendRequest }; + + await (client as any).verifyProtocolVersion(); + + const connectCall = sendRequest.mock.calls.find(([method]) => method === "connect"); + expect(connectCall, "connect was not called").toBeTruthy(); + return connectCall![1] as Record; + } + + it("forwards a declared client identity on the connect handshake", async () => { + const clientInfo = { + applicationName: "acme-developer-portal", + applicationVersion: "2.4.0", + integrationName: "copilot-assistant", + integrationVersion: "1.5.0", + }; + + const params = await captureConnectParams({ clientInfo }); + + expect(params.clientInfo).toEqual({ + editorName: "acme-developer-portal", + editorVersion: "2.4.0", + extensionName: "copilot-assistant", + extensionVersion: "1.5.0", + }); + }); + + it("omits clientInfo from the handshake when the host declares none", async () => { + const params = await captureConnectParams(); + + expect(params).not.toHaveProperty("clientInfo"); + expect(params.supportedTaskKinds).toEqual(["agent", "client", "shell"]); + }); + + it("drops empty fields and omits an all-empty identity", async () => { + const allEmpty = await captureConnectParams({ + clientInfo: { + applicationName: "", + applicationVersion: "", + integrationName: "", + integrationVersion: "", + }, + }); + expect(allEmpty).not.toHaveProperty("clientInfo"); + + const partial = await captureConnectParams({ + clientInfo: { applicationName: "example-app", applicationVersion: "" }, + }); + expect(partial.clientInfo).toEqual({ editorName: "example-app" }); + }); + + it("keeps telemetry forwarding alongside a declared identity", async () => { + const params = await captureConnectParams({ + clientInfo: { applicationName: "example-app" }, + onGitHubTelemetry: () => {}, + }); + + expect(params).toMatchObject({ + clientInfo: { editorName: "example-app" }, + enableGitHubTelemetryForwarding: true, + }); + }); +}); diff --git a/nodejs/test/e2e/auto_tier.e2e.test.ts b/nodejs/test/e2e/auto_tier.e2e.test.ts new file mode 100644 index 0000000000..0cb2a1a266 --- /dev/null +++ b/nodejs/test/e2e/auto_tier.e2e.test.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +/** + * The runtime stages an Auto routing preference instead of applying it immediately: a + * request is "unclaimed" until a later turn using the `auto` model mints a usable model + * and token pair. These tests observe that staged state through `model.getCurrent`, so + * they assert what the runtime actually recorded rather than what the SDK serialized. + */ +describe("Auto tier switching", async () => { + const { copilotClient: client } = await createSdkTestContext(); + + it("should stage and reset auto tier preference", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: "auto", + }); + + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBeUndefined(); + + const staged = await session.setAutoTier("efficiency"); + expect(staged.status).toBe("pending"); + expect(staged.pendingAutoTier).toBe("efficiency"); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBe("efficiency"); + + // A second request replaces the first and reports the one it displaced. + const superseded = await session.setAutoTier("intelligence"); + expect(superseded.status).toBe("pending"); + expect(superseded.pendingAutoTier).toBe("intelligence"); + expect(superseded.supersededAutoTier).toBe("efficiency"); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBe("intelligence"); + + // Passing null returns the session to provider-default routing. The status is + // `unchanged` because provider-default was already the committed preference; + // the request's effect is cancelling the staged one. + const reset = await session.setAutoTier(null); + expect(reset.status).toBe("unchanged"); + expect(reset.supersededAutoTier).toBe("intelligence"); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBeUndefined(); + + await session.disconnect(); + }); + + it("should preserve auto tier when set model omits it", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + model: "auto", + }); + + await session.setAutoTier("balance"); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBe("balance"); + + // Omitting the option leaves the staged preference alone. + await session.setModel("auto"); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBe("balance"); + + // Supplying a tier replaces it. + await session.setModel("auto", { autoTier: "intelligence" }); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBe("intelligence"); + + // Supplying null clears it. Omission, a value, and null are three distinct + // outcomes, which is why the option cannot collapse to a plain optional field. + await session.setModel("auto", { autoTier: null }); + expect((await session.rpc.model.getCurrent()).pendingAutoTier).toBeUndefined(); + + await session.disconnect(); + }); +}); diff --git a/nodejs/test/e2e/client.e2e.test.ts b/nodejs/test/e2e/client.e2e.test.ts index bc3421bfa1..a529ea8e4c 100644 --- a/nodejs/test/e2e/client.e2e.test.ts +++ b/nodejs/test/e2e/client.e2e.test.ts @@ -107,7 +107,7 @@ describe("Client", () => { expect(errors[0].message).toContain("Failed to disconnect session"); } }, - // Generous timeout: client.stop() must wait for session.destroy to time out + // Generous timeout: client.stop() must wait for session.detach to time out // when the server process is dead. The default 30s can flake on slow CI under load. 60_000 ); diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index e3dc41343b..4d261bea52 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -99,6 +99,11 @@ function handleMessage(message) { return; } + if (message.method === "session.detach") { + writeResponse(message.id, { success: true }); + return; + } + if (message.method === "session.resume") { const sessionId = message.params?.sessionId ?? message.params?.[0]?.sessionId ?? "fake-session"; writeResponse(message.id, { @@ -467,7 +472,7 @@ describe("Client options", async () => { }); const session = await client.createSession({ clientName: "advanced-create-client", - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", reasoningEffort: "medium", reasoningSummary: "detailed", contextTier: "long_context", @@ -532,7 +537,7 @@ describe("Client options", async () => { provider: "create-provider", id: "create-model", name: "Create Model", - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", wireModel: "create-wire-model", maxContextWindowTokens: 12_000, maxPromptTokens: 10_000, @@ -544,7 +549,7 @@ describe("Client options", async () => { const createRequest = getCapturedRequest(capturePath, "session.create"); expect(createRequest.clientName).toBe("advanced-create-client"); - expect(createRequest.model).toBe("claude-sonnet-4.5"); + expect(createRequest.model).toBe("claude-sonnet-5"); expect(createRequest.reasoningEffort).toBe("medium"); expect(createRequest.reasoningSummary).toBe("detailed"); expect(createRequest.contextTier).toBe("long_context"); @@ -609,7 +614,7 @@ describe("Client options", async () => { await client.start(); const session = await client.createSession({ - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: { type: "azure", wireApi: "responses", @@ -619,7 +624,7 @@ describe("Client options", async () => { bearerToken: "provider-bearer-token", azure: { apiVersion: "2024-02-15-preview" }, headers: { "X-Provider-Wire": "yes" }, - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", wireModel: "azure-deployment", maxPromptTokens: 8192, maxOutputTokens: 1024, @@ -636,7 +641,7 @@ describe("Client options", async () => { expect(provider.bearerToken).toBe("provider-bearer-token"); expect(getObject(provider.azure).apiVersion).toBe("2024-02-15-preview"); expect(getObject(provider.headers)["X-Provider-Wire"]).toBe("yes"); - expect(provider.modelId).toBe("claude-sonnet-4.5"); + expect(provider.modelId).toBe("claude-sonnet-5"); expect(provider.wireModel).toBe("azure-deployment"); expect(provider.maxPromptTokens).toBe(8192); expect(provider.maxOutputTokens).toBe(1024); diff --git a/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts b/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts index 69bacd4f6e..b0af6524a1 100644 --- a/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_cancel_error.e2e.test.ts @@ -56,8 +56,8 @@ function serveNonInference(url: string): Response { const MODEL_CATALOG_JSON = JSON.stringify({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -65,7 +65,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({ model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { diff --git a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts index 309250d852..04bccb7e08 100644 --- a/nodejs/test/e2e/copilot_request_handler.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_handler.e2e.test.ts @@ -42,8 +42,8 @@ async function startFakeUpstream(): Promise<{ sendJson(res, 200, { data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -52,7 +52,7 @@ async function startFakeUpstream(): Promise<{ supported_endpoints: ["/responses", "ws:/responses"], capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, diff --git a/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts b/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts index bd070c20ca..9b0dbbb3bc 100644 --- a/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts +++ b/nodejs/test/e2e/copilot_request_session_id.e2e.test.ts @@ -171,7 +171,7 @@ const CHAT_COMPLETION_STREAM_EVENTS: string[] = (() => { id: "chatcmpl-stub-1", object: "chat.completion.chunk", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }; return [ `data: ${JSON.stringify({ @@ -210,7 +210,7 @@ const BUFFERED_CHAT_COMPLETION_JSON = JSON.stringify({ id: "chatcmpl-stub-1", object: "chat.completion", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -224,8 +224,8 @@ const BUFFERED_CHAT_COMPLETION_JSON = JSON.stringify({ const MODEL_CATALOG_JSON = JSON.stringify({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -233,7 +233,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({ model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { @@ -300,14 +300,14 @@ describe("CopilotRequestHandler threads the runtime session id (CAPI + BYOK)", a const session = await client.createSession({ onPermissionRequest: approveAll, // BYOK providers require an explicit model id. - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: { type: "openai", wireApi: "responses", baseUrl: "https://byok.invalid/v1", apiKey: "byok-secret", - modelId: "claude-sonnet-4.5", - wireModel: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", + wireModel: "claude-sonnet-5", }, }); const byokSessionId = session.sessionId; diff --git a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts index 611898f11d..8478f93288 100644 --- a/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts +++ b/nodejs/test/e2e/disabled_mcp_servers.e2e.test.ts @@ -154,7 +154,7 @@ const CHAT_COMPLETION_STREAM = [ id: "persisted-session", object: "chat.completion.chunk", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -167,7 +167,7 @@ const CHAT_COMPLETION_STREAM = [ id: "persisted-session", object: "chat.completion.chunk", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [{ index: 0, delta: {}, finish_reason: "stop" }], }, ] @@ -179,7 +179,7 @@ const CHAT_COMPLETION_RESPONSE_JSON = JSON.stringify({ id: "persisted-session", object: "chat.completion", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -193,8 +193,8 @@ const CHAT_COMPLETION_RESPONSE_JSON = JSON.stringify({ const MODEL_CATALOG_JSON = JSON.stringify({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -202,7 +202,7 @@ const MODEL_CATALOG_JSON = JSON.stringify({ model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { streaming: true, tool_calls: true, parallel_tool_calls: true }, diff --git a/nodejs/test/e2e/event_fidelity.e2e.test.ts b/nodejs/test/e2e/event_fidelity.e2e.test.ts index da4b8105ad..8ae2091b33 100644 --- a/nodejs/test/e2e/event_fidelity.e2e.test.ts +++ b/nodejs/test/e2e/event_fidelity.e2e.test.ts @@ -35,9 +35,9 @@ describe("Event Fidelity", async () => { const assistantIdx = types.lastIndexOf("assistant.message"); expect(userIdx).toBeLessThan(assistantIdx); - // session.idle should be last + // session.idle completes the conversational turn; metadata may follow it. const idleIdx = types.lastIndexOf("session.idle"); - expect(idleIdx).toBe(types.length - 1); + expect(assistantIdx).toBeLessThan(idleIdx); await session.disconnect(); }); diff --git a/nodejs/test/e2e/extension_env_access.e2e.test.ts b/nodejs/test/e2e/extension_env_access.e2e.test.ts index 38210a22fb..f0f84a35ed 100644 --- a/nodejs/test/e2e/extension_env_access.e2e.test.ts +++ b/nodejs/test/e2e/extension_env_access.e2e.test.ts @@ -17,7 +17,7 @@ import { import { approveAll, RuntimeConnection } from "../../src/index.js"; import { getSdkProtocolVersion } from "../../src/sdkProtocolVersion.js"; import { createSdkTestContext, getLegacyCliPathForTests } from "./harness/sdkTestContext.js"; -import { retry } from "./harness/sdkTestHelper.js"; +import { retry, stopChildProcess } from "./harness/sdkTestHelper.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const FIXTURE = join(__dirname, "fixtures", "env-access-extension.mjs"); @@ -39,7 +39,7 @@ interface ExtensionRun { * stdio, so a stub host observes exactly what the CLI observes. That is the only * way to cover this feature end to end today: the released CLI predates the host * half (github/copilot-agent-runtime#15144), so it ignores the request and grants - * nothing. Once the `@github/copilot` dependency carries the host half, the + * nothing. Once the pinned CLI release carries the host half, the * real-CLI case below can assert the grant instead. */ async function runExtensionAgainstStubHost(options: { @@ -120,16 +120,12 @@ async function runExtensionAgainstStubHost(options: { }; } finally { connection.dispose(); - child.kill(); - // Windows keeps the directory locked until the child is gone. - await new Promise((resolveExit) => { - if (child.exitCode !== null || child.signalCode !== null) { - resolveExit(); - return; - } - child.once("exit", () => resolveExit()); - }); - await rm(dir, { recursive: true, force: true, maxRetries: 20, retryDelay: 100 }); + try { + await stopChildProcess(child); + } finally { + // Windows keeps the directory locked until the child is gone. + await rm(dir, { recursive: true, force: true, maxRetries: 20, retryDelay: 100 }); + } } } @@ -188,7 +184,7 @@ const cliObservations = mkdtempSync(join(tmpdir(), "copilot-env-access-cli-")); const cliResultFile = join(cliObservations, "result"); const cliContext = await createSdkTestContext({ copilotClientOptions: { - connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + connection: RuntimeConnection.forStdio({ path: await getLegacyCliPathForTests() }), env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS", EXTENSION_ENV_REQUEST: "E2E_SDK_TOKEN", @@ -201,7 +197,7 @@ const cliContext = await createSdkTestContext({ // The released CLI ignores `requestedEnvironmentVariables`, so this covers the // half a real CLI can prove today: asking for variables does not break the join. -// It becomes the grant test once `@github/copilot` carries the host half. +// It becomes the grant test once the pinned CLI release carries the host half. it("joins a real CLI that does not support environment requests", async () => { const { workDir, copilotClient } = cliContext; const extensionDir = join(workDir, ".github", "extensions", "env-access"); diff --git a/nodejs/test/e2e/external-tool-cancellation.e2e.test.ts b/nodejs/test/e2e/external-tool-cancellation.e2e.test.ts new file mode 100644 index 0000000000..4e71ac60a2 --- /dev/null +++ b/nodejs/test/e2e/external-tool-cancellation.e2e.test.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, it } from "vitest"; +import { z } from "zod"; +import { approveAll, defineTool } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("External tool cancellation", async () => { + const { copilotClient: client } = await createSdkTestContext(); + + async function withTimeout(promise: Promise, ms: number, label: string): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`Timeout: ${label}`)), ms); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } + } + + it("should cancel tool handler when session disconnects", { timeout: 120_000 }, async () => { + let toolStartedResolve!: () => void; + const toolStarted = new Promise((resolve) => { + toolStartedResolve = resolve; + }); + let toolCancelledResolve!: () => void; + const toolCancelled = new Promise((resolve) => { + toolCancelledResolve = resolve; + }); + let releaseToolResolve!: () => void; + const releaseTool = new Promise((resolve) => { + releaseToolResolve = resolve; + }); + + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [ + defineTool("slow_analysis", { + description: "A slow analysis tool that blocks until released", + parameters: z.object({ + value: z.string().describe("Value to analyze"), + }), + handler: async (_args, invocation) => { + toolStartedResolve(); + await Promise.race([ + releaseTool, + new Promise((_, reject) => + setImmediate(() => { + const onAbort = () => { + toolCancelledResolve(); + reject(new Error("aborted")); + }; + if (invocation.signal?.aborted) { + onAbort(); + return; + } + invocation.signal?.addEventListener("abort", onAbort, { + once: true, + }); + }) + ), + ]); + return "RELEASED"; + }, + }), + ], + }); + + try { + void session.send({ + prompt: "Use slow_analysis with value 'test_abort'. Wait for the result.", + }); + + await withTimeout(toolStarted, 60_000, "slow_analysis start"); + await session.disconnect(); + await withTimeout(toolCancelled, 60_000, "slow_analysis cancellation"); + } finally { + releaseToolResolve(); + } + }); +}); diff --git a/nodejs/test/e2e/factory.e2e.test.ts b/nodejs/test/e2e/factory.e2e.test.ts index 2bf3ff17fb..98004406f9 100644 --- a/nodejs/test/e2e/factory.e2e.test.ts +++ b/nodejs/test/e2e/factory.e2e.test.ts @@ -15,7 +15,7 @@ import { retry } from "./harness/sdkTestHelper.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const factoryTestContext = await createSdkTestContext({ copilotClientOptions: { - connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + connection: RuntimeConnection.forStdio({ path: await getLegacyCliPathForTests() }), env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "EXTENSIONS,AGENT_FACTORIES", }, diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index 58c275c800..c4befb148e 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -12,6 +12,8 @@ import { afterAll, afterEach, beforeEach, onTestFailed, TestContext } from "vite import { CopilotClient, CopilotClientOptions, RuntimeConnection } from "../../../src"; import { CapiProxy } from "./CapiProxy"; import { formatError, retry } from "./sdkTestHelper"; +import { ensureCopilotPackage } from "../../../scripts/releaseArtifacts"; +import { COPILOT_CLI_VERSION } from "../../../src/cliVersion"; export const isCI = process.env.GITHUB_ACTIONS === "true"; export const DEFAULT_GITHUB_TOKEN = "fake-token-for-e2e-tests"; @@ -50,27 +52,10 @@ function getCliPathForTests(): string | undefined { return undefined; } -function getCliPlatformPackageNames(): string[] { - const variants = - process.platform === "linux" - ? process.report?.getReport().header.glibcVersionRuntime - ? ["linux", "linuxmusl"] - : ["linuxmusl", "linux"] - : [process.platform]; - return variants.map((variant) => `@github/copilot-${variant}-${process.arch}`); -} - /** Resolves the legacy SEA only for tests that explicitly exercise Node-hosted features. */ -export function getLegacyCliPathForTests(): string { - const cliName = process.platform === "win32" ? "copilot.exe" : "copilot"; - const githubModules = resolve(__dirname, "../../../node_modules/@github"); - for (const packageName of getCliPlatformPackageNames()) { - const cliPath = join(githubModules, packageName.slice("@github/".length), cliName); - if (fs.existsSync(cliPath)) { - return cliPath; - } - } - throw new Error("Legacy Copilot CLI binary not found in the installed platform package."); +export async function getLegacyCliPathForTests(): Promise { + const packageRoot = await ensureCopilotPackage(COPILOT_CLI_VERSION); + return join(packageRoot, "app.js"); } export async function createSdkTestContext({ diff --git a/nodejs/test/e2e/harness/sdkTestHelper.ts b/nodejs/test/e2e/harness/sdkTestHelper.ts index de230b1338..c30aa9ea6f 100644 --- a/nodejs/test/e2e/harness/sdkTestHelper.ts +++ b/nodejs/test/e2e/harness/sdkTestHelper.ts @@ -2,8 +2,57 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +import type { ChildProcess } from "node:child_process"; import { AssistantMessageEvent, CopilotSession, SessionEvent } from "../../../src"; +const CHILD_SHUTDOWN_TIMEOUT_MS = 1_000; + +export async function stopChildProcess(child: ChildProcess): Promise { + if (hasChildExited(child)) { + return; + } + + child.kill("SIGTERM"); + if (await waitForChildExit(child, CHILD_SHUTDOWN_TIMEOUT_MS)) { + return; + } + + child.kill("SIGKILL"); + if (!(await waitForChildExit(child, CHILD_SHUTDOWN_TIMEOUT_MS))) { + throw new Error("Child process did not exit after SIGKILL"); + } +} + +function hasChildExited(child: ChildProcess): boolean { + return child.exitCode !== null || child.signalCode !== null; +} + +function waitForChildExit(child: ChildProcess, timeoutMs: number): Promise { + if (hasChildExited(child)) { + return Promise.resolve(true); + } + + return new Promise((resolvePromise) => { + let settled = false; + const finish = (exited: boolean) => { + if (settled) { + return; + } + settled = true; + clearTimeout(timeout); + child.off("exit", onExit); + resolvePromise(exited); + }; + const onExit = () => finish(true); + const timeout = setTimeout(() => finish(false), timeoutMs); + + child.once("exit", onExit); + if (hasChildExited(child)) { + onExit(); + } + }); +} + export async function getFinalAssistantMessage( session: CopilotSession, { alreadyIdle = false }: { alreadyIdle?: boolean } = {} diff --git a/nodejs/test/e2e/mcp_oauth.e2e.test.ts b/nodejs/test/e2e/mcp_oauth.e2e.test.ts index 5a00526b6d..c9f1cfb1e7 100644 --- a/nodejs/test/e2e/mcp_oauth.e2e.test.ts +++ b/nodejs/test/e2e/mcp_oauth.e2e.test.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ -import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { spawn } from "node:child_process"; import { dirname, resolve } from "node:path"; import { createInterface } from "node:readline"; import { fileURLToPath } from "node:url"; @@ -10,7 +10,7 @@ import { describe, expect, it, onTestFinished } from "vitest"; import type { CopilotSession, MCPServerConfig, McpAuthRequest } from "../../src/index.js"; import { approveAll } from "../../src/index.js"; import { createSdkTestContext } from "./harness/sdkTestContext.js"; -import { waitForCondition } from "./harness/sdkTestHelper.js"; +import { stopChildProcess, waitForCondition } from "./harness/sdkTestHelper.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -59,6 +59,7 @@ describe("MCP OAuth host auth", async () => { }); onTestFinished(() => disconnectSession(session)); + await session.rpc.mcp.reload(); await waitForMcpServerStatus(session, serverName); const tools = await session.rpc.mcp.listTools({ serverName }); @@ -95,10 +96,7 @@ describe("MCP OAuth host auth", async () => { async () => { const oauthServer = await startOAuthMcpServer(); const serverName = "oauth-direct-rpc-mcp"; - let resolveAuthRequest!: (request: McpAuthRequest) => void; - const authRequest = new Promise((resolve) => { - resolveAuthRequest = resolve; - }); + const authRequests = createAsyncQueue(); let releaseHandler!: (value: unknown) => void; const handlerResult = new Promise((resolve) => { releaseHandler = resolve; @@ -108,7 +106,7 @@ describe("MCP OAuth host auth", async () => { onPermissionRequest: approveAll, enableMcpApps: true, onMcpAuthRequest: async (request) => { - resolveAuthRequest(request); + authRequests.push(request); await handlerResult; return { kind: "token", accessToken: EXPECTED_TOKEN }; }, @@ -124,8 +122,25 @@ describe("MCP OAuth host auth", async () => { }); onTestFinished(() => disconnectSession(session)); + const reload = session.rpc.mcp.reload(); const connected = waitForMcpServerStatus(session, serverName); - const request = await authRequest; + let request = await authRequests.next(); + while ( + !( + await session.rpc.mcp.oauth.handlePendingRequest({ + requestId: request.requestId, + result: { + kind: "token", + accessToken: EXPECTED_TOKEN, + tokenType: "Bearer", + expiresIn: 3600, + }, + }) + ).success + ) { + request = await authRequests.next(); + } + expect(request).toMatchObject({ requestId: expect.any(String), serverName, @@ -138,21 +153,11 @@ describe("MCP OAuth host auth", async () => { }, }); - const handled = await session.rpc.mcp.oauth.handlePendingRequest({ - requestId: request.requestId, - result: { - kind: "token", - accessToken: EXPECTED_TOKEN, - tokenType: "Bearer", - expiresIn: 3600, - }, - }); - expect(handled.success).toBe(true); - + releaseHandler(undefined); + await reload; await connected; const tools = await session.rpc.mcp.listTools({ serverName }); expect(tools.tools.map((tool) => tool.name)).toContain("whoami"); - releaseHandler(undefined); } ); @@ -197,18 +202,18 @@ describe("MCP OAuth host auth", async () => { }); onTestFinished(() => disconnectSession(session)); + await session.rpc.mcp.reload(); await waitForMcpServerStatus(session, serverName); + refreshCount = 0; await callWhoami(session, serverName, "refresh"); await callWhoami(session, serverName, "upscope"); await callWhoami(session, serverName, "reauth"); - expect(authRequests.map((request) => request.reason)).toEqual([ - "initial", - "refresh", - "upscope", - "refresh", - "reauth", - ]); + expect( + authRequests + .filter((request) => request.reason !== "initial") + .map((request) => request.reason) + ).toEqual(["refresh", "upscope", "refresh", "reauth"]); const upscopeRequest = authRequests.find((request) => request.reason === "upscope"); expect(upscopeRequest?.wwwAuthenticateParams).toEqual({ @@ -263,6 +268,7 @@ describe("MCP OAuth host auth", async () => { }); onTestFinished(() => disconnectSession(session)); + await session.rpc.mcp.reload(); await waitForMcpServerStatus(session, serverName, "needs-auth"); expect(await authRequest).toMatchObject({ @@ -316,7 +322,7 @@ async function startOAuthMcpServer(): Promise<{ env: { ...process.env, EXPECTED_TOKEN }, stdio: ["ignore", "pipe", "pipe"], }); - onTestFinished(() => stopChild(child)); + onTestFinished(() => stopChildProcess(child)); const stderr: string[] = []; child.stderr.on("data", (chunk) => stderr.push(String(chunk))); @@ -369,13 +375,23 @@ async function disconnectSession(session: CopilotSession): Promise { } } -function stopChild(child: ChildProcessWithoutNullStreams): Promise { - if (child.exitCode !== null || child.killed) { - return Promise.resolve(); - } - const exitPromise = new Promise((resolvePromise) => { - child.once("exit", () => resolvePromise()); - }); - child.kill("SIGTERM"); - return exitPromise; +function createAsyncQueue(): { push(value: T): void; next(): Promise } { + const values: T[] = []; + const waiters: Array<(value: T) => void> = []; + return { + push(value) { + const waiter = waiters.shift(); + if (waiter) { + waiter(value); + } else { + values.push(value); + } + }, + next() { + const value = values.shift(); + return value === undefined + ? new Promise((resolvePromise) => waiters.push(resolvePromise)) + : Promise.resolve(value); + }, + }; } diff --git a/nodejs/test/e2e/rewind.e2e.test.ts b/nodejs/test/e2e/rewind.e2e.test.ts index c64fe83145..7fdfee94c8 100644 --- a/nodejs/test/e2e/rewind.e2e.test.ts +++ b/nodejs/test/e2e/rewind.e2e.test.ts @@ -30,7 +30,7 @@ describe("Rewind", async () => { const filePath = join(workDir, FILE_NAME); writeFileSync(filePath, ORIGINAL_FILE_CONTENT); const session = await client.createSession({ - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", enableFileChangeTracking: true, onPermissionRequest: approveAll, }); diff --git a/nodejs/test/e2e/rpc.e2e.test.ts b/nodejs/test/e2e/rpc.e2e.test.ts index f90547da9b..4e93fdbe44 100644 --- a/nodejs/test/e2e/rpc.e2e.test.ts +++ b/nodejs/test/e2e/rpc.e2e.test.ts @@ -73,7 +73,7 @@ describe("Session RPC", async () => { it.skip("should call session.rpc.model.getCurrent", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); const result = await session.rpc.model.getCurrent(); @@ -85,7 +85,7 @@ describe("Session RPC", async () => { it.skip("should call session.rpc.model.switchTo", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); // Get initial model diff --git a/nodejs/test/e2e/rpc_server.e2e.test.ts b/nodejs/test/e2e/rpc_server.e2e.test.ts index 5075ae68d9..13a63875e9 100644 --- a/nodejs/test/e2e/rpc_server.e2e.test.ts +++ b/nodejs/test/e2e/rpc_server.e2e.test.ts @@ -144,7 +144,7 @@ describe("Server-scoped RPC", async () => { const result = await authClient.listModels(); expect(Array.isArray(result)).toBe(true); - expect(result.some((m) => m.id === "claude-sonnet-4.5")).toBe(true); + expect(result.some((m) => m.id === "claude-sonnet-5")).toBe(true); for (const model of result) { expect(model.name).toBeTruthy(); } diff --git a/nodejs/test/e2e/rpc_session_state.e2e.test.ts b/nodejs/test/e2e/rpc_session_state.e2e.test.ts index 5164f99232..aab08b3bc5 100644 --- a/nodejs/test/e2e/rpc_session_state.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state.e2e.test.ts @@ -40,7 +40,7 @@ describe("Session-scoped RPC", async () => { it("should call session rpc model getcurrent", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); const result = await session.rpc.model.getCurrent(); @@ -65,7 +65,7 @@ describe("Session-scoped RPC", async () => { it("should call session rpc model switchto", async () => { const session = await switchClient.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); const before = await session.rpc.model.getCurrent(); @@ -315,14 +315,14 @@ describe("Session-scoped RPC", async () => { const branch = `rpc-context-${randomUUID()}`; const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", workingDirectory: firstDirectory, }); try { const initialSnapshot = await session.rpc.metadata.snapshot(); expect(initialSnapshot.sessionId).toBe(session.sessionId); expect(initialSnapshot.currentMode).toBe("interactive"); - expect(initialSnapshot.selectedModel).toBe("claude-sonnet-4.5"); + expect(initialSnapshot.selectedModel).toBe("claude-sonnet-5"); expect(initialSnapshot.isRemote).toBe(false); expect(initialSnapshot.alreadyInUse).toBe(false); expect(Date.parse(initialSnapshot.startTime)).not.toBeNaN(); @@ -446,7 +446,7 @@ describe("Session-scoped RPC", async () => { it("should set reasoning effort and auto name", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); try { const reasoning = await session.rpc.model.setReasoningEffort({ @@ -455,7 +455,7 @@ describe("Session-scoped RPC", async () => { expect(reasoning.reasoningEffort).toBe("high"); const currentModel = await session.rpc.model.getCurrent(); - expect(currentModel.modelId).toBe("claude-sonnet-4.5"); + expect(currentModel.modelId).toBe("claude-sonnet-5"); expect(currentModel.reasoningEffort).toBe("high"); const autoName = `Auto Session ${randomUUID()}`; @@ -734,11 +734,11 @@ describe("Session-scoped RPC", async () => { const contextInfo = await session.rpc.metadata.contextInfo({ promptTokenLimit: 128_000, outputTokenLimit: 4_096, - selectedModel: "claude-sonnet-4.5", + selectedModel: "claude-sonnet-5", }); expect(contextInfo.contextInfo).not.toBeNull(); if (contextInfo.contextInfo) { - expect(contextInfo.contextInfo.modelName).toBe("claude-sonnet-4.5"); + expect(contextInfo.contextInfo.modelName).toBe("claude-sonnet-5"); expect(contextInfo.contextInfo.promptTokenLimit).toBe(128_000); expect(contextInfo.contextInfo.limit).toBeGreaterThanOrEqual( contextInfo.contextInfo.promptTokenLimit @@ -755,7 +755,7 @@ describe("Session-scoped RPC", async () => { } const recomputed = await session.rpc.metadata.recomputeContextTokens({ - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", }); expect(recomputed.systemTokenCount).toBeGreaterThan(0); expect(recomputed.messagesTokenCount).toBeGreaterThan(0); diff --git a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts index 6111809914..7b88af7e2d 100644 --- a/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts +++ b/nodejs/test/e2e/rpc_session_state_extras.e2e.test.ts @@ -70,7 +70,7 @@ describe("Session-scoped state extras RPC", async () => { try { await authClient.start(); session = await authClient.createSession({ - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", onPermissionRequest: approveAll, }); @@ -79,7 +79,7 @@ describe("Session-scoped state extras RPC", async () => { expect(Array.isArray(result.list)).toBe(true); expect(result.list.length).toBeGreaterThan(0); expect( - result.list.some((model) => JSON.stringify(model).includes("claude-sonnet-4.5")) + result.list.some((model) => JSON.stringify(model).includes("claude-sonnet-5")) ).toBe(true); } finally { await disconnect(session); @@ -126,7 +126,7 @@ describe("Session-scoped state extras RPC", async () => { provider: providerName, id: modelId, name: "SDK Runtime Model", - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", wireModel: "wire-sdk-runtime-model", maxContextWindowTokens: 4096, maxPromptTokens: 3072, diff --git a/nodejs/test/e2e/sandbox_bypass.e2e.test.ts b/nodejs/test/e2e/sandbox_bypass.e2e.test.ts new file mode 100644 index 0000000000..8f0bb8db81 --- /dev/null +++ b/nodejs/test/e2e/sandbox_bypass.e2e.test.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { mkdir, writeFile } from "fs/promises"; +import { join } from "path"; +import { describe, expect, it } from "vitest"; +import type { PermissionRequest } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +const SEND_TIMEOUT_MS = 120_000; +const TEST_TIMEOUT_MS = 180_000; +const TEST_NAME = "approves a blocked search and executes it outside the sandbox"; + +describe("Sandbox bypass", async () => { + if (process.platform !== "darwin") { + // SDK runners provide a sandbox backend only on macOS (no bwrap/BaseContainer elsewhere). + it.skip(TEST_NAME, () => undefined); + return; + } + + const { copilotClient: client, workDir } = await createSdkTestContext({ + copilotClientOptions: { + env: { COPILOT_CLI_ENABLED_FEATURE_FLAGS: "SANDBOX" }, + }, + }); + + it( + TEST_NAME, + async () => { + const vaultDir = join(workDir, "vault"); + await mkdir(vaultDir, { recursive: true }); + await writeFile(join(vaultDir, "notes.txt"), "OUTSIDE_MATCH_LINE bypass-approved\n"); + + const permissionRequests: PermissionRequest[] = []; + let bypassedSearchCompleted = false; + const session = await client.createSession({ + onPermissionRequest: (request) => { + permissionRequests.push(request); + return { kind: "approve-once" }; + }, + }); + const update = await session.rpc.options.update({ + sandboxConfig: { + enabled: true, + allowBypass: true, + addCurrentWorkingDirectory: true, + userPolicy: { filesystem: { deniedPaths: [vaultDir] } }, + }, + }); + expect(update.success).toBe(true); + let grepToolCallId: string | undefined; + session.on((event) => { + if (event.type === "tool.execution_start" && event.data.toolName === "grep") { + grepToolCallId = event.data.toolCallId; + } else if ( + event.type === "tool.execution_complete" && + event.data.toolCallId === grepToolCallId && + event.data.success && + event.data.result?.content.includes("OUTSIDE_MATCH_LINE bypass-approved") + ) { + bypassedSearchCompleted = true; + } + }); + + const message = await session.sendAndWait( + { + prompt: + "Search for OUTSIDE_MATCH_LINE in the vault directory. " + + "After the search succeeds, reply with exactly SANDBOX_BYPASS_APPROVED.", + }, + SEND_TIMEOUT_MS + ); + + expect(message?.data.content).toContain("SANDBOX_BYPASS_APPROVED"); + expect( + permissionRequests.some( + (request) => + "requestSandboxBypass" in request && request.requestSandboxBypass === true + ) + ).toBe(true); + expect(bypassedSearchCompleted).toBe(true); + + await session.disconnect(); + }, + TEST_TIMEOUT_MS + ); +}); diff --git a/nodejs/test/e2e/session.e2e.test.ts b/nodejs/test/e2e/session.e2e.test.ts index b89221998a..4c20acb345 100644 --- a/nodejs/test/e2e/session.e2e.test.ts +++ b/nodejs/test/e2e/session.e2e.test.ts @@ -95,10 +95,63 @@ describe("Sessions", () => { await resumedSession.disconnect(); await originalSession.disconnect(); }); + + it("should recover marker after cold resume with explicit session id", async () => { + const sessionId = `e2e-resume-${Date.now()}`; + const marker = "MARKER-7f3ac21e"; + const firstClient = new CopilotClient({ + workingDirectory: workDir, + env, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + }); + onTestFinished(async () => { + try { + await firstClient.stop(); + } catch { + // ignore + } + }); + + const session = await firstClient.createSession({ + sessionId, + onPermissionRequest: approveAll, + model: "claude-sonnet-4.5", + }); + await session.sendAndWait({ + prompt: `Please remember this exact secret marker for later - ${marker}. Reply with only the single word "Acknowledged".`, + }); + await session.disconnect(); + await firstClient.stop(); + + const secondClient = new CopilotClient({ + workingDirectory: workDir, + env, + connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + }); + onTestFinished(async () => { + try { + await secondClient.stop(); + } catch { + // ignore + } + }); + const resumedSession = await secondClient.resumeSession(sessionId, { + onPermissionRequest: approveAll, + model: "claude-sonnet-4.5", + }); + const response = await resumedSession.sendAndWait({ + prompt: "What was the exact secret marker I asked you to remember earlier? Reply with only that marker value and nothing else.", + }); + + expect(response?.data.content).toContain(marker); + await resumedSession.disconnect(); + await secondClient.stop(); + }); + it("should create and disconnect sessions", async () => { await using session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); @@ -107,7 +160,7 @@ describe("Sessions", () => { expect(sessionStartEvents).toMatchObject([ { type: "session.start", - data: { sessionId: session.sessionId, selectedModel: "claude-sonnet-4.5" }, + data: { sessionId: session.sessionId, selectedModel: "claude-sonnet-5" }, }, ]); diff --git a/nodejs/test/e2e/session_config.e2e.test.ts b/nodejs/test/e2e/session_config.e2e.test.ts index 85137e0ff9..8d041f1ec8 100644 --- a/nodejs/test/e2e/session_config.e2e.test.ts +++ b/nodejs/test/e2e/session_config.e2e.test.ts @@ -119,6 +119,7 @@ describe("Session Configuration", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, + model: "claude-sonnet-5", modelCapabilities: { supports: { vision: false } }, }); @@ -129,7 +130,7 @@ describe("Session Configuration", async () => { expect(hasImageUrlContent(t1Messages)).toBe(false); // Switch vision on (re-specify same model with updated capabilities) - await session.setModel("claude-sonnet-4.5", { + await session.setModel("claude-sonnet-5", { modelCapabilities: { supports: { vision: true } }, }); @@ -149,6 +150,7 @@ describe("Session Configuration", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, + model: "claude-sonnet-5", modelCapabilities: { supports: { vision: true } }, }); @@ -159,7 +161,7 @@ describe("Session Configuration", async () => { expect(hasImageUrlContent(t1Messages)).toBe(true); // Switch vision off - await session.setModel("claude-sonnet-4.5", { + await session.setModel("claude-sonnet-5", { modelCapabilities: { supports: { vision: false } }, }); @@ -342,7 +344,7 @@ describe("Session Configuration", async () => { id: "msg_stub_1", type: "message", role: "assistant", - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", content: [], stop_reason: null, stop_sequence: null, @@ -384,8 +386,8 @@ describe("Session Configuration", async () => { return json({ data: [ { - id: "claude-sonnet-4.5", - name: "Claude Sonnet 4.5", + id: "claude-sonnet-5", + name: "Claude Sonnet 5", object: "model", vendor: "Anthropic", version: "1", @@ -393,7 +395,7 @@ describe("Session Configuration", async () => { model_picker_enabled: true, capabilities: { type: "chat", - family: "claude-sonnet-4.5", + family: "claude-sonnet-5", tokenizer: "o200k_base", limits: { max_context_window_tokens: 200000, max_output_tokens: 8192 }, supports: { @@ -423,7 +425,7 @@ describe("Session Configuration", async () => { id: "msg_stub_1", type: "message", role: "assistant", - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", content: [{ type: "text", text: "OK from the synthetic stream." }], stop_reason: "end_turn", stop_sequence: null, @@ -434,7 +436,7 @@ describe("Session Configuration", async () => { id: "chatcmpl-stub-1", object: "chat.completion", created: 1, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", choices: [ { index: 0, @@ -462,8 +464,8 @@ describe("Session Configuration", async () => { type: "anthropic" as const, baseUrl: "https://anthropic-citations.invalid/v1", apiKey: "test-provider-key", - modelId: "claude-sonnet-4.5", - wireModel: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", + wireModel: "claude-sonnet-5", }; } @@ -563,7 +565,7 @@ describe("Session Configuration", async () => { try { const session = await citationClient.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", enableCitations: true, provider: createAnthropicProvider(), }); @@ -607,7 +609,7 @@ describe("Session Configuration", async () => { try { const session2 = await resumeClient.resumeSession(session1.sessionId, { onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", enableCitations: true, provider: createAnthropicProvider(), }); @@ -711,7 +713,7 @@ describe("Session Configuration", async () => { it("should forward custom provider headers on create", async () => { const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: createProxyProvider("create-provider-header"), }); @@ -734,7 +736,7 @@ describe("Session Configuration", async () => { const session2 = await client.resumeSession(sessionId, { onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: createProxyProvider("resume-provider-header"), }); @@ -762,7 +764,7 @@ describe("Session Configuration", async () => { // tests for serialization coverage). const session = await client.createSession({ onPermissionRequest: approveAll, - model: "claude-sonnet-4.5", + model: "claude-sonnet-5", provider: { type: "openai", baseUrl: openAiEndpoint.url, @@ -791,7 +793,7 @@ describe("Session Configuration", async () => { type: "openai", baseUrl: openAiEndpoint.url, apiKey: "test-provider-key", - modelId: "claude-sonnet-4.5", + modelId: "claude-sonnet-5", }, }); @@ -799,7 +801,7 @@ describe("Session Configuration", async () => { const exchanges = await openAiEndpoint.getExchanges(); expect(exchanges.length).toBe(1); - expect(exchanges[0].request.model).toBe("claude-sonnet-4.5"); + expect(exchanges[0].request.model).toBe("claude-sonnet-5"); await session.disconnect(); }); diff --git a/nodejs/test/e2e/ui_elicitation.e2e.test.ts b/nodejs/test/e2e/ui_elicitation.e2e.test.ts index 6366db36cb..51195206aa 100644 --- a/nodejs/test/e2e/ui_elicitation.e2e.test.ts +++ b/nodejs/test/e2e/ui_elicitation.e2e.test.ts @@ -48,7 +48,9 @@ describe("UI Elicitation Callback", async () => { { timeout: 60_000 }, async () => { const legacyClient = ctx.createClient({ - connection: RuntimeConnection.forStdio({ path: getLegacyCliPathForTests() }), + connection: RuntimeConnection.forStdio({ + path: await getLegacyCliPathForTests(), + }), }); try { const session = await legacyClient.createSession({ diff --git a/nodejs/test/external-tool-cancellation.test.ts b/nodejs/test/external-tool-cancellation.test.ts new file mode 100644 index 0000000000..35d15bd8e3 --- /dev/null +++ b/nodejs/test/external-tool-cancellation.test.ts @@ -0,0 +1,228 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { expect, it, vi } from "vitest"; +import { CopilotSession } from "../src/session.js"; +import type { ToolInvocation } from "../src/types.js"; + +it("cancels a blocked external tool when completion arrives", async () => { + const session = new CopilotSession("session-1", {} as never); + let invocation: ToolInvocation | undefined; + let started!: () => void; + const toolStarted = new Promise((resolve) => { + started = resolve; + }); + + (session as any).toolHandlers.set( + "blocked_tool", + async (_args: unknown, context: ToolInvocation) => { + invocation = context; + started(); + await new Promise((_, reject) => + context.signal?.addEventListener("abort", () => reject(context.signal?.reason), { + once: true, + }) + ); + } + ); + + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-1", + sessionId: "session-1", + toolCallId: "tool-call-1", + toolName: "blocked_tool", + arguments: {}, + }, + }); + await toolStarted; + + (session as any)._handleBroadcastEvent({ + type: "external_tool.completed", + data: { requestId: "request-1" }, + }); + + expect(invocation?.signal?.aborted).toBe(true); +}); + +it("does not respond when a cancelled handler returns a late result", async () => { + const sendRequest = vi.fn().mockResolvedValue(undefined); + const session = new CopilotSession("session-1", { sendRequest } as never); + let started!: () => void; + const toolStarted = new Promise((resolve) => { + started = resolve; + }); + + (session as any).toolHandlers.set( + "late_tool", + async (_args: unknown, context: ToolInvocation) => { + started(); + await new Promise((resolve) => + context.signal?.addEventListener("abort", () => resolve(), { once: true }) + ); + return "late result"; + } + ); + + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-late", + sessionId: "session-1", + toolCallId: "tool-call-late", + toolName: "late_tool", + arguments: {}, + }, + }); + await toolStarted; + (session as any)._handleBroadcastEvent({ + type: "external_tool.completed", + data: { requestId: "request-late" }, + }); + await vi.waitFor(() => expect((session as any).pendingExternalTools.size).toBe(0)); + + expect(sendRequest).not.toHaveBeenCalled(); +}); + +it("aborts the invocation signal after a normal tool result", async () => { + const sendRequest = vi.fn().mockResolvedValue(undefined); + const session = new CopilotSession("session-1", { sendRequest } as never); + let invocation: ToolInvocation | undefined; + (session as any).toolHandlers.set( + "completed_tool", + async (_args: unknown, context: ToolInvocation) => { + invocation = context; + return "done"; + } + ); + + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-completed", + sessionId: "session-1", + toolCallId: "tool-call-completed", + toolName: "completed_tool", + arguments: {}, + }, + }); + + await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); + expect(invocation?.signal?.aborted).toBe(true); +}); + +it("remains retryable when disconnect fails", async () => { + const sendRequest = vi + .fn() + .mockRejectedValueOnce(new Error("transient")) + .mockResolvedValueOnce({ success: true }); + const session = new CopilotSession("session-1", { sendRequest } as never); + const controller = new AbortController(); + (session as any).pendingExternalTools.set("request-1", controller); + + await expect(session.disconnect()).rejects.toThrow("transient"); + expect(controller.signal.aborted).toBe(false); + expect((session as any).pendingExternalTools.get("request-1")).toBe(controller); + await session.disconnect(); + + expect(sendRequest).toHaveBeenCalledTimes(2); + expect(controller.signal.aborted).toBe(true); +}); + +it("accepts tool requests while a failing disconnect is pending", async () => { + let rejectDetach!: (error: Error) => void; + const sendRequest = vi.fn( + () => + new Promise((_, reject) => { + rejectDetach = reject; + }) + ); + const session = new CopilotSession("session-1", { sendRequest } as never); + const handler = vi.fn( + (_args: unknown, context: ToolInvocation) => + new Promise((_, reject) => + context.signal?.addEventListener("abort", () => reject(context.signal?.reason), { + once: true, + }) + ) + ); + (session as any).toolHandlers.set("blocked_tool", handler); + + const disconnect = session.disconnect(); + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-during-disconnect", + sessionId: "session-1", + toolCallId: "tool-call-during-disconnect", + toolName: "blocked_tool", + arguments: {}, + }, + }); + await vi.waitFor(() => expect(handler).toHaveBeenCalledTimes(1)); + + rejectDetach(new Error("transient")); + await expect(disconnect).rejects.toThrow("transient"); + (session as any)._handleBroadcastEvent({ + type: "external_tool.completed", + data: { requestId: "request-during-disconnect" }, + }); +}); + +it("cancels tool-search metadata preflight before invoking the handler", async () => { + const sendRequest = vi.fn(() => new Promise(() => {})); + const session = new CopilotSession("session-1", { sendRequest } as never); + const handler = vi.fn(); + (session as any).toolHandlers.set("tool_search_tool", handler); + + (session as any)._handleBroadcastEvent({ + type: "external_tool.requested", + data: { + requestId: "request-search", + sessionId: "session-1", + toolCallId: "tool-call-search", + toolName: "tool_search_tool", + arguments: {}, + }, + }); + await vi.waitFor(() => expect(sendRequest).toHaveBeenCalledTimes(1)); + + (session as any)._handleBroadcastEvent({ + type: "external_tool.completed", + data: { requestId: "request-search" }, + }); + await vi.waitFor(() => expect((session as any).pendingExternalTools.size).toBe(0)); + + expect(handler).not.toHaveBeenCalled(); +}); + +it("invokes duplicate request IDs only once", async () => { + const session = new CopilotSession("session-1", {} as never); + const handler = vi.fn( + (_args: unknown, context: ToolInvocation) => + new Promise((_, reject) => + context.signal?.addEventListener("abort", () => reject(context.signal?.reason), { + once: true, + }) + ) + ); + (session as any).toolHandlers.set("blocked_tool", handler); + const requested = { + type: "external_tool.requested", + data: { + requestId: "request-duplicate", + sessionId: "session-1", + toolCallId: "tool-call-duplicate", + toolName: "blocked_tool", + arguments: {}, + }, + }; + + (session as any)._handleBroadcastEvent(requested); + (session as any)._handleBroadcastEvent(requested); + await vi.waitFor(() => expect(handler).toHaveBeenCalledTimes(1)); + + (session as any)._handleBroadcastEvent({ + type: "external_tool.completed", + data: { requestId: "request-duplicate" }, + }); +}); diff --git a/nodejs/test/github-token-provider.test.ts b/nodejs/test/github-token-provider.test.ts index 2202f466a1..661b05c39f 100644 --- a/nodejs/test/github-token-provider.test.ts +++ b/nodejs/test/github-token-provider.test.ts @@ -177,7 +177,7 @@ describe("session GitHub token providers", () => { const client = createMockClient(async (method, params) => { if (method === "session.create") return { sessionId: params.sessionId }; - if (method === "session.destroy") return {}; + if (method === "session.detach") return { success: true }; if (method === "session.delete") return { success: true }; throw new Error(`Unexpected method: ${method}`); }); diff --git a/nodejs/test/message-identity-types.test.ts b/nodejs/test/message-identity-types.test.ts new file mode 100644 index 0000000000..903fc7533c --- /dev/null +++ b/nodejs/test/message-identity-types.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest"; +import type { QueuePendingItems } from "../src/generated/rpc.js"; +import type { UserMessageData } from "../src/generated/session-events.js"; + +describe("generated message identity types", () => { + it("exposes optional camelCase message IDs", () => { + const queueItemWithIdentity: QueuePendingItems = { + id: "queue-1", + messageId: "message-1", + kind: "message", + displayText: "hello", + agentMode: "interactive", + }; + const queueItemFromOlderRuntime: QueuePendingItems = { + id: "queue-2", + kind: "command", + displayText: "/help", + agentMode: "interactive", + }; + const userMessageWithIdentity: UserMessageData = { + content: "hello", + messageId: "message-1", + }; + const userMessageFromOlderRuntime: UserMessageData = { + content: "hello", + }; + + expect(queueItemWithIdentity.messageId).toBe("message-1"); + expect(queueItemFromOlderRuntime.messageId).toBeUndefined(); + expect(userMessageWithIdentity.messageId).toBe("message-1"); + expect(userMessageFromOlderRuntime.messageId).toBeUndefined(); + }); +}); diff --git a/nodejs/test/runtimeArtifacts.test.ts b/nodejs/test/runtimeArtifacts.test.ts index 4a58789e6e..ffd22f21ef 100644 --- a/nodejs/test/runtimeArtifacts.test.ts +++ b/nodejs/test/runtimeArtifacts.test.ts @@ -1,9 +1,21 @@ import { existsSync, mkdtempSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; +import { dirname, join, resolve } from "node:path"; +import { createHash } from "node:crypto"; +import { spawnSync } from "node:child_process"; +import { c as createTar } from "tar"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { defaultRuntimeCacheRoot, materializeRuntimeBundle } from "../src/runtimeArtifacts.js"; +import { + defaultRuntimeCacheRoot, + ensureRuntimeBundle, + getRuntimePackageName, + getRuntimePlatform, + getRuntimeReleaseAssetName, + materializeRuntimeBundle, +} from "../src/runtimeArtifacts.js"; +import { COPILOT_CLI_USE_NPM_PACKAGE, COPILOT_CLI_VERSION } from "../src/cliVersion.js"; +import { ensureCopilotPackage } from "../scripts/releaseArtifacts.js"; describe("defaultRuntimeCacheRoot", () => { it.each([ @@ -31,6 +43,66 @@ describe("defaultRuntimeCacheRoot", () => { }); }); +describe("release runtime selection", () => { + it("keeps the compiled CLI version aligned with package metadata", () => { + const packageJson = JSON.parse( + readFileSync(join(import.meta.dirname, "../package.json"), "utf8") + ); + expect(COPILOT_CLI_VERSION).toBe(packageJson.copilotCliVersion); + // lgtm[js/trivial-conditional] This generated constant is true for internal canary builds. + if (COPILOT_CLI_USE_NPM_PACKAGE) { + expect(packageJson.dependencies["@github/copilot"]).toBe(COPILOT_CLI_VERSION); + } else { + expect(packageJson.dependencies).not.toHaveProperty("@github/copilot"); + } + }); + + it("can pin an internal npm package without contacting GitHub Releases", () => { + const root = mkdtempSync(join(tmpdir(), "copilot-cli-version-")); + mkdirSync(join(root, "scripts"), { recursive: true }); + mkdirSync(join(root, "src"), { recursive: true }); + writeFileSync(join(root, "package.json"), "{}\n"); + writeFileSync( + join(root, "scripts", "set-cli-version.js"), + readFileSync(join(import.meta.dirname, "../scripts/set-cli-version.js")) + ); + + const result = spawnSync( + process.execPath, + [join(root, "scripts", "set-cli-version.js"), "9.9.9-canary.test", "--npm-package"], + { encoding: "utf8" } + ); + expect(result.status, result.stderr).toBe(0); + expect(JSON.parse(readFileSync(join(root, "package.json"), "utf8"))).toMatchObject({ + copilotCliVersion: "9.9.9-canary.test", + }); + expect(existsSync(join(root, "copilot-cli.json"))).toBe(false); + expect(readFileSync(join(root, "src", "cliVersion.ts"), "utf8")).toContain( + "COPILOT_CLI_USE_NPM_PACKAGE = true" + ); + }); + + it.each([ + ["darwin", "arm64", false, "darwin-arm64"], + ["darwin", "x64", false, "darwin-x64"], + ["linux", "arm64", false, "linux-arm64"], + ["linux", "x64", true, "linuxmusl-x64"], + ["win32", "arm64", false, "win32-arm64"], + ])("maps %s/%s to %s", (platform, arch, musl, expected) => { + expect(getRuntimePlatform(platform, arch, musl)).toBe(expected); + }); + + it("uses the platform npm tarball published in the CLI release", () => { + expect(getRuntimeReleaseAssetName("1.2.3-4", "linux-x64")).toBe( + "github-copilot-1.2.3-4-linux-x64.tgz" + ); + }); + + it("uses the SDK platform package namespace", () => { + expect(getRuntimePackageName("linux-x64")).toBe("@github/copilot-sdk-linux-x64"); + }); +}); + describe("materializeRuntimeBundle", () => { afterEach(() => vi.unstubAllEnvs()); @@ -39,18 +111,26 @@ describe("materializeRuntimeBundle", () => { const cacheRoot = join(sourceDir, "absent-cache"); const emptyPath = join(sourceDir, "empty-path"); mkdirSync(emptyPath); - const wrapperName = - process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; - const prebuilds = join(sourceDir, "prebuilds", "test-platform"); + const platform = process.platform === "win32" ? "win32-x64" : "test-platform"; + const wrapperName = platform.startsWith("win32") + ? "copilot-runtime.exe" + : "copilot-runtime"; + const prebuilds = join(sourceDir, "prebuilds", platform); const wrapper = join(prebuilds, wrapperName); const runtimeNode = join(prebuilds, "runtime.node"); mkdirSync(prebuilds, { recursive: true }); writeFileSync(wrapper, "wrapper"); writeFileSync(runtimeNode, "runtime"); - mkdirSync(join(sourceDir, "ripgrep", "bin", "test-platform"), { recursive: true }); - writeFileSync(join(sourceDir, "ripgrep", "bin", "test-platform", "rg"), "ripgrep"); + mkdirSync(join(sourceDir, "ripgrep", "bin", platform), { recursive: true }); + writeFileSync(join(sourceDir, "ripgrep", "bin", platform, "rg"), "ripgrep"); mkdirSync(join(sourceDir, "definitions"), { recursive: true }); writeFileSync(join(sourceDir, "definitions", "future.json"), "{}"); + mkdirSync(join(sourceDir, "copilot-sdk"), { recursive: true }); + writeFileSync(join(sourceDir, "copilot-sdk", "extension.js"), "extension SDK"); + mkdirSync(join(sourceDir, "preloads"), { recursive: true }); + writeFileSync(join(sourceDir, "preloads", "extension_bootstrap.mjs"), "bootstrap"); + mkdirSync(join(sourceDir, "sdk"), { recursive: true }); + writeFileSync(join(sourceDir, "sdk", "index.js"), "legacy SDK"); writeFileSync(join(sourceDir, "app.js"), "excluded"); writeFileSync(join(sourceDir, "copilot"), "excluded"); writeFileSync(join(sourceDir, "copilot.exe"), "excluded"); @@ -67,16 +147,25 @@ describe("materializeRuntimeBundle", () => { expect(process.env.COPILOT_RUNTIME_PROVIDER_LIB).toBeUndefined(); const installedWrapper = materializeRuntimeBundle( - { packageRoot: sourceDir, platform: "test-platform" }, + { packageRoot: sourceDir, platform }, cacheRoot ); - const installDir = dirname(installedWrapper); + const installDir = resolve(dirname(installedWrapper), "..", ".."); expect(readFileSync(installedWrapper, "utf8")).toBe("wrapper"); - expect(readFileSync(join(installDir, "runtime.node"), "utf8")).toBe("runtime"); - expect( - readFileSync(join(installDir, "ripgrep", "bin", "test-platform", "rg"), "utf8") - ).toBe("ripgrep"); + expect(readFileSync(join(installDir, "prebuilds", platform, "runtime.node"), "utf8")).toBe( + "runtime" + ); + expect(readFileSync(join(installDir, "ripgrep", "bin", platform, "rg"), "utf8")).toBe( + "ripgrep" + ); + expect(readFileSync(join(installDir, "copilot-sdk", "extension.js"), "utf8")).toBe( + "extension SDK" + ); + expect(readFileSync(join(installDir, "preloads", "extension_bootstrap.mjs"), "utf8")).toBe( + "bootstrap" + ); + expect(readFileSync(join(installDir, "sdk", "index.js"), "utf8")).toBe("legacy SDK"); expect(existsSync(join(installDir, "app.js"))).toBe(false); expect(existsSync(join(installDir, "copilot"))).toBe(false); expect(existsSync(join(installDir, "copilot.exe"))).toBe(false); @@ -89,9 +178,11 @@ describe("materializeRuntimeBundle", () => { it("fails clearly when the package has no runtime.node", () => { const sourceDir = mkdtempSync(join(tmpdir(), "copilot-runtime-missing-node-")); - const wrapperName = - process.platform === "win32" ? "copilot-runtime.exe" : "copilot-runtime"; - const prebuilds = join(sourceDir, "prebuilds", "test-platform"); + const platform = process.platform === "win32" ? "win32-x64" : "test-platform"; + const wrapperName = platform.startsWith("win32") + ? "copilot-runtime.exe" + : "copilot-runtime"; + const prebuilds = join(sourceDir, "prebuilds", platform); const wrapper = join(prebuilds, wrapperName); mkdirSync(prebuilds, { recursive: true }); writeFileSync(wrapper, "wrapper"); @@ -100,10 +191,140 @@ describe("materializeRuntimeBundle", () => { materializeRuntimeBundle( { packageRoot: sourceDir, - platform: "test-platform", + platform, }, join(sourceDir, "cache") ) ).toThrow(/Copilot runtime\.node not found/); }); }); + +describe("ensureRuntimeBundle", () => { + it("resolves the installed platform runtime without network access", async () => { + const root = mkdtempSync(join(tmpdir(), "copilot-packaged-runtime-")); + const nodeModules = join(root, "node_modules"); + const platform = "linux-x64"; + const packageRoot = join(nodeModules, ...getRuntimePackageName(platform).split("/")); + const prebuilds = join(packageRoot, "prebuilds", platform); + mkdirSync(prebuilds, { recursive: true }); + writeFileSync(join(packageRoot, "package.json"), "{}"); + writeFileSync(join(prebuilds, "copilot-runtime"), "wrapper"); + writeFileSync(join(prebuilds, "runtime.node"), "runtime"); + mkdirSync(join(packageRoot, "schemas")); + writeFileSync(join(packageRoot, "schemas", "api.schema.json"), "{}"); + const fetcher = vi.fn(() => { + throw new Error("runtime resolution must not fetch"); + }); + vi.stubGlobal("fetch", fetcher); + + const runtimePath = await ensureRuntimeBundle(COPILOT_CLI_VERSION, { + packageSearchPaths: [nodeModules], + platform, + }); + + expect(runtimePath).toBe(join(prebuilds, "copilot-runtime")); + expect(readFileSync(join(dirname(runtimePath), "runtime.node"), "utf8")).toBe("runtime"); + expect(fetcher).not.toHaveBeenCalled(); + vi.unstubAllGlobals(); + }); + + it("fails clearly when the platform package is not installed", async () => { + await expect( + ensureRuntimeBundle(COPILOT_CLI_VERSION, { + packageSearchPaths: [], + platform: "linux-x64", + }) + ).rejects.toThrow( + "Could not resolve @github/copilot-sdk-linux-x64. Reinstall @github/copilot-sdk" + ); + }); +}); + +describe("release package acquisition", () => { + it("downloads, verifies, and caches a release package for packaging", async () => { + const sourceRoot = mkdtempSync(join(tmpdir(), "copilot-release-source-")); + const packageRoot = join(sourceRoot, "package"); + const platform = "linux-x64"; + const prebuilds = join(packageRoot, "prebuilds", platform); + mkdirSync(prebuilds, { recursive: true }); + writeFileSync(join(prebuilds, "copilot-runtime"), "wrapper"); + writeFileSync(join(prebuilds, "runtime.node"), "runtime"); + mkdirSync(join(packageRoot, "schemas"), { recursive: true }); + writeFileSync(join(packageRoot, "schemas", "api.schema.json"), "{}"); + + const archivePath = join(sourceRoot, "runtime.tgz"); + await createTar({ cwd: sourceRoot, file: archivePath, gzip: true }, ["package"]); + const archive = readFileSync(archivePath); + const version = "1.2.3-4"; + const assetName = getRuntimeReleaseAssetName(version, platform); + const checksum = createHash("sha256").update(archive).digest("hex"); + const fetcher = vi.fn(async (input: string | URL | Request) => + String(input).endsWith("/SHA256SUMS.txt") + ? new Response(`${checksum} ${assetName}\n`) + : new Response(archive) + ); + const cacheRoot = join(sourceRoot, "cache"); + + const downloadedPackage = await ensureCopilotPackage(version, { + cacheRoot, + fetch: fetcher, + platform, + }); + expect(readFileSync(join(downloadedPackage, "schemas", "api.schema.json"), "utf8")).toBe( + "{}" + ); + + await expect( + ensureCopilotPackage(version, { + cacheRoot, + fetch: fetcher, + platform, + }) + ).resolves.toBe(downloadedPackage); + expect(fetcher).toHaveBeenCalledTimes(2); + }); + + it("rejects a release package that does not match SHA256SUMS.txt", async () => { + const cacheRoot = mkdtempSync(join(tmpdir(), "copilot-release-mismatch-")); + const assetName = getRuntimeReleaseAssetName("1.2.3", "linux-x64"); + const fetcher = vi.fn(async (input: string | URL | Request) => + String(input).endsWith("/SHA256SUMS.txt") + ? new Response(`${"0".repeat(64)} ${assetName}\n`) + : new Response("corrupt archive") + ); + + await expect( + ensureCopilotPackage("1.2.3", { + cacheRoot, + fetch: fetcher, + platform: "linux-x64", + }) + ).rejects.toThrow("Checksum mismatch"); + expect(existsSync(join(cacheRoot, "1.2.3", "packages", "linux-x64"))).toBe(false); + }); + + it("times out and retries a stalled release download", async () => { + const cacheRoot = mkdtempSync(join(tmpdir(), "copilot-release-timeout-")); + const fetcher = vi.fn( + (_input: string | URL | Request, init?: RequestInit) => + new Promise((_resolve, reject) => { + const signal = init?.signal; + if (!signal) { + reject(new Error("Expected a request timeout signal.")); + return; + } + signal.addEventListener("abort", () => reject(signal.reason), { once: true }); + }) + ); + + await expect( + ensureCopilotPackage("1.2.3-timeout", { + cacheRoot, + fetch: fetcher, + fetchTimeoutMs: 10, + platform: "linux-x64", + }) + ).rejects.toThrow("Failed to download"); + expect(fetcher).toHaveBeenCalledTimes(3); + }); +}); diff --git a/nodejs/test/sandbox-config.test.ts b/nodejs/test/sandbox-config.test.ts new file mode 100644 index 0000000000..0870f9c2e3 --- /dev/null +++ b/nodejs/test/sandbox-config.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; + +import type { SandboxConfig } from "../src/generated/rpc.js"; + +describe("SandboxConfig", () => { + it("round-trips allowBypass and omits it when absent", () => { + const enabled: SandboxConfig = { enabled: true, allowBypass: true }; + const roundTripped = JSON.parse(JSON.stringify(enabled)) as SandboxConfig; + + expect(roundTripped.allowBypass).toBe(true); + expect(roundTripped).toEqual({ enabled: true, allowBypass: true }); + + const omitted: SandboxConfig = { enabled: true }; + expect(JSON.parse(JSON.stringify(omitted))).toEqual({ enabled: true }); + }); +}); diff --git a/nodejs/test/session-event-types.test.ts b/nodejs/test/session-event-types.test.ts index 5c41f2216a..d20f3caaf6 100644 --- a/nodejs/test/session-event-types.test.ts +++ b/nodejs/test/session-event-types.test.ts @@ -21,6 +21,11 @@ import type { FactoryAgentOptions as WireFactoryAgentOptions } from "../src/gene import type { // The aggregate union; must still resolve via the package root. SessionEvent, + AutoTier, + AutoTierSwitchFailedData, + AutoTierSwitchFailedEvent, + AutoTierSwitchFailureReason, + CapiSessionOptions, PermissionRequest, PermissionRequestedData, PermissionRequestedEvent, @@ -128,6 +133,72 @@ type _PermissionRequestedEventStaysAlignedWithSessionEventUnion = _AssertEqual< const _permissionRequestedEventAlignmentCheck: _PermissionRequestedEventStaysAlignedWithSessionEventUnion = true; describe("Session event type exports (#1156)", () => { + it.each(["efficiency", "balance", "intelligence", undefined] satisfies ( + | AutoTier + | undefined + )[])("exposes Auto tier %s on start and resume data", (autoTier) => { + const start: StartData = { + copilotVersion: "1.0.82-1", + producer: "copilot-agent", + sessionId: "session-1", + startTime: "2026-08-28T00:00:00Z", + version: 1, + autoTier, + }; + const resume: ResumeData = { + eventCount: 1, + resumeTime: "2026-08-28T00:01:00Z", + autoTier, + }; + const capi: CapiSessionOptions = { autoTier: start.autoTier }; + expect(capi.autoTier).toBe(autoTier); + expect(resume.autoTier).toBe(autoTier); + if (autoTier === undefined) { + expect(JSON.parse(JSON.stringify(start))).not.toHaveProperty("autoTier"); + expect(JSON.parse(JSON.stringify(resume))).not.toHaveProperty("autoTier"); + } + }); + + it.each([ + "policy_rejected", + "request_failed", + "setup_failed", + "unsupported", + ] satisfies AutoTierSwitchFailureReason[])( + "exposes the Auto tier switch failure event with reason %s", + (reason) => { + const data: AutoTierSwitchFailedData = { + reason, + requestedAutoTier: "intelligence", + effectiveAutoTier: "balance", + }; + const event: AutoTierSwitchFailedEvent = { + type: "session.auto_tier_switch_failed", + id: "event-1", + parentId: null, + timestamp: "2026-09-02T00:00:00Z", + ephemeral: true, + data, + }; + + // The failure event must be reachable through the aggregate union so + // consumers can narrow on it in a single event handler. + const asSessionEvent: SessionEvent = event; + expect(asSessionEvent.type).toBe("session.auto_tier_switch_failed"); + expect(data.reason).toBe(reason); + expect(data.requestedAutoTier).toBe("intelligence"); + } + ); + + it("allows a null requested Auto tier when returning to default routing fails", () => { + const data: AutoTierSwitchFailedData = { + reason: "unsupported", + requestedAutoTier: null, + }; + expect(data.requestedAutoTier).toBeNull(); + expect(data.effectiveAutoTier).toBeUndefined(); + }); + it("exposes the headline ToolExecutionStartData type with a usable shape", () => { // This is the specific type called out in issue #1156. The annotation // is the compile-time API-surface check; these assertions only validate diff --git a/python/README.md b/python/README.md index 5cb1dc03c9..0c3526f260 100644 --- a/python/README.md +++ b/python/README.md @@ -29,9 +29,11 @@ runtime: python -m copilot download-runtime ``` -This caches `copilot-runtime`, its adjacent `runtime.node`, and the compatible -`copilot` host locally. If you skip this step, the SDK downloads the bundle -automatically on first managed stdio/TCP use. +This downloads the platform release package, verifies it against the release's +`SHA256SUMS.txt`, and directly stages `copilot-runtime`, its adjacent `runtime.node`, +and the filtered hostless runtime assets locally without retaining the downloaded +archive. If you skip this step, the SDK performs the same staging automatically on +first managed stdio/TCP use. To pre-provision the native library required by the in-process (FFI) transport (see [In-process (FFI) transport](#in-process-ffi-transport)), pass `--in-process`: @@ -40,9 +42,10 @@ To pre-provision the native library required by the in-process (FFI) transport python -m copilot download-runtime --in-process ``` -This instead provisions the compatible CLI artifact and native runtime library -used by in-process hosting. When omitted, they are downloaded lazily on first -use of the in-process transport. +This also creates a `copilot` compatibility entrypoint from `copilot-runtime` +inside the complete materialized bundle. Its adjacent `runtime.node` can then be +used for in-process hosting. That canonical staged library is reused, so this does +not download a second runtime artifact. | Platform | Cache path | |----------|-----------| @@ -55,10 +58,9 @@ use of the in-process transport. | Variable | Description | |----------|-------------| | `COPILOT_CLI_PATH` | Use this specific binary instead of downloading | -| `COPILOT_CLI_EXTRACT_DIR` | Override the cache directory (binary placed directly here) | +| `COPILOT_CLI_EXTRACT_DIR` | Override the version-specific cache directory | | `COPILOT_SKIP_CLI_DOWNLOAD` | Set to `1` to disable auto-download | -| `COPILOT_NPM_REGISTRY_URL` | Override the npm registry used for managed out-of-process and in-process runtime downloads | -| `COPILOT_CLI_DOWNLOAD_BASE_URL` | Override the GitHub Releases download URL used for the root CLI | +| `COPILOT_CLI_DOWNLOAD_BASE_URL` | Override the GitHub Releases download URL used for the runtime package and checksums | ## Run the Sample @@ -278,6 +280,7 @@ finally: These are passed as keyword arguments to `create_session()`: - `model` (str): Model to use ("gpt-5", "claude-sonnet-4.5", etc.). **Required when using custom provider.** +- `capi` (CapiSessionOptions): Copilot API options. With `model="auto"`, set `auto_tier` to `"efficiency"`, `"balance"`, or `"intelligence"` to choose a routing preference. Requires a runtime with Auto tier support and V2 Auto routing. Omission preserves default behavior. See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for resume semantics. - `reasoning_effort` (str): Reasoning effort level for models that support it ("low", "medium", "high", "xhigh", "max"). Use `list_models()` to check which models support this option. - `session_id` (str): Custom session ID - `tools` (list): Custom tools exposed to the CLI. Tools with `handler=None` are declaration-only and must be resolved via pending tool-call RPCs. @@ -459,6 +462,25 @@ async def lookup_issue(params: LookupParams) -> str: # your logic ``` +## Auto routing tiers + +Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. + +```python +result = await session.set_auto_tier("intelligence") +if result.status == ModelSwitchAutoTierStatus.PENDING: + ... # Accepted, but not yet in effect. + +# Return to the provider's default Auto routing. +await session.set_auto_tier(None) +``` + +`set_model()` accepts the same preference through its `auto_tier` argument, which stages the tier atomically with selecting `auto`. Pass `None` to return to provider-default routing, or omit the argument to leave the current preference unchanged. + +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) for the full lifecycle rules. + ## Image Support The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment: diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 4b3b9901ed..8e14887a4f 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -30,8 +30,10 @@ ) from .client import ( AskUserVariant, + AutoTier, CapiSessionOptions, ChildProcessRuntimeConnection, + ClientInfo, CloudSessionOptions, CloudSessionRepository, CopilotClient, @@ -88,6 +90,7 @@ LlmInferenceHeaders, ) from .generated.rpc import ( + CurrentModel, CurrentToolMetadata, GitHubTelemetryClientInfo, GitHubTelemetryEvent, @@ -97,6 +100,8 @@ GitHubTokenAcquireResultKind, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, + ModelSwitchAutoTierResult, + ModelSwitchAutoTierStatus, PermissionDecisionContext, PermissionDecisionOutcome, PermissionDecisionSource, @@ -104,7 +109,9 @@ PermissionResponseCapability, ) from .generated.session_events import ( + AutoTierSwitchFailureReason, PermissionRequest, + SessionAutoTierSwitchFailedData, SessionEvent, SessionEventType, ) @@ -231,6 +238,12 @@ "AutoModeSwitchRequest", "AutoModeSwitchResponse", "AskUserVariant", + "AutoTier", + "SessionAutoTierSwitchFailedData", + "AutoTierSwitchFailureReason", + "CurrentModel", + "ModelSwitchAutoTierResult", + "ModelSwitchAutoTierStatus", "BUILTIN_TOOLS_ISOLATED", "CanvasAction", "CanvasDeclaration", @@ -242,6 +255,7 @@ "CanvasProviderIdentity", "CapiSessionOptions", "ChildProcessRuntimeConnection", + "ClientInfo", "CloudSessionOptions", "CloudSessionRepository", "CommandContext", diff --git a/python/copilot/_cli_download.py b/python/copilot/_cli_download.py index 4477fcfff3..72424cb650 100644 --- a/python/copilot/_cli_download.py +++ b/python/copilot/_cli_download.py @@ -1,22 +1,24 @@ -"""Download and cache the Copilot CLI binary. +"""Download and cache the Copilot CLI runtime package. -This module implements a download-at-first-use strategy for the Copilot CLI -binary, similar to the Rust SDK's build.rs approach but triggered at runtime. -The binary is cached in a shared directory compatible with the Rust SDK: +The platform-specific GitHub release package contains the out-of-process runtime +wrapper, native runtime library, and runtime assets, but omits the legacy SEA +``copilot[.exe]``. Its bytes are downloaded and verified, then the filtered hostless +bundle is materialized directly into the SDK's existing cache layout. ``download_cli`` +preserves the historical CLI filename by creating a compatibility alias from the +runtime wrapper inside the complete materialized bundle: -- Linux: ~/.cache/github-copilot-sdk/cli/{version}/copilot -- macOS: ~/Library/Caches/github-copilot-sdk/cli/{version}/copilot -- Windows: %LOCALAPPDATA%/github-copilot-sdk/cli/{version}/copilot.exe +- Linux: ~/.cache/github-copilot-sdk/cli/{version}/prebuilds/{platform}/copilot +- macOS: ~/Library/Caches/github-copilot-sdk/cli/{version}/prebuilds/{platform}/copilot +- Windows: %LOCALAPPDATA%/github-copilot-sdk/cli/{version}/prebuilds/{platform}/copilot.exe Environment variables: -- COPILOT_CLI_EXTRACT_DIR: Override the cache directory (binary placed directly here). +- COPILOT_CLI_EXTRACT_DIR: Override the runtime bundle cache root. - COPILOT_SKIP_CLI_DOWNLOAD: Set to "1" or "true" to disable auto-download. - COPILOT_CLI_DOWNLOAD_BASE_URL: Override the GitHub Releases base URL. """ from __future__ import annotations -import base64 import hashlib import io import os @@ -26,23 +28,24 @@ import tarfile import tempfile import time -import zipfile +from http.client import IncompleteRead from pathlib import Path, PurePosixPath from urllib.error import HTTPError, URLError from urllib.request import urlopen from ._cli_version import ( CLI_VERSION, - get_asset_info, get_checksums_url, + get_cli_binary_name, get_download_url, - get_npm_platform, - get_runtime_lib_packument_url, - get_runtime_lib_url, + get_release_asset_name, + get_runtime_platform, ) _CACHE_DIR_NAME = "github-copilot-sdk" _MAX_RETRIES = 3 +_RETRIABLE_DOWNLOAD_ERRORS = (HTTPError, URLError, IncompleteRead) +_HOSTLESS_ASSETS_MARKER = ".hostless-runtime-assets-v2" def _sanitize_version(version: str) -> str: @@ -55,13 +58,12 @@ def _sanitize_version(version: str) -> str: def get_cache_dir(version: str | None = None) -> Path: - """Return the cache directory for CLI binaries. + """Return the cache directory for runtime bundles. Args: version: CLI version string. If None, returns the root cache dir. """ - # COPILOT_CLI_EXTRACT_DIR overrides the entire version-specific directory - # (binary lives directly at $dir/, no version subdir). Matches Rust SDK. + # COPILOT_CLI_EXTRACT_DIR overrides the entire version-specific directory. extract_override = os.environ.get("COPILOT_CLI_EXTRACT_DIR") if extract_override: return Path(extract_override) @@ -87,7 +89,7 @@ def get_cache_dir(version: str | None = None) -> Path: def get_cached_cli_path(version: str | None = None) -> str | None: - """Return the path to the cached CLI binary if it exists. + """Return the cached compatibility entrypoint for a complete runtime bundle. Args: version: CLI version. Defaults to the pinned CLI_VERSION. @@ -100,12 +102,21 @@ def get_cached_cli_path(version: str | None = None) -> str | None: return None try: - _, binary_name = get_asset_info() + runtime_platform = get_runtime_platform() except RuntimeError: return None - binary_path = get_cache_dir(ver) / binary_name + binary_name = get_cli_binary_name() + wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime" + pair_dir = get_cache_dir(ver) / "prebuilds" / runtime_platform + binary_path = pair_dir / binary_name + required = ( + binary_path, + pair_dir / wrapper_name, + pair_dir / "runtime.node", + pair_dir / _HOSTLESS_ASSETS_MARKER, + ) - if binary_path.exists(): + if all(path.is_file() and path.stat().st_size > 0 for path in required): return str(binary_path) return None @@ -122,30 +133,22 @@ def _fetch_checksums(version: str) -> dict[str, str]: Returns a dict mapping filename → sha256 hex digest. """ url = get_checksums_url(version) - last_exc: Exception | None = None - for attempt in range(_MAX_RETRIES): - try: - with urlopen(url, timeout=30) as response: - text = response.read().decode("utf-8") - break - except (HTTPError, URLError) as exc: - last_exc = exc - if attempt < _MAX_RETRIES - 1: - time.sleep(2**attempt) - else: + try: + text = _fetch_url_bytes(url, timeout=30).decode("utf-8") + except (RuntimeError, UnicodeDecodeError) as exc: raise RuntimeError( - f"Failed to download checksums from {url}: {last_exc}\n\n" + f"Failed to download checksums from {url}: {exc}\n\n" "If you are in an offline or firewalled environment, set " "COPILOT_CLI_PATH to point to a manually-installed binary." - ) from last_exc + ) from exc checksums: dict[str, str] = {} for line in text.strip().splitlines(): parts = line.split() - if len(parts) == 2: + if len(parts) == 2 and re.fullmatch(r"[a-fA-F0-9]{64}", parts[0]): digest, filename = parts # Some formats use *filename (binary mode indicator) - checksums[filename.lstrip("*")] = digest + checksums[filename.lstrip("*")] = digest.lower() return checksums @@ -158,65 +161,36 @@ def _verify_checksum(data: bytes, expected_hash: str, filename: str) -> None: ) -def _extract_tar_gz(data: bytes, binary_name: str, dest_dir: Path) -> Path: - """Extract the CLI binary from a .tar.gz archive.""" - with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tf: - # Find the binary in the archive (may be at top level or in a subdirectory) - members = tf.getnames() - target_member = None - for name in members: - if name == binary_name or name.endswith(f"/{binary_name}"): - target_member = name - break - - if target_member is None: - raise RuntimeError( - f"Binary '{binary_name}' not found in archive. Archive contains: {members}" - ) - - member = tf.getmember(target_member) - f = tf.extractfile(member) - if f is None: - raise RuntimeError(f"Could not extract '{target_member}' from archive") - - dest_path = dest_dir / binary_name - with open(dest_path, "wb") as out: - out.write(f.read()) - - return dest_path - - -def _extract_zip(data: bytes, binary_name: str, dest_dir: Path) -> Path: - """Extract the CLI binary from a .zip archive.""" - with zipfile.ZipFile(io.BytesIO(data)) as zf: - names = zf.namelist() - target_member = None - for name in names: - if name == binary_name or name.endswith(f"/{binary_name}"): - target_member = name - break - - if target_member is None: - raise RuntimeError( - f"Binary '{binary_name}' not found in archive. Archive contains: {names}" - ) +def _fetch_verified_release_package(version: str, runtime_platform: str) -> bytes: + """Download and verify the unified platform release package.""" + asset_name = get_release_asset_name(version, runtime_platform) + expected_hash = _fetch_checksums(version).get(asset_name) + if not expected_hash: + raise RuntimeError(f"SHA256SUMS.txt does not contain {asset_name}.") + url = get_download_url(version, asset_name) + data = _fetch_url_bytes(url, timeout=600) + _verify_checksum(data, expected_hash, asset_name) + return data - dest_path = dest_dir / binary_name - with zf.open(target_member) as src, open(dest_path, "wb") as out: - out.write(src.read()) - return dest_path +def _runtime_bundle_is_complete(pair_dir: Path, wrapper_name: str) -> bool: + required = ( + pair_dir / wrapper_name, + pair_dir / "runtime.node", + pair_dir / _HOSTLESS_ASSETS_MARKER, + ) + return all(path.is_file() and path.stat().st_size > 0 for path in required) def download_cli(version: str | None = None, *, force: bool = False) -> str: - """Download the Copilot CLI binary and cache it. + """Provision a complete runtime bundle with a ``copilot[.exe]`` alias. Args: version: CLI version to download. Defaults to the pinned CLI_VERSION. force: If True, re-download even if already cached. Returns: - Path to the cached binary. + Path to the compatibility entrypoint adjacent to the complete runtime bundle. Raises: RuntimeError: If the version is not set, download fails, or @@ -229,81 +203,33 @@ def download_cli(version: str | None = None, *, force: bool = False) -> str: "set COPILOT_CLI_PATH or install a published wheel." ) - archive_name, binary_name = get_asset_info() - cache_dir = get_cache_dir(ver) - binary_path = cache_dir / binary_name + binary_name = get_cli_binary_name() - # Return cached binary if available (unless force) - if not force and binary_path.exists(): - return str(binary_path) - - # Fetch checksums - checksums = _fetch_checksums(ver) - expected_hash = checksums.get(archive_name) - if not expected_hash: - raise RuntimeError( - f"No checksum found for '{archive_name}' in SHA256SUMS.txt. " - f"Available files: {list(checksums.keys())}" - ) + if not force: + cached = get_cached_cli_path(ver) + if cached is not None: + return cached - # Download archive with retries - url = get_download_url(ver, archive_name) - last_exc: Exception | None = None - data: bytes | None = None - for attempt in range(_MAX_RETRIES): - try: - with urlopen(url, timeout=120) as response: - data = response.read() - break - except (HTTPError, URLError) as exc: - last_exc = exc - if attempt < _MAX_RETRIES - 1: - time.sleep(2**attempt) - if data is None: - raise RuntimeError( - f"Failed to download runtime from {url}: {last_exc}\n\n" - "If you are in an offline or firewalled environment, you can:\n" - f"1. Manually download the archive from: {url}\n" - f"2. Extract the '{binary_name}' binary to: {binary_path}\n" - "Or set COPILOT_CLI_PATH to point to an existing binary." - ) from last_exc - - # Verify checksum - _verify_checksum(data, expected_hash, archive_name) - - # Extract to a temporary directory, then atomically move into place. - # This prevents partial/corrupt cache entries if the process is interrupted. - cache_dir.mkdir(parents=True, exist_ok=True) - staging_dir = Path(tempfile.mkdtemp(dir=cache_dir, prefix=".download-")) + wrapper_path = Path(ensure_runtime_wrapper(ver, force=force)) + binary_path = wrapper_path.with_name(binary_name) + fd, temp_name = tempfile.mkstemp(dir=wrapper_path.parent, prefix=".cli-") try: - if archive_name.endswith(".tar.gz"): - extracted = _extract_tar_gz(data, binary_name, staging_dir) - elif archive_name.endswith(".zip"): - extracted = _extract_zip(data, binary_name, staging_dir) - else: - raise RuntimeError(f"Unknown archive format: {archive_name}") - - # Make executable on Unix + with os.fdopen(fd, "wb") as destination: + destination.write(wrapper_path.read_bytes()) + staged = Path(temp_name) if sys.platform != "win32": - extracted.chmod(extracted.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - - # Atomic rename into final location. Handle concurrent processes: - # another process may have written the file while we were downloading. - try: - extracted.replace(binary_path) - except OSError: - if not force and binary_path.exists(): - return str(binary_path) - raise - finally: - # Clean up staging directory + staged.chmod(staged.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + os.replace(staged, binary_path) + except OSError: try: - staging_dir.rmdir() + os.unlink(temp_name) except OSError: - # May not be empty if rename failed or other files were extracted - import shutil - - shutil.rmtree(staging_dir, ignore_errors=True) + pass + if not force: + cached = get_cached_cli_path(ver) + if cached is not None: + return cached + raise return str(binary_path) @@ -315,78 +241,13 @@ def _fetch_url_bytes(url: str, *, timeout: int) -> bytes: try: with urlopen(url, timeout=timeout) as response: return response.read() - except (HTTPError, URLError) as exc: + except _RETRIABLE_DOWNLOAD_ERRORS as exc: last_exc = exc if attempt < _MAX_RETRIES - 1: time.sleep(2**attempt) raise RuntimeError(f"Failed to download from {url}: {last_exc}") from last_exc -def _fetch_runtime_integrity(npm_platform: str, version: str) -> str | None: - """Return the npm ``dist.integrity`` (Subresource Integrity) for the tarball. - - Best-effort: returns None if the packument can't be fetched or parsed. - """ - import json - - url = get_runtime_lib_packument_url(npm_platform) - try: - raw = _fetch_url_bytes(url, timeout=30) - packument = json.loads(raw) - dist = packument.get("versions", {}).get(version, {}).get("dist", {}) - integrity = dist.get("integrity") - return integrity if isinstance(integrity, str) else None - except (RuntimeError, ValueError, KeyError): - return None - - -def _verify_integrity(data: bytes, integrity: str) -> None: - """Verify data against an npm Subresource Integrity string (e.g. ``sha512-``).""" - algo, _, b64 = integrity.partition("-") - algo = algo.lower() - if algo not in ("sha512", "sha384", "sha256"): - # Fail closed: an unrecognized algorithm means we cannot verify this native - # library, so refuse rather than loading unverified native code. - raise RuntimeError( - f"Unsupported integrity algorithm '{algo}' for the in-process runtime " - "library; refusing to load unverified native code." - ) - expected = base64.b64decode(b64) - actual = hashlib.new(algo, data).digest() - if actual != expected: - raise RuntimeError( - f"Integrity mismatch for runtime library ({algo}): " - "downloaded tarball does not match the npm registry checksum." - ) - - -def _extract_runtime_node(data: bytes, npm_platform: str) -> bytes: - """Extract ``package/prebuilds//runtime.node`` from an npm tarball.""" - target = f"package/prebuilds/{npm_platform}/runtime.node" - with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tf: - for name in tf.getnames(): - if name == target or name.endswith(f"/prebuilds/{npm_platform}/runtime.node"): - member = tf.getmember(name) - extracted = tf.extractfile(member) - if extracted is not None: - return extracted.read() - raise RuntimeError(f"'{target}' not found in runtime package for {npm_platform}.") - - -def _extract_runtime_wrapper(data: bytes, npm_platform: str) -> bytes: - """Extract the SDK out-of-process wrapper from an npm platform tarball.""" - wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime" - target = f"package/prebuilds/{npm_platform}/{wrapper_name}" - with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tf: - for name in tf.getnames(): - if name == target or name.endswith(f"/prebuilds/{npm_platform}/{wrapper_name}"): - member = tf.getmember(name) - extracted = tf.extractfile(member) - if extracted is not None: - return extracted.read() - raise RuntimeError(f"'{target}' not found in runtime package for {npm_platform}.") - - _HOSTLESS_EXCLUDED_TOP_LEVEL = { "app.js", "assets", @@ -410,7 +271,9 @@ def _extract_runtime_wrapper(data: bytes, npm_platform: str) -> bytes: } -def _hostless_runtime_path(member_name: str, npm_platform: str) -> Path | None: +def _hostless_runtime_path(member_name: str, runtime_platform: str) -> Path | None: + if "\\" in member_name: + raise RuntimeError(f"Unsafe runtime package path: {member_name}") parts = PurePosixPath(member_name).parts if not parts or parts[0] != "package" or len(parts) < 2: return None @@ -427,7 +290,7 @@ def _hostless_runtime_path(member_name: str, npm_platform: str) -> Path | None: ): return None if top_level == "prebuilds": - if len(relative) < 3 or relative[1] != npm_platform: + if len(relative) < 3 or relative[1] != runtime_platform: return None relative = relative[2:] destination = Path(*relative) @@ -436,11 +299,11 @@ def _hostless_runtime_path(member_name: str, npm_platform: str) -> Path | None: return destination -def _extract_runtime_bundle(data: bytes, npm_platform: str, destination: Path) -> None: +def _materialize_runtime_bundle(data: bytes, runtime_platform: str, destination: Path) -> None: """Extract the hostless runtime tree, retaining unknown package assets by default.""" with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive: for member in archive: - relative = _hostless_runtime_path(member.name, npm_platform) + relative = _hostless_runtime_path(member.name, runtime_platform) if relative is None or member.isdir(): continue if not member.isfile(): @@ -456,20 +319,20 @@ def _extract_runtime_bundle(data: bytes, npm_platform: str, destination: Path) - def ensure_runtime_wrapper(version: str | None = None, force: bool = False) -> str: - """Provision the runtime pair and its retained npm package assets.""" + """Provision the runtime pair and retained assets from the release package.""" ver = version or CLI_VERSION if not ver: raise RuntimeError("No runtime version is pinned.") - npm_platform = get_npm_platform() + runtime_platform = get_runtime_platform() wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime" - pair_dir = get_cache_dir(ver) / "prebuilds" / npm_platform + pair_dir = get_cache_dir(ver) / "prebuilds" / runtime_platform wrapper_path = pair_dir / wrapper_name runtime_path = pair_dir / "runtime.node" - assets_marker = pair_dir / ".hostless-runtime-assets-v2" + assets_marker = pair_dir / _HOSTLESS_ASSETS_MARKER wrapper_exists = wrapper_path.is_file() and wrapper_path.stat().st_size > 0 runtime_exists = runtime_path.is_file() and runtime_path.stat().st_size > 0 - if wrapper_exists and runtime_exists and assets_marker.is_file() and not force: + if _runtime_bundle_is_complete(pair_dir, wrapper_name) and not force: return str(wrapper_path) if not force and wrapper_exists != runtime_exists: raise RuntimeError( @@ -482,20 +345,13 @@ def ensure_runtime_wrapper(version: str | None = None, force: bool = False) -> s "and automatic downloads are disabled." ) - data = _fetch_url_bytes(get_runtime_lib_url(ver, npm_platform), timeout=600) - integrity = _fetch_runtime_integrity(npm_platform, ver) - if not integrity: - raise RuntimeError( - "No Subresource Integrity value available for the Copilot runtime " - f"package ({npm_platform}@{ver}); refusing to stage unverified native code." - ) - _verify_integrity(data, integrity) + data = _fetch_verified_release_package(ver, runtime_platform) import shutil pair_dir.parent.mkdir(parents=True, exist_ok=True) staging_dir = Path(tempfile.mkdtemp(dir=pair_dir.parent, prefix=".runtime-bundle-")) try: - _extract_runtime_bundle(data, npm_platform, staging_dir) + _materialize_runtime_bundle(data, runtime_platform, staging_dir) staged_wrapper = staging_dir / wrapper_name staged_runtime = staging_dir / "runtime.node" if ( @@ -534,15 +390,15 @@ def ensure_runtime_wrapper(version: str | None = None, force: bool = False) -> s def ensure_runtime_library(cli_path: str, version: str | None = None) -> str | None: """Ensure the native in-process (FFI) runtime library sits next to ``cli_path``. - The library is NOT part of the GitHub Releases CLI archive; it ships in the npm - platform package ``@github/copilot-`` under - ``package/prebuilds//runtime.node``. This helper downloads that tarball - and writes the library next to the CLI binary under its natural platform name - (``libcopilot_runtime.so`` / ``.dylib`` / ``copilot_runtime.dll``). + The canonical staged bundle contains ``prebuilds//runtime.node``. + This helper reuses that verified library and copies it next to the CLI binary + under its natural platform name (``libcopilot_runtime.so`` / ``.dylib`` / + ``copilot_runtime.dll``). - This is opt-in — only invoked when the in-process transport is actually selected - (lazy) or via ``python -m copilot download-runtime --in-process`` (explicit). The - default stdio download path never fetches these extra bytes. + Copying the library next to an external CLI is opt-in — this is only invoked when + the in-process transport is selected (lazy) or via + ``python -m copilot download-runtime --in-process`` (explicit). The default stdio + path leaves the library in the canonical staged bundle. Returns the absolute path to the library, or None if it could not be provisioned (e.g. download disabled or unsupported platform). Raises RuntimeError on @@ -556,15 +412,12 @@ def ensure_runtime_library(cli_path: str, version: str | None = None) -> str | N if existing is not None: return existing - if _should_skip_download(): - return None - ver = version or CLI_VERSION if not ver: return None try: - npm_platform = get_npm_platform() + runtime_platform = get_runtime_platform() except RuntimeError: return None @@ -573,31 +426,22 @@ def ensure_runtime_library(cli_path: str, version: str | None = None) -> str | N if lib_path.exists(): return str(lib_path) - url = get_runtime_lib_url(ver, npm_platform) - data = _fetch_url_bytes(url, timeout=600) - - integrity = _fetch_runtime_integrity(npm_platform, ver) - if not integrity: - # Fail closed: this native library is loaded into the host process, so it must - # be verified before use. The npm packument (which carries dist.integrity) was - # unavailable, so refuse rather than loading unverified native code — mirroring - # the CLI download, which requires a checksum. Retry when the registry is - # reachable, or install a runtime package that ships the library. - raise RuntimeError( - "No Subresource Integrity value available for the in-process runtime " - f"library ({npm_platform}@{ver}); refusing to load unverified native code." - ) - _verify_integrity(data, integrity) - - lib_bytes = _extract_runtime_node(data, npm_platform) + pair_dir = get_cache_dir(ver) / "prebuilds" / runtime_platform + wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime" + if _should_skip_download() and not _runtime_bundle_is_complete(pair_dir, wrapper_name): + return None + wrapper_path = Path(ensure_runtime_wrapper(ver)) + canonical_runtime = wrapper_path.with_name("runtime.node") # Write atomically next to the CLI so concurrent starts don't observe a partial # library. A rename within the same directory is atomic on POSIX and Windows. + import shutil + cli_dir.mkdir(parents=True, exist_ok=True) fd, tmp_name = tempfile.mkstemp(dir=cli_dir, prefix=".runtime-lib-") try: - with os.fdopen(fd, "wb") as out: - out.write(lib_bytes) + with os.fdopen(fd, "wb") as out, canonical_runtime.open("rb") as source: + shutil.copyfileobj(source, out) os.replace(tmp_name, lib_path) except OSError: try: @@ -632,16 +476,18 @@ def get_or_download_cli(version: str | None = None) -> str | None: if cached: return cached - # Check if download is disabled - if _should_skip_download(): - return None - # Check platform support before attempting download try: - get_asset_info() + runtime_platform = get_runtime_platform() except RuntimeError: return None + if _should_skip_download(): + pair_dir = get_cache_dir(ver) / "prebuilds" / runtime_platform + wrapper_name = "copilot-runtime.exe" if sys.platform == "win32" else "copilot-runtime" + if not _runtime_bundle_is_complete(pair_dir, wrapper_name): + return None + # Download return download_cli(ver) diff --git a/python/copilot/_cli_version.py b/python/copilot/_cli_version.py index cb5939820a..4fd0921c2d 100644 --- a/python/copilot/_cli_version.py +++ b/python/copilot/_cli_version.py @@ -16,35 +16,10 @@ # DO NOT reformat this line — the inject script matches it exactly. CLI_VERSION: str | None = None -# Maps (sys.platform, platform.machine()) → (archive filename, binary name inside archive). -PLATFORM_ASSETS: dict[tuple[str, str], tuple[str, str]] = { - ("linux", "x86_64"): ("copilot-linux-x64.tar.gz", "copilot"), - ("linux", "aarch64"): ("copilot-linux-arm64.tar.gz", "copilot"), - ("linux", "arm64"): ("copilot-linux-arm64.tar.gz", "copilot"), - ("darwin", "x86_64"): ("copilot-darwin-x64.tar.gz", "copilot"), - ("darwin", "arm64"): ("copilot-darwin-arm64.tar.gz", "copilot"), - ("win32", "AMD64"): ("copilot-win32-x64.zip", "copilot.exe"), - ("win32", "ARM64"): ("copilot-win32-arm64.zip", "copilot.exe"), -} - -# Musl (Alpine) variants — detected at runtime via _is_musl(). -_MUSL_ASSETS: dict[str, tuple[str, str]] = { - "x86_64": ("copilot-linuxmusl-x64.tar.gz", "copilot"), - "aarch64": ("copilot-linuxmusl-arm64.tar.gz", "copilot"), - "arm64": ("copilot-linuxmusl-arm64.tar.gz", "copilot"), -} - _DOWNLOAD_BASE_URL = "https://github.com/github/copilot-cli/releases/download" -# The native in-process (FFI) runtime library (`runtime.node`) is NOT part of the -# GitHub Releases `copilot-` archive (that ships only the CLI binary). It -# lives in the npm platform package `@github/copilot-`, under -# `package/prebuilds//runtime.node`. Mirrors the .NET SDK targets, -# which download the same npm tarball. -_NPM_REGISTRY_BASE_URL = "https://registry.npmjs.org" - -# Maps (sys.platform, platform.machine()) → npm platform name (glibc Linux/macOS/Windows). -NPM_PLATFORMS: dict[tuple[str, str], str] = { +# Maps (sys.platform, platform.machine()) to the platform segment used by release assets. +RUNTIME_PLATFORMS: dict[tuple[str, str], str] = { ("linux", "x86_64"): "linux-x64", ("linux", "aarch64"): "linux-arm64", ("linux", "arm64"): "linux-arm64", @@ -54,8 +29,8 @@ ("win32", "ARM64"): "win32-arm64", } -# Musl (Alpine) npm platform variants — detected at runtime via _is_musl(). -_MUSL_NPM_PLATFORMS: dict[str, str] = { +# Musl (Alpine) runtime platform variants — detected at runtime via _is_musl(). +_MUSL_RUNTIME_PLATFORMS: dict[str, str] = { "x86_64": "linuxmusl-x64", "aarch64": "linuxmusl-arm64", "arm64": "linuxmusl-arm64", @@ -82,33 +57,11 @@ def get_platform_key() -> tuple[str, str]: return (sys.platform, platform.machine()) -def get_asset_info() -> tuple[str, str]: - """Return (archive_filename, binary_name) for the current platform. - - Raises RuntimeError if the platform is not supported. - """ - key = get_platform_key() - - # On Linux, check for musl/Alpine first - if key[0] == "linux" and _is_musl(): - musl_info = _MUSL_ASSETS.get(key[1]) - if musl_info: - return musl_info - - info = PLATFORM_ASSETS.get(key) - if info is None: - raise RuntimeError( - f"Unsupported platform: {key[0]}/{key[1]}. " - f"Supported platforms: {', '.join(f'{p}/{m}' for p, m in PLATFORM_ASSETS)}" - ) - return info - - def get_download_url(version: str, archive_name: str) -> str: """Return the download URL for a given version and archive.""" import os - base = os.environ.get("COPILOT_CLI_DOWNLOAD_BASE_URL", _DOWNLOAD_BASE_URL) + base = os.environ.get("COPILOT_CLI_DOWNLOAD_BASE_URL", _DOWNLOAD_BASE_URL).rstrip("/") return f"{base}/v{version}/{archive_name}" @@ -116,47 +69,38 @@ def get_checksums_url(version: str) -> str: """Return the URL for the SHA256SUMS.txt file.""" import os - base = os.environ.get("COPILOT_CLI_DOWNLOAD_BASE_URL", _DOWNLOAD_BASE_URL) + base = os.environ.get("COPILOT_CLI_DOWNLOAD_BASE_URL", _DOWNLOAD_BASE_URL).rstrip("/") return f"{base}/v{version}/SHA256SUMS.txt" -def get_npm_platform() -> str: - """Return the npm platform name (e.g. ``linux-x64``) for the current host. +def get_runtime_platform() -> str: + """Return the release asset platform name (e.g. ``linux-x64``) for this host. - Used to locate the native in-process runtime library. Raises RuntimeError if - the platform is not supported. + The name matches the ``prebuilds`` folder embedded in the release package. + Raises RuntimeError if the platform is not supported. """ key = get_platform_key() if key[0] == "linux" and _is_musl(): - musl = _MUSL_NPM_PLATFORMS.get(key[1]) + musl = _MUSL_RUNTIME_PLATFORMS.get(key[1]) if musl: return musl - npm_platform = NPM_PLATFORMS.get(key) - if npm_platform is None: + runtime_platform = RUNTIME_PLATFORMS.get(key) + if runtime_platform is None: raise RuntimeError( - f"Unsupported platform for in-process runtime: {key[0]}/{key[1]}. " - f"Supported platforms: {', '.join(f'{p}/{m}' for p, m in NPM_PLATFORMS)}" + f"Unsupported Copilot runtime platform: {key[0]}/{key[1]}. " + f"Supported platforms: {', '.join(f'{p}/{m}' for p, m in RUNTIME_PLATFORMS)}" ) - return npm_platform + return runtime_platform -def get_runtime_lib_packument_url(npm_platform: str) -> str: - """Return the npm packument URL for the platform runtime package.""" - import os +def get_release_asset_name(version: str, runtime_platform: str | None = None) -> str: + """Return the unified runtime package asset name for a version and platform.""" + platform_name = runtime_platform or get_runtime_platform() + return f"github-copilot-{version}-{platform_name}.tgz" - base = os.environ.get("COPILOT_NPM_REGISTRY_URL", _NPM_REGISTRY_BASE_URL).rstrip("/") - return f"{base}/@github/copilot-{npm_platform}" - - -def get_runtime_lib_url(version: str, npm_platform: str) -> str: - """Return the download URL for the platform runtime tarball. - - Mirrors the .NET targets' URL layout - ``/@github/copilot-/-/copilot--.tgz``. - """ - import os - base = os.environ.get("COPILOT_NPM_REGISTRY_URL", _NPM_REGISTRY_BASE_URL).rstrip("/") - return f"{base}/@github/copilot-{npm_platform}/-/copilot-{npm_platform}-{version}.tgz" +def get_cli_binary_name() -> str: + """Return the CLI executable name inside the release package.""" + return "copilot.exe" if sys.platform == "win32" else "copilot" diff --git a/python/copilot/_ffi_runtime_host.py b/python/copilot/_ffi_runtime_host.py index 98aa776600..511cbae9c4 100644 --- a/python/copilot/_ffi_runtime_host.py +++ b/python/copilot/_ffi_runtime_host.py @@ -121,7 +121,8 @@ def resolve_library_path(runtime_entrypoint: str) -> str | None: 1. The natural platform library name next to the CLI (bundled/flat layout, what the Python download-at-first-use path writes). - 2. ``prebuilds//runtime.node`` next to the CLI (dev/package layout). + 2. ``runtime.node`` next to the CLI (prepared release-package layout). + 3. ``prebuilds//runtime.node`` next to the CLI (package-root layout). Returns the absolute path, or ``None`` when neither exists. """ @@ -131,6 +132,10 @@ def resolve_library_path(runtime_entrypoint: str) -> str | None: if flat.is_file(): return str(flat) + adjacent_prebuilt = directory / "runtime.node" + if adjacent_prebuilt.is_file(): + return str(adjacent_prebuilt) + folder = get_prebuilds_folder() if folder is not None: prebuilt = directory / "prebuilds" / folder / "runtime.node" @@ -206,7 +211,7 @@ def _load_library(library_path: str) -> _FfiLibrary: return _FfiLibrary(_loaded_library) # Load with immediate binding (RTLD_NOW) on POSIX, matching the .NET/Rust - # hosts. The runtime cdylib from the npm platform package is self-contained; + # hosts. The runtime cdylib from the platform release package is self-contained; # eager binding surfaces any load problem here rather than at first call. if sys.platform == "win32": lib = ctypes.WinDLL(library_path) diff --git a/python/copilot/_jsonrpc.py b/python/copilot/_jsonrpc.py index 6427a8007f..11baf0bd10 100644 --- a/python/copilot/_jsonrpc.py +++ b/python/copilot/_jsonrpc.py @@ -296,29 +296,31 @@ def _read_loop(self): def _fail_pending_requests(self): """Fail all pending requests when process exits""" + error_msg = self._get_process_exit_error() + + # Fail all pending requests + with self._pending_lock: + for request_id, future in list(self.pending_requests.items()): + if not future.done(): + exc = ProcessExitedError(error_msg) + loop = future.get_loop() + loop.call_soon_threadsafe(future.set_exception, exc) + + def _get_process_exit_error(self) -> str: + """Build an error message after the process and stderr readers finish.""" if self._stderr_thread and self._stderr_thread is not threading.current_thread(): self._stderr_thread.join(timeout=1.0) - # Build error message with stderr output stderr_output = self.get_stderr_output() return_code = None if hasattr(self.process, "poll"): return_code = self.process.poll() if stderr_output: - error_msg = f"CLI process exited with code {return_code}\nstderr: {stderr_output}" - elif return_code is not None: - error_msg = f"CLI process exited with code {return_code}" - else: - error_msg = "CLI process exited unexpectedly" - - # Fail all pending requests - with self._pending_lock: - for request_id, future in list(self.pending_requests.items()): - if not future.done(): - exc = ProcessExitedError(error_msg) - loop = future.get_loop() - loop.call_soon_threadsafe(future.set_exception, exc) + return f"CLI process exited with code {return_code}\nstderr: {stderr_output}" + if return_code is not None: + return f"CLI process exited with code {return_code}" + return "CLI process exited unexpectedly" def _read_exact(self, num_bytes: int) -> bytes: """ diff --git a/python/copilot/client.py b/python/copilot/client.py index e50e5fa307..dd531a2be9 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -16,6 +16,7 @@ import asyncio import inspect +import ipaddress import logging import os import re @@ -91,6 +92,7 @@ ) from .session import ( AutoModeSwitchHandler, + AutoTier, BearerTokenProvider, CommandDefinition, ContextTier, @@ -266,6 +268,20 @@ def _exp_assignment_response_to_dict( class CapiSessionOptions(TypedDict, total=False): """Provider-scoped Copilot API (CAPI) session options.""" + auto_tier: AutoTier + """Routing preference used when the session model is ``auto``. + + Requires a runtime with Auto tier support and V2 Auto routing. When omitted + on create, the runtime uses its default routing behavior. The runtime persists + this preference across cold resume; when omitted on cold resume, it restores + the last committed preference. On resident resume, a different tier requests a + safe switch that takes effect after resume succeeds and never disturbs a turn + that is already running. + + To change the preference on a live session, call + :meth:`CopilotSession.set_auto_tier` instead. + """ + enable_web_socket_responses: bool """Whether to use WebSocket transport for the CAPI Responses API. @@ -292,6 +308,8 @@ def _cloud_session_options_to_dict(options: CloudSessionOptions) -> dict[str, An def _capi_session_options_to_wire(options: CapiSessionOptions) -> dict[str, Any]: wire: dict[str, Any] = {} + if "auto_tier" in options: + wire["autoTier"] = options["auto_tier"] if "enable_web_socket_responses" in options: wire["enableWebSocketResponses"] = options["enable_web_socket_responses"] return wire @@ -491,6 +509,46 @@ class TelemetryConfig(TypedDict, total=False): """Whether to capture message content. Sets OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT.""" # noqa: E501 +class ClientInfo(TypedDict, total=False): + """Identity of the integrating application, declared on ``server.connect``. + + Declaring it lets the telemetry the runtime emits on this connection be + attributed to a single, consistent surface (the application and its Copilot + integration) instead of the runtime's own build. All fields are optional; + omit any of them (or the whole object) to keep the default attribution. + """ + + application_name: str + """Name of the application using the SDK, e.g. ``"acme-developer-portal"``.""" + application_version: str + """Version of the application using the SDK, e.g. ``"2.4.0"``.""" + integration_name: str + """Optional name of an application integration, such as an extension or plugin.""" + integration_version: str + """Optional version of the integration named by ``integration_name``.""" + + +def _client_info_to_wire(client_info: ClientInfo | None) -> dict[str, str] | None: + """Map a snake_case :class:`ClientInfo` onto the camelCase connect wire shape. + + Empty fields are dropped. Returns ``None`` when no field carries a non-empty + value so the caller omits the ``clientInfo`` field entirely and keeps the + runtime's default attribution. + """ + if not client_info: + return None + wire: dict[str, str] = {} + if client_info.get("application_name"): + wire["editorName"] = client_info["application_name"] + if client_info.get("application_version"): + wire["editorVersion"] = client_info["application_version"] + if client_info.get("integration_name"): + wire["extensionName"] = client_info["integration_name"] + if client_info.get("integration_version"): + wire["extensionVersion"] = client_info["integration_version"] + return wire or None + + @dataclass class RuntimeConnection: """Discriminated config describing how to reach the Copilot runtime. @@ -760,6 +818,7 @@ class _CopilotClientOptions: request_handler: CopilotRequestHandler | None = None session_idle_timeout_seconds: int | None = None enable_remote_sessions: bool = False + client_info: ClientInfo | None = None on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None on_github_telemetry: Callable[[GitHubTelemetryNotification], None | Awaitable[None]] | None = ( None @@ -1514,6 +1573,7 @@ def __init__( request_handler: CopilotRequestHandler | None = None, session_idle_timeout_seconds: int | None = None, enable_remote_sessions: bool = False, + client_info: ClientInfo | None = None, on_list_models: Callable[[], list[ModelInfo] | Awaitable[list[ModelInfo]]] | None = None, on_github_telemetry: Callable[[GitHubTelemetryNotification], None | Awaitable[None]] | None = None, @@ -1563,6 +1623,11 @@ def __init__( Control integration). When ``True``, sessions in a GitHub repository working directory are accessible from GitHub web and mobile. + client_info: Identity of the integrating application, forwarded to the + runtime on the ``server.connect`` handshake. Declaring it lets + the telemetry the runtime emits on this connection be attributed + to a consistent surface instead of the runtime's own build. All + fields are optional; omit it to keep the default attribution. on_list_models: Custom handler for :meth:`list_models`. When provided, the handler is called instead of querying the runtime server. @@ -1600,6 +1665,7 @@ def __init__( request_handler=request_handler, session_idle_timeout_seconds=session_idle_timeout_seconds, enable_remote_sessions=enable_remote_sessions, + client_info=client_info, on_list_models=on_list_models, on_github_telemetry=on_github_telemetry, mode=mode, @@ -1774,8 +1840,8 @@ def _parse_cli_url(self, url: str) -> tuple[str, int]: """ Parse CLI URL into host and port. - Supports formats: "host:port", "http://host:port", "https://host:port", - or just "port". + Supports formats: "host:port", "[ipv6]:port", "http://host:port", + "https://host:port", or just "port". Args: url: The CLI URL to parse. @@ -1786,9 +1852,6 @@ def _parse_cli_url(self, url: str) -> tuple[str, int]: Raises: ValueError: If the URL format is invalid or the port is out of range. """ - import re - - # Remove protocol if present clean_url = re.sub(r"^https?://", "", url) # Check if it's just a port number @@ -1798,14 +1861,24 @@ def _parse_cli_url(self, url: str) -> tuple[str, int]: raise ValueError(f"Invalid port in cli_url: {url}") return ("localhost", port) - # Parse host:port format - parts = clean_url.split(":") - if len(parts) != 2: - raise ValueError(f"Invalid cli_url format: {url}") + ipv6_match = re.match(r"^\[([^\]]+)\]:(.*)$", clean_url) + if ipv6_match: + host = ipv6_match.group(1) + port_text = ipv6_match.group(2) + try: + ipaddress.IPv6Address(host) + except ValueError as e: + raise ValueError(f"Invalid cli_url format: {url}") from e + else: + # Parse host:port format + parts = clean_url.split(":") + if len(parts) != 2: + raise ValueError(f"Invalid cli_url format: {url}") + host = parts[0] if parts[0] else "localhost" + port_text = parts[1] - host = parts[0] if parts[0] else "localhost" try: - port = int(parts[1]) + port = int(port_text) except ValueError as e: raise ValueError(f"Invalid port in cli_url: {url}") from e @@ -1948,13 +2021,14 @@ async def start(self) -> None: # Check if process exited and capture any remaining stderr process = self._cli_process if self._cli_process is not None else self._process if process and hasattr(process, "poll"): + if isinstance(e, BrokenPipeError) and process.poll() is None: + try: + await asyncio.to_thread(process.wait, timeout=1.0) + except subprocess.TimeoutExpired: + pass return_code = process.poll() if return_code is not None and self._client: - stderr_output = self._client.get_stderr_output() - if stderr_output: - raise RuntimeError( - f"CLI process exited with code {return_code}\nstderr: {stderr_output}" - ) from e + raise RuntimeError(self._client._get_process_exit_error()) from e raise async def stop(self) -> None: @@ -2118,7 +2192,10 @@ async def force_stop(self) -> None: """ # Clear sessions immediately without trying to destroy them with self._sessions_lock: + sessions = list(self._sessions.values()) self._sessions.clear() + for session in sessions: + session._mark_disconnected() with self._github_token_providers_lock: self._github_token_providers.clear() @@ -2298,7 +2375,9 @@ async def create_session( hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. provider: Provider configuration for Azure or custom endpoints. - capi: CAPI provider-scoped options. WebSocket transport is the + capi: CAPI provider-scoped options. Set ``auto_tier`` to ``efficiency``, + ``balance``, or ``intelligence`` to select an Auto routing preference + on a runtime with Auto tier support. WebSocket transport is the default for the CAPI Responses API whenever the model advertises the ``ws:/responses`` endpoint. Set ``enable_web_socket_responses=False`` to force the HTTP @@ -3076,7 +3155,10 @@ async def resume_session( hooks: Lifecycle hooks for the session. working_directory: Working directory for the session. provider: Provider configuration for Azure or custom endpoints. - capi: CAPI provider-scoped options. WebSocket transport is the + capi: CAPI provider-scoped options. Omit ``auto_tier`` to preserve the + current or persisted Auto routing preference. An explicit tier + overrides it on cold resume, but cannot change it on an + already-resident session. WebSocket transport is the default for the CAPI Responses API whenever the model advertises the ``ws:/responses`` endpoint. Set ``enable_web_socket_responses=False`` to force the HTTP @@ -4024,7 +4106,9 @@ async def _verify_protocol_version(self) -> None: server_version: int | None try: - connect_params: dict[str, Any] = {} + connect_params: dict[str, Any] = { + "supportedTaskKinds": ["agent", "client", "shell"], + } if self._effective_connection_token is not None: connect_params["token"] = self._effective_connection_token # Opt in to GitHub telemetry forwarding at the connection level when a @@ -4033,6 +4117,12 @@ async def _verify_protocol_version(self) -> None: # event is forwarded). Also sent on session.create/resume for older CLIs. if self._on_github_telemetry is not None: connect_params["enableGitHubTelemetryForwarding"] = True + # Declare the integrating application's identity so the runtime attributes + # the telemetry it emits on this connection to a consistent surface + # instead of its own build. Omitted when the app didn't supply it. + client_info = _client_info_to_wire(self._options.client_info) + if client_info is not None: + connect_params["clientInfo"] = client_info connect_result = _ConnectResult.from_dict( await self._client.request("connect", connect_params) ) @@ -4555,22 +4645,22 @@ async def _connect_via_tcp(self) -> None: if not self._runtime_port: raise RuntimeError("Server port not available") - # Create a TCP socket connection with timeout + # Create a TCP socket connection with timeout. create_connection resolves + # both IPv4 and IPv6 addresses instead of forcing AF_INET. import socket # Connection timeout constant TCP_CONNECTION_TIMEOUT = 10 # seconds - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.settimeout(TCP_CONNECTION_TIMEOUT) - try: tcp_connect_start = time.perf_counter() logger.info( "CopilotClient._connect_via_tcp connecting to CLI server", extra={"host": self._actual_host, "port": self._runtime_port}, ) - sock.connect((self._actual_host, self._runtime_port)) + sock = socket.create_connection( + (self._actual_host, self._runtime_port), timeout=TCP_CONNECTION_TIMEOUT + ) sock.settimeout(None) # Remove timeout after connection log_timing( logger, @@ -4717,7 +4807,10 @@ async def _apply_post_create_options_patch( try: await session.disconnect() except BaseException: - pass + logger.debug( + "Error disconnecting session after options update failure", + exc_info=True, + ) raise async def _set_session_fs_provider(self) -> None: @@ -4770,8 +4863,22 @@ def _register_github_token_provider( def _handle_connection_close(self) -> None: self._state = "disconnected" + with self._sessions_lock: + sessions = list(self._sessions.values()) with self._github_token_providers_lock: self._github_token_providers.clear() + client = self._client + loop = client._loop if client is not None else None + if loop is not None and not loop.is_closed(): + + def cancel_pending_external_tools() -> None: + for session in sessions: + session._cancel_pending_external_tools() + + try: + loop.call_soon_threadsafe(cancel_pending_external_tools) + except RuntimeError: + logger.debug("Event loop closed while handling connection loss") def _assign_github_token_provider(self, registration_id: str | None, session_id: str) -> None: if registration_id is None: diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 1340d726be..ac4cc5441d 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -6,7 +6,7 @@ from typing import ClassVar, TYPE_CHECKING -from .session_events import AbortReason, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity +from .session_events import AbortReason, AgentModelPolicy, Attachment, AutoTier, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, ModelChangeSource, PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, RemediationAction, SessionEvent, SessionLimitsConfig, SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity if TYPE_CHECKING: from .._jsonrpc import JsonRpcClient @@ -743,6 +743,48 @@ def to_dict(self) -> dict: result["githubMessage"] = from_union([from_str, from_none], self.github_message) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AutopilotObjectiveCreditLimit: + """Current per-window credit limit and consumption for an autopilot objective. + + Current per-window consumption and optional cap, when a credit-tracking window is present. + """ + credits_used: float + """Window consumption in fractional AI credits, for display.""" + + credits_used_nano_aiu: str + """Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string.""" + + credits: float | None = None + """Configured AI-credit cap, when one is set.""" + + @staticmethod + def from_dict(obj: Any) -> 'AutopilotObjectiveCreditLimit': + assert isinstance(obj, dict) + credits_used = from_float(obj.get("creditsUsed")) + credits_used_nano_aiu = from_str(obj.get("creditsUsedNanoAiu")) + credits = from_union([from_float, from_none], obj.get("credits")) + return AutopilotObjectiveCreditLimit(credits_used, credits_used_nano_aiu, credits) + + def to_dict(self) -> dict: + result: dict = {} + result["creditsUsed"] = to_float(self.credits_used) + result["creditsUsedNanoAiu"] = from_str(self.credits_used_nano_aiu) + if self.credits is not None: + result["credits"] = from_union([to_float, from_none], self.credits) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class AutopilotObjectiveStatus(Enum): + """Current normalized lifecycle status. + + Current normalized autopilot objective lifecycle status. + """ + ACTIVE = "active" + COMPLETED = "completed" + PAUSED = "paused" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class BuiltInModelCatalogEntry: @@ -1052,9 +1094,12 @@ class CapiSessionOptions: """Options scoped to the built-in CAPI (Copilot API) provider.""" auto_tier: AutoTier | None = None - """Routing preference used when the session model is `auto`. The runtime persists the - preference across cold resume. When omitted, the default routing behavior is used. - Resuming an already-resident session cannot change its preference. + """Routing preference for sessions whose model is `auto`. On create or cold resume, this + establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold + resume, the runtime restores the last committed preference. On resident resume, a + different value requests a safe switch after resume succeeds and cannot change an + in-flight turn. Successful switches are persisted for later cold resume. When no + preference is supplied or restored, CAPI default routing is used. """ enable_web_socket_responses: bool | None = None """Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when @@ -1331,8 +1376,11 @@ class CatalogNetworkFailureReason(Enum): DNS = "dns" HTTP_STATUS = "http-status" OFFLINE = "offline" + PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required" + RATE_LIMITED = "rate-limited" REDIRECT_REJECTED = "redirect-rejected" RESPONSE_TOO_LARGE = "response-too-large" + SERVICE_UNAVAILABLE = "service-unavailable" TIMEOUT = "timeout" TLS = "tls" @@ -1427,12 +1475,15 @@ class CatalogSearchResultReason(Enum): NO_CREDENTIAL = "no-credential" OFFLINE = "offline" PLANNING_UNAVAILABLE = "planning-unavailable" + PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required" PROXY_REJECTED = "proxy-rejected" + RATE_LIMITED = "rate-limited" REDIRECT_REJECTED = "redirect-rejected" REDIRECT_TO_BLOCKED_ADDRESS = "redirect-to-blocked-address" RESPONSE_TOO_LARGE = "response-too-large" SCHEMA_VIOLATION = "schema-violation" SEARCH_UNAVAILABLE = "search-unavailable" + SERVICE_UNAVAILABLE = "service-unavailable" SIZE_LIMIT_EXCEEDED = "size-limit-exceeded" TIMEOUT = "timeout" TLS = "tls" @@ -1491,6 +1542,34 @@ class CatalogUnsafeRetrievalReason(Enum): class CatalogUnsupportedKindErrorKind(Enum): UNSUPPORTED_KIND = "unsupported-kind" +# Experimental: this type is part of an experimental API and may change or be removed. +class ClientTaskCancelReason(Enum): + """Why the runtime requests client-task cancellation. + + Reason the runtime requests cancellation + """ + CANCEL_REQUESTED = "cancel_requested" + SESSION_SHUTDOWN = "session_shutdown" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ClientTaskCancelResult: + """Whether the client authoritatively confirmed its external work stopped.""" + + cancelled: bool + """True only when the owner confirms that external work stopped before responding""" + + @staticmethod + def from_dict(obj: Any) -> 'ClientTaskCancelResult': + assert isinstance(obj, dict) + cancelled = from_bool(obj.get("cancelled")) + return ClientTaskCancelResult(cancelled) + + def to_dict(self) -> dict: + result: dict = {} + result["cancelled"] = from_bool(self.cancelled) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandInputChoice: @@ -1826,34 +1905,14 @@ def to_dict(self) -> dict: return result # Experimental: this type is part of an experimental API and may change or be removed. -# Internal: this type is an internal SDK API and is not part of the public surface. -@dataclass -class _ConnectResult: - """Handshake result reporting the server's protocol version and package version on success.""" +class TaskKind(Enum): + """Closed set of public task kinds a connection can negotiate. - ok: bool - """Always true on success""" - - protocol_version: int - """Server protocol version number""" - - version: str - """Server package version""" - - @staticmethod - def from_dict(obj: Any) -> '_ConnectResult': - assert isinstance(obj, dict) - ok = from_bool(obj.get("ok")) - protocol_version = from_int(obj.get("protocolVersion")) - version = from_str(obj.get("version")) - return _ConnectResult(ok, protocol_version, version) - - def to_dict(self) -> dict: - result: dict = {} - result["ok"] = from_bool(self.ok) - result["protocolVersion"] = from_int(self.protocol_version) - result["version"] = from_str(self.version) - return result + Discriminator for a client-owned task. + """ + AGENT = "agent" + CLIENT = "client" + SHELL = "shell" # Experimental: this type is part of an experimental API and may change or be removed. class ConnectedRemoteSessionMetadataKind(Enum): @@ -2260,9 +2319,20 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class CurrentModel: - """The currently selected model, reasoning effort, and context tier for the session. The - context tier reflects `Session.getContextTier()`, restored from the session journal on - resume. + """The session's authoritative model snapshot. Auto preference fields are configuration for + the virtual `auto` model and do not change the selected model identifier. The context + tier reflects `Session.getContextTier()`, restored from the session journal on resume. + + Authoritative model and Auto preference state after an immediate switch. For deferred + switches this remains the current state until the queued change drains. + """ + activating_auto_tier: AutoTier | None = None + """Auto preference currently claimed by an in-progress activation. Null means the activation + is returning to provider-default routing. + """ + auto_tier: AutoTier | None = None + """Auto preference currently committed for the session. This can remain available while + another model is selected so a later switch to `auto` can reuse it. """ context_tier: ContextTier | None = None """Context tier for models that support multiple context-window sizes.""" @@ -2270,6 +2340,10 @@ class CurrentModel: model_id: str | None = None """Currently active model identifier""" + pending_auto_tier: AutoTier | None = None + """Latest unclaimed Auto preference waiting for a future user turn. Null means the pending + request is returning to provider-default routing. + """ reasoning_effort: str | None = None """Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the @@ -2279,17 +2353,26 @@ class CurrentModel: @staticmethod def from_dict(obj: Any) -> 'CurrentModel': assert isinstance(obj, dict) + activating_auto_tier = from_union([AutoTier, from_none], obj.get("activatingAutoTier")) + auto_tier = from_union([AutoTier, from_none], obj.get("autoTier")) context_tier = from_union([ContextTier, from_none], obj.get("contextTier")) model_id = from_union([from_str, from_none], obj.get("modelId")) + pending_auto_tier = from_union([AutoTier, from_none], obj.get("pendingAutoTier")) reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) - return CurrentModel(context_tier, model_id, reasoning_effort) + return CurrentModel(activating_auto_tier, auto_tier, context_tier, model_id, pending_auto_tier, reasoning_effort) def to_dict(self) -> dict: result: dict = {} + if self.activating_auto_tier is not None: + result["activatingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.activating_auto_tier) + if self.auto_tier is not None: + result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier) if self.context_tier is not None: result["contextTier"] = from_union([lambda x: to_enum(ContextTier, x), from_none], self.context_tier) if self.model_id is not None: result["modelId"] = from_union([from_str, from_none], self.model_id) + if self.pending_auto_tier is not None: + result["pendingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.pending_auto_tier) if self.reasoning_effort is not None: result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) return result @@ -2492,6 +2575,42 @@ def to_dict(self) -> dict: result["ids"] = from_list(from_str, self.ids) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class HookType(Enum): + """Hook event that invokes this action. + + Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally + support callback-only events. + """ + AGENT_STOP = "agentStop" + ERROR_OCCURRED = "errorOccurred" + NOTIFICATION = "notification" + PERMISSION_REQUEST = "permissionRequest" + POST_RESULT = "postResult" + POST_TOOL_USE = "postToolUse" + POST_TOOL_USE_FAILURE = "postToolUseFailure" + PRE_COMPACT = "preCompact" + PRE_MCP_TOOL_CALL = "preMcpToolCall" + PRE_PR_DESCRIPTION = "prePRDescription" + PRE_TOOL_USE = "preToolUse" + SESSION_END = "sessionEnd" + SESSION_START = "sessionStart" + SUBAGENT_START = "subagentStart" + SUBAGENT_STOP = "subagentStop" + USER_PROMPT_SUBMITTED = "userPromptSubmitted" + USER_PROMPT_TRANSFORMED = "userPromptTransformed" + +# Experimental: this type is part of an experimental API and may change or be removed. +class HookOrigin(Enum): + """Configuration tier that contributed this hook action. + + Configuration tier that contributed a discovered hook action. + """ + PLUGIN = "plugin" + POLICY = "policy" + REPOSITORY = "repository" + USER = "user" + # Experimental: this type is part of an experimental API and may change or be removed. class DiscoveredMCPServerType(Enum): """Server transport type: stdio, http, sse (deprecated), or memory""" @@ -2579,6 +2698,9 @@ class EventsReadDirection(Enum): cursor toward newer events; 'backward' returns the newest window first (tail-first) and pages toward older events. Events within a returned batch are always chronological (oldest-to-newest), even for a backward read. + + Direction to page through persisted history. Forward starts at the beginning; backward + starts with the newest events. Events in each page remain chronological. """ BACKWARD = "backward" FORWARD = "forward" @@ -3355,6 +3477,7 @@ class FactoryRunFailureType(Enum): FACTORY_ACCOUNTING_INCOMPLETE = "factory_accounting_incomplete" FACTORY_DURABLE_FAILURE = "factory_durable_failure" FACTORY_LIMIT_REACHED = "factory_limit_reached" + FACTORY_PROVIDER_DISCONNECTED = "factory_provider_disconnected" FACTORY_RESUME_DECLINED = "factory_resume_declined" # Experimental: this type is part of an experimental API and may change or be removed. @@ -4002,28 +4125,6 @@ def to_dict(self) -> dict: class HMACAuthInfoType(Enum): HMAC = "hmac" -# Internal: this type is an internal SDK API and is not part of the public surface. -class _HookType(Enum): - """Hook event name dispatched through the SDK callback transport.""" - - AGENT_STOP = "agentStop" - ERROR_OCCURRED = "errorOccurred" - NOTIFICATION = "notification" - PERMISSION_REQUEST = "permissionRequest" - POST_RESULT = "postResult" - POST_TOOL_USE = "postToolUse" - POST_TOOL_USE_FAILURE = "postToolUseFailure" - PRE_COMPACT = "preCompact" - PRE_MCP_TOOL_CALL = "preMcpToolCall" - PRE_PR_DESCRIPTION = "prePRDescription" - PRE_TOOL_USE = "preToolUse" - SESSION_END = "sessionEnd" - SESSION_START = "sessionStart" - SUBAGENT_START = "subagentStart" - SUBAGENT_STOP = "subagentStop" - USER_PROMPT_SUBMITTED = "userPromptSubmitted" - USER_PROMPT_TRANSFORMED = "userPromptTransformed" - # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass class _HookInvokeResponse: @@ -4043,6 +4144,38 @@ def to_dict(self) -> dict: result["output"] = self.output return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HooksDiscoverRequest: + """Optional project paths and host-exclusion behavior for server-scoped hook discovery.""" + + exclude_host_hooks: bool | None = None + """When true, omit host-owned user and plugin hook rows and their diagnostics. + Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks + still contribute to each remaining row's effective enabled state. This filters sources + rather than simulating a host with no settings. + """ + project_paths: list[str] | None = None + """Optional project directory paths whose trusted repository and project-expanded plugin + hooks should be discovered. When omitted or empty, user, managed-policy, and globally + enabled installed or explicit plugin hooks are returned without project expansion. + """ + + @staticmethod + def from_dict(obj: Any) -> 'HooksDiscoverRequest': + assert isinstance(obj, dict) + exclude_host_hooks = from_union([from_bool, from_none], obj.get("excludeHostHooks")) + project_paths = from_union([lambda x: from_list(from_str, x), from_none], obj.get("projectPaths")) + return HooksDiscoverRequest(exclude_host_hooks, project_paths) + + def to_dict(self) -> dict: + result: dict = {} + if self.exclude_host_hooks is not None: + result["excludeHostHooks"] = from_union([from_bool, from_none], self.exclude_host_hooks) + if self.project_paths is not None: + result["projectPaths"] = from_union([lambda x: from_list(from_str, x), from_none], self.project_paths) + return result + class InstalledPluginSourceURLSource(Enum): GITHUB = "github" LOCAL = "local" @@ -5015,15 +5148,21 @@ class MCPConfigRemoveRequest: name: str """Name of the MCP server to remove""" + auth_client_id_metadata_url: str | None = None + """OAuth Client ID Metadata Document URL whose persisted credentials should also be removed.""" + @staticmethod def from_dict(obj: Any) -> 'MCPConfigRemoveRequest': assert isinstance(obj, dict) name = from_str(obj.get("name")) - return MCPConfigRemoveRequest(name) + auth_client_id_metadata_url = from_union([from_str, from_none], obj.get("authClientIdMetadataUrl")) + return MCPConfigRemoveRequest(name, auth_client_id_metadata_url) def to_dict(self) -> dict: result: dict = {} result["name"] = from_str(self.name) + if self.auth_client_id_metadata_url is not None: + result["authClientIdMetadataUrl"] = from_union([from_str, from_none], self.auth_client_id_metadata_url) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -5736,7 +5875,9 @@ class MCPPlanInstallResultReason(Enum): OFFLINE = "offline" PLANNING_UNAVAILABLE = "planning-unavailable" POLICY_FORBIDS = "policy-forbids" + PROXY_AUTHENTICATION_REQUIRED = "proxy-authentication-required" PROXY_REJECTED = "proxy-rejected" + RATE_LIMITED = "rate-limited" REDIRECT_REJECTED = "redirect-rejected" REDIRECT_TO_BLOCKED_ADDRESS = "redirect-to-blocked-address" REMOTE_ENUMERATION_UNAVAILABLE = "remote-enumeration-unavailable" @@ -5744,6 +5885,7 @@ class MCPPlanInstallResultReason(Enum): RESPONSE_TOO_LARGE = "response-too-large" SCHEMA_VIOLATION = "schema-violation" SEARCH_UNAVAILABLE = "search-unavailable" + SERVICE_UNAVAILABLE = "service-unavailable" SIZE_LIMIT_EXCEEDED = "size-limit-exceeded" STALE = "stale" TIMEOUT = "timeout" @@ -6596,6 +6738,13 @@ class ModelBillingPromo: """Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. """ + show_banner: bool | None = None + """Whether the service asked hosts to give this promotion a prominent surface, such as a + dedicated banner, in addition to listing it with the model. `true` requests that surface + and `false` asks for the model list only. Absent means the service expressed no + preference — for example a response that predates the field — so hosts should apply their + own default rather than read it as `false`. + """ @staticmethod def from_dict(obj: Any) -> 'ModelBillingPromo': @@ -6604,7 +6753,8 @@ def from_dict(obj: Any) -> 'ModelBillingPromo': ends_at = from_union([from_str, from_none], obj.get("endsAt")) id = from_union([from_str, from_none], obj.get("id")) message = from_union([from_str, from_none], obj.get("message")) - return ModelBillingPromo(discount_percent, ends_at, id, message) + show_banner = from_union([from_bool, from_none], obj.get("showBanner")) + return ModelBillingPromo(discount_percent, ends_at, id, message, show_banner) def to_dict(self) -> dict: result: dict = {} @@ -6616,6 +6766,8 @@ def to_dict(self) -> dict: result["id"] = from_union([from_str, from_none], self.id) if self.message is not None: result["message"] = from_union([from_str, from_none], self.message) + if self.show_banner is not None: + result["showBanner"] = from_union([from_bool, from_none], self.show_banner) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -6856,6 +7008,45 @@ def to_dict(self) -> dict: result["reasoningEffort"] = from_str(self.reasoning_effort) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelSwitchAutoTierRequest: + """An Auto preference request for the session. This updates Auto configuration only; it does + not change the selected model to `auto`. + """ + auto_tier: AutoTier | None = None + """Auto preference to activate when a future user turn using the `auto` model safely mints a + replacement model and token pair. Pass null to return to provider-default Auto routing. + """ + source: ModelChangeSource | None = None + """Origin to record on the effective `session.model_change` event. Defaults to `sdk` when + omitted. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelSwitchAutoTierRequest': + assert isinstance(obj, dict) + auto_tier = from_union([AutoTier, from_none], obj.get("autoTier")) + source = from_union([ModelChangeSource, from_none], obj.get("source")) + return ModelSwitchAutoTierRequest(auto_tier, source) + + def to_dict(self) -> dict: + result: dict = {} + result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier) + if self.source is not None: + result["source"] = from_union([lambda x: to_enum(ModelChangeSource, x), from_none], self.source) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +class ModelSwitchAutoTierStatus(Enum): + """Immediate request status. `pending` means accepted but not committed. + + Whether the requested preference was already effective or was accepted for later + transactional activation. + """ + PENDING = "pending" + UNCHANGED = "unchanged" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ModelsListRequest: @@ -8153,6 +8344,15 @@ def to_dict(self) -> dict: result["version"] = from_union([from_str, from_none], self.version) return result +# Experimental: this type is part of an experimental API and may change or be removed. +class PluginInstallStagingMode(Enum): + """Where the completed plugin tree was staged before atomic promotion + + Where completed plugin content was staged before atomic promotion. + """ + DESTINATION_SIBLING = "destination_sibling" + EXTERNAL = "external" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class PluginUpdateResult: @@ -9588,22 +9788,25 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SandboxConfigUserPolicyNetworkProxy: - """HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and - cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. - Credentials go in the separate `username`/`password` fields. A credential-free http:// - loopback proxy URL is routed through the localhost proxy automatically; an https:// or - authenticated loopback URL is used as-is. + """HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, + requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is + accepted and routed through the IPv4 gateway), and does not support proxy credentials. + macOS relies on applications honoring proxy environment variables. Windows also + configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's + networking stack. Configure supported credentials in the separate `username` and + `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, + while an https:// or authenticated loopback URL uses the URL form. HTTP proxy configuration for sandboxed traffic. """ url: str """Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the - scheme's standard port when omitted. Credentials must not be embedded here — a - `user:pass@` authority is rejected; put them in the separate `username`/`password` - fields. A credential-free http:// loopback URL is routed through the localhost proxy - automatically; loopback covers localhost and any *.localhost subdomain, the whole - 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or - one with a username/password set, is used as-is. + scheme's standard port when omitted; an explicit port must be between 1 and 65535. + Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in + the separate `username`/`password` fields. A credential-free http:// loopback proxy URL + is routed through the localhost proxy automatically; loopback covers localhost and any + *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback + (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. """ password: str | None = None """Optional password for proxy authentication, combined with the URL at spawn time. The @@ -12653,6 +12856,114 @@ class SkillDiscoveryScope(Enum): PERSONAL_COPILOT = "personal-copilot" PROJECT = "project" +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillProviderDescriptor: + """Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched + separately and lazily. + """ + description: str + """Description used in skill catalogs without fetching content.""" + + name: str + """Invocation and display name.""" + + argument_hint: str | None = None + """Optional freeform argument hint used by slash-command catalogs.""" + + disable_model_invocation: bool | None = None + """Whether model invocation is disabled. Defaults to false.""" + + user_invocable: bool | None = None + """Whether users may invoke the skill directly. Defaults to true.""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillProviderDescriptor': + assert isinstance(obj, dict) + description = from_str(obj.get("description")) + name = from_str(obj.get("name")) + argument_hint = from_union([from_str, from_none], obj.get("argumentHint")) + disable_model_invocation = from_union([from_bool, from_none], obj.get("disableModelInvocation")) + user_invocable = from_union([from_bool, from_none], obj.get("userInvocable")) + return SkillProviderDescriptor(description, name, argument_hint, disable_model_invocation, user_invocable) + + def to_dict(self) -> dict: + result: dict = {} + result["description"] = from_str(self.description) + result["name"] = from_str(self.name) + if self.argument_hint is not None: + result["argumentHint"] = from_union([from_str, from_none], self.argument_hint) + if self.disable_model_invocation is not None: + result["disableModelInvocation"] = from_union([from_bool, from_none], self.disable_model_invocation) + if self.user_invocable is not None: + result["userInvocable"] = from_union([from_bool, from_none], self.user_invocable) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SkillProviderListRequest: + """Identifies the target session.""" + + session_id: str + """Target session identifier""" + + @staticmethod + def from_dict(obj: Any) -> 'SkillProviderListRequest': + assert isinstance(obj, dict) + session_id = from_str(obj.get("sessionId")) + return SkillProviderListRequest(session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["sessionId"] = from_str(self.session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _SkillProviderReadRequest: + """Identifies one SDK-provided skill by invocation name.""" + + name: str + """Invocation name of the skill to read.""" + + session_id: str + """Target session identifier""" + + @staticmethod + def from_dict(obj: Any) -> '_SkillProviderReadRequest': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + session_id = from_str(obj.get("sessionId")) + return _SkillProviderReadRequest(name, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + result["sessionId"] = from_str(self.session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _SkillProviderReadResult: + """Complete text-only SKILL.md content returned by an SDK session's skill provider. Related + files and assets are not supported. + """ + markdown: str + """Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit.""" + + @staticmethod + def from_dict(obj: Any) -> '_SkillProviderReadResult': + assert isinstance(obj, dict) + markdown = from_str(obj.get("markdown")) + return _SkillProviderReadResult(markdown) + + def to_dict(self) -> dict: + result: dict = {} + result["markdown"] = from_str(self.markdown) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SkillsConfigSetSkillDisabledRequest: @@ -12813,6 +13124,11 @@ class SlashCommandTimelineEntry: type: str """Timeline entry presentation type.""" + remediation: RemediationAction | None = None + """What the user must do to recover, when the entry reports a failure the runtime knows an + action for. The `text` never names a client affordance, so a client that offers one + renders it from this value. + """ url: str | None = None """Optional URL associated with the timeline entry.""" @@ -12821,13 +13137,16 @@ def from_dict(obj: Any) -> 'SlashCommandTimelineEntry': assert isinstance(obj, dict) text = from_str(obj.get("text")) type = from_str(obj.get("type")) + remediation = from_union([RemediationAction, from_none], obj.get("remediation")) url = from_union([from_str, from_none], obj.get("url")) - return SlashCommandTimelineEntry(text, type, url) + return SlashCommandTimelineEntry(text, type, remediation, url) def to_dict(self) -> dict: result: dict = {} result["text"] = from_str(self.text) result["type"] = from_str(self.type) + if self.remediation is not None: + result["remediation"] = from_union([lambda x: to_enum(RemediationAction, x), from_none], self.remediation) if self.url is not None: result["url"] = from_union([from_str, from_none], self.url) return result @@ -12907,8 +13226,12 @@ class SubagentSettingsEntryContextTier(Enum): # Experimental: this type is part of an experimental API and may change or be removed. class TaskExecutionMode(Enum): - """Whether task execution is synchronously awaited or managed in the background""" + """Whether task execution is synchronously awaited or managed in the background + Client-owned tasks always execute outside the runtime in background mode. + + Execution mode, which is always background for client-owned tasks + """ BACKGROUND = "background" SYNC = "sync" @@ -12949,6 +13272,70 @@ def to_dict(self) -> dict: result["timestamp"] = self.timestamp.isoformat() return result +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientActiveStatus(Enum): + """Active status a client owner may publish with a progress update. + + Optional active status transition + """ + IDLE = "idle" + RUNNING = "running" + +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientExecutionMode(Enum): + """Client-owned tasks always execute outside the runtime in background mode. + + Execution mode, which is always background for client-owned tasks + """ + BACKGROUND = "background" + +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientOwnerKind(Enum): + """Class of the task owner + + Connection class owning a client task. + """ + EXTENSION = "extension" + SDK = "sdk" + +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientOwnerPresence(Enum): + """Whether this task's bound join is currently connected + + Presence of the task's bound join. + """ + CONNECTED = "connected" + DISCONNECTED = "disconnected" + +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientStatus(Enum): + """Client task lifecycle status + + Lifecycle status of a client-owned task. + + Current client task lifecycle status + + Current lifecycle status of the task + """ + CANCELLED = "cancelled" + COMPLETED = "completed" + FAILED = "failed" + IDLE = "idle" + ORPHANED = "orphaned" + RUNNING = "running" + +# Experimental: this type is part of an experimental API and may change or be removed. +class TaskClientType(Enum): + """Discriminator for a client-owned task.""" + + CLIENT = "client" + +class TaskClientUpdateKind(Enum): + CANCELLED = "cancelled" + COMPLETED = "completed" + FAILED = "failed" + PROGRESS = "progress" + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TaskCompleteData: @@ -13060,10 +13447,6 @@ class TaskShellInfoAttachmentMode(Enum): ATTACHED = "attached" DETACHED = "detached" -class TaskInfoType(Enum): - AGENT = "agent" - SHELL = "shell" - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TaskList: @@ -14810,6 +15193,64 @@ def to_dict(self) -> dict: result["field"] = from_union([lambda x: to_enum(AgentRegistrySpawnValidationErrorField, x), from_none], self.field) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AutopilotObjectiveState: + """Public, persistence-independent projection of an autopilot objective.""" + + credit_count_nano_aiu: str + """Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a + decimal string. + """ + id: int + """Session-local objective identifier.""" + + objective: str + """User-provided objective text.""" + + status: AutopilotObjectiveStatus + """Current normalized lifecycle status.""" + + turn_count: int + """Number of objective turns started.""" + + completion_summary: str | None = None + """Optional summary recorded when the objective completed.""" + + credit_limit: AutopilotObjectiveCreditLimit | None = None + """Current per-window consumption and optional cap, when a credit-tracking window is present.""" + + pause_reason: str | None = None + """Optional reason the objective is paused.""" + + @staticmethod + def from_dict(obj: Any) -> 'AutopilotObjectiveState': + assert isinstance(obj, dict) + credit_count_nano_aiu = from_str(obj.get("creditCountNanoAiu")) + id = from_int(obj.get("id")) + objective = from_str(obj.get("objective")) + status = AutopilotObjectiveStatus(obj.get("status")) + turn_count = from_int(obj.get("turnCount")) + completion_summary = from_union([from_str, from_none], obj.get("completionSummary")) + credit_limit = from_union([AutopilotObjectiveCreditLimit.from_dict, from_none], obj.get("creditLimit")) + pause_reason = from_union([from_str, from_none], obj.get("pauseReason")) + return AutopilotObjectiveState(credit_count_nano_aiu, id, objective, status, turn_count, completion_summary, credit_limit, pause_reason) + + def to_dict(self) -> dict: + result: dict = {} + result["creditCountNanoAiu"] = from_str(self.credit_count_nano_aiu) + result["id"] = from_int(self.id) + result["objective"] = from_str(self.objective) + result["status"] = to_enum(AutopilotObjectiveStatus, self.status) + result["turnCount"] = from_int(self.turn_count) + if self.completion_summary is not None: + result["completionSummary"] = from_union([from_str, from_none], self.completion_summary) + if self.credit_limit is not None: + result["creditLimit"] = from_union([lambda x: to_class(AutopilotObjectiveCreditLimit, x), from_none], self.credit_limit) + if self.pause_reason is not None: + result["pauseReason"] = from_union([from_str, from_none], self.pause_reason) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class BuiltInModelCatalog: @@ -15112,8 +15553,9 @@ class CatalogSearchRequest: """Protocol version and capabilities the caller requires.""" query: str - """Free-text search query. Never written to logs or telemetry.""" - + """Free-text search query. Persisted as tool input for session continuity, but omitted from + telemetry. + """ kinds: list[CatalogCandidateKind] | None = None """Restrict results to these candidate kinds. When omitted, every kind the runtime supports is searched. @@ -15402,6 +15844,11 @@ class CatalogNetworkFailureError: reason: CatalogNetworkFailureReason """Categorised failure, low cardinality so it can be aggregated without carrying a URL.""" + retry_after_seconds: int | None = None + """Bounded cooldown in seconds before another catalog request should be attempted, when the + authority supplied a numeric Retry-After value or the runtime applied its documented + fallback. + """ status_code: int | None = None """HTTP status code, when the failure was a rejected response.""" @@ -15410,14 +15857,17 @@ def from_dict(obj: Any) -> 'CatalogNetworkFailureError': assert isinstance(obj, dict) message = from_str(obj.get("message")) reason = CatalogNetworkFailureReason(obj.get("reason")) + retry_after_seconds = from_union([from_int, from_none], obj.get("retryAfterSeconds")) status_code = from_union([from_int, from_none], obj.get("statusCode")) - return CatalogNetworkFailureError(message, reason, status_code) + return CatalogNetworkFailureError(message, reason, retry_after_seconds, status_code) def to_dict(self) -> dict: result: dict = {} result["kind"] = self.kind result["message"] = from_str(self.message) result["reason"] = to_enum(CatalogNetworkFailureReason, self.reason) + if self.retry_after_seconds is not None: + result["retryAfterSeconds"] = from_union([from_int, from_none], self.retry_after_seconds) if self.status_code is not None: result["statusCode"] = from_union([from_int, from_none], self.status_code) return result @@ -15606,6 +16056,45 @@ def to_dict(self) -> dict: result["supportedKinds"] = from_list(lambda x: to_enum(CatalogCandidateKind, x), self.supported_kinds) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ClientTaskCancelRequest: + """Runtime-to-owner cancellation request for a client-owned task.""" + + cancellation_id: str + """Opaque identifier shared by coalesced cancellation callers""" + + client_task_id: str + """Owner-scoped task key included for correlation""" + + id: str + """Canonical runtime-generated task identifier""" + + reason: ClientTaskCancelReason + """Reason the runtime requests cancellation""" + + session_id: str + """Session that owns the client task""" + + @staticmethod + def from_dict(obj: Any) -> 'ClientTaskCancelRequest': + assert isinstance(obj, dict) + cancellation_id = from_str(obj.get("cancellationId")) + client_task_id = from_str(obj.get("clientTaskId")) + id = from_str(obj.get("id")) + reason = ClientTaskCancelReason(obj.get("reason")) + session_id = from_str(obj.get("sessionId")) + return ClientTaskCancelRequest(cancellation_id, client_task_id, id, reason, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["cancellationId"] = from_str(self.cancellation_id) + result["clientTaskId"] = from_str(self.client_task_id) + result["id"] = from_str(self.id) + result["reason"] = to_enum(ClientTaskCancelReason, self.reason) + result["sessionId"] = from_str(self.session_id) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandInput: @@ -15759,6 +16248,10 @@ class _ConnectRequest: using the process-global gate for ordinary events and an explicit session-scoped decision for host-only events. """ + supported_task_kinds: list[TaskKind] | None = None + """Task kinds this connection can decode when observing session tasks. Omit to retain agent + and shell compatibility. + """ token: str | None = None """Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN""" @@ -15767,8 +16260,9 @@ def from_dict(obj: Any) -> '_ConnectRequest': assert isinstance(obj, dict) client_info = from_union([_ConnectClientInfo.from_dict, from_none], obj.get("clientInfo")) enable_git_hub_telemetry_forwarding = from_union([from_bool, from_none], obj.get("enableGitHubTelemetryForwarding")) + supported_task_kinds = from_union([lambda x: from_list(TaskKind, x), from_none], obj.get("supportedTaskKinds")) token = from_union([from_str, from_none], obj.get("token")) - return _ConnectRequest(client_info, enable_git_hub_telemetry_forwarding, token) + return _ConnectRequest(client_info, enable_git_hub_telemetry_forwarding, supported_task_kinds, token) def to_dict(self) -> dict: result: dict = {} @@ -15776,10 +16270,48 @@ def to_dict(self) -> dict: result["clientInfo"] = from_union([lambda x: to_class(_ConnectClientInfo, x), from_none], self.client_info) if self.enable_git_hub_telemetry_forwarding is not None: result["enableGitHubTelemetryForwarding"] = from_union([from_bool, from_none], self.enable_git_hub_telemetry_forwarding) + if self.supported_task_kinds is not None: + result["supportedTaskKinds"] = from_union([lambda x: from_list(lambda x: to_enum(TaskKind, x), x), from_none], self.supported_task_kinds) if self.token is not None: result["token"] = from_union([from_str, from_none], self.token) return result +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _ConnectResult: + """Handshake result reporting the server's protocol version and package version on success.""" + + ok: bool + """Always true on success""" + + protocol_version: int + """Server protocol version number""" + + version: str + """Server package version""" + + task_kinds: list[TaskKind] | None = None + """Task kinds the server may return to this connection.""" + + @staticmethod + def from_dict(obj: Any) -> '_ConnectResult': + assert isinstance(obj, dict) + ok = from_bool(obj.get("ok")) + protocol_version = from_int(obj.get("protocolVersion")) + version = from_str(obj.get("version")) + task_kinds = from_union([lambda x: from_list(TaskKind, x), from_none], obj.get("taskKinds")) + return _ConnectResult(ok, protocol_version, version, task_kinds) + + def to_dict(self) -> dict: + result: dict = {} + result["ok"] = from_bool(self.ok) + result["protocolVersion"] = from_int(self.protocol_version) + result["version"] = from_str(self.version) + if self.task_kinds is not None: + result["taskKinds"] = from_union([lambda x: from_list(lambda x: to_enum(TaskKind, x), x), from_none], self.task_kinds) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ConnectedRemoteSessionMetadata: @@ -16298,6 +16830,132 @@ def to_dict(self) -> dict: result["plugin"] = from_union([lambda x: to_class(DiscoveredExtensionPlugin, x), from_none], self.plugin) return result +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _HookInvokeRequest: + """Runtime-owned wire payload for a server-to-client hook callback invocation.""" + + hook_type: HookType + input: Any + session_id: str + + @staticmethod + def from_dict(obj: Any) -> '_HookInvokeRequest': + assert isinstance(obj, dict) + hook_type = HookType(obj.get("hookType")) + input = obj.get("input") + session_id = from_str(obj.get("sessionId")) + return _HookInvokeRequest(hook_type, input, session_id) + + def to_dict(self) -> dict: + result: dict = {} + result["hookType"] = to_enum(HookType, self.hook_type) + result["input"] = self.input + result["sessionId"] = from_str(self.session_id) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class DiscoveredHook: + """One server-discovered hook action from user, repository, plugin, or managed-policy + configuration. + """ + enabled: bool + """Whether this action is enabled under the server-side discovery settings. Concrete + sessions may differ because they can add session-specific directories, plugins, or trust. + False when its disable key is present in the user's disabled-hooks setting or disable-all + settings suppress the action. + """ + hook_type: HookType + """Hook event that invokes this action.""" + + id: str + """Deterministic identifier for this server-discovered action row. It remains stable while + the project, origin, source, event, action content, and duplicate ordinal are unchanged. + This is row identity, not the key persisted in disabledHooks. + """ + origin: HookOrigin + """Configuration tier that contributed this hook action.""" + + disable_key: str | None = None + """Durable content hash used by hook enablement. Identical actions may intentionally share + this key. Omitted when changing the user's disabled-hooks setting cannot change the + action's current server-discovered state, including managed-policy hooks, session-start + prompt actions, actions suppressed by disable-all settings, and projectless plugin + actions that require project-directory expansion. + """ + project_path: str | None = None + """Input project path for which this server-side action was resolved. Set on every row + returned for project-scoped discovery, including repeated user and policy actions. + """ + source: str | None = None + """Human-readable source label, such as a hook file path, settings source, or plugin name.""" + + @staticmethod + def from_dict(obj: Any) -> 'DiscoveredHook': + assert isinstance(obj, dict) + enabled = from_bool(obj.get("enabled")) + hook_type = HookType(obj.get("hookType")) + id = from_str(obj.get("id")) + origin = HookOrigin(obj.get("origin")) + disable_key = from_union([from_str, from_none], obj.get("disableKey")) + project_path = from_union([from_str, from_none], obj.get("projectPath")) + source = from_union([from_str, from_none], obj.get("source")) + return DiscoveredHook(enabled, hook_type, id, origin, disable_key, project_path, source) + + def to_dict(self) -> dict: + result: dict = {} + result["enabled"] = from_bool(self.enabled) + result["hookType"] = to_enum(HookType, self.hook_type) + result["id"] = from_str(self.id) + result["origin"] = to_enum(HookOrigin, self.origin) + if self.disable_key is not None: + result["disableKey"] = from_union([from_str, from_none], self.disable_key) + if self.project_path is not None: + result["projectPath"] = from_union([from_str, from_none], self.project_path) + if self.source is not None: + result["source"] = from_union([from_str, from_none], self.source) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionsReadPersistedEventsRequest: + """Pagination options for reading an inactive or active local session's persisted event + journal. + """ + session_id: str + """Session ID whose persisted event journal should be read.""" + + cursor: str | None = None + """Opaque cursor returned by a previous persisted-event read. Omit on the first call.""" + + direction: EventsReadDirection | None = None + """Direction to page through persisted history. Forward starts at the beginning; backward + starts with the newest events. Events in each page remain chronological. + """ + max: int | None = None + """Maximum number of events to return in this batch (1–1000, default 200).""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionsReadPersistedEventsRequest': + assert isinstance(obj, dict) + session_id = from_str(obj.get("sessionId")) + cursor = from_union([from_str, from_none], obj.get("cursor")) + direction = from_union([EventsReadDirection, from_none], obj.get("direction")) + max = from_union([from_int, from_none], obj.get("max")) + return SessionsReadPersistedEventsRequest(session_id, cursor, direction, max) + + def to_dict(self) -> dict: + result: dict = {} + result["sessionId"] = from_str(self.session_id) + if self.cursor is not None: + result["cursor"] = from_union([from_str, from_none], self.cursor) + if self.direction is not None: + result["direction"] = from_union([lambda x: to_enum(EventsReadDirection, x), from_none], self.direction) + if self.max is not None: + result["max"] = from_union([from_int, from_none], self.max) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class EventLogReadRequest: @@ -16969,9 +17627,12 @@ class FactoryRunFailure: Machine-readable factory run failure. - Machine-readable failure details for an errored run. + Machine-readable failure details for a halted or errored run. The run stopped because its usage accounting could not be completed. + + The extension that owns the factory disconnected while the run was executing, so the host + halted it. The run's journaled subagent results are preserved so a resume can reuse them. """ run_id: str """Factory run identifier. @@ -17573,30 +18234,6 @@ def to_dict(self) -> dict: result["mode"] = to_enum(HistoryRewindMode, self.mode) return result -# Internal: this type is an internal SDK API and is not part of the public surface. -@dataclass -class _HookInvokeRequest: - """Runtime-owned wire payload for a server-to-client hook callback invocation.""" - - hook_type: _HookType - input: Any - session_id: str - - @staticmethod - def from_dict(obj: Any) -> '_HookInvokeRequest': - assert isinstance(obj, dict) - hook_type = _HookType(obj.get("hookType")) - input = obj.get("input") - session_id = from_str(obj.get("sessionId")) - return _HookInvokeRequest(hook_type, input, session_id) - - def to_dict(self) -> dict: - result: dict = {} - result["hookType"] = to_enum(_HookType, self.hook_type) - result["input"] = self.input - result["sessionId"] = from_str(self.session_id) - return result - # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class InstalledPluginSource: @@ -19651,6 +20288,10 @@ class ModelSwitchToResult: model_id: str | None = None """Currently active model identifier after the switch""" + model_state: CurrentModel | None = None + """Authoritative model and Auto preference state after an immediate switch. For deferred + switches this remains the current state until the queued change drains. + """ persistence_error: str | None = None """Persistence failure encountered after applying the model switch.""" @@ -19668,10 +20309,11 @@ def from_dict(obj: Any) -> 'ModelSwitchToResult': deprecation_warnings = from_union([lambda x: from_list(from_str, x), from_none], obj.get("deprecationWarnings")) message = from_union([from_str, from_none], obj.get("message")) model_id = from_union([from_str, from_none], obj.get("modelId")) + model_state = from_union([CurrentModel.from_dict, from_none], obj.get("modelState")) persistence_error = from_union([from_str, from_none], obj.get("persistenceError")) status = from_union([from_str, from_none], obj.get("status")) warning = from_union([from_str, from_none], obj.get("warning")) - return ModelSwitchToResult(confirmation, deferred, deprecation_warnings, message, model_id, persistence_error, status, warning) + return ModelSwitchToResult(confirmation, deferred, deprecation_warnings, message, model_id, model_state, persistence_error, status, warning) def to_dict(self) -> dict: result: dict = {} @@ -19685,6 +20327,8 @@ def to_dict(self) -> dict: result["message"] = from_union([from_str, from_none], self.message) if self.model_id is not None: result["modelId"] = from_union([from_str, from_none], self.model_id) + if self.model_state is not None: + result["modelState"] = from_union([lambda x: to_class(CurrentModel, x), from_none], self.model_state) if self.persistence_error is not None: result["persistenceError"] = from_union([from_str, from_none], self.persistence_error) if self.status is not None: @@ -19894,6 +20538,53 @@ def to_dict(self) -> dict: result["vision"] = from_union([lambda x: to_class(ModelCapabilitiesOverrideLimitsVision, x), from_none], self.vision) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class ModelSwitchAutoTierResult: + """Immediate acknowledgement and Auto preference snapshot after a switch request. This + result never implies that a pending preference committed. + """ + status: ModelSwitchAutoTierStatus + """Immediate request status. `pending` means accepted but not committed.""" + + activating_auto_tier: AutoTier | None = None + """Auto preference currently claimed by an in-progress activation. Null means the activation + is returning to provider-default routing. + """ + effective_auto_tier: AutoTier | None = None + """Auto preference currently committed for the session.""" + + pending_auto_tier: AutoTier | None = None + """Latest unclaimed Auto preference waiting for a future user turn.""" + + superseded_auto_tier: AutoTier | None = None + """Earlier unclaimed preference replaced by this request. This can be present with either + status, including when selecting the effective preference cancels pending work. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelSwitchAutoTierResult': + assert isinstance(obj, dict) + status = ModelSwitchAutoTierStatus(obj.get("status")) + activating_auto_tier = from_union([AutoTier, from_none], obj.get("activatingAutoTier")) + effective_auto_tier = from_union([AutoTier, from_none], obj.get("effectiveAutoTier")) + pending_auto_tier = from_union([AutoTier, from_none], obj.get("pendingAutoTier")) + superseded_auto_tier = from_union([AutoTier, from_none], obj.get("supersededAutoTier")) + return ModelSwitchAutoTierResult(status, activating_auto_tier, effective_auto_tier, pending_auto_tier, superseded_auto_tier) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(ModelSwitchAutoTierStatus, self.status) + if self.activating_auto_tier is not None: + result["activatingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.activating_auto_tier) + if self.effective_auto_tier is not None: + result["effectiveAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.effective_auto_tier) + if self.pending_auto_tier is not None: + result["pendingAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.pending_auto_tier) + if self.superseded_auto_tier is not None: + result["supersededAutoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.superseded_auto_tier) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class NamedProviderConfig: @@ -21376,6 +22067,11 @@ class MCPServer: error: str | None = None """Error message if the server failed to connect""" + server_metadata: McpServerMetadata | None = None + """Server-advertised metadata for a connected server. Omitted when no live connection + metadata is available, including while pending or when failed, disabled, stopped, or not + configured. + """ source: McpServerSource | None = None """Configuration source: user, workspace, plugin, or builtin""" @@ -21391,10 +22087,11 @@ def from_dict(obj: Any) -> 'MCPServer': name = from_str(obj.get("name")) status = McpServerStatus(obj.get("status")) error = from_union([from_str, from_none], obj.get("error")) + server_metadata = from_union([McpServerMetadata.from_dict, from_none], obj.get("serverMetadata")) source = from_union([McpServerSource, from_none], obj.get("source")) source_plugin = from_union([from_str, from_none], obj.get("sourcePlugin")) source_plugin_version = from_union([from_str, from_none], obj.get("sourcePluginVersion")) - return MCPServer(name, status, error, source, source_plugin, source_plugin_version) + return MCPServer(name, status, error, server_metadata, source, source_plugin, source_plugin_version) def to_dict(self) -> dict: result: dict = {} @@ -21402,6 +22099,8 @@ def to_dict(self) -> dict: result["status"] = to_enum(McpServerStatus, self.status) if self.error is not None: result["error"] = from_union([from_str, from_none], self.error) + if self.server_metadata is not None: + result["serverMetadata"] = from_union([lambda x: to_class(McpServerMetadata, x), from_none], self.server_metadata) if self.source is not None: result["source"] = from_union([lambda x: to_enum(McpServerSource, x), from_none], self.source) if self.source_plugin is not None: @@ -22353,6 +23052,11 @@ class QueuePendingItems: kind: QueuePendingItemsKind """Whether this item is a queued user message or a queued slash command / model change""" + message_id: str | None = None + """Stable identity of the queued user message. Present for message rows and absent for slash + commands and model changes. + """ + @staticmethod def from_dict(obj: Any) -> 'QueuePendingItems': assert isinstance(obj, dict) @@ -22360,7 +23064,8 @@ def from_dict(obj: Any) -> 'QueuePendingItems': display_text = from_str(obj.get("displayText")) id = from_str(obj.get("id")) kind = QueuePendingItemsKind(obj.get("kind")) - return QueuePendingItems(agent_mode, display_text, id, kind) + message_id = from_union([from_str, from_none], obj.get("messageId")) + return QueuePendingItems(agent_mode, display_text, id, kind, message_id) def to_dict(self) -> dict: result: dict = {} @@ -22368,6 +23073,8 @@ def to_dict(self) -> dict: result["displayText"] = from_str(self.display_text) result["id"] = from_str(self.id) result["kind"] = to_enum(QueuePendingItemsKind, self.kind) + if self.message_id is not None: + result["messageId"] = from_union([from_str, from_none], self.message_id) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -22630,11 +23337,14 @@ class SandboxConfigUserPolicyNetwork: """Whether outbound network traffic is allowed at all.""" proxy: SandboxConfigUserPolicyNetworkProxy | None = None - """HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and - cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. - Credentials go in the separate `username`/`password` fields. A credential-free http:// - loopback proxy URL is routed through the localhost proxy automatically; an https:// or - authenticated loopback URL is used as-is. + """HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, + requires that endpoint to be reachable over IPv4 (the [::] dual-stack wildcard is + accepted and routed through the IPv4 gateway), and does not support proxy credentials. + macOS relies on applications honoring proxy environment variables. Windows also + configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's + networking stack. Configure supported credentials in the separate `username` and + `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, + while an https:// or authenticated loopback URL uses the URL form. """ @staticmethod @@ -23437,7 +24147,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AgentInfo: - """Agent metadata, including identifiers, display details, source, tools, model, MCP + """Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. The newly selected custom agent @@ -23464,6 +24174,13 @@ class AgentInfo: """Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. """ + model_policy: AgentModelPolicy | None = None + """Whether authored models are preferences or required constraints.""" + + models: list[str] | None = None + """Authored preferred model ids for this agent, in priority order. Runtime model selection + chooses the first available model; omitted means no authored preference. + """ path: str | None = None """Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. @@ -23495,13 +24212,15 @@ def from_dict(obj: Any) -> 'AgentInfo': name = from_str(obj.get("name")) mcp_servers = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("mcpServers")) model = from_union([from_str, from_none], obj.get("model")) + model_policy = from_union([AgentModelPolicy, from_none], obj.get("modelPolicy")) + models = from_union([lambda x: from_list(from_str, x), from_none], obj.get("models")) path = from_union([from_str, from_none], obj.get("path")) prompt = from_union([from_str, from_none], obj.get("prompt")) skills = from_union([lambda x: from_list(from_str, x), from_none], obj.get("skills")) source = from_union([AgentInfoSource, from_none], obj.get("source")) tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("tools")) user_invocable = from_union([from_bool, from_none], obj.get("userInvocable")) - return AgentInfo(description, display_name, id, name, mcp_servers, model, path, prompt, skills, source, tools, user_invocable) + return AgentInfo(description, display_name, id, name, mcp_servers, model, model_policy, models, path, prompt, skills, source, tools, user_invocable) def to_dict(self) -> dict: result: dict = {} @@ -23513,6 +24232,10 @@ def to_dict(self) -> dict: result["mcpServers"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.mcp_servers) if self.model is not None: result["model"] = from_union([from_str, from_none], self.model) + if self.model_policy is not None: + result["modelPolicy"] = from_union([lambda x: to_enum(AgentModelPolicy, x), from_none], self.model_policy) + if self.models is not None: + result["models"] = from_union([lambda x: from_list(from_str, x), from_none], self.models) if self.path is not None: result["path"] = from_union([from_str, from_none], self.path) if self.prompt is not None: @@ -23580,11 +24303,15 @@ class SkillsInvokedSkill: """Unique identifier for the skill""" path: str - """Path to the SKILL.md file""" - + """Path to the SKILL.md file, or an empty string for an SDK-provided skill without a + filesystem identity + """ allowed_tools: list[str] | None = None """Tools that should be auto-approved when this skill is active, captured at invocation time""" + disable_model_invocation: bool | None = None + """Whether model invocation was disabled when this skill was invoked""" + @staticmethod def from_dict(obj: Any) -> 'SkillsInvokedSkill': assert isinstance(obj, dict) @@ -23593,7 +24320,8 @@ def from_dict(obj: Any) -> 'SkillsInvokedSkill': name = from_str(obj.get("name")) path = from_str(obj.get("path")) allowed_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("allowedTools")) - return SkillsInvokedSkill(content, invoked_at_turn, name, path, allowed_tools) + disable_model_invocation = from_union([from_bool, from_none], obj.get("disableModelInvocation")) + return SkillsInvokedSkill(content, invoked_at_turn, name, path, allowed_tools, disable_model_invocation) def to_dict(self) -> dict: result: dict = {} @@ -23603,6 +24331,8 @@ def to_dict(self) -> dict: result["path"] = from_str(self.path) if self.allowed_tools is not None: result["allowedTools"] = from_union([lambda x: from_list(from_str, x), from_none], self.allowed_tools) + if self.disable_model_invocation is not None: + result["disableModelInvocation"] = from_union([from_bool, from_none], self.disable_model_invocation) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -23643,6 +24373,29 @@ def to_dict(self) -> dict: result["projectPath"] = from_union([from_str, from_none], self.project_path) return result +# Experimental: this type is part of an experimental API and may change or be removed. +# Internal: this type is an internal SDK API and is not part of the public surface. +@dataclass +class _SkillProviderListResult: + """Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to + 1024 descriptors and 1 MiB of aggregate metadata. + """ + skills: list[SkillProviderDescriptor] + """Skill descriptors in provider order. Invocation names must be unique under + case-insensitive comparison. + """ + + @staticmethod + def from_dict(obj: Any) -> '_SkillProviderListResult': + assert isinstance(obj, dict) + skills = from_list(SkillProviderDescriptor.from_dict, obj.get("skills")) + return _SkillProviderListResult(skills) + + def to_dict(self) -> dict: + result: dict = {} + result["skills"] = from_list(lambda x: to_class(SkillProviderDescriptor, x), self.skills) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SlashCommandAddTimelineEntryResult: @@ -23737,6 +24490,9 @@ class SlashCommandCompletedResult: message: str | None = None """Optional user-facing message describing the completed command""" + mode: SessionMode | None = None + """Optional target session mode applied without submitting an agent prompt""" + runtime_settings_changed: bool | None = None """True when the invocation mutated user runtime settings; consumers caching settings should refresh @@ -23746,14 +24502,17 @@ class SlashCommandCompletedResult: def from_dict(obj: Any) -> 'SlashCommandCompletedResult': assert isinstance(obj, dict) message = from_union([from_str, from_none], obj.get("message")) + mode = from_union([SessionMode, from_none], obj.get("mode")) runtime_settings_changed = from_union([from_bool, from_none], obj.get("runtimeSettingsChanged")) - return SlashCommandCompletedResult(message, runtime_settings_changed) + return SlashCommandCompletedResult(message, mode, runtime_settings_changed) def to_dict(self) -> dict: result: dict = {} result["kind"] = self.kind if self.message is not None: result["message"] = from_union([from_str, from_none], self.message) + if self.mode is not None: + result["mode"] = from_union([lambda x: to_enum(SessionMode, x), from_none], self.mode) if self.runtime_settings_changed is not None: result["runtimeSettingsChanged"] = from_union([from_bool, from_none], self.runtime_settings_changed) return result @@ -23864,23 +24623,100 @@ def to_dict(self) -> dict: result["latestIntent"] = from_union([from_str, from_none], self.latest_intent) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskClientOwner: + """Public attribution and presence for the task owner + + Public owner attribution for a client-owned task. Identifiers are opaque and never + authorize requests. + """ + join_id: str + """Opaque identity of the currently or most recently bound session join""" + + kind: TaskClientOwnerKind + """Class of the task owner""" + + participant_id: str + """Opaque session-scoped participant identity""" + + presence: TaskClientOwnerPresence + """Whether this task's bound join is currently connected""" + + disconnected_at: datetime | None = None + """ISO 8601 timestamp when the bound join disconnected""" + + display_name: str | None = None + """Display-only owner name""" + + source: str | None = None + """Display-only owner source""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskClientOwner': + assert isinstance(obj, dict) + join_id = from_str(obj.get("joinId")) + kind = TaskClientOwnerKind(obj.get("kind")) + participant_id = from_str(obj.get("participantId")) + presence = TaskClientOwnerPresence(obj.get("presence")) + disconnected_at = from_union([from_datetime, from_none], obj.get("disconnectedAt")) + display_name = from_union([from_str, from_none], obj.get("displayName")) + source = from_union([from_str, from_none], obj.get("source")) + return TaskClientOwner(join_id, kind, participant_id, presence, disconnected_at, display_name, source) + + def to_dict(self) -> dict: + result: dict = {} + result["joinId"] = from_str(self.join_id) + result["kind"] = to_enum(TaskClientOwnerKind, self.kind) + result["participantId"] = from_str(self.participant_id) + result["presence"] = to_enum(TaskClientOwnerPresence, self.presence) + if self.disconnected_at is not None: + result["disconnectedAt"] = from_union([lambda x: x.isoformat(), from_none], self.disconnected_at) + if self.display_name is not None: + result["displayName"] = from_union([from_str, from_none], self.display_name) + if self.source is not None: + result["source"] = from_union([from_str, from_none], self.source) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TaskProgress: """Progress snapshot for an agent task, with recent activity lines and optional latest intent. + Generic progress for a client-owned task. + Progress snapshot for a shell task, with recent stdout/stderr output and optional process ID. """ - type: TaskInfoType + type: TaskKind """Progress kind""" latest_intent: str | None = None """The most recent intent reported by the agent""" recent_activity: list[TaskProgressLine] | None = None - """Recent tool execution events converted to display lines""" + """Recent tool execution events converted to display lines + + Recent server-timestamped progress messages + """ + last_message: str | None = None + """Most recent nonempty progress message""" + + percentage: float | None = None + """Current completion percentage from zero through one hundred""" + + phase: str | None = None + """Current owner-defined progress phase""" + + sequence: int | None = None + """Sequence number of the latest accepted owner update""" + + status: TaskClientStatus | None = None + """Current client task lifecycle status""" + + updated_at: datetime | None = None + """ISO 8601 timestamp of the latest accepted lifecycle change""" pid: int | None = None """Process ID when available""" @@ -23891,26 +24727,226 @@ class TaskProgress: @staticmethod def from_dict(obj: Any) -> 'TaskProgress': assert isinstance(obj, dict) - type = TaskInfoType(obj.get("type")) + type = TaskKind(obj.get("type")) latest_intent = from_union([from_str, from_none], obj.get("latestIntent")) recent_activity = from_union([lambda x: from_list(TaskProgressLine.from_dict, x), from_none], obj.get("recentActivity")) + last_message = from_union([from_str, from_none], obj.get("lastMessage")) + percentage = from_union([from_float, from_none], obj.get("percentage")) + phase = from_union([from_str, from_none], obj.get("phase")) + sequence = from_union([from_int, from_none], obj.get("sequence")) + status = from_union([TaskClientStatus, from_none], obj.get("status")) + updated_at = from_union([from_datetime, from_none], obj.get("updatedAt")) pid = from_union([from_int, from_none], obj.get("pid")) recent_output = from_union([from_str, from_none], obj.get("recentOutput")) - return TaskProgress(type, latest_intent, recent_activity, pid, recent_output) + return TaskProgress(type, latest_intent, recent_activity, last_message, percentage, phase, sequence, status, updated_at, pid, recent_output) def to_dict(self) -> dict: result: dict = {} - result["type"] = to_enum(TaskInfoType, self.type) + result["type"] = to_enum(TaskKind, self.type) if self.latest_intent is not None: result["latestIntent"] = from_union([from_str, from_none], self.latest_intent) if self.recent_activity is not None: result["recentActivity"] = from_union([lambda x: from_list(lambda x: to_class(TaskProgressLine, x), x), from_none], self.recent_activity) + if self.last_message is not None: + result["lastMessage"] = from_union([from_str, from_none], self.last_message) + if self.percentage is not None: + result["percentage"] = from_union([to_float, from_none], self.percentage) + if self.phase is not None: + result["phase"] = from_union([from_str, from_none], self.phase) + if self.sequence is not None: + result["sequence"] = from_union([from_int, from_none], self.sequence) + if self.status is not None: + result["status"] = from_union([lambda x: to_enum(TaskClientStatus, x), from_none], self.status) + if self.updated_at is not None: + result["updatedAt"] = from_union([lambda x: x.isoformat(), from_none], self.updated_at) if self.pid is not None: result["pid"] = from_union([from_int, from_none], self.pid) if self.recent_output is not None: result["recentOutput"] = from_union([from_str, from_none], self.recent_output) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskClientProgress: + """Generic progress for a client-owned task.""" + + recent_activity: list[TaskProgressLine] + """Recent server-timestamped progress messages""" + + sequence: int + """Sequence number of the latest accepted owner update""" + + status: TaskClientStatus + """Current client task lifecycle status""" + + type: TaskClientType + """Progress kind""" + + updated_at: datetime + """ISO 8601 timestamp of the latest accepted lifecycle change""" + + last_message: str | None = None + """Most recent nonempty progress message""" + + percentage: float | None = None + """Current completion percentage from zero through one hundred""" + + phase: str | None = None + """Current owner-defined progress phase""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskClientProgress': + assert isinstance(obj, dict) + recent_activity = from_list(TaskProgressLine.from_dict, obj.get("recentActivity")) + sequence = from_int(obj.get("sequence")) + status = TaskClientStatus(obj.get("status")) + type = TaskClientType(obj.get("type")) + updated_at = from_datetime(obj.get("updatedAt")) + last_message = from_union([from_str, from_none], obj.get("lastMessage")) + percentage = from_union([from_float, from_none], obj.get("percentage")) + phase = from_union([from_str, from_none], obj.get("phase")) + return TaskClientProgress(recent_activity, sequence, status, type, updated_at, last_message, percentage, phase) + + def to_dict(self) -> dict: + result: dict = {} + result["recentActivity"] = from_list(lambda x: to_class(TaskProgressLine, x), self.recent_activity) + result["sequence"] = from_int(self.sequence) + result["status"] = to_enum(TaskClientStatus, self.status) + result["type"] = to_enum(TaskClientType, self.type) + result["updatedAt"] = self.updated_at.isoformat() + if self.last_message is not None: + result["lastMessage"] = from_union([from_str, from_none], self.last_message) + if self.percentage is not None: + result["percentage"] = from_union([to_float, from_none], self.percentage) + if self.phase is not None: + result["phase"] = from_union([from_str, from_none], self.phase) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksRegisterRequest: + """Registers or reclaims a client-owned task.""" + + cancellable: bool + """Whether the owner supports runtime cancellation requests""" + + client_task_id: str + """Owner-scoped idempotency key used for registration and reclaim""" + + description: str + """Human-readable description of the external work""" + + type: TaskClientType + """Task kind""" + + display_name: str | None = None + """Optional short display name for the external work""" + + expected_sequence: int | None = None + """Expected current sequence for idempotent registration or orphan reclaim""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksRegisterRequest': + assert isinstance(obj, dict) + cancellable = from_bool(obj.get("cancellable")) + client_task_id = from_str(obj.get("clientTaskId")) + description = from_str(obj.get("description")) + type = TaskClientType(obj.get("type")) + display_name = from_union([from_str, from_none], obj.get("displayName")) + expected_sequence = from_union([from_int, from_none], obj.get("expectedSequence")) + return TasksRegisterRequest(cancellable, client_task_id, description, type, display_name, expected_sequence) + + def to_dict(self) -> dict: + result: dict = {} + result["cancellable"] = from_bool(self.cancellable) + result["clientTaskId"] = from_str(self.client_task_id) + result["description"] = from_str(self.description) + result["type"] = to_enum(TaskClientType, self.type) + if self.display_name is not None: + result["displayName"] = from_union([from_str, from_none], self.display_name) + if self.expected_sequence is not None: + result["expectedSequence"] = from_union([from_int, from_none], self.expected_sequence) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskClientUpdate: + """Progress or terminal update for a client-owned task. + + Progress or terminal update payload + + Publishes nonterminal progress for a running or idle client task. + + Reports successful terminal completion. + + Reports terminal failure. + + Reports terminal cancellation after external work stopped. + """ + kind: TaskClientUpdateKind + """Client task update variant discriminator.""" + + message: str | None = None + """Optional progress message appended to recent activity when nonempty + + Optional final progress message + """ + percentage: float | None = None + """Optional completion percentage; null clears the current percentage""" + + phase: str | None = None + """Optional progress phase; null clears the current phase""" + + status: TaskClientActiveStatus | None = None + """Optional active status transition""" + + result: Any = None + """Optional opaque successful terminal result""" + + code: str | None = None + """Optional owner-supplied terminal failure code""" + + error: str | None = None + """Human-readable terminal failure message""" + + reason: str | None = None + """Optional human-readable cancellation reason""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskClientUpdate': + assert isinstance(obj, dict) + kind = TaskClientUpdateKind(obj.get("kind")) + message = from_union([from_str, from_none], obj.get("message")) + percentage = from_union([from_float, from_none], obj.get("percentage")) + phase = from_union([from_none, from_str], obj.get("phase")) + status = from_union([TaskClientActiveStatus, from_none], obj.get("status")) + result = obj.get("result") + code = from_union([from_str, from_none], obj.get("code")) + error = from_union([from_str, from_none], obj.get("error")) + reason = from_union([from_str, from_none], obj.get("reason")) + return TaskClientUpdate(kind, message, percentage, phase, status, result, code, error, reason) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(TaskClientUpdateKind, self.kind) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + if self.percentage is not None: + result["percentage"] = from_union([to_float, from_none], self.percentage) + if self.phase is not None: + result["phase"] = from_union([from_none, from_str], self.phase) + if self.status is not None: + result["status"] = from_union([lambda x: to_enum(TaskClientActiveStatus, x), from_none], self.status) + if self.result is not None: + result["result"] = self.result + if self.code is not None: + result["code"] = from_union([from_str, from_none], self.code) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.reason is not None: + result["reason"] = from_union([from_str, from_none], self.reason) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TaskShellInfo: @@ -25097,6 +26133,25 @@ def to_dict(self) -> dict: result["logCapture"] = from_union([lambda x: to_class(AgentRegistryLogCapture, x), from_none], self.log_capture) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AutopilotObjectiveGetStateResult: + """Canonical runtime state for the session's current autopilot objective.""" + + state: AutopilotObjectiveState | None = None + """Current objective state, or `null` when the session has no objective.""" + + @staticmethod + def from_dict(obj: Any) -> 'AutopilotObjectiveGetStateResult': + assert isinstance(obj, dict) + state = from_union([AutopilotObjectiveState.from_dict, from_none], obj.get("state")) + return AutopilotObjectiveGetStateResult(state) + + def to_dict(self) -> dict: + result: dict = {} + result["state"] = from_union([lambda x: to_class(AutopilotObjectiveState, x), from_none], self.state) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPPlanProvenance: @@ -25568,6 +26623,43 @@ def to_dict(self) -> dict: result["mode"] = to_enum(DiscoveredExtensionMode, self.mode) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class HooksDiscoverResult: + """Server-discovered hook actions and partial-load diagnostics from user, repository, + plugin, and managed-policy sources. Concrete sessions may include additional + session-specific hook sources. + """ + errors: list[str] + """Errors for hook sources or actions that could not be loaded, making the result partially + incomplete. Other valid actions are still returned. Project-resolution and + repository-settings errors are prefixed with their project path. + """ + hooks: list[DiscoveredHook] + """All discovered hook actions. Byte-identical actions remain separate rows even when they + share a disable key. + """ + warnings: list[str] + """Non-fatal source-loading warnings. Discovery remains complete for the affected source, + although the source had a recoverable issue. Repository-settings warnings are prefixed + with their project path when attribution is available. + """ + + @staticmethod + def from_dict(obj: Any) -> 'HooksDiscoverResult': + assert isinstance(obj, dict) + errors = from_list(from_str, obj.get("errors")) + hooks = from_list(DiscoveredHook.from_dict, obj.get("hooks")) + warnings = from_list(from_str, obj.get("warnings")) + return HooksDiscoverResult(errors, hooks, warnings) + + def to_dict(self) -> dict: + result: dict = {} + result["errors"] = from_list(from_str, self.errors) + result["hooks"] = from_list(lambda x: to_class(DiscoveredHook, x), self.hooks) + result["warnings"] = from_list(from_str, self.warnings) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class ExtensionList: @@ -26040,11 +27132,15 @@ class FactoryRunResult: status: FactoryRunStatus """Current or terminal factory run status.""" + attempt: int | None = None + """One-based execution attempt represented by this envelope. Absent before the first attempt + starts or when returned by an older runtime. + """ error: str | None = None """Error message for an errored run.""" failure: FactoryRunFailure | None = None - """Machine-readable failure details for an errored run.""" + """Machine-readable failure details for a halted or errored run.""" reason: str | None = None """Reason for a halted or cancelled run.""" @@ -26060,17 +27156,20 @@ def from_dict(obj: Any) -> 'FactoryRunResult': assert isinstance(obj, dict) run_id = from_str(obj.get("runId")) status = FactoryRunStatus(obj.get("status")) + attempt = from_union([from_int, from_none], obj.get("attempt")) error = from_union([from_str, from_none], obj.get("error")) failure = from_union([FactoryRunFailure.from_dict, from_none], obj.get("failure")) reason = from_union([from_str, from_none], obj.get("reason")) result = obj.get("result") snapshot = obj.get("snapshot") - return FactoryRunResult(run_id, status, error, failure, reason, result, snapshot) + return FactoryRunResult(run_id, status, attempt, error, failure, reason, result, snapshot) def to_dict(self) -> dict: result: dict = {} result["runId"] = from_str(self.run_id) result["status"] = to_enum(FactoryRunStatus, self.status) + if self.attempt is not None: + result["attempt"] = from_union([from_int, from_none], self.attempt) if self.error is not None: result["error"] = from_union([from_str, from_none], self.error) if self.failure is not None: @@ -27364,6 +28463,9 @@ class PluginInstallResult: post_install_message: str | None = None """Optional post-install message provided by the plugin (e.g. setup instructions)""" + staging_mode: PluginInstallStagingMode | None = None + """Where the completed plugin tree was staged before atomic promotion""" + @staticmethod def from_dict(obj: Any) -> 'PluginInstallResult': assert isinstance(obj, dict) @@ -27371,7 +28473,8 @@ def from_dict(obj: Any) -> 'PluginInstallResult': skills_installed = from_int(obj.get("skillsInstalled")) deprecation_warning = from_union([from_str, from_none], obj.get("deprecationWarning")) post_install_message = from_union([from_str, from_none], obj.get("postInstallMessage")) - return PluginInstallResult(plugin, skills_installed, deprecation_warning, post_install_message) + staging_mode = from_union([PluginInstallStagingMode, from_none], obj.get("stagingMode")) + return PluginInstallResult(plugin, skills_installed, deprecation_warning, post_install_message, staging_mode) def to_dict(self) -> dict: result: dict = {} @@ -27381,6 +28484,8 @@ def to_dict(self) -> dict: result["deprecationWarning"] = from_union([from_str, from_none], self.deprecation_warning) if self.post_install_message is not None: result["postInstallMessage"] = from_union([from_str, from_none], self.post_install_message) + if self.staging_mode is not None: + result["stagingMode"] = from_union([lambda x: to_enum(PluginInstallStagingMode, x), from_none], self.staging_mode) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -28143,6 +29248,143 @@ def to_dict(self) -> dict: result["paths"] = from_list(lambda x: to_class(SkillDiscoveryPath, x), self.paths) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TaskClientInfo: + """Tracked client-owned task metadata. + + Authoritative registered or reclaimed task + + Authoritative task after processing the update + """ + active_time_ms: int + """Accumulated active execution time in milliseconds""" + + can_cancel: bool + """Whether the currently bound owner can receive a cancellation request""" + + client_task_id: str + """Owner-scoped registration and reclaim key""" + + description: str + """Task description""" + + execution_mode: TaskClientExecutionMode + """Execution mode, which is always background for client-owned tasks""" + + id: str + """Canonical runtime-generated task identifier""" + + owner: TaskClientOwner + """Public attribution and presence for the task owner""" + + sequence: int + """Sequence number of the latest accepted owner update""" + + started_at: datetime + """ISO 8601 timestamp when the task started""" + + status: TaskClientStatus + """Client task lifecycle status""" + + type: ClassVar[str] = "client" + """Task kind""" + + updated_at: datetime + """ISO 8601 timestamp of the latest accepted lifecycle change""" + + active_started_at: datetime | None = None + """ISO 8601 timestamp when the current active segment started""" + + cancellation_reason: str | None = None + """Human-readable reason for terminal cancellation""" + + completed_at: datetime | None = None + """ISO 8601 timestamp when the task reached a terminal status""" + + display_name: str | None = None + """Optional task display name""" + + error: str | None = None + """Human-readable terminal failure message""" + + error_code: str | None = None + """Optional owner-supplied terminal failure code""" + + idle_since: datetime | None = None + """ISO 8601 timestamp when the connected owner entered idle status""" + + orphaned_at: datetime | None = None + """ISO 8601 timestamp of the most recent orphan transition""" + + reclaimed_at: datetime | None = None + """ISO 8601 timestamp of the most recent successful reclaim""" + + result: Any = None + """Opaque successful terminal result supplied by the task owner""" + + @staticmethod + def from_dict(obj: Any) -> 'TaskClientInfo': + assert isinstance(obj, dict) + active_time_ms = from_int(obj.get("activeTimeMs")) + can_cancel = from_bool(obj.get("canCancel")) + client_task_id = from_str(obj.get("clientTaskId")) + description = from_str(obj.get("description")) + execution_mode = TaskClientExecutionMode(obj.get("executionMode")) + id = from_str(obj.get("id")) + owner = TaskClientOwner.from_dict(obj.get("owner")) + sequence = from_int(obj.get("sequence")) + started_at = from_datetime(obj.get("startedAt")) + status = TaskClientStatus(obj.get("status")) + updated_at = from_datetime(obj.get("updatedAt")) + active_started_at = from_union([from_datetime, from_none], obj.get("activeStartedAt")) + cancellation_reason = from_union([from_str, from_none], obj.get("cancellationReason")) + completed_at = from_union([from_datetime, from_none], obj.get("completedAt")) + display_name = from_union([from_str, from_none], obj.get("displayName")) + error = from_union([from_str, from_none], obj.get("error")) + error_code = from_union([from_str, from_none], obj.get("errorCode")) + idle_since = from_union([from_datetime, from_none], obj.get("idleSince")) + orphaned_at = from_union([from_datetime, from_none], obj.get("orphanedAt")) + reclaimed_at = from_union([from_datetime, from_none], obj.get("reclaimedAt")) + result = obj.get("result") + return TaskClientInfo(active_time_ms, can_cancel, client_task_id, description, execution_mode, id, owner, sequence, started_at, status, updated_at, active_started_at, cancellation_reason, completed_at, display_name, error, error_code, idle_since, orphaned_at, reclaimed_at, result) + + def to_dict(self) -> dict: + result: dict = {} + result["activeTimeMs"] = from_int(self.active_time_ms) + result["canCancel"] = from_bool(self.can_cancel) + result["clientTaskId"] = from_str(self.client_task_id) + result["description"] = from_str(self.description) + result["executionMode"] = to_enum(TaskClientExecutionMode, self.execution_mode) + result["id"] = from_str(self.id) + result["owner"] = to_class(TaskClientOwner, self.owner) + result["sequence"] = from_int(self.sequence) + result["startedAt"] = self.started_at.isoformat() + result["status"] = to_enum(TaskClientStatus, self.status) + result["type"] = self.type + result["updatedAt"] = self.updated_at.isoformat() + if self.active_started_at is not None: + result["activeStartedAt"] = from_union([lambda x: x.isoformat(), from_none], self.active_started_at) + if self.cancellation_reason is not None: + result["cancellationReason"] = from_union([from_str, from_none], self.cancellation_reason) + if self.completed_at is not None: + result["completedAt"] = from_union([lambda x: x.isoformat(), from_none], self.completed_at) + if self.display_name is not None: + result["displayName"] = from_union([from_str, from_none], self.display_name) + if self.error is not None: + result["error"] = from_union([from_str, from_none], self.error) + if self.error_code is not None: + result["errorCode"] = from_union([from_str, from_none], self.error_code) + if self.idle_since is not None: + result["idleSince"] = from_union([lambda x: x.isoformat(), from_none], self.idle_since) + if self.orphaned_at is not None: + result["orphanedAt"] = from_union([lambda x: x.isoformat(), from_none], self.orphaned_at) + if self.reclaimed_at is not None: + result["reclaimedAt"] = from_union([lambda x: x.isoformat(), from_none], self.reclaimed_at) + if self.result is not None: + result["result"] = self.result + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class TasksGetProgressResult: @@ -28165,6 +29407,35 @@ def to_dict(self) -> dict: result["progress"] = from_union([lambda x: to_class(TaskProgress, x), from_none], self.progress) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksUpdateRequest: + """Updates a client-owned task.""" + + id: str + """Canonical runtime-generated task identifier""" + + sequence: int + """Owner update sequence to apply""" + + update: TaskClientUpdate + """Progress or terminal update payload""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksUpdateRequest': + assert isinstance(obj, dict) + id = from_str(obj.get("id")) + sequence = from_int(obj.get("sequence")) + update = TaskClientUpdate.from_dict(obj.get("update")) + return TasksUpdateRequest(id, sequence, update) + + def to_dict(self) -> dict: + result: dict = {} + result["id"] = from_str(self.id) + result["sequence"] = from_int(self.sequence) + result["update"] = to_class(TaskClientUpdate, self.update) + return result + # Experimental: this type is part of an experimental API and may change or be removed. # Internal: this type is an internal SDK API and is not part of the public surface. @dataclass @@ -30369,6 +31640,13 @@ class SandboxConfig: add_current_working_directory: bool | None = None """Whether to auto-add the current working directory to readwritePaths. Default: true.""" + allow_bypass: bool | None = None + """Whether the agent may request that an individual command run outside the sandbox, which + the host then approves or denies through the usual permission flow. A host capability + flag rather than part of the policy: it is stripped from the effective spawn policy and + only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this + object: omitting it offers no bypass. Default: false (opt-in). + """ allow_dev_tool_access: bool | None = None """Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common @@ -30385,6 +31663,29 @@ class SandboxConfig: auth: SandboxConfigAuth | None = None """Credential-injection capability flags.""" + # Internal: this field is an internal SDK API and is not part of the public surface. + managed_lsp_routing_locked: bool | None = None + """The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`.""" + + # Internal: this field is an internal SDK API and is not part of the public surface. + managed_mcp_routing_locked: bool | None = None + """Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local + opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at + the administrator instead of a setting the next managed merge would override, and it is + ignored when comparing two configs for change. Only the managed merge may set it; a + caller-supplied value is stripped. + """ + sandbox_lsp_servers: bool | None = None + """Whether language servers the session launches are confined by the sandbox. Only an + explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by + default; set to false to opt out). + """ + sandbox_mcp_servers: bool | None = None + """Whether MCP servers the session launches are confined by the sandbox. Only an explicit + `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and + `enabled` are always read together. Ignored while `enabled` is false. Default: true + (enabled by default; set to false to opt out). + """ user_policy: SandboxConfigUserPolicy | None = None """User-managed sandbox policy fragment merged into the auto-discovered base policy.""" @@ -30393,20 +31694,35 @@ def from_dict(obj: Any) -> 'SandboxConfig': assert isinstance(obj, dict) enabled = from_bool(obj.get("enabled")) add_current_working_directory = from_union([from_bool, from_none], obj.get("addCurrentWorkingDirectory")) + allow_bypass = from_union([from_bool, from_none], obj.get("allowBypass")) allow_dev_tool_access = from_union([from_bool, from_none], obj.get("allowDevToolAccess")) auth = from_union([SandboxConfigAuth.from_dict, from_none], obj.get("auth")) + managed_lsp_routing_locked = from_union([from_bool, from_none], obj.get("managedLspRoutingLocked")) + managed_mcp_routing_locked = from_union([from_bool, from_none], obj.get("managedMcpRoutingLocked")) + sandbox_lsp_servers = from_union([from_bool, from_none], obj.get("sandboxLspServers")) + sandbox_mcp_servers = from_union([from_bool, from_none], obj.get("sandboxMcpServers")) user_policy = from_union([SandboxConfigUserPolicy.from_dict, from_none], obj.get("userPolicy")) - return SandboxConfig(enabled, add_current_working_directory, allow_dev_tool_access, auth, user_policy) + return SandboxConfig(enabled, add_current_working_directory, allow_bypass, allow_dev_tool_access, auth, managed_lsp_routing_locked, managed_mcp_routing_locked, sandbox_lsp_servers, sandbox_mcp_servers, user_policy) def to_dict(self) -> dict: result: dict = {} result["enabled"] = from_bool(self.enabled) if self.add_current_working_directory is not None: result["addCurrentWorkingDirectory"] = from_union([from_bool, from_none], self.add_current_working_directory) + if self.allow_bypass is not None: + result["allowBypass"] = from_union([from_bool, from_none], self.allow_bypass) if self.allow_dev_tool_access is not None: result["allowDevToolAccess"] = from_union([from_bool, from_none], self.allow_dev_tool_access) if self.auth is not None: result["auth"] = from_union([lambda x: to_class(SandboxConfigAuth, x), from_none], self.auth) + if self.managed_lsp_routing_locked is not None: + result["managedLspRoutingLocked"] = from_union([from_bool, from_none], self.managed_lsp_routing_locked) + if self.managed_mcp_routing_locked is not None: + result["managedMcpRoutingLocked"] = from_union([from_bool, from_none], self.managed_mcp_routing_locked) + if self.sandbox_lsp_servers is not None: + result["sandboxLspServers"] = from_union([from_bool, from_none], self.sandbox_lsp_servers) + if self.sandbox_mcp_servers is not None: + result["sandboxMcpServers"] = from_union([from_bool, from_none], self.sandbox_mcp_servers) if self.user_policy is not None: result["userPolicy"] = from_union([lambda x: to_class(SandboxConfigUserPolicy, x), from_none], self.user_policy) return result @@ -30436,6 +31752,64 @@ def to_dict(self) -> dict: result["error"] = from_union([lambda x: to_class(SessionFSSqliteTransactionError, x), from_none], self.error) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksRegisterResult: + """Result of registering or reclaiming a client-owned task.""" + + created: bool + """True only when this invocation created a new task""" + + reclaimed: bool + """True only when this invocation reclaimed an orphaned task""" + + task: TaskClientInfo + """Authoritative registered or reclaimed task""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksRegisterResult': + assert isinstance(obj, dict) + created = from_bool(obj.get("created")) + reclaimed = from_bool(obj.get("reclaimed")) + task = TaskClientInfo.from_dict(obj.get("task")) + return TasksRegisterResult(created, reclaimed, task) + + def to_dict(self) -> dict: + result: dict = {} + result["created"] = from_bool(self.created) + result["reclaimed"] = from_bool(self.reclaimed) + result["task"] = to_class(TaskClientInfo, self.task) + return result + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class TasksUpdateResult: + """Result of publishing a client-owned task update.""" + + applied: bool + """Whether this invocation changed task state""" + + duplicate: bool + """Whether this invocation repeated the latest accepted update""" + + task: TaskClientInfo + """Authoritative task after processing the update""" + + @staticmethod + def from_dict(obj: Any) -> 'TasksUpdateResult': + assert isinstance(obj, dict) + applied = from_bool(obj.get("applied")) + duplicate = from_bool(obj.get("duplicate")) + task = TaskClientInfo.from_dict(obj.get("task")) + return TasksUpdateResult(applied, duplicate, task) + + def to_dict(self) -> dict: + result: dict = {} + result["applied"] = from_bool(self.applied) + result["duplicate"] = from_bool(self.duplicate) + result["task"] = to_class(TaskClientInfo, self.task) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPConfigAddRequest: @@ -30831,6 +32205,9 @@ class SessionOpenOptions: ask_user_disabled: bool | None = None """Whether ask_user is explicitly disabled.""" + auth_client_id_metadata_url: str | None = None + """OAuth Client ID Metadata Document URL used by this host for MCP authorization.""" + auth_info: AuthInfo | None = None """Initial authentication info for the session.""" @@ -30913,6 +32290,10 @@ class SessionOpenOptions: enable_script_safety: bool | None = None """Whether shell-script safety heuristics are enabled.""" + enable_skills: bool | None = None + """Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by + default. + """ enable_streaming: bool | None = None """Whether model responses stream as delta events.""" @@ -30943,6 +32324,14 @@ class SessionOpenOptions: feature_flags: dict[str, bool] | None = None """Feature-flag values resolved by the host.""" + # Internal: this field is an internal SDK API and is not part of the public surface. + has_skill_provider: bool | None = None + """Whether the requesting SDK session has a skill provider. The provider remains ephemeral + and is never persisted in session options or history. When enableSkills is false, it + remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw + sessions.open flows reject it because they cannot safely pre-register the callback + handler. + """ included_builtin_agents: list[str] | None = None """Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the @@ -31071,6 +32460,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': agent_context = from_union([from_str, from_none], obj.get("agentContext")) allow_all_mcp_server_instructions = from_union([from_bool, from_none], obj.get("allowAllMcpServerInstructions")) ask_user_disabled = from_union([from_bool, from_none], obj.get("askUserDisabled")) + auth_client_id_metadata_url = from_union([from_str, from_none], obj.get("authClientIdMetadataUrl")) auth_info = from_union([_load_AuthInfo, from_none], obj.get("authInfo")) available_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("availableTools")) capi = from_union([CapiSessionOptions.from_dict, from_none], obj.get("capi")) @@ -31091,6 +32481,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': enable_managed_settings = from_union([from_bool, from_none], obj.get("enableManagedSettings")) enable_on_demand_instruction_discovery = from_union([from_bool, from_none], obj.get("enableOnDemandInstructionDiscovery")) enable_script_safety = from_union([from_bool, from_none], obj.get("enableScriptSafety")) + enable_skills = from_union([from_bool, from_none], obj.get("enableSkills")) enable_streaming = from_union([from_bool, from_none], obj.get("enableStreaming")) env_value_mode = from_union([MCPSetEnvValueModeDetails, from_none], obj.get("envValueMode")) events_log_directory = from_union([from_str, from_none], obj.get("eventsLogDirectory")) @@ -31099,6 +32490,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': excluded_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("excludedTools")) exp_assignments = obj.get("expAssignments") feature_flags = from_union([lambda x: from_dict(from_bool, x), from_none], obj.get("featureFlags")) + has_skill_provider = from_union([from_bool, from_none], obj.get("hasSkillProvider")) included_builtin_agents = from_union([lambda x: from_list(from_str, x), from_none], obj.get("includedBuiltinAgents")) included_builtin_skills = from_union([lambda x: from_list(from_str, x), from_none], obj.get("includedBuiltinSkills")) installed_plugins = from_union([lambda x: from_list(InstalledPlugin.from_dict, x), from_none], obj.get("installedPlugins")) @@ -31135,7 +32527,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': verbosity = from_union([Verbosity, from_none], obj.get("verbosity")) working_directory = from_union([from_str, from_none], obj.get("workingDirectory")) working_directory_context = from_union([SessionContext.from_dict, from_none], obj.get("workingDirectoryContext")) - return SessionOpenOptions(additional_content_exclusion_policies, additional_directories, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, auth_info, available_tools, capi, client_kind, client_name, coauthor_enabled, config_dir, continue_on_auto_mode, copilot_url, custom_agents_local_only, detached_from_spawning_parent_engagement_id, detached_from_spawning_parent_session_id, disabled_instruction_sources, disabled_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, max_inline_binary_bytes, memory, model, model_capabilities_overrides, models, name, provider, providers, reasoning_effort, reasoning_summary, remote_defaulted_on, remote_exporting, remote_steerable, running_in_interactive_mode, sandbox_config, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context) + return SessionOpenOptions(additional_content_exclusion_policies, additional_directories, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, auth_client_id_metadata_url, auth_info, available_tools, capi, client_kind, client_name, coauthor_enabled, config_dir, continue_on_auto_mode, copilot_url, custom_agents_local_only, detached_from_spawning_parent_engagement_id, detached_from_spawning_parent_session_id, disabled_instruction_sources, disabled_mcp_servers, disabled_skills, enable_citations, enable_file_change_tracking, enable_managed_settings, enable_on_demand_instruction_discovery, enable_script_safety, enable_skills, enable_streaming, env_value_mode, events_log_directory, events_log_includes_subagents, excluded_builtin_agents, excluded_tools, exp_assignments, feature_flags, has_skill_provider, included_builtin_agents, included_builtin_skills, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, managed_settings, max_inline_binary_bytes, memory, model, model_capabilities_overrides, models, name, provider, providers, reasoning_effort, reasoning_summary, remote_defaulted_on, remote_exporting, remote_steerable, running_in_interactive_mode, sandbox_config, sandbox_config_source, session_capabilities, session_id, session_limits, shell, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, verbosity, working_directory, working_directory_context) def to_dict(self) -> dict: result: dict = {} @@ -31149,6 +32541,8 @@ def to_dict(self) -> dict: result["allowAllMcpServerInstructions"] = from_union([from_bool, from_none], self.allow_all_mcp_server_instructions) if self.ask_user_disabled is not None: result["askUserDisabled"] = from_union([from_bool, from_none], self.ask_user_disabled) + if self.auth_client_id_metadata_url is not None: + result["authClientIdMetadataUrl"] = from_union([from_str, from_none], self.auth_client_id_metadata_url) if self.auth_info is not None: result["authInfo"] = from_union([lambda x: (x).to_dict(), from_none], self.auth_info) if self.available_tools is not None: @@ -31189,6 +32583,8 @@ def to_dict(self) -> dict: result["enableOnDemandInstructionDiscovery"] = from_union([from_bool, from_none], self.enable_on_demand_instruction_discovery) if self.enable_script_safety is not None: result["enableScriptSafety"] = from_union([from_bool, from_none], self.enable_script_safety) + if self.enable_skills is not None: + result["enableSkills"] = from_union([from_bool, from_none], self.enable_skills) if self.enable_streaming is not None: result["enableStreaming"] = from_union([from_bool, from_none], self.enable_streaming) if self.env_value_mode is not None: @@ -31205,6 +32601,8 @@ def to_dict(self) -> dict: result["expAssignments"] = self.exp_assignments if self.feature_flags is not None: result["featureFlags"] = from_union([lambda x: from_dict(from_bool, x), from_none], self.feature_flags) + if self.has_skill_provider is not None: + result["hasSkillProvider"] = from_union([from_bool, from_none], self.has_skill_provider) if self.included_builtin_agents is not None: result["includedBuiltinAgents"] = from_union([lambda x: from_list(from_str, x), from_none], self.included_builtin_agents) if self.included_builtin_skills is not None: @@ -31352,8 +32750,9 @@ class SessionUpdateOptionsParams: """Whether to enable cross-session store writes and reads.""" enable_skills: bool | None = None - """Whether to enable skill directory scanning and loading. Falls back to - enableConfigDiscovery when unset. + """Whether skill loading is enabled. Explicit false disables every source, including a bound + SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, + creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. """ enable_streaming: bool | None = None """Whether to stream model responses.""" @@ -33877,6 +35276,11 @@ class Model: a recommended alternative. Present only when the service published at least one notice. Hosts should surface these without implying anything is wrong with the model. """ + metadata: dict[str, Any] | None = None + """Provider-supplied model metadata. Keys and JSON-compatible values are preserved + unchanged. This is factual metadata published by the model provider; it carries no picker + or UX semantics. + """ model_picker_category: ModelPickerCategory | None = None """Model capability category for grouping in the model picker""" @@ -33914,6 +35318,7 @@ def from_dict(obj: Any) -> 'Model': billing = from_union([ModelBilling.from_dict, from_none], obj.get("billing")) default_reasoning_effort = from_union([from_str, from_none], obj.get("defaultReasoningEffort")) info_messages = from_union([lambda x: from_list(ModelMessage.from_dict, x), from_none], obj.get("infoMessages")) + metadata = from_union([lambda x: from_dict(lambda x: x, x), from_none], obj.get("metadata")) model_picker_category = from_union([ModelPickerCategory, from_none], obj.get("modelPickerCategory")) model_picker_price_category = from_union([ModelPickerPriceCategory, from_none], obj.get("modelPickerPriceCategory")) policy = from_union([ModelPolicy.from_dict, from_none], obj.get("policy")) @@ -33921,7 +35326,7 @@ def from_dict(obj: Any) -> 'Model': supported_reasoning_efforts = from_union([lambda x: from_list(from_str, x), from_none], obj.get("supportedReasoningEfforts")) warning_messages = from_union([lambda x: from_list(ModelMessage.from_dict, x), from_none], obj.get("warningMessages")) warning_text = from_union([ModelWarningText.from_dict, from_none], obj.get("warningText")) - return Model(capabilities, id, name, billing, default_reasoning_effort, info_messages, model_picker_category, model_picker_price_category, policy, supported_context_tiers, supported_reasoning_efforts, warning_messages, warning_text) + return Model(capabilities, id, name, billing, default_reasoning_effort, info_messages, metadata, model_picker_category, model_picker_price_category, policy, supported_context_tiers, supported_reasoning_efforts, warning_messages, warning_text) def to_dict(self) -> dict: result: dict = {} @@ -33934,6 +35339,8 @@ def to_dict(self) -> dict: result["defaultReasoningEffort"] = from_union([from_str, from_none], self.default_reasoning_effort) if self.info_messages is not None: result["infoMessages"] = from_union([lambda x: from_list(lambda x: to_class(ModelMessage, x), x), from_none], self.info_messages) + if self.metadata is not None: + result["metadata"] = from_union([lambda x: from_dict(lambda x: x, x), from_none], self.metadata) if self.model_picker_category is not None: result["modelPickerCategory"] = from_union([lambda x: to_enum(ModelPickerCategory, x), from_none], self.model_picker_category) if self.model_picker_price_category is not None: @@ -33964,6 +35371,11 @@ class ModelApplyStartupOverlayRequest: device_managed_model: str | None = None """Model required by device-managed policy, when configured.""" + policy_helper_model: str | None = None + """Startup default model from the enterprise policy helper, when configured. Weakest of the + managed sources: it applies only when neither device nor server policy names a model, and + an explicit user selection still wins. + """ repo_context_tier: str | None = None """Context tier selected by repository settings, when configured.""" @@ -33982,11 +35394,12 @@ def from_dict(obj: Any) -> 'ModelApplyStartupOverlayRequest': cli_model = from_union([from_str, from_none], obj.get("cliModel")) deferred_resume = from_union([from_bool, from_none], obj.get("deferredResume")) device_managed_model = from_union([from_str, from_none], obj.get("deviceManagedModel")) + policy_helper_model = from_union([from_str, from_none], obj.get("policyHelperModel")) repo_context_tier = from_union([from_str, from_none], obj.get("repoContextTier")) repo_model = from_union([from_str, from_none], obj.get("repoModel")) repo_reasoning_effort = from_union([from_str, from_none], obj.get("repoReasoningEffort")) server_managed_model = from_union([from_str, from_none], obj.get("serverManagedModel")) - return ModelApplyStartupOverlayRequest(cli_model, deferred_resume, device_managed_model, repo_context_tier, repo_model, repo_reasoning_effort, server_managed_model) + return ModelApplyStartupOverlayRequest(cli_model, deferred_resume, device_managed_model, policy_helper_model, repo_context_tier, repo_model, repo_reasoning_effort, server_managed_model) def to_dict(self) -> dict: result: dict = {} @@ -33996,6 +35409,8 @@ def to_dict(self) -> dict: result["deferredResume"] = from_union([from_bool, from_none], self.deferred_resume) if self.device_managed_model is not None: result["deviceManagedModel"] = from_union([from_str, from_none], self.device_managed_model) + if self.policy_helper_model is not None: + result["policyHelperModel"] = from_union([from_str, from_none], self.policy_helper_model) if self.repo_context_tier is not None: result["repoContextTier"] = from_union([from_str, from_none], self.repo_context_tier) if self.repo_model is not None: @@ -34037,6 +35452,11 @@ class ModelSwitchToRequest: `claude-sonnet-4.6`) names a Copilot (CAPI) model; a provider-qualified id (`provider/id`, e.g. `acme/claude-sonnet`) targets a registry BYOK model. """ + auto_tier: AutoTier | None = None + """Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to + return to provider-default Auto routing. This field is rejected when `modelId` is not + `auto`. + """ compaction_decision: str | None = None """Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. @@ -34079,8 +35499,8 @@ class ModelSwitchToRequest: """When true, evaluate context-window compaction policy before applying the switch.""" source: ModelChangeSource | None = None - """Origin to record on the effective `session.model_change` event. Defaults to `sdk` when - omitted. + """Origin to record on the effective `session.model_change` event for trusted in-process + calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. """ verbosity: Verbosity | None = None """Output verbosity level to request for supported models""" @@ -34089,6 +35509,7 @@ class ModelSwitchToRequest: def from_dict(obj: Any) -> 'ModelSwitchToRequest': assert isinstance(obj, dict) model_id = from_str(obj.get("modelId")) + auto_tier = from_union([AutoTier, from_none], obj.get("autoTier")) compaction_decision = from_union([from_str, from_none], obj.get("compactionDecision")) context_tier = from_union([ContextTier, from_none], obj.get("contextTier")) defer_if_model_change_queued = from_union([from_bool, from_none], obj.get("deferIfModelChangeQueued")) @@ -34102,11 +35523,13 @@ def from_dict(obj: Any) -> 'ModelSwitchToRequest': run_compaction_preflight = from_union([from_bool, from_none], obj.get("runCompactionPreflight")) source = from_union([ModelChangeSource, from_none], obj.get("source")) verbosity = from_union([Verbosity, from_none], obj.get("verbosity")) - return ModelSwitchToRequest(model_id, compaction_decision, context_tier, defer_if_model_change_queued, model_capabilities, model_change_scope, picker_persistence, reasoning_effort, reasoning_summary, repo_scope, require_available, run_compaction_preflight, source, verbosity) + return ModelSwitchToRequest(model_id, auto_tier, compaction_decision, context_tier, defer_if_model_change_queued, model_capabilities, model_change_scope, picker_persistence, reasoning_effort, reasoning_summary, repo_scope, require_available, run_compaction_preflight, source, verbosity) def to_dict(self) -> dict: result: dict = {} result["modelId"] = from_str(self.model_id) + if self.auto_tier is not None: + result["autoTier"] = from_union([lambda x: to_enum(AutoTier, x), from_none], self.auto_tier) if self.compaction_decision is not None: result["compactionDecision"] = from_union([from_str, from_none], self.compaction_decision) if self.context_tier is not None: @@ -34805,13 +36228,17 @@ class SubagentSettingsEntry: model: str | None = None """Model override for matching subagents""" + model_policy: AgentModelPolicy | None = None + """Whether the configured model strategy is preferred or required""" + @staticmethod def from_dict(obj: Any) -> 'SubagentSettingsEntry': assert isinstance(obj, dict) context_tier = from_union([SubagentSettingsEntryContextTier, from_none], obj.get("contextTier")) effort_level = from_union([from_str, from_none], obj.get("effortLevel")) model = from_union([from_str, from_none], obj.get("model")) - return SubagentSettingsEntry(context_tier, effort_level, model) + model_policy = from_union([AgentModelPolicy, from_none], obj.get("modelPolicy")) + return SubagentSettingsEntry(context_tier, effort_level, model, model_policy) def to_dict(self) -> dict: result: dict = {} @@ -34821,6 +36248,8 @@ def to_dict(self) -> dict: result["effortLevel"] = from_union([from_str, from_none], self.effort_level) if self.model is not None: result["model"] = from_union([from_str, from_none], self.model) + if self.model_policy is not None: + result["modelPolicy"] = from_union([lambda x: to_enum(AgentModelPolicy, x), from_none], self.model_policy) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -35108,6 +36537,10 @@ class RPC: auth_info_type: AuthInfoType auth_validation_error: AuthValidationError auth_validation_errors: list[AuthValidationError] + autopilot_objective_credit_limit: AutopilotObjectiveCreditLimit + autopilot_objective_get_state_result: AutopilotObjectiveGetStateResult + autopilot_objective_state: AutopilotObjectiveState + autopilot_objective_status: AutopilotObjectiveStatus built_in_model_catalog: BuiltInModelCatalog built_in_model_catalog_entry: BuiltInModelCatalogEntry builtin_tool_descriptor: BuiltinToolDescriptor @@ -35182,6 +36615,9 @@ class RPC: catalog_unsafe_retrieval_error: CatalogUnsafeRetrievalError catalog_unsafe_retrieval_reason: CatalogUnsafeRetrievalReason catalog_unsupported_kind_error: CatalogUnsupportedKindError + client_task_cancel_reason: ClientTaskCancelReason + client_task_cancel_request: ClientTaskCancelRequest + client_task_cancel_result: ClientTaskCancelResult command_list: CommandList commands_finalize_invocation_effect_request: CommandsFinalizeInvocationEffectRequest commands_finalize_invocation_effect_result: CommandsFinalizeInvocationEffectResult @@ -35237,6 +36673,7 @@ class RPC: discovered_extensions_disable_request: DiscoveredExtensionsDisableRequest discovered_extensions_enable_request: DiscoveredExtensionsEnableRequest discovered_extension_source: DiscoveredExtensionSource + discovered_hook: DiscoveredHook discovered_mcp_server: DiscoveredMCPServer discovered_mcp_server_type: DiscoveredMCPServerType enqueue_command_params: EnqueueCommandParams @@ -35359,7 +36796,10 @@ class RPC: hmac_auth_info: HMACAuthInfo hook_invoke_request: _HookInvokeRequest hook_invoke_response: _HookInvokeResponse - hook_type: _HookType + hook_origin: HookOrigin + hooks_discover_request: HooksDiscoverRequest + hooks_discover_result: HooksDiscoverResult + hook_type: HookType installed_plugin: InstalledPlugin installed_plugin_info: InstalledPluginInfo installed_plugin_source: InstalledPluginSource | str @@ -35597,6 +37037,9 @@ class RPC: model_set_reasoning_effort_request: ModelSetReasoningEffortRequest model_set_reasoning_effort_result: ModelSetReasoningEffortResult models_list_request: ModelsListRequest + model_switch_auto_tier_request: ModelSwitchAutoTierRequest + model_switch_auto_tier_result: ModelSwitchAutoTierResult + model_switch_auto_tier_status: ModelSwitchAutoTierStatus model_switch_confirmation: ModelSwitchConfirmation model_switch_to_request: ModelSwitchToRequest model_switch_to_result: ModelSwitchToResult @@ -35736,6 +37179,7 @@ class RPC: plan_update_request: PlanUpdateRequest plugin: Plugin plugin_install_result: PluginInstallResult + plugin_install_staging_mode: PluginInstallStagingMode plugin_list: PluginList plugin_list_result: PluginListResult plugins_builtin_set_request: PluginsBuiltinSetRequest @@ -36032,6 +37476,7 @@ class RPC: sessions_open_status: SessionsOpenStatus session_source: SessionSource sessions_prune_old_request: SessionsPruneOldRequest + sessions_read_persisted_events_request: SessionsReadPersistedEventsRequest sessions_register_extension_tools_on_session_options: SessionsRegisterExtensionToolsOnSessionOptions sessions_release_lock_request: SessionsReleaseLockRequest sessions_release_lock_result: SessionsReleaseLockResult @@ -36071,6 +37516,11 @@ class RPC: skill_discovery_path_list: SkillDiscoveryPathList skill_discovery_scope: SkillDiscoveryScope skill_list: SkillList + skill_provider_descriptor: SkillProviderDescriptor + skill_provider_list_request: SkillProviderListRequest + skill_provider_list_result: _SkillProviderListResult + skill_provider_read_request: _SkillProviderReadRequest + skill_provider_read_result: _SkillProviderReadResult skills_config_set_disabled_skills_request: SkillsConfigSetDisabledSkillsRequest skills_config_set_skill_disabled_request: SkillsConfigSetSkillDisabledRequest skills_disable_request: SkillsDisableRequest @@ -36101,10 +37551,21 @@ class RPC: subagent_settings_entry_context_tier: SubagentSettingsEntryContextTier task_agent_info: TaskAgentInfo task_agent_progress: TaskAgentProgress + task_client_active_status: TaskClientActiveStatus + task_client_execution_mode: TaskClientExecutionMode + task_client_info: TaskClientInfo + task_client_owner: TaskClientOwner + task_client_owner_kind: TaskClientOwnerKind + task_client_owner_presence: TaskClientOwnerPresence + task_client_progress: TaskClientProgress + task_client_status: TaskClientStatus + task_client_type: TaskClientType + task_client_update: TaskClientUpdate task_complete_data: TaskCompleteData task_completion_decision: TaskCompletionDecision task_execution_mode: TaskExecutionMode task_info: TaskInfo + task_kind: TaskKind task_list: TaskList task_progress_line: TaskProgressLine tasks_cancel_request: TasksCancelRequest @@ -36119,6 +37580,8 @@ class RPC: tasks_promote_to_background_request: TasksPromoteToBackgroundRequest tasks_promote_to_background_result: TasksPromoteToBackgroundResult tasks_refresh_result: TasksRefreshResult + tasks_register_request: TasksRegisterRequest + tasks_register_result: TasksRegisterResult tasks_remove_request: TasksRemoveRequest tasks_remove_result: TasksRemoveResult tasks_send_message_request: TasksSendMessageRequest @@ -36126,6 +37589,8 @@ class RPC: tasks_start_agent_request: TasksStartAgentRequest tasks_start_agent_result: TasksStartAgentResult task_status: TaskStatus + tasks_update_request: TasksUpdateRequest + tasks_update_result: TasksUpdateResult tasks_wait_for_pending_result: TasksWaitForPendingResult telemetry_set_feature_overrides_request: TelemetrySetFeatureOverridesRequest token_auth_info: TokenAuthInfo @@ -36292,6 +37757,10 @@ def from_dict(obj: Any) -> 'RPC': auth_info_type = AuthInfoType(obj.get("AuthInfoType")) auth_validation_error = AuthValidationError.from_dict(obj.get("AuthValidationError")) auth_validation_errors = from_list(AuthValidationError.from_dict, obj.get("AuthValidationErrors")) + autopilot_objective_credit_limit = AutopilotObjectiveCreditLimit.from_dict(obj.get("AutopilotObjectiveCreditLimit")) + autopilot_objective_get_state_result = AutopilotObjectiveGetStateResult.from_dict(obj.get("AutopilotObjectiveGetStateResult")) + autopilot_objective_state = AutopilotObjectiveState.from_dict(obj.get("AutopilotObjectiveState")) + autopilot_objective_status = AutopilotObjectiveStatus(obj.get("AutopilotObjectiveStatus")) built_in_model_catalog = BuiltInModelCatalog.from_dict(obj.get("BuiltInModelCatalog")) built_in_model_catalog_entry = BuiltInModelCatalogEntry.from_dict(obj.get("BuiltInModelCatalogEntry")) builtin_tool_descriptor = BuiltinToolDescriptor.from_dict(obj.get("BuiltinToolDescriptor")) @@ -36366,6 +37835,9 @@ def from_dict(obj: Any) -> 'RPC': catalog_unsafe_retrieval_error = CatalogUnsafeRetrievalError.from_dict(obj.get("CatalogUnsafeRetrievalError")) catalog_unsafe_retrieval_reason = CatalogUnsafeRetrievalReason(obj.get("CatalogUnsafeRetrievalReason")) catalog_unsupported_kind_error = CatalogUnsupportedKindError.from_dict(obj.get("CatalogUnsupportedKindError")) + client_task_cancel_reason = ClientTaskCancelReason(obj.get("ClientTaskCancelReason")) + client_task_cancel_request = ClientTaskCancelRequest.from_dict(obj.get("ClientTaskCancelRequest")) + client_task_cancel_result = ClientTaskCancelResult.from_dict(obj.get("ClientTaskCancelResult")) command_list = CommandList.from_dict(obj.get("CommandList")) commands_finalize_invocation_effect_request = CommandsFinalizeInvocationEffectRequest.from_dict(obj.get("CommandsFinalizeInvocationEffectRequest")) commands_finalize_invocation_effect_result = CommandsFinalizeInvocationEffectResult.from_dict(obj.get("CommandsFinalizeInvocationEffectResult")) @@ -36421,6 +37893,7 @@ def from_dict(obj: Any) -> 'RPC': discovered_extensions_disable_request = DiscoveredExtensionsDisableRequest.from_dict(obj.get("DiscoveredExtensionsDisableRequest")) discovered_extensions_enable_request = DiscoveredExtensionsEnableRequest.from_dict(obj.get("DiscoveredExtensionsEnableRequest")) discovered_extension_source = DiscoveredExtensionSource(obj.get("DiscoveredExtensionSource")) + discovered_hook = DiscoveredHook.from_dict(obj.get("DiscoveredHook")) discovered_mcp_server = DiscoveredMCPServer.from_dict(obj.get("DiscoveredMcpServer")) discovered_mcp_server_type = DiscoveredMCPServerType(obj.get("DiscoveredMcpServerType")) enqueue_command_params = EnqueueCommandParams.from_dict(obj.get("EnqueueCommandParams")) @@ -36543,7 +38016,10 @@ def from_dict(obj: Any) -> 'RPC': hmac_auth_info = HMACAuthInfo.from_dict(obj.get("HMACAuthInfo")) hook_invoke_request = _HookInvokeRequest.from_dict(obj.get("HookInvokeRequest")) hook_invoke_response = _HookInvokeResponse.from_dict(obj.get("HookInvokeResponse")) - hook_type = _HookType(obj.get("HookType")) + hook_origin = HookOrigin(obj.get("HookOrigin")) + hooks_discover_request = HooksDiscoverRequest.from_dict(obj.get("HooksDiscoverRequest")) + hooks_discover_result = HooksDiscoverResult.from_dict(obj.get("HooksDiscoverResult")) + hook_type = HookType(obj.get("HookType")) installed_plugin = InstalledPlugin.from_dict(obj.get("InstalledPlugin")) installed_plugin_info = InstalledPluginInfo.from_dict(obj.get("InstalledPluginInfo")) installed_plugin_source = from_union([InstalledPluginSource.from_dict, from_str], obj.get("InstalledPluginSource")) @@ -36781,6 +38257,9 @@ def from_dict(obj: Any) -> 'RPC': model_set_reasoning_effort_request = ModelSetReasoningEffortRequest.from_dict(obj.get("ModelSetReasoningEffortRequest")) model_set_reasoning_effort_result = ModelSetReasoningEffortResult.from_dict(obj.get("ModelSetReasoningEffortResult")) models_list_request = ModelsListRequest.from_dict(obj.get("ModelsListRequest")) + model_switch_auto_tier_request = ModelSwitchAutoTierRequest.from_dict(obj.get("ModelSwitchAutoTierRequest")) + model_switch_auto_tier_result = ModelSwitchAutoTierResult.from_dict(obj.get("ModelSwitchAutoTierResult")) + model_switch_auto_tier_status = ModelSwitchAutoTierStatus(obj.get("ModelSwitchAutoTierStatus")) model_switch_confirmation = ModelSwitchConfirmation.from_dict(obj.get("ModelSwitchConfirmation")) model_switch_to_request = ModelSwitchToRequest.from_dict(obj.get("ModelSwitchToRequest")) model_switch_to_result = ModelSwitchToResult.from_dict(obj.get("ModelSwitchToResult")) @@ -36920,6 +38399,7 @@ def from_dict(obj: Any) -> 'RPC': plan_update_request = PlanUpdateRequest.from_dict(obj.get("PlanUpdateRequest")) plugin = Plugin.from_dict(obj.get("Plugin")) plugin_install_result = PluginInstallResult.from_dict(obj.get("PluginInstallResult")) + plugin_install_staging_mode = PluginInstallStagingMode(obj.get("PluginInstallStagingMode")) plugin_list = PluginList.from_dict(obj.get("PluginList")) plugin_list_result = PluginListResult.from_dict(obj.get("PluginListResult")) plugins_builtin_set_request = PluginsBuiltinSetRequest.from_dict(obj.get("PluginsBuiltinSetRequest")) @@ -37216,6 +38696,7 @@ def from_dict(obj: Any) -> 'RPC': sessions_open_status = SessionsOpenStatus(obj.get("SessionsOpenStatus")) session_source = SessionSource(obj.get("SessionSource")) sessions_prune_old_request = SessionsPruneOldRequest.from_dict(obj.get("SessionsPruneOldRequest")) + sessions_read_persisted_events_request = SessionsReadPersistedEventsRequest.from_dict(obj.get("SessionsReadPersistedEventsRequest")) sessions_register_extension_tools_on_session_options = SessionsRegisterExtensionToolsOnSessionOptions.from_dict(obj.get("SessionsRegisterExtensionToolsOnSessionOptions")) sessions_release_lock_request = SessionsReleaseLockRequest.from_dict(obj.get("SessionsReleaseLockRequest")) sessions_release_lock_result = SessionsReleaseLockResult.from_dict(obj.get("SessionsReleaseLockResult")) @@ -37255,6 +38736,11 @@ def from_dict(obj: Any) -> 'RPC': skill_discovery_path_list = SkillDiscoveryPathList.from_dict(obj.get("SkillDiscoveryPathList")) skill_discovery_scope = SkillDiscoveryScope(obj.get("SkillDiscoveryScope")) skill_list = SkillList.from_dict(obj.get("SkillList")) + skill_provider_descriptor = SkillProviderDescriptor.from_dict(obj.get("SkillProviderDescriptor")) + skill_provider_list_request = SkillProviderListRequest.from_dict(obj.get("SkillProviderListRequest")) + skill_provider_list_result = _SkillProviderListResult.from_dict(obj.get("SkillProviderListResult")) + skill_provider_read_request = _SkillProviderReadRequest.from_dict(obj.get("SkillProviderReadRequest")) + skill_provider_read_result = _SkillProviderReadResult.from_dict(obj.get("SkillProviderReadResult")) skills_config_set_disabled_skills_request = SkillsConfigSetDisabledSkillsRequest.from_dict(obj.get("SkillsConfigSetDisabledSkillsRequest")) skills_config_set_skill_disabled_request = SkillsConfigSetSkillDisabledRequest.from_dict(obj.get("SkillsConfigSetSkillDisabledRequest")) skills_disable_request = SkillsDisableRequest.from_dict(obj.get("SkillsDisableRequest")) @@ -37285,10 +38771,21 @@ def from_dict(obj: Any) -> 'RPC': subagent_settings_entry_context_tier = SubagentSettingsEntryContextTier(obj.get("SubagentSettingsEntryContextTier")) task_agent_info = TaskAgentInfo.from_dict(obj.get("TaskAgentInfo")) task_agent_progress = TaskAgentProgress.from_dict(obj.get("TaskAgentProgress")) + task_client_active_status = TaskClientActiveStatus(obj.get("TaskClientActiveStatus")) + task_client_execution_mode = TaskClientExecutionMode(obj.get("TaskClientExecutionMode")) + task_client_info = TaskClientInfo.from_dict(obj.get("TaskClientInfo")) + task_client_owner = TaskClientOwner.from_dict(obj.get("TaskClientOwner")) + task_client_owner_kind = TaskClientOwnerKind(obj.get("TaskClientOwnerKind")) + task_client_owner_presence = TaskClientOwnerPresence(obj.get("TaskClientOwnerPresence")) + task_client_progress = TaskClientProgress.from_dict(obj.get("TaskClientProgress")) + task_client_status = TaskClientStatus(obj.get("TaskClientStatus")) + task_client_type = TaskClientType(obj.get("TaskClientType")) + task_client_update = TaskClientUpdate.from_dict(obj.get("TaskClientUpdate")) task_complete_data = TaskCompleteData.from_dict(obj.get("TaskCompleteData")) task_completion_decision = TaskCompletionDecision.from_dict(obj.get("TaskCompletionDecision")) task_execution_mode = TaskExecutionMode(obj.get("TaskExecutionMode")) task_info = _load_TaskInfo(obj.get("TaskInfo")) + task_kind = TaskKind(obj.get("TaskKind")) task_list = TaskList.from_dict(obj.get("TaskList")) task_progress_line = TaskProgressLine.from_dict(obj.get("TaskProgressLine")) tasks_cancel_request = TasksCancelRequest.from_dict(obj.get("TasksCancelRequest")) @@ -37303,6 +38800,8 @@ def from_dict(obj: Any) -> 'RPC': tasks_promote_to_background_request = TasksPromoteToBackgroundRequest.from_dict(obj.get("TasksPromoteToBackgroundRequest")) tasks_promote_to_background_result = TasksPromoteToBackgroundResult.from_dict(obj.get("TasksPromoteToBackgroundResult")) tasks_refresh_result = TasksRefreshResult.from_dict(obj.get("TasksRefreshResult")) + tasks_register_request = TasksRegisterRequest.from_dict(obj.get("TasksRegisterRequest")) + tasks_register_result = TasksRegisterResult.from_dict(obj.get("TasksRegisterResult")) tasks_remove_request = TasksRemoveRequest.from_dict(obj.get("TasksRemoveRequest")) tasks_remove_result = TasksRemoveResult.from_dict(obj.get("TasksRemoveResult")) tasks_send_message_request = TasksSendMessageRequest.from_dict(obj.get("TasksSendMessageRequest")) @@ -37310,6 +38809,8 @@ def from_dict(obj: Any) -> 'RPC': tasks_start_agent_request = TasksStartAgentRequest.from_dict(obj.get("TasksStartAgentRequest")) tasks_start_agent_result = TasksStartAgentResult.from_dict(obj.get("TasksStartAgentResult")) task_status = TaskStatus(obj.get("TaskStatus")) + tasks_update_request = TasksUpdateRequest.from_dict(obj.get("TasksUpdateRequest")) + tasks_update_result = TasksUpdateResult.from_dict(obj.get("TasksUpdateResult")) tasks_wait_for_pending_result = TasksWaitForPendingResult.from_dict(obj.get("TasksWaitForPendingResult")) telemetry_set_feature_overrides_request = TelemetrySetFeatureOverridesRequest.from_dict(obj.get("TelemetrySetFeatureOverridesRequest")) token_auth_info = TokenAuthInfo.from_dict(obj.get("TokenAuthInfo")) @@ -37423,7 +38924,7 @@ def from_dict(obj: Any) -> 'RPC': subagent_settings = from_union([SubagentSettings.from_dict, from_none], obj.get("SubagentSettings")) task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress")) workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary")) - return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_type, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, managed_settings_read_result, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_external_tool_defer, protocol_external_tool_definition, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) + return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, adaptive_thinking_support, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_list_request, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agent_set_prompt_request, agents_get_discovery_paths_request, api_key_auth_info, auth_identity, auth_info, auth_info_type, auth_validation_error, auth_validation_errors, autopilot_objective_credit_limit, autopilot_objective_get_state_result, autopilot_objective_state, autopilot_objective_status, built_in_model_catalog, built_in_model_catalog_entry, builtin_tool_descriptor, builtin_tool_format, builtin_tool_format_type, builtin_tool_input_schema, builtin_tool_input_schema_type, builtin_tool_safe_for_telemetry, builtin_tool_safe_telemetry_fields, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_provider_register_request, canvas_provider_unregister_request, canvas_session_context, capi_session_options, card_digest, card_digest_algorithm, card_digest_value, catalog_ai_skill_candidate, catalog_ai_skill_candidate_provenance, catalog_authentication_required_error, catalog_authentication_required_reason, catalog_candidate, catalog_candidate_kind, catalog_candidate_source, catalog_candidate_source_embedded, catalog_candidate_source_url, catalog_capability, catalog_capability_id, catalog_client_contract, catalog_contract_violation_error, catalog_contract_violation_reason, catalog_handle_rejected_error, catalog_handle_rejection_reason, catalog_handle_type, catalog_invalid_request_error, catalog_invalid_request_field, catalog_malformed_card_error, catalog_malformed_card_reason, catalog_mcp_server_candidate, catalog_mcp_server_candidate_provenance, catalog_mcp_server_installability, catalog_media_type, catalog_negotiated_contract, catalog_negotiation_refused_error, catalog_negotiation_refused_reason, catalog_network_failure_error, catalog_network_failure_reason, catalog_not_installable_error, catalog_not_installable_reason, catalog_policy_rejected_error, catalog_search_request, catalog_search_result, catalog_search_succeeded, catalog_unavailable_error, catalog_unavailable_reason, catalog_unavailable_transport_error, catalog_unavailable_transport_reason, catalog_unsafe_retrieval_error, catalog_unsafe_retrieval_reason, catalog_unsupported_kind_error, client_task_cancel_reason, client_task_cancel_request, client_task_cancel_result, command_list, commands_finalize_invocation_effect_request, commands_finalize_invocation_effect_result, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invocation_effect_outcome, commands_invocation_origin, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, completions_get_trigger_characters_result, completions_request_request, completions_request_result, configure_session_extensions_params, connect_client_info, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_exclusion_check_paths_request, content_exclusion_check_paths_result, content_exclusion_path_check, content_filter_mode, context_heaviest_message, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, debug_collect_logs_collected_entry, debug_collect_logs_destination, debug_collect_logs_entry, debug_collect_logs_entry_kind, debug_collect_logs_include, debug_collect_logs_redaction, debug_collect_logs_request, debug_collect_logs_result, debug_collect_logs_result_kind, debug_collect_logs_skipped_entry, debug_collect_logs_source, discovered_canvas, discovered_extension, discovered_extension_mode, discovered_extension_plugin, discovered_extensions, discovered_extensions_disable_request, discovered_extensions_enable_request, discovered_extension_source, discovered_hook, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_direction, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_launch_profile, extension_launch_provider_resolve_request, extension_launch_provider_resolve_result, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_shell_exit, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, factory_abort_request, factory_ack_result, factory_agent_options, factory_agent_request, factory_agent_result, factory_agent_summary, factory_cancel_request, factory_current_phase, factory_declared_limits, factory_durable_operation, factory_execute_request, factory_execute_result, factory_get_run_progress_request, factory_get_run_request, factory_journal_get_request, factory_journal_get_result, factory_journal_put_request, factory_list_runs_request, factory_list_runs_result, factory_log_line, factory_log_line_kind, factory_log_request, factory_phase_observation, factory_phase_status, factory_progress_line, factory_progress_page, factory_resume_request, factory_resume_result, factory_run_consumed, factory_run_detail, factory_run_failure, factory_run_failure_kind, factory_run_limits, factory_run_request, factory_run_result, factory_run_status, factory_run_summary, factory_run_terminal, factory_tool_resume_request, factory_tool_run_options, factory_tool_run_request, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, git_hub_telemetry_client_info, git_hub_telemetry_event, git_hub_telemetry_notification, git_hub_token_acquire_reason, git_hub_token_acquire_request, git_hub_token_acquire_result, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_clear_context_request, history_clear_context_result, history_compact_context_window, history_compact_request, history_compact_result, history_file_restore_skip_reason, history_list_rewind_points_result, history_preview_rewind_request, history_preview_rewind_result, history_rewind_change_type, history_rewind_file_preview, history_rewind_mode, history_rewind_outcome, history_rewind_point, history_rewind_request, history_rewind_result, history_rewind_unavailable_reason, history_skipped_file_restore, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, hook_invoke_request, hook_invoke_response, hook_origin, hooks_discover_request, hooks_discover_result, hook_type, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, interrupt_main_turn_request, interrupt_main_turn_result, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, managed_settings_read_result, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_elicitation_form_mode, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_failed_server, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_install_plan, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_authentication_state_changed_request, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_probe_needs_auth_reason, mcp_oauth_probe_request, mcp_oauth_probe_result, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_plan_configuration_change, mcp_plan_configuration_operation, mcp_plan_enum_value_type, mcp_plan_install_planned, mcp_plan_install_request, mcp_plan_install_result, mcp_plan_install_source, mcp_plan_install_source_candidate, mcp_plan_install_source_candidate_kind, mcp_plan_install_source_card, mcp_plan_install_source_card_kind, mcp_plan_package_install_method, mcp_plan_package_transport, mcp_plan_policy_decision, mcp_plan_policy_result, mcp_plan_policy_source, mcp_plan_provenance, mcp_plan_remote_install_method, mcp_plan_remote_transport, mcp_plan_required_value, mcp_plan_required_value_enum, mcp_plan_required_value_enum_kind, mcp_plan_required_value_scalar, mcp_plan_required_value_scalar_kind, mcp_plan_resource_identity, mcp_plan_scalar_value_type, mcp_plan_scope, mcp_plan_secret_placeholder, mcp_plan_secret_reference, mcp_plan_target, mcp_plan_transport_choice, mcp_plan_transport_choice_package, mcp_plan_transport_choice_remote, mcp_plan_value_category, mcp_register_external_client_request, mcp_reload_config, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_resource, mcp_resource_annotations, mcp_resource_content, mcp_resource_icon, mcp_resources_list_request, mcp_resources_list_result, mcp_resources_list_templates_request, mcp_resources_list_templates_result, mcp_resources_read_request, mcp_resources_read_result, mcp_resource_template, mcp_restart_server_request, mcp_safe_for_telemetry, mcp_safe_for_telemetry_fields, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_serializable_server_config, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_card_embedded, mcp_server_card_embedded_kind, mcp_server_card_media_type, mcp_server_card_reference, mcp_server_card_url, mcp_server_card_url_kind, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_memory, mcp_server_config_memory_type, mcp_server_config_stdio, mcp_server_config_stdio_type, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_task_metadata, mcp_tools, mcp_tool_ui, mcp_tool_ui_visibility, mcp_unregister_external_client_request, memory_configuration, metadata_context_attribution_result, metadata_context_heaviest_messages_request, metadata_context_heaviest_messages_result, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_apply_startup_overlay_request, model_billing, model_billing_promo, model_billing_token_prices, model_billing_token_prices_long_context, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_list_request, model_message, model_picker_category, model_picker_persistence_request, model_picker_price_category, model_picker_settings_context, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_auto_tier_request, model_switch_auto_tier_result, model_switch_auto_tier_status, model_switch_confirmation, model_switch_to_request, model_switch_to_result, model_warning_text, mode_set_request, mode_set_result, move_mcp_loading_to_background_result, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_env_access, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_factory, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_env_access, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_factory, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, permission_decision_context, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_outcome, permission_decision_reject, permission_decision_request, permission_decision_source, permission_decision_surface, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_mode_source, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_response_capability, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_mode_request, permissions_get_mode_result, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_env_access, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_factory, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_mode_request, permissions_set_mode_result, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_install_staging_mode, plugin_list, plugin_list_result, plugins_builtin_set_request, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, protocol_external_tool_defer, protocol_external_tool_definition, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_actions_job, push_attachment_git_hub_commit, push_attachment_git_hub_file, push_attachment_git_hub_file_diff, push_attachment_git_hub_file_diff_side, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_git_hub_release, push_attachment_git_hub_repository, push_attachment_git_hub_snippet, push_attachment_git_hub_tree_comparison, push_attachment_git_hub_tree_comparison_side, push_attachment_git_hub_url, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, push_git_hub_repo_ref, queue_begin_deferred_idle_drain_request, queue_begin_deferred_idle_drain_result, queue_consume_system_notifications_request, queued_command_handled, queued_command_not_handled, queued_command_result, queue_defer_session_idle_request, queue_duplicate_at_request, queue_duplicate_at_result, queue_enqueue_resume_pending_result, queue_finish_deferred_idle_drain_request, queue_finish_deferred_idle_drain_result, queue_has_pending_result, queue_insert_at_request, queue_insert_at_result, queue_insert_message, queue_move_item_request, queue_move_item_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_at_request, queue_remove_at_result, queue_remove_most_recent_result, queue_send_now_request, queue_send_now_result, queue_set_drain_paused_request, queue_snapshot_result, queue_update_text_request, queue_update_text_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_host_status, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, run_options, sandbox_config, sandbox_config_auth, sandbox_config_source, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_network_proxy, sandbox_config_user_policy_seatbelt, sandbox_enforcement_status, schedule_add_at_request, schedule_add_cron_request, schedule_add_request, schedule_add_result, schedule_add_self_paced_request, schedule_entry, schedule_has_self_paced_result, schedule_list, schedule_rearm_self_paced_request, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_message_item, send_messages_request, send_messages_result, send_mode, send_request, send_result, send_system_notification_request, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_agent_list_request, session_auth_login_request, session_auth_logout_user_request, session_auth_status, session_auth_switch_request, session_bulk_delete_result, session_cancel_all_background_agents_result, session_capability, session_commands_list_request, session_completion_item, session_context, session_context_host_type, session_enrich_metadata_result, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_sqlite_transaction_error, session_fs_sqlite_transaction_error_class, session_fs_sqlite_transaction_request, session_fs_sqlite_transaction_result, session_fs_sqlite_transaction_statement, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_git_hub_auth_get_all_auth_available_result, session_git_hub_auth_logout_result, session_git_hub_auth_logout_user_result, session_history_compact_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_limit_prediction_baseline_data, session_limit_prediction_client_type, session_limit_prediction_details, session_limit_prediction_predict_request, session_limit_prediction_request, session_limit_prediction_result, session_limit_prediction_source, session_limit_prediction_tier, session_limit_prediction_tier_option, session_limit_prediction_unavailable_reason, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_managed_permissions, session_managed_settings, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_model_list_request, session_model_price_category, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_plugins_reload_request, session_provider_get_endpoint_request, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_delete_request, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, session_settings_built_in_tool_availability_snapshot, session_settings_evaluate_predicate_request, session_settings_evaluate_predicate_result, session_settings_job_snapshot, session_settings_model_snapshot, session_settings_online_evaluation_snapshot, session_settings_predicate_name, session_settings_repo_snapshot, session_settings_snapshot, session_settings_validation_snapshot, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_metadata_request, sessions_get_metadata_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_non_empty_session_ids_request, sessions_list_non_empty_session_ids_result, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_prune_old_request, sessions_read_persisted_events_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_visibility_status, session_working_directory_context, session_working_directory_context_host_type, settable_auth_info, settable_token_auth_info, shell_cancel_user_requested_request, shell_credentials, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_init_profile, shell_init_script, shell_init_script_shell, shell_kill_request, shell_kill_result, shell_kill_signal, shell_options, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skill_provider_descriptor, skill_provider_list_request, skill_provider_list_result, skill_provider_read_request, skill_provider_read_result, skills_config_set_disabled_skills_request, skills_config_set_skill_disabled_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_add_timeline_entry_result, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_choice, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_model_picker_dialog, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_set_model_result, slash_command_set_plan_model_result, slash_command_show_dialog_result, slash_command_text_result, slash_command_timeline_entry, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_client_active_status, task_client_execution_mode, task_client_info, task_client_owner, task_client_owner_kind, task_client_owner_presence, task_client_progress, task_client_status, task_client_type, task_client_update, task_complete_data, task_completion_decision, task_execution_mode, task_info, task_kind, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_register_request, tasks_register_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_update_request, tasks_update_result, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, token_provider_auth_info, tool, tool_list, tool_result, tool_result_expanded, tool_result_new_message, tool_result_type, tools_execute_request, tools_get_builtin_descriptors_request, tools_get_builtin_descriptors_result, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_set_request, tools_set_result, tools_shell_descriptor_config, tools_task_complete_event_data_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_session_limits_exhausted_request, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_session_limits_exhausted_response, ui_session_limits_exhausted_response_action, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_agent_metric, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, user_setting_metadata, user_settings_get_result, user_settings_set_request, user_settings_set_result, visibility_get_result, visibility_set_request, visibility_set_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_add_summary_request, workspaces_add_summary_result, workspaces_autopilot_objective_exists_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_delete_autopilot_objective_result, workspaces_diff_request, workspaces_ensure_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_autopilot_objective_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspaces_truncate_summaries_request, workspace_summary_host_type, workspaces_update_metadata_request, workspaces_workspace_details_host_type, workspaces_write_autopilot_objective_request, workspaces_write_autopilot_objective_result, session_auth_info_result, session_context_attribution, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -37476,6 +38977,10 @@ def to_dict(self) -> dict: result["AuthInfoType"] = to_enum(AuthInfoType, self.auth_info_type) result["AuthValidationError"] = to_class(AuthValidationError, self.auth_validation_error) result["AuthValidationErrors"] = from_list(lambda x: to_class(AuthValidationError, x), self.auth_validation_errors) + result["AutopilotObjectiveCreditLimit"] = to_class(AutopilotObjectiveCreditLimit, self.autopilot_objective_credit_limit) + result["AutopilotObjectiveGetStateResult"] = to_class(AutopilotObjectiveGetStateResult, self.autopilot_objective_get_state_result) + result["AutopilotObjectiveState"] = to_class(AutopilotObjectiveState, self.autopilot_objective_state) + result["AutopilotObjectiveStatus"] = to_enum(AutopilotObjectiveStatus, self.autopilot_objective_status) result["BuiltInModelCatalog"] = to_class(BuiltInModelCatalog, self.built_in_model_catalog) result["BuiltInModelCatalogEntry"] = to_class(BuiltInModelCatalogEntry, self.built_in_model_catalog_entry) result["BuiltinToolDescriptor"] = to_class(BuiltinToolDescriptor, self.builtin_tool_descriptor) @@ -37550,6 +39055,9 @@ def to_dict(self) -> dict: result["CatalogUnsafeRetrievalError"] = to_class(CatalogUnsafeRetrievalError, self.catalog_unsafe_retrieval_error) result["CatalogUnsafeRetrievalReason"] = to_enum(CatalogUnsafeRetrievalReason, self.catalog_unsafe_retrieval_reason) result["CatalogUnsupportedKindError"] = to_class(CatalogUnsupportedKindError, self.catalog_unsupported_kind_error) + result["ClientTaskCancelReason"] = to_enum(ClientTaskCancelReason, self.client_task_cancel_reason) + result["ClientTaskCancelRequest"] = to_class(ClientTaskCancelRequest, self.client_task_cancel_request) + result["ClientTaskCancelResult"] = to_class(ClientTaskCancelResult, self.client_task_cancel_result) result["CommandList"] = to_class(CommandList, self.command_list) result["CommandsFinalizeInvocationEffectRequest"] = to_class(CommandsFinalizeInvocationEffectRequest, self.commands_finalize_invocation_effect_request) result["CommandsFinalizeInvocationEffectResult"] = to_class(CommandsFinalizeInvocationEffectResult, self.commands_finalize_invocation_effect_result) @@ -37605,6 +39113,7 @@ def to_dict(self) -> dict: result["DiscoveredExtensionsDisableRequest"] = to_class(DiscoveredExtensionsDisableRequest, self.discovered_extensions_disable_request) result["DiscoveredExtensionsEnableRequest"] = to_class(DiscoveredExtensionsEnableRequest, self.discovered_extensions_enable_request) result["DiscoveredExtensionSource"] = to_enum(DiscoveredExtensionSource, self.discovered_extension_source) + result["DiscoveredHook"] = to_class(DiscoveredHook, self.discovered_hook) result["DiscoveredMcpServer"] = to_class(DiscoveredMCPServer, self.discovered_mcp_server) result["DiscoveredMcpServerType"] = to_enum(DiscoveredMCPServerType, self.discovered_mcp_server_type) result["EnqueueCommandParams"] = to_class(EnqueueCommandParams, self.enqueue_command_params) @@ -37727,7 +39236,10 @@ def to_dict(self) -> dict: result["HMACAuthInfo"] = to_class(HMACAuthInfo, self.hmac_auth_info) result["HookInvokeRequest"] = to_class(_HookInvokeRequest, self.hook_invoke_request) result["HookInvokeResponse"] = to_class(_HookInvokeResponse, self.hook_invoke_response) - result["HookType"] = to_enum(_HookType, self.hook_type) + result["HookOrigin"] = to_enum(HookOrigin, self.hook_origin) + result["HooksDiscoverRequest"] = to_class(HooksDiscoverRequest, self.hooks_discover_request) + result["HooksDiscoverResult"] = to_class(HooksDiscoverResult, self.hooks_discover_result) + result["HookType"] = to_enum(HookType, self.hook_type) result["InstalledPlugin"] = to_class(InstalledPlugin, self.installed_plugin) result["InstalledPluginInfo"] = to_class(InstalledPluginInfo, self.installed_plugin_info) result["InstalledPluginSource"] = from_union([lambda x: to_class(InstalledPluginSource, x), from_str], self.installed_plugin_source) @@ -37965,6 +39477,9 @@ def to_dict(self) -> dict: result["ModelSetReasoningEffortRequest"] = to_class(ModelSetReasoningEffortRequest, self.model_set_reasoning_effort_request) result["ModelSetReasoningEffortResult"] = to_class(ModelSetReasoningEffortResult, self.model_set_reasoning_effort_result) result["ModelsListRequest"] = to_class(ModelsListRequest, self.models_list_request) + result["ModelSwitchAutoTierRequest"] = to_class(ModelSwitchAutoTierRequest, self.model_switch_auto_tier_request) + result["ModelSwitchAutoTierResult"] = to_class(ModelSwitchAutoTierResult, self.model_switch_auto_tier_result) + result["ModelSwitchAutoTierStatus"] = to_enum(ModelSwitchAutoTierStatus, self.model_switch_auto_tier_status) result["ModelSwitchConfirmation"] = to_class(ModelSwitchConfirmation, self.model_switch_confirmation) result["ModelSwitchToRequest"] = to_class(ModelSwitchToRequest, self.model_switch_to_request) result["ModelSwitchToResult"] = to_class(ModelSwitchToResult, self.model_switch_to_result) @@ -38104,6 +39619,7 @@ def to_dict(self) -> dict: result["PlanUpdateRequest"] = to_class(PlanUpdateRequest, self.plan_update_request) result["Plugin"] = to_class(Plugin, self.plugin) result["PluginInstallResult"] = to_class(PluginInstallResult, self.plugin_install_result) + result["PluginInstallStagingMode"] = to_enum(PluginInstallStagingMode, self.plugin_install_staging_mode) result["PluginList"] = to_class(PluginList, self.plugin_list) result["PluginListResult"] = to_class(PluginListResult, self.plugin_list_result) result["PluginsBuiltinSetRequest"] = to_class(PluginsBuiltinSetRequest, self.plugins_builtin_set_request) @@ -38400,6 +39916,7 @@ def to_dict(self) -> dict: result["SessionsOpenStatus"] = to_enum(SessionsOpenStatus, self.sessions_open_status) result["SessionSource"] = to_enum(SessionSource, self.session_source) result["SessionsPruneOldRequest"] = to_class(SessionsPruneOldRequest, self.sessions_prune_old_request) + result["SessionsReadPersistedEventsRequest"] = to_class(SessionsReadPersistedEventsRequest, self.sessions_read_persisted_events_request) result["SessionsRegisterExtensionToolsOnSessionOptions"] = to_class(SessionsRegisterExtensionToolsOnSessionOptions, self.sessions_register_extension_tools_on_session_options) result["SessionsReleaseLockRequest"] = to_class(SessionsReleaseLockRequest, self.sessions_release_lock_request) result["SessionsReleaseLockResult"] = to_class(SessionsReleaseLockResult, self.sessions_release_lock_result) @@ -38439,6 +39956,11 @@ def to_dict(self) -> dict: result["SkillDiscoveryPathList"] = to_class(SkillDiscoveryPathList, self.skill_discovery_path_list) result["SkillDiscoveryScope"] = to_enum(SkillDiscoveryScope, self.skill_discovery_scope) result["SkillList"] = to_class(SkillList, self.skill_list) + result["SkillProviderDescriptor"] = to_class(SkillProviderDescriptor, self.skill_provider_descriptor) + result["SkillProviderListRequest"] = to_class(SkillProviderListRequest, self.skill_provider_list_request) + result["SkillProviderListResult"] = to_class(_SkillProviderListResult, self.skill_provider_list_result) + result["SkillProviderReadRequest"] = to_class(_SkillProviderReadRequest, self.skill_provider_read_request) + result["SkillProviderReadResult"] = to_class(_SkillProviderReadResult, self.skill_provider_read_result) result["SkillsConfigSetDisabledSkillsRequest"] = to_class(SkillsConfigSetDisabledSkillsRequest, self.skills_config_set_disabled_skills_request) result["SkillsConfigSetSkillDisabledRequest"] = to_class(SkillsConfigSetSkillDisabledRequest, self.skills_config_set_skill_disabled_request) result["SkillsDisableRequest"] = to_class(SkillsDisableRequest, self.skills_disable_request) @@ -38469,10 +39991,21 @@ def to_dict(self) -> dict: result["SubagentSettingsEntryContextTier"] = to_enum(SubagentSettingsEntryContextTier, self.subagent_settings_entry_context_tier) result["TaskAgentInfo"] = to_class(TaskAgentInfo, self.task_agent_info) result["TaskAgentProgress"] = to_class(TaskAgentProgress, self.task_agent_progress) + result["TaskClientActiveStatus"] = to_enum(TaskClientActiveStatus, self.task_client_active_status) + result["TaskClientExecutionMode"] = to_enum(TaskClientExecutionMode, self.task_client_execution_mode) + result["TaskClientInfo"] = to_class(TaskClientInfo, self.task_client_info) + result["TaskClientOwner"] = to_class(TaskClientOwner, self.task_client_owner) + result["TaskClientOwnerKind"] = to_enum(TaskClientOwnerKind, self.task_client_owner_kind) + result["TaskClientOwnerPresence"] = to_enum(TaskClientOwnerPresence, self.task_client_owner_presence) + result["TaskClientProgress"] = to_class(TaskClientProgress, self.task_client_progress) + result["TaskClientStatus"] = to_enum(TaskClientStatus, self.task_client_status) + result["TaskClientType"] = to_enum(TaskClientType, self.task_client_type) + result["TaskClientUpdate"] = to_class(TaskClientUpdate, self.task_client_update) result["TaskCompleteData"] = to_class(TaskCompleteData, self.task_complete_data) result["TaskCompletionDecision"] = to_class(TaskCompletionDecision, self.task_completion_decision) result["TaskExecutionMode"] = to_enum(TaskExecutionMode, self.task_execution_mode) result["TaskInfo"] = (self.task_info).to_dict() + result["TaskKind"] = to_enum(TaskKind, self.task_kind) result["TaskList"] = to_class(TaskList, self.task_list) result["TaskProgressLine"] = to_class(TaskProgressLine, self.task_progress_line) result["TasksCancelRequest"] = to_class(TasksCancelRequest, self.tasks_cancel_request) @@ -38487,6 +40020,8 @@ def to_dict(self) -> dict: result["TasksPromoteToBackgroundRequest"] = to_class(TasksPromoteToBackgroundRequest, self.tasks_promote_to_background_request) result["TasksPromoteToBackgroundResult"] = to_class(TasksPromoteToBackgroundResult, self.tasks_promote_to_background_result) result["TasksRefreshResult"] = to_class(TasksRefreshResult, self.tasks_refresh_result) + result["TasksRegisterRequest"] = to_class(TasksRegisterRequest, self.tasks_register_request) + result["TasksRegisterResult"] = to_class(TasksRegisterResult, self.tasks_register_result) result["TasksRemoveRequest"] = to_class(TasksRemoveRequest, self.tasks_remove_request) result["TasksRemoveResult"] = to_class(TasksRemoveResult, self.tasks_remove_result) result["TasksSendMessageRequest"] = to_class(TasksSendMessageRequest, self.tasks_send_message_request) @@ -38494,6 +40029,8 @@ def to_dict(self) -> dict: result["TasksStartAgentRequest"] = to_class(TasksStartAgentRequest, self.tasks_start_agent_request) result["TasksStartAgentResult"] = to_class(TasksStartAgentResult, self.tasks_start_agent_result) result["TaskStatus"] = to_enum(TaskStatus, self.task_status) + result["TasksUpdateRequest"] = to_class(TasksUpdateRequest, self.tasks_update_request) + result["TasksUpdateResult"] = to_class(TasksUpdateResult, self.tasks_update_result) result["TasksWaitForPendingResult"] = to_class(TasksWaitForPendingResult, self.tasks_wait_for_pending_result) result["TelemetrySetFeatureOverridesRequest"] = to_class(TelemetrySetFeatureOverridesRequest, self.telemetry_set_feature_overrides_request) result["TokenAuthInfo"] = to_class(TokenAuthInfo, self.token_auth_info) @@ -38950,14 +40487,15 @@ def _load_SlashCommandInvocationResult(obj: Any) -> "SlashCommandInvocationResul case "set-plan-model": return SlashCommandSetPlanModelResult.from_dict(obj) case _: raise ValueError(f"Unknown SlashCommandInvocationResult kind: {kind!r}") -# Tracked task union returned by task APIs, containing either an agent task or a shell task. -TaskInfo = TaskAgentInfo | TaskShellInfo +# Tracked task union returned by task APIs, containing an agent, client, or shell task. +TaskInfo = TaskAgentInfo | TaskClientInfo | TaskShellInfo def _load_TaskInfo(obj: Any) -> "TaskInfo": assert isinstance(obj, dict) kind = obj.get("type") match kind: case "agent": return TaskAgentInfo.from_dict(obj) + case "client": return TaskClientInfo.from_dict(obj) case "shell": return TaskShellInfo.from_dict(obj) case _: raise ValueError(f"Unknown TaskInfo type: {kind!r}") @@ -39068,6 +40606,17 @@ def _patch_model_capabilities(data: dict) -> dict: return data +# Experimental: this API group is experimental and may change or be removed. +class ServerHooksApi: + def __init__(self, client: "JsonRpcClient"): + self._client = client + + async def discover(self, params: HooksDiscoverRequest, *, timeout: float | None = None) -> HooksDiscoverResult: + "Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources.\n\nArgs:\n params: Optional project paths and host-exclusion behavior for server-scoped hook discovery.\n\nReturns:\n Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return HooksDiscoverResult.from_dict(await self._client.request("hooks.discover", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class ServerModelsApi: def __init__(self, client: "JsonRpcClient"): @@ -39413,6 +40962,10 @@ async def read(self, *, timeout: float | None = None) -> ManagedSettingsReadResu "Discovers device-managed settings from production MDM and managed-file sources, validates them against the runtime-owned managed-settings schema, and returns the canonical JSON without requiring a session.\n\nReturns:\n Validated device-managed settings discovered before a session exists." return ManagedSettingsReadResult.from_dict(await self._client.request("managedSettings.read", {}, **_timeout_kwargs(timeout))) + async def clear_cache(self, *, timeout: float | None = None) -> None: + "Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing \"sync account policy\" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed." + await self._client.request("managedSettings.clearCache", {}, **_timeout_kwargs(timeout)) + # Experimental: this API group is experimental and may change or be removed. class ServerRuntimeApi: @@ -39480,6 +41033,11 @@ async def list(self, params: SessionsListRequest, *, timeout: float | None = Non params_dict = {k: v for k, v in params.to_dict().items() if v is not None} return SessionList.from_dict(await self._client.request("sessions.list", params_dict, **_timeout_kwargs(timeout))) + async def read_persisted_events(self, params: SessionsReadPersistedEventsRequest, *, timeout: float | None = None) -> EventsReadResult: + "Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session.\n\nArgs:\n params: Pagination options for reading an inactive or active local session's persisted event journal.\n\nReturns:\n Batch of session events returned by a read, with cursor and continuation metadata." + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + return EventsReadResult.from_dict(await self._client.request("sessions.readPersistedEvents", params_dict, **_timeout_kwargs(timeout))) + async def find_by_task_id(self, params: SessionsFindByTaskIDRequest, *, timeout: float | None = None) -> SessionsFindByTaskIDResult: "Finds the local session bound to a GitHub task ID, if any.\n\nArgs:\n params: GitHub task ID to look up.\n\nReturns:\n ID of the local session bound to the given GitHub task, or omitted when none." params_dict = {k: v for k, v in params.to_dict().items() if v is not None} @@ -39589,6 +41147,7 @@ class ServerRpc: """Typed server-scoped RPC methods.""" def __init__(self, client: "JsonRpcClient"): self._client = client + self.hooks = ServerHooksApi(client) self.models = ServerModelsApi(client) self.tools = ServerToolsApi(client) self.account = ServerAccountApi(client) @@ -39847,7 +41406,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id async def get_current(self, *, timeout: float | None = None) -> CurrentModel: - "Gets the currently selected model for the session.\n\nReturns:\n The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume." + "Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn.\n\nReturns:\n The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume." return CurrentModel.from_dict(await self._client.request("session.model.getCurrent", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def switch_to(self, params: ModelSwitchToRequest, *, timeout: float | None = None) -> ModelSwitchToResult: @@ -39856,6 +41415,12 @@ async def switch_to(self, params: ModelSwitchToRequest, *, timeout: float | None params_dict["sessionId"] = self._session_id return ModelSwitchToResult.from_dict(await self._client.request("session.model.switchTo", params_dict, **_timeout_kwargs(timeout))) + async def switch_auto_tier(self, params: ModelSwitchAutoTierRequest, *, timeout: float | None = None) -> ModelSwitchAutoTierResult: + "Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`.\n\nArgs:\n params: An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`.\n\nReturns:\n Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return ModelSwitchAutoTierResult.from_dict(await self._client.request("session.model.switchAutoTier", params_dict, **_timeout_kwargs(timeout))) + async def set_reasoning_effort(self, params: ModelSetReasoningEffortRequest, *, timeout: float | None = None) -> ModelSetReasoningEffortResult: "Updates the session's reasoning effort without changing the selected model.\n\nArgs:\n params: Reasoning effort level to apply to the currently selected model.\n\nReturns:\n Update the session's reasoning effort without changing the selected model. Use `switchTo` instead when you also need to change the model. The runtime stores the effort on the session and applies it to subsequent turns." params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} @@ -40029,6 +41594,17 @@ async def diff(self, params: WorkspacesDiffRequest, *, timeout: float | None = N return WorkspaceDiffResult.from_dict(await self._client.request("session.workspaces.diff", params_dict, **_timeout_kwargs(timeout))) +# Experimental: this API group is experimental and may change or be removed. +class AutopilotObjectiveApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def get_state(self, *, timeout: float | None = None) -> AutopilotObjectiveGetStateResult: + "Reads the current canonical autopilot objective state for this session.\n\nReturns:\n Canonical runtime state for the session's current autopilot objective." + return AutopilotObjectiveGetStateResult.from_dict(await self._client.request("session.autopilotObjective.getState", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class CompletionsApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -40123,6 +41699,18 @@ async def list(self, *, timeout: float | None = None) -> TaskList: "Lists background tasks tracked by the session.\n\nReturns:\n Background tasks currently tracked by the session." return TaskList.from_dict(await self._client.request("session.tasks.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + async def register(self, params: TasksRegisterRequest, *, timeout: float | None = None) -> TasksRegisterResult: + "Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal.\n\nArgs:\n params: Registers or reclaims a client-owned task.\n\nReturns:\n Result of registering or reclaiming a client-owned task." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return TasksRegisterResult.from_dict(await self._client.request("session.tasks.register", params_dict, **_timeout_kwargs(timeout))) + + async def update(self, params: TasksUpdateRequest, *, timeout: float | None = None) -> TasksUpdateResult: + "Publishes generic progress or a terminal outcome for a client-owned task.\n\nArgs:\n params: Updates a client-owned task.\n\nReturns:\n Result of publishing a client-owned task update." + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return TasksUpdateResult.from_dict(await self._client.request("session.tasks.update", params_dict, **_timeout_kwargs(timeout))) + async def refresh(self, *, timeout: float | None = None) -> TasksRefreshResult: "Refreshes metadata for any detached background shells the runtime knows about.\n\nReturns:\n Refresh metadata for any detached background shells the runtime knows about. Use after a long pause to pick up exit/output state for shells running outside the agent loop." return TasksRefreshResult.from_dict(await self._client.request("session.tasks.refresh", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) @@ -41174,6 +42762,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.name = NameApi(client, session_id) self.plan = PlanApi(client, session_id) self.workspaces = WorkspacesApi(client, session_id) + self.autopilot_objective = AutopilotObjectiveApi(client, session_id) self.completions = CompletionsApi(client, session_id) self.instructions = InstructionsApi(client, session_id) self.fleet = FleetApi(client, session_id) @@ -41544,6 +43133,12 @@ async def abort(self, params: FactoryAbortRequest) -> FactoryACKResult: "Asks the owning extension connection to abort a running factory cooperatively.\n\nArgs:\n params: Parameters for cooperatively aborting a factory body.\n\nReturns:\n Acknowledgement that a factory request was accepted." pass +# Experimental: this API group is experimental and may change or be removed. +class TasksHandler(Protocol): + async def cancel(self, params: ClientTaskCancelRequest) -> ClientTaskCancelResult: + "Asks the client currently bound to a client-owned session task to confirm that its external work stopped.\n\nArgs:\n params: Runtime-to-owner cancellation request for a client-owned task.\n\nReturns:\n Whether the client authoritatively confirmed its external work stopped." + pass + # Experimental: this API group is experimental and may change or be removed. class SessionFsHandler(Protocol): async def read_file(self, params: SessionFSReadFileRequest) -> SessionFSReadFileResult: @@ -41602,6 +43197,7 @@ async def invoke(self, params: CanvasProviderInvokeActionRequest) -> Any: class ClientSessionApiHandlers: provider_token: ProviderTokenHandler | None = None factory: FactoryHandler | None = None + tasks: TasksHandler | None = None session_fs: SessionFsHandler | None = None canvas: CanvasHandler | None = None @@ -41631,6 +43227,13 @@ async def handle_factory_abort(params: dict) -> dict | None: result = await handler.abort(request) return result.to_dict() client.set_request_handler("factory.abort", handle_factory_abort) + async def handle_tasks_cancel(params: dict) -> dict | None: + request = ClientTaskCancelRequest.from_dict(params) + handler = get_handlers(request.session_id).tasks + if handler is None: raise RuntimeError(f"No tasks handler registered for session: {request.session_id}") + result = await handler.cancel(request) + return result.to_dict() + client.set_request_handler("tasks.cancel", handle_tasks_cancel) async def handle_session_fs_read_file(params: dict) -> dict | None: request = SessionFSReadFileRequest.from_dict(params) handler = get_handlers(request.session_id).session_fs @@ -41897,6 +43500,11 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "AuthInfoType", "AuthValidationError", "AuthValidationErrors", + "AutopilotObjectiveApi", + "AutopilotObjectiveCreditLimit", + "AutopilotObjectiveGetStateResult", + "AutopilotObjectiveState", + "AutopilotObjectiveStatus", "BuiltInModelCatalog", "BuiltInModelCatalogEntry", "BuiltinToolDescriptor", @@ -41999,6 +43607,9 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "Categories", "ClientGlobalApiHandlers", "ClientSessionApiHandlers", + "ClientTaskCancelReason", + "ClientTaskCancelRequest", + "ClientTaskCancelResult", "CommandList", "CommandsApi", "CommandsFinalizeInvocationEffectRequest", @@ -42056,6 +43667,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "DiscoveredExtensions", "DiscoveredExtensionsDisableRequest", "DiscoveredExtensionsEnableRequest", + "DiscoveredHook", "DiscoveredMCPServer", "DiscoveredMCPServerType", "EnqueueCommandParams", @@ -42199,6 +43811,10 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "HistorySummarizeForHandoffResult", "HistoryTruncateRequest", "HistoryTruncateResult", + "HookOrigin", + "HookType", + "HooksDiscoverRequest", + "HooksDiscoverResult", "HooksHandler", "Host", "HostType", @@ -42477,6 +44093,9 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ModelPolicyState", "ModelSetReasoningEffortRequest", "ModelSetReasoningEffortResult", + "ModelSwitchAutoTierRequest", + "ModelSwitchAutoTierResult", + "ModelSwitchAutoTierStatus", "ModelSwitchConfirmation", "ModelSwitchToRequest", "ModelSwitchToResult", @@ -42650,6 +44269,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "PlanUpdateRequest", "Plugin", "PluginInstallResult", + "PluginInstallStagingMode", "PluginList", "PluginListResult", "PluginUpdateAllEntry", @@ -42833,6 +44453,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "ServerCatalogApi", "ServerCommandsApi", "ServerExtensionsApi", + "ServerHooksApi", "ServerInstructionSourceList", "ServerInstructionsApi", "ServerLlmInferenceApi", @@ -43018,6 +44639,7 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SessionsOpenResumeLastKind", "SessionsOpenStatus", "SessionsPruneOldRequest", + "SessionsReadPersistedEventsRequest", "SessionsRegisterExtensionToolsOnSessionOptions", "SessionsReleaseLockRequest", "SessionsReleaseLockResult", @@ -43054,6 +44676,8 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "SkillDiscoveryPathList", "SkillDiscoveryScope", "SkillList", + "SkillProviderDescriptor", + "SkillProviderListRequest", "SkillsApi", "SkillsConfigSetDisabledSkillsRequest", "SkillsConfigSetSkillDisabledRequest", @@ -43097,13 +44721,24 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "TaskAgentInfo", "TaskAgentInfoType", "TaskAgentProgress", + "TaskClientActiveStatus", + "TaskClientExecutionMode", + "TaskClientInfo", + "TaskClientOwner", + "TaskClientOwnerKind", + "TaskClientOwnerPresence", + "TaskClientProgress", + "TaskClientStatus", + "TaskClientType", + "TaskClientUpdate", + "TaskClientUpdateKind", "TaskCompleteData", "TaskCompletionDecision", "TaskExecutionMode", "TaskInfo", "TaskInfoExecutionMode", "TaskInfoStatus", - "TaskInfoType", + "TaskKind", "TaskList", "TaskProgress", "TaskProgressLine", @@ -43119,16 +44754,21 @@ async def handle_git_hub_token_get_token(params: dict) -> dict | None: "TasksGetCurrentPromotableResult", "TasksGetProgressRequest", "TasksGetProgressResult", + "TasksHandler", "TasksPromoteCurrentToBackgroundResult", "TasksPromoteToBackgroundRequest", "TasksPromoteToBackgroundResult", "TasksRefreshResult", + "TasksRegisterRequest", + "TasksRegisterResult", "TasksRemoveRequest", "TasksRemoveResult", "TasksSendMessageRequest", "TasksSendMessageResult", "TasksStartAgentRequest", "TasksStartAgentResult", + "TasksUpdateRequest", + "TasksUpdateResult", "TasksWaitForPendingResult", "TelemetryApi", "TelemetrySetFeatureOverridesRequest", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index fd41b6c6c6..52f1053a1f 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -136,7 +136,9 @@ class SessionEventType(Enum): SESSION_INFO = "session.info" SESSION_WARNING = "session.warning" SESSION_MODEL_CHANGE = "session.model_change" + SESSION_AUTO_TIER_SWITCH_FAILED = "session.auto_tier_switch_failed" SESSION_MODE_CHANGED = "session.mode_changed" + SESSION_MODE_NOTICE_DELIVERED = "session.mode_notice_delivered" SESSION_SESSION_LIMITS_CHANGED = "session.session_limits_changed" # Experimental: this event is part of an experimental API and may change or be removed. SESSION_PERMISSIONS_CHANGED = "session.permissions_changed" @@ -155,6 +157,8 @@ class SessionEventType(Enum): SESSION_COMPACTION_COMPLETE = "session.compaction_complete" SESSION_TASK_COMPLETE = "session.task_complete" # Experimental: this event is part of an experimental API and may change or be removed. + SESSION_COMPLETION_RECEIPT = "session.completion_receipt" + # Experimental: this event is part of an experimental API and may change or be removed. SESSION_FUSION_ROUTE_STARTED = "session.fusion_route_started" # Experimental: this event is part of an experimental API and may change or be removed. SESSION_FUSION_ROUTE_FAILED = "session.fusion_route_failed" @@ -171,6 +175,8 @@ class SessionEventType(Enum): # Experimental: this event is part of an experimental API and may change or be removed. ASSISTANT_FUSION_PHASE_STARTED = "assistant.fusion_phase_started" # Experimental: this event is part of an experimental API and may change or be removed. + ASSISTANT_FUSION_PHASE_ACTIVITY = "assistant.fusion_phase_activity" + # Experimental: this event is part of an experimental API and may change or be removed. ASSISTANT_FUSION_PHASE_COMPLETED = "assistant.fusion_phase_completed" # Experimental: this event is part of an experimental API and may change or be removed. ASSISTANT_FUSION_PHASE_FAILED = "assistant.fusion_phase_failed" @@ -257,6 +263,8 @@ class SessionEventType(Enum): SESSION_CUSTOM_AGENTS_UPDATED = "session.custom_agents_updated" SESSION_MCP_SERVERS_LOADED = "session.mcp_servers_loaded" SESSION_MCP_SERVER_STATUS_CHANGED = "session.mcp_server_status_changed" + SESSION_MCP_SERVER_REMOVED = "session.mcp_server_removed" + SESSION_MCP_SERVER_NEEDS_RECONNECT = "session.mcp_server_needs_reconnect" MCP_TOOLS_LIST_CHANGED = "mcp.tools.list_changed" MCP_RESOURCES_LIST_CHANGED = "mcp.resources.list_changed" MCP_PROMPTS_LIST_CHANGED = "mcp.prompts.list_changed" @@ -397,6 +405,60 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class AssistantFusionPhaseActivityData: + "Experimental content-safe activity signal for a running HydraFusion phase." + activity: FusionPhaseActivityKind + conversation_scope: FusionConversationScope + fusion_id: str + pattern: FusionPattern + phase_id: str + phase_kind: FusionPhaseKind + role: str + tool_call_id: str | None = None + total_response_size_bytes: int | None = None + + @staticmethod + def from_dict(obj: Any) -> "AssistantFusionPhaseActivityData": + assert isinstance(obj, dict) + activity = parse_enum(FusionPhaseActivityKind, obj.get("activity")) + conversation_scope = parse_enum(FusionConversationScope, obj.get("conversationScope")) + fusion_id = from_str(obj.get("fusionId")) + pattern = parse_enum(FusionPattern, obj.get("pattern")) + phase_id = from_str(obj.get("phaseId")) + phase_kind = parse_enum(FusionPhaseKind, obj.get("phaseKind")) + role = from_str(obj.get("role")) + tool_call_id = from_union([from_none, from_str], obj.get("toolCallId")) + total_response_size_bytes = from_union([from_none, from_int], obj.get("totalResponseSizeBytes")) + return AssistantFusionPhaseActivityData( + activity=activity, + conversation_scope=conversation_scope, + fusion_id=fusion_id, + pattern=pattern, + phase_id=phase_id, + phase_kind=phase_kind, + role=role, + tool_call_id=tool_call_id, + total_response_size_bytes=total_response_size_bytes, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["activity"] = to_enum(FusionPhaseActivityKind, self.activity) + result["conversationScope"] = to_enum(FusionConversationScope, self.conversation_scope) + result["fusionId"] = from_str(self.fusion_id) + result["pattern"] = to_enum(FusionPattern, self.pattern) + result["phaseId"] = from_str(self.phase_id) + result["phaseKind"] = to_enum(FusionPhaseKind, self.phase_kind) + result["role"] = from_str(self.role) + if self.tool_call_id is not None: + result["toolCallId"] = from_union([from_none, from_str], self.tool_call_id) + if self.total_response_size_bytes is not None: + result["totalResponseSizeBytes"] = from_union([from_none, to_int], self.total_response_size_bytes) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class AssistantFusionPhaseCompletedData: @@ -1201,6 +1263,38 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class FusionPhasePlanStep: + "Presentation-neutral phase planned for a HydraFusion turn." + conditional: bool + kind: FusionPhaseKind + role: str + scope: FusionConversationScope + + @staticmethod + def from_dict(obj: Any) -> "FusionPhasePlanStep": + assert isinstance(obj, dict) + conditional = from_bool(obj.get("conditional")) + kind = parse_enum(FusionPhaseKind, obj.get("kind")) + role = from_str(obj.get("role")) + scope = parse_enum(FusionConversationScope, obj.get("scope")) + return FusionPhasePlanStep( + conditional=conditional, + kind=kind, + role=role, + scope=scope, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["conditional"] = from_bool(self.conditional) + result["kind"] = to_enum(FusionPhaseKind, self.kind) + result["role"] = from_str(self.role) + result["scope"] = to_enum(FusionConversationScope, self.scope) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class FusionPhaseUsage: @@ -1677,6 +1771,55 @@ def to_dict(self) -> dict: return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class SessionCompletionReceiptData: + "Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted." + attempt: int + event_range: CompletionReceiptEventRange + failed_tool_count: int + schema_version: int + source_event_id: str + stop_reason: CompletionReceiptStopReason + successful_tool_count: int + final_tool: CompletionReceiptFinalTool | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionCompletionReceiptData": + assert isinstance(obj, dict) + attempt = from_int(obj.get("attempt")) + event_range = CompletionReceiptEventRange.from_dict(obj.get("eventRange")) + failed_tool_count = from_int(obj.get("failedToolCount")) + schema_version = from_int(obj.get("schemaVersion")) + source_event_id = from_str(obj.get("sourceEventId")) + stop_reason = parse_enum(CompletionReceiptStopReason, obj.get("stopReason")) + successful_tool_count = from_int(obj.get("successfulToolCount")) + final_tool = from_union([from_none, CompletionReceiptFinalTool.from_dict], obj.get("finalTool")) + return SessionCompletionReceiptData( + attempt=attempt, + event_range=event_range, + failed_tool_count=failed_tool_count, + schema_version=schema_version, + source_event_id=source_event_id, + stop_reason=stop_reason, + successful_tool_count=successful_tool_count, + final_tool=final_tool, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["attempt"] = to_int(self.attempt) + result["eventRange"] = to_class(CompletionReceiptEventRange, self.event_range) + result["failedToolCount"] = to_int(self.failed_tool_count) + result["schemaVersion"] = to_int(self.schema_version) + result["sourceEventId"] = from_str(self.source_event_id) + result["stopReason"] = to_enum(CompletionReceiptStopReason, self.stop_reason) + result["successfulToolCount"] = to_int(self.successful_tool_count) + if self.final_tool is not None: + result["finalTool"] = from_union([from_none, lambda x: to_class(CompletionReceiptFinalTool, x)], self.final_tool) + return result + + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionFusionCompletedData: @@ -1782,6 +1925,8 @@ class SessionFusionResolvedData: turn_id: str follow_up: FusionFollowUpRecommendation | None = None model_universe_version: str | None = None + # Experimental: this field is part of an experimental API and may change or be removed. + phase_plan: list[FusionPhasePlanStep] | None = None plan_version: str | None = None policy_version: str | None = None route_source: str | None = None @@ -1806,6 +1951,7 @@ def from_dict(obj: Any) -> "SessionFusionResolvedData": turn_id = from_str(obj.get("turnId")) follow_up = from_union([from_none, FusionFollowUpRecommendation.from_dict], obj.get("followUp")) model_universe_version = from_union([from_none, from_str], obj.get("modelUniverseVersion")) + phase_plan = from_union([from_none, lambda x: from_list(FusionPhasePlanStep.from_dict, x)], obj.get("phasePlan")) plan_version = from_union([from_none, from_str], obj.get("planVersion")) policy_version = from_union([from_none, from_str], obj.get("policyVersion")) route_source = from_union([from_none, from_str], obj.get("routeSource")) @@ -1827,6 +1973,7 @@ def from_dict(obj: Any) -> "SessionFusionResolvedData": turn_id=turn_id, follow_up=follow_up, model_universe_version=model_universe_version, + phase_plan=phase_plan, plan_version=plan_version, policy_version=policy_version, route_source=route_source, @@ -1853,6 +2000,8 @@ def to_dict(self) -> dict: result["followUp"] = from_union([from_none, lambda x: to_class(FusionFollowUpRecommendation, x)], self.follow_up) if self.model_universe_version is not None: result["modelUniverseVersion"] = from_union([from_none, from_str], self.model_universe_version) + if self.phase_plan is not None: + result["phasePlan"] = from_union([from_none, lambda x: from_list(lambda x: to_class(FusionPhasePlanStep, x), x)], self.phase_plan) if self.plan_version is not None: result["planVersion"] = from_union([from_none, from_str], self.plan_version) if self.policy_version is not None: @@ -1992,7 +2141,7 @@ def to_dict(self) -> dict: # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class SessionManagedSettingsResolvedData: - "Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes." + "Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes." bypass_permissions_disabled: bool device_managed: bool fail_closed: bool @@ -2001,6 +2150,7 @@ class SessionManagedSettingsResolvedData: source: ManagedSettingsResolvedSource client_managed: bool | None = None permissions_allow_intersected: bool | None = None + policy_helper_managed: bool | None = None sandbox_enabled_by_undetermined_policy: bool | None = None settings: Any = None @@ -2015,6 +2165,7 @@ def from_dict(obj: Any) -> "SessionManagedSettingsResolvedData": source = parse_enum(ManagedSettingsResolvedSource, obj.get("source")) client_managed = from_union([from_none, from_bool], obj.get("clientManaged")) permissions_allow_intersected = from_union([from_none, from_bool], obj.get("permissionsAllowIntersected")) + policy_helper_managed = from_union([from_none, from_bool], obj.get("policyHelperManaged")) sandbox_enabled_by_undetermined_policy = from_union([from_none, from_bool], obj.get("sandboxEnabledByUndeterminedPolicy")) settings = obj.get("settings") return SessionManagedSettingsResolvedData( @@ -2026,6 +2177,7 @@ def from_dict(obj: Any) -> "SessionManagedSettingsResolvedData": source=source, client_managed=client_managed, permissions_allow_intersected=permissions_allow_intersected, + policy_helper_managed=policy_helper_managed, sandbox_enabled_by_undetermined_policy=sandbox_enabled_by_undetermined_policy, settings=settings, ) @@ -2042,6 +2194,8 @@ def to_dict(self) -> dict: result["clientManaged"] = from_union([from_none, from_bool], self.client_managed) if self.permissions_allow_intersected is not None: result["permissionsAllowIntersected"] = from_union([from_none, from_bool], self.permissions_allow_intersected) + if self.policy_helper_managed is not None: + result["policyHelperManaged"] = from_union([from_none, from_bool], self.policy_helper_managed) if self.sandbox_enabled_by_undetermined_policy is not None: result["sandboxEnabledByUndeterminedPolicy"] = from_union([from_none, from_bool], self.sandbox_enabled_by_undetermined_policy) if self.settings is not None: @@ -4103,9 +4257,65 @@ def to_dict(self) -> dict: return result +@dataclass +class CompletionReceiptEventRange: + "Inclusive durable event range summarized by a completion receipt." + end_event_id: str + start_event_id: str + + @staticmethod + def from_dict(obj: Any) -> "CompletionReceiptEventRange": + assert isinstance(obj, dict) + end_event_id = from_str(obj.get("endEventId")) + start_event_id = from_str(obj.get("startEventId")) + return CompletionReceiptEventRange( + end_event_id=end_event_id, + start_event_id=start_event_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["endEventId"] = from_str(self.end_event_id) + result["startEventId"] = from_str(self.start_event_id) + return result + + +@dataclass +class CompletionReceiptFinalTool: + "Final structured tool completion in the covered event range." + status: CompletionReceiptToolStatus + tool_call_id: str + exit_code: int | None = None + tool_name: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "CompletionReceiptFinalTool": + assert isinstance(obj, dict) + status = parse_enum(CompletionReceiptToolStatus, obj.get("status")) + tool_call_id = from_str(obj.get("toolCallId")) + exit_code = from_union([from_none, from_int], obj.get("exitCode")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + return CompletionReceiptFinalTool( + status=status, + tool_call_id=tool_call_id, + exit_code=exit_code, + tool_name=tool_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["status"] = to_enum(CompletionReceiptToolStatus, self.status) + result["toolCallId"] = from_str(self.tool_call_id) + if self.exit_code is not None: + result["exitCode"] = from_union([from_none, to_int], self.exit_code) + if self.tool_name is not None: + result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result + + @dataclass class CustomAgentsUpdatedAgent: - "A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override." + "A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration." description: str display_name: str id: str @@ -4114,6 +4324,8 @@ class CustomAgentsUpdatedAgent: tools: list[str] | None user_invocable: bool model: str | None = None + model_policy: AgentModelPolicy | None = None + models: list[str] | None = None @staticmethod def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent": @@ -4126,6 +4338,8 @@ def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent": tools = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("tools")) user_invocable = from_bool(obj.get("userInvocable")) model = from_union([from_none, from_str], obj.get("model")) + model_policy = from_union([from_none, lambda x: parse_enum(AgentModelPolicy, x)], obj.get("modelPolicy")) + models = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("models")) return CustomAgentsUpdatedAgent( description=description, display_name=display_name, @@ -4135,6 +4349,8 @@ def from_dict(obj: Any) -> "CustomAgentsUpdatedAgent": tools=tools, user_invocable=user_invocable, model=model, + model_policy=model_policy, + models=models, ) def to_dict(self) -> dict: @@ -4148,6 +4364,10 @@ def to_dict(self) -> dict: result["userInvocable"] = from_bool(self.user_invocable) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.model_policy is not None: + result["modelPolicy"] = from_union([from_none, lambda x: to_enum(AgentModelPolicy, x)], self.model_policy) + if self.models is not None: + result["models"] = from_union([from_none, lambda x: from_list(from_str, x)], self.models) return result @@ -5136,6 +5356,25 @@ def to_dict(self) -> dict: return result +@dataclass +class McpServerMetadata: + "Server-advertised metadata learned through modern discovery or legacy initialization." + instructions: str | None + + @staticmethod + def from_dict(obj: Any) -> "McpServerMetadata": + assert isinstance(obj, dict) + instructions = from_union([from_none, from_str], obj.get("instructions")) + return McpServerMetadata( + instructions=instructions, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["instructions"] = from_union([from_none, from_str], self.instructions) + return result + + @dataclass class McpServersLoadedServer: "A single MCP server status summary in `session.mcp_servers_loaded`, including name, status, source, transport, and plugin metadata." @@ -5144,6 +5383,7 @@ class McpServersLoadedServer: error: str | None = None plugin_name: str | None = None plugin_version: str | None = None + server_metadata: McpServerMetadata | None = None source: McpServerSource | None = None transport: McpServerTransport | None = None @@ -5155,6 +5395,7 @@ def from_dict(obj: Any) -> "McpServersLoadedServer": error = from_union([from_none, from_str], obj.get("error")) plugin_name = from_union([from_none, from_str], obj.get("pluginName")) plugin_version = from_union([from_none, from_str], obj.get("pluginVersion")) + server_metadata = from_union([from_none, McpServerMetadata.from_dict], obj.get("serverMetadata")) source = from_union([from_none, lambda x: parse_enum(McpServerSource, x)], obj.get("source")) transport = from_union([from_none, lambda x: parse_enum(McpServerTransport, x)], obj.get("transport")) return McpServersLoadedServer( @@ -5163,6 +5404,7 @@ def from_dict(obj: Any) -> "McpServersLoadedServer": error=error, plugin_name=plugin_name, plugin_version=plugin_version, + server_metadata=server_metadata, source=source, transport=transport, ) @@ -5177,6 +5419,8 @@ def to_dict(self) -> dict: result["pluginName"] = from_union([from_none, from_str], self.plugin_name) if self.plugin_version is not None: result["pluginVersion"] = from_union([from_none, from_str], self.plugin_version) + if self.server_metadata is not None: + result["serverMetadata"] = from_union([from_none, lambda x: to_class(McpServerMetadata, x)], self.server_metadata) if self.source is not None: result["source"] = from_union([from_none, lambda x: to_enum(McpServerSource, x)], self.source) if self.transport is not None: @@ -7072,6 +7316,7 @@ class PermissionRequestedData: "Permission request notification requiring client approval with request details" permission_request: PermissionRequest request_id: str + agent_mode: SessionMode | None = None prompt_request: PermissionPromptRequest | None = None resolved_by_hook: bool | None = None risk_assessment: Any = None @@ -7081,12 +7326,14 @@ def from_dict(obj: Any) -> "PermissionRequestedData": assert isinstance(obj, dict) permission_request = _load_PermissionRequest(obj.get("permissionRequest")) request_id = from_str(obj.get("requestId")) + agent_mode = from_union([from_none, lambda x: parse_enum(SessionMode, x)], obj.get("agentMode")) prompt_request = from_union([from_none, _load_PermissionPromptRequest], obj.get("promptRequest")) resolved_by_hook = from_union([from_none, from_bool], obj.get("resolvedByHook")) risk_assessment = obj.get("riskAssessment") return PermissionRequestedData( permission_request=permission_request, request_id=request_id, + agent_mode=agent_mode, prompt_request=prompt_request, resolved_by_hook=resolved_by_hook, risk_assessment=risk_assessment, @@ -7096,6 +7343,8 @@ def to_dict(self) -> dict: result: dict = {} result["permissionRequest"] = self.permission_request.to_dict() result["requestId"] = from_str(self.request_id) + if self.agent_mode is not None: + result["agentMode"] = from_union([from_none, lambda x: to_enum(SessionMode, x)], self.agent_mode) if self.prompt_request is not None: result["promptRequest"] = from_union([from_none, lambda x: x.to_dict()], self.prompt_request) if self.resolved_by_hook is not None: @@ -7364,6 +7613,34 @@ def to_dict(self) -> dict: return {} +@dataclass +class SessionAutoTierSwitchFailedData: + "A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume." + reason: AutoTierSwitchFailureReason + requested_auto_tier: AutoTier | None + effective_auto_tier: AutoTier | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionAutoTierSwitchFailedData": + assert isinstance(obj, dict) + reason = parse_enum(AutoTierSwitchFailureReason, obj.get("reason")) + requested_auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("requestedAutoTier")) + effective_auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("effectiveAutoTier")) + return SessionAutoTierSwitchFailedData( + reason=reason, + requested_auto_tier=requested_auto_tier, + effective_auto_tier=effective_auto_tier, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["reason"] = to_enum(AutoTierSwitchFailureReason, self.reason) + result["requestedAutoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.requested_auto_tier) + if self.effective_auto_tier is not None: + result["effectiveAutoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.effective_auto_tier) + return result + + @dataclass class SessionAutopilotObjectiveChangedData: "Autopilot objective state file operation details indicating what changed" @@ -7774,6 +8051,7 @@ class SessionErrorData: eligible_for_auto_switch: bool | None = None error_code: str | None = None provider_call_id: str | None = None + remediation: RemediationAction | None = None service_request_id: str | None = None stack: str | None = None status_code: int | None = None @@ -7787,6 +8065,7 @@ def from_dict(obj: Any) -> "SessionErrorData": eligible_for_auto_switch = from_union([from_none, from_bool], obj.get("eligibleForAutoSwitch")) error_code = from_union([from_none, from_str], obj.get("errorCode")) provider_call_id = from_union([from_none, from_str], obj.get("providerCallId")) + remediation = from_union([from_none, lambda x: parse_enum(RemediationAction, x)], obj.get("remediation")) service_request_id = from_union([from_none, from_str], obj.get("serviceRequestId")) stack = from_union([from_none, from_str], obj.get("stack")) status_code = from_union([from_none, from_int], obj.get("statusCode")) @@ -7797,6 +8076,7 @@ def from_dict(obj: Any) -> "SessionErrorData": eligible_for_auto_switch=eligible_for_auto_switch, error_code=error_code, provider_call_id=provider_call_id, + remediation=remediation, service_request_id=service_request_id, stack=stack, status_code=status_code, @@ -7813,6 +8093,8 @@ def to_dict(self) -> dict: result["errorCode"] = from_union([from_none, from_str], self.error_code) if self.provider_call_id is not None: result["providerCallId"] = from_union([from_none, from_str], self.provider_call_id) + if self.remediation is not None: + result["remediation"] = from_union([from_none, lambda x: to_enum(RemediationAction, x)], self.remediation) if self.service_request_id is not None: result["serviceRequestId"] = from_union([from_none, from_str], self.service_request_id) if self.stack is not None: @@ -8067,6 +8349,44 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionMcpServerNeedsReconnectData: + "Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established." + server_name: str + + @staticmethod + def from_dict(obj: Any) -> "SessionMcpServerNeedsReconnectData": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + return SessionMcpServerNeedsReconnectData( + server_name=server_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + return result + + +@dataclass +class SessionMcpServerRemovedData: + "Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs." + server_name: str + + @staticmethod + def from_dict(obj: Any) -> "SessionMcpServerRemovedData": + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + return SessionMcpServerRemovedData( + server_name=server_name, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + return result + + @dataclass class SessionMcpServerStatusChangedData: "Payload of `session.mcp_server_status_changed` for one MCP server's status and optional failure error." @@ -8137,12 +8457,38 @@ def to_dict(self) -> dict: return result +@dataclass +class SessionModeNoticeDeliveredData: + "Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume." + mode: SessionMode + content: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "SessionModeNoticeDeliveredData": + assert isinstance(obj, dict) + mode = parse_enum(SessionMode, obj.get("mode")) + content = from_union([from_none, from_str], obj.get("content")) + return SessionModeNoticeDeliveredData( + mode=mode, + content=content, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["mode"] = to_enum(SessionMode, self.mode) + if self.content is not None: + result["content"] = from_union([from_none, from_str], self.content) + return result + + @dataclass class SessionModelChangeData: "Model change details including previous and new model identifiers" new_model: str + auto_tier: AutoTier | None = None cause: str | None = None context_tier: ContextTier | None = None + previous_auto_tier: AutoTier | None = None previous_model: str | None = None previous_reasoning_effort: str | None = None previous_reasoning_summary: ReasoningSummary | None = None @@ -8156,8 +8502,10 @@ class SessionModelChangeData: def from_dict(obj: Any) -> "SessionModelChangeData": assert isinstance(obj, dict) new_model = from_str(obj.get("newModel")) + auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("autoTier")) cause = from_union([from_none, from_str], obj.get("cause")) context_tier = from_union([from_none, lambda x: parse_enum(ContextTier, x)], obj.get("contextTier")) + previous_auto_tier = from_union([from_none, lambda x: parse_enum(AutoTier, x)], obj.get("previousAutoTier")) previous_model = from_union([from_none, from_str], obj.get("previousModel")) previous_reasoning_effort = from_union([from_none, from_str], obj.get("previousReasoningEffort")) previous_reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("previousReasoningSummary")) @@ -8168,8 +8516,10 @@ def from_dict(obj: Any) -> "SessionModelChangeData": verbosity = from_union([from_none, lambda x: parse_enum(Verbosity, x)], obj.get("verbosity")) return SessionModelChangeData( new_model=new_model, + auto_tier=auto_tier, cause=cause, context_tier=context_tier, + previous_auto_tier=previous_auto_tier, previous_model=previous_model, previous_reasoning_effort=previous_reasoning_effort, previous_reasoning_summary=previous_reasoning_summary, @@ -8183,10 +8533,14 @@ def from_dict(obj: Any) -> "SessionModelChangeData": def to_dict(self) -> dict: result: dict = {} result["newModel"] = from_str(self.new_model) + if self.auto_tier is not None: + result["autoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.auto_tier) if self.cause is not None: result["cause"] = from_union([from_none, from_str], self.cause) if self.context_tier is not None: result["contextTier"] = from_union([from_none, lambda x: to_enum(ContextTier, x)], self.context_tier) + if self.previous_auto_tier is not None: + result["previousAutoTier"] = from_union([from_none, lambda x: to_enum(AutoTier, x)], self.previous_auto_tier) if self.previous_model is not None: result["previousModel"] = from_union([from_none, from_str], self.previous_model) if self.previous_reasoning_effort is not None: @@ -8911,6 +9265,7 @@ class SessionWarningData: "Warning message for timeline display with categorization" message: str warning_type: str + remediation: RemediationAction | None = None url: str | None = None @staticmethod @@ -8918,10 +9273,12 @@ def from_dict(obj: Any) -> "SessionWarningData": assert isinstance(obj, dict) message = from_str(obj.get("message")) warning_type = from_str(obj.get("warningType")) + remediation = from_union([from_none, lambda x: parse_enum(RemediationAction, x)], obj.get("remediation")) url = from_union([from_none, from_str], obj.get("url")) return SessionWarningData( message=message, warning_type=warning_type, + remediation=remediation, url=url, ) @@ -8929,6 +9286,8 @@ def to_dict(self) -> dict: result: dict = {} result["message"] = from_str(self.message) result["warningType"] = from_str(self.warning_type) + if self.remediation is not None: + result["remediation"] = from_union([from_none, lambda x: to_enum(RemediationAction, x)], self.remediation) if self.url is not None: result["url"] = from_union([from_none, from_str], self.url) return result @@ -9164,6 +9523,7 @@ class SkillInvokedData: path: str allowed_tools: list[str] | None = None description: str | None = None + disable_model_invocation: bool | None = None model: str | None = None plugin_name: str | None = None plugin_version: str | None = None @@ -9178,6 +9538,7 @@ def from_dict(obj: Any) -> "SkillInvokedData": path = from_str(obj.get("path")) allowed_tools = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("allowedTools")) description = from_union([from_none, from_str], obj.get("description")) + disable_model_invocation = from_union([from_none, from_bool], obj.get("disableModelInvocation")) model = from_union([from_none, from_str], obj.get("model")) plugin_name = from_union([from_none, from_str], obj.get("pluginName")) plugin_version = from_union([from_none, from_str], obj.get("pluginVersion")) @@ -9189,6 +9550,7 @@ def from_dict(obj: Any) -> "SkillInvokedData": path=path, allowed_tools=allowed_tools, description=description, + disable_model_invocation=disable_model_invocation, model=model, plugin_name=plugin_name, plugin_version=plugin_version, @@ -9205,6 +9567,8 @@ def to_dict(self) -> dict: result["allowedTools"] = from_union([from_none, lambda x: from_list(from_str, x)], self.allowed_tools) if self.description is not None: result["description"] = from_union([from_none, from_str], self.description) + if self.disable_model_invocation is not None: + result["disableModelInvocation"] = from_union([from_none, from_bool], self.disable_model_invocation) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) if self.plugin_name is not None: @@ -9282,6 +9646,7 @@ class SubagentCompletedData: explicit_model_override: str | None = None first_dispatched_model: str | None = None model: str | None = None + model_override_reason: str | None = None total_tokens: int | None = None total_tool_calls: int | None = None @@ -9299,6 +9664,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData": explicit_model_override = from_union([from_none, from_str], obj.get("explicitModelOverride")) first_dispatched_model = from_union([from_none, from_str], obj.get("firstDispatchedModel")) model = from_union([from_none, from_str], obj.get("model")) + model_override_reason = from_union([from_none, from_str], obj.get("modelOverrideReason")) total_tokens = from_union([from_none, from_int], obj.get("totalTokens")) total_tool_calls = from_union([from_none, from_int], obj.get("totalToolCalls")) return SubagentCompletedData( @@ -9313,6 +9679,7 @@ def from_dict(obj: Any) -> "SubagentCompletedData": explicit_model_override=explicit_model_override, first_dispatched_model=first_dispatched_model, model=model, + model_override_reason=model_override_reason, total_tokens=total_tokens, total_tool_calls=total_tool_calls, ) @@ -9338,6 +9705,8 @@ def to_dict(self) -> dict: result["firstDispatchedModel"] = from_union([from_none, from_str], self.first_dispatched_model) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.model_override_reason is not None: + result["modelOverrideReason"] = from_union([from_none, from_str], self.model_override_reason) if self.total_tokens is not None: result["totalTokens"] = from_union([from_none, to_int], self.total_tokens) if self.total_tool_calls is not None: @@ -9404,6 +9773,7 @@ class SubagentFailedData: explicit_model_override: str | None = None first_dispatched_model: str | None = None model: str | None = None + model_override_reason: str | None = None total_tokens: int | None = None total_tool_calls: int | None = None @@ -9421,6 +9791,7 @@ def from_dict(obj: Any) -> "SubagentFailedData": explicit_model_override = from_union([from_none, from_str], obj.get("explicitModelOverride")) first_dispatched_model = from_union([from_none, from_str], obj.get("firstDispatchedModel")) model = from_union([from_none, from_str], obj.get("model")) + model_override_reason = from_union([from_none, from_str], obj.get("modelOverrideReason")) total_tokens = from_union([from_none, from_int], obj.get("totalTokens")) total_tool_calls = from_union([from_none, from_int], obj.get("totalToolCalls")) return SubagentFailedData( @@ -9435,6 +9806,7 @@ def from_dict(obj: Any) -> "SubagentFailedData": explicit_model_override=explicit_model_override, first_dispatched_model=first_dispatched_model, model=model, + model_override_reason=model_override_reason, total_tokens=total_tokens, total_tool_calls=total_tool_calls, ) @@ -9459,6 +9831,8 @@ def to_dict(self) -> dict: result["firstDispatchedModel"] = from_union([from_none, from_str], self.first_dispatched_model) if self.model is not None: result["model"] = from_union([from_none, from_str], self.model) + if self.model_override_reason is not None: + result["modelOverrideReason"] = from_union([from_none, from_str], self.model_override_reason) if self.total_tokens is not None: result["totalTokens"] = from_union([from_none, to_int], self.total_tokens) if self.total_tool_calls is not None: @@ -10242,15 +10616,18 @@ class ToolExecutionCompleteError: "Error details when the tool execution failed" message: str code: str | None = None + remediation: RemediationAction | None = None @staticmethod def from_dict(obj: Any) -> "ToolExecutionCompleteError": assert isinstance(obj, dict) message = from_str(obj.get("message")) code = from_union([from_none, from_str], obj.get("code")) + remediation = from_union([from_none, lambda x: parse_enum(RemediationAction, x)], obj.get("remediation")) return ToolExecutionCompleteError( message=message, code=code, + remediation=remediation, ) def to_dict(self) -> dict: @@ -10258,6 +10635,8 @@ def to_dict(self) -> dict: result["message"] = from_str(self.message) if self.code is not None: result["code"] = from_union([from_none, from_str], self.code) + if self.remediation is not None: + result["remediation"] = from_union([from_none, lambda x: to_enum(RemediationAction, x)], self.remediation) return result @@ -10987,6 +11366,7 @@ class UserMessageData: delivery: UserMessageDelivery | None = None interaction_id: str | None = None is_autopilot_continuation: bool | None = None + message_id: str | None = None native_document_path_fallback_paths: list[str] | None = None parent_agent_task_id: str | None = None source: str | None = None @@ -11003,6 +11383,7 @@ def from_dict(obj: Any) -> "UserMessageData": delivery = from_union([from_none, lambda x: parse_enum(UserMessageDelivery, x)], obj.get("delivery")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) is_autopilot_continuation = from_union([from_none, from_bool], obj.get("isAutopilotContinuation")) + message_id = from_union([from_none, from_str], obj.get("messageId")) native_document_path_fallback_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("nativeDocumentPathFallbackPaths")) parent_agent_task_id = from_union([from_none, from_str], obj.get("parentAgentTaskId")) source = from_union([from_none, from_str], obj.get("source")) @@ -11016,6 +11397,7 @@ def from_dict(obj: Any) -> "UserMessageData": delivery=delivery, interaction_id=interaction_id, is_autopilot_continuation=is_autopilot_continuation, + message_id=message_id, native_document_path_fallback_paths=native_document_path_fallback_paths, parent_agent_task_id=parent_agent_task_id, source=source, @@ -11037,6 +11419,8 @@ def to_dict(self) -> dict: result["interactionId"] = from_union([from_none, from_str], self.interaction_id) if self.is_autopilot_continuation is not None: result["isAutopilotContinuation"] = from_union([from_none, from_bool], self.is_autopilot_continuation) + if self.message_id is not None: + result["messageId"] = from_union([from_none, from_str], self.message_id) if self.native_document_path_fallback_paths is not None: result["nativeDocumentPathFallbackPaths"] = from_union([from_none, lambda x: from_list(from_str, x)], self.native_document_path_fallback_paths) if self.parent_agent_task_id is not None: @@ -11560,6 +11944,17 @@ class FusionPattern(Enum): CRITIQUE = "critique" +# Experimental: this enum is part of an experimental API and may change or be removed. +class FusionPhaseActivityKind(Enum): + "Content-safe activity observed while a HydraFusion phase is running." + # The provider produced additional private output bytes. + MODEL_OUTPUT = "model_output" + # A tool began executing inside the phase. + TOOL_STARTED = "tool_started" + # A tool finished executing inside the phase. + TOOL_COMPLETED = "tool_completed" + + # Experimental: this enum is part of an experimental API and may change or be removed. class FusionPhaseKind(Enum): "HydraFusion phase kind." @@ -11675,6 +12070,14 @@ class AgentInterruptedCancelPhase(Enum): MID_STREAM = "mid_stream" +class AgentModelPolicy(Enum): + "Whether configured models are advisory preferences or required constraints" + # Treat the authored models as advisory preferences that callers may override. + PREFERRED = "preferred" + # Require subagent execution to use one of the authored models. + REQUIRED = "required" + + class AssistantMessageToolRequestCallerType(Enum): "Hosted program caller type" PROGRAM = "program" @@ -11748,6 +12151,18 @@ class AutoTier(Enum): INTELLIGENCE = "intelligence" +class AutoTierSwitchFailureReason(Enum): + "Terminal reason an Auto preference activation failed." + # The candidate model was rejected by model policy. + POLICY_REJECTED = "policy_rejected" + # The Auto routing request failed or returned an unusable response. + REQUEST_FAILED = "request_failed" + # The runtime could not prepare the Auto routing request. + SETUP_FAILED = "setup_failed" + # The provider does not support Auto routing. + UNSUPPORTED = "unsupported" + + class AutopilotObjectiveChangedOperation(Enum): "The type of operation performed on the autopilot objective state file" # Autopilot objective state file was created for a new objective. @@ -11800,6 +12215,30 @@ class CompactionTrigger(Enum): MODEL_SWITCH = "model_switch" +class CompletionReceiptStopReason(Enum): + "Runtime reason the completion decision was accepted." + # The model reached a natural terminal response. + NATURAL = "natural" + # A terminal tool ended the interaction. + TERMINAL_TOOL = "terminal_tool" + # The configured agentStop continuation limit was reached. + AGENT_STOP_BLOCK_LIMIT = "agent_stop_block_limit" + + +class CompletionReceiptToolStatus(Enum): + "Structured terminal status from a tool completion event." + # The tool completed successfully. + SUCCESS = "success" + # The tool failed without a more specific structured status. + FAILURE = "failure" + # The tool exceeded its time budget. + TIMEOUT = "timeout" + # The user rejected the tool call. + REJECTED = "rejected" + # The permissions service denied the tool call. + DENIED = "denied" + + class ContextTier(Enum): "Allowed values for the `ContextTier` enumeration." # Default context tier with standard context window size. @@ -11920,7 +12359,9 @@ class ManagedSettingsResolvedSource(Enum): DEVICE = "device" # Only session-local SDK-host injection contributed. CLIENT = "client" - # More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + # A policy helper registered by device or server policy contributed. Device registration takes priority when present. + POLICY_HELPER = "policyHelper" + # More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. MIXED = "mixed" # No managed policy is in force (no channel contributed). NONE = "none" @@ -12158,6 +12599,20 @@ class ReasoningSummary(Enum): DETAILED = "detailed" +class RemediationAction(Enum): + "What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take." + # Authenticate again with the Copilot backend. The current credential is absent, expired, or rejected. + SIGN_IN = "sign_in" + # Authenticate as a different account. The current account exists but lacks access to the requested resource. + SWITCH_ACCOUNT = "switch_account" + # Inspect which account is currently authenticated before deciding what to change. + SHOW_ACCOUNT = "show_account" + # Review or widen the sandbox policy. The blocked path or host is named by the accompanying message or by the tool result the action arrived with. + REVIEW_SANDBOX_POLICY = "review_sandbox_policy" + # Permit outbound network access in the sandbox policy. + ALLOW_SANDBOX_OUTBOUND = "allow_sandbox_outbound" + + class ScheduleOrigin(Enum): "Who created the schedule: `user` (an explicit user action such as `/every` or `/after`) or `model` (the agent via the `manage_schedule` tool). Gates whether a scheduled skill that opted out of model invocation may fire: only user-created schedules may." # The schedule was created by an explicit user action, such as `/every` or `/after`. @@ -12207,7 +12662,7 @@ class SkillInvokedTrigger(Enum): class SkillSource(Enum): - "Source location type (e.g., project, personal-copilot, plugin, builtin)" + "Source location type (e.g., project, personal-copilot, plugin, builtin, sdk)" # Skill defined in the current project's skill directories. PROJECT = "project" # Skill discovered from a parent directory in the current workspace tree. @@ -12222,6 +12677,8 @@ class SkillSource(Enum): CUSTOM = "custom" # Skill bundled with the runtime. BUILTIN = "builtin" + # Pathless skill supplied lazily by an SDK skill provider. + SDK = "sdk" class SystemMessageRole(Enum): @@ -12334,7 +12791,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data +SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionAutoTierSwitchFailedData | SessionModeChangedData | SessionModeNoticeDeliveredData | SessionSessionLimitsChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionUsageCheckpointData | SessionContextChangedData | SessionUsageInfoData | SessionContextClearedData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | SessionCompletionReceiptData | SessionFusionRouteStartedData | SessionFusionRouteFailedData | SessionFusionResolvedData | SessionFusionCompletedData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantTurnRetryData | AgentInterruptedData | AssistantIntentData | AssistantFusionPhaseStartedData | AssistantFusionPhaseActivityData | AssistantFusionPhaseCompletedData | AssistantFusionPhaseFailedData | AssistantServerToolProgressData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantToolCallDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | PromptCacheBreakData | ModelCallFailureData | ModelCallFinishedData | ModelCallStartData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | ToolSearchActivatedData | SkillInvokedData | SandboxDecisionData | SubagentStartedData | SubagentConfiguredData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | UiEphemeralQueryData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | SessionLimitsExhaustedRequestedData | SessionLimitsExhaustedCompletedData | SessionAutoModeResolvedData | SessionManagedSettingsResolvedData | SessionManagedSettingsEnforcedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | FactoryRunUpdatedData | FactoryRunStartedData | FactoryRunSettledData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionMcpServerRemovedData | SessionMcpServerNeedsReconnectData | McpToolsListChangedData | McpResourcesListChangedData | McpPromptsListChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data @dataclass @@ -12373,7 +12830,9 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_INFO: data = SessionInfoData.from_dict(data_obj) case SessionEventType.SESSION_WARNING: data = SessionWarningData.from_dict(data_obj) case SessionEventType.SESSION_MODEL_CHANGE: data = SessionModelChangeData.from_dict(data_obj) + case SessionEventType.SESSION_AUTO_TIER_SWITCH_FAILED: data = SessionAutoTierSwitchFailedData.from_dict(data_obj) case SessionEventType.SESSION_MODE_CHANGED: data = SessionModeChangedData.from_dict(data_obj) + case SessionEventType.SESSION_MODE_NOTICE_DELIVERED: data = SessionModeNoticeDeliveredData.from_dict(data_obj) case SessionEventType.SESSION_SESSION_LIMITS_CHANGED: data = SessionSessionLimitsChangedData.from_dict(data_obj) case SessionEventType.SESSION_PERMISSIONS_CHANGED: data = SessionPermissionsChangedData.from_dict(data_obj) case SessionEventType.SESSION_PLAN_CHANGED: data = SessionPlanChangedData.from_dict(data_obj) @@ -12390,6 +12849,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_COMPACTION_START: data = SessionCompactionStartData.from_dict(data_obj) case SessionEventType.SESSION_COMPACTION_COMPLETE: data = SessionCompactionCompleteData.from_dict(data_obj) case SessionEventType.SESSION_TASK_COMPLETE: data = SessionTaskCompleteData.from_dict(data_obj) + case SessionEventType.SESSION_COMPLETION_RECEIPT: data = SessionCompletionReceiptData.from_dict(data_obj) case SessionEventType.SESSION_FUSION_ROUTE_STARTED: data = SessionFusionRouteStartedData.from_dict(data_obj) case SessionEventType.SESSION_FUSION_ROUTE_FAILED: data = SessionFusionRouteFailedData.from_dict(data_obj) case SessionEventType.SESSION_FUSION_RESOLVED: data = SessionFusionResolvedData.from_dict(data_obj) @@ -12401,6 +12861,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.AGENT_INTERRUPTED: data = AgentInterruptedData.from_dict(data_obj) case SessionEventType.ASSISTANT_INTENT: data = AssistantIntentData.from_dict(data_obj) case SessionEventType.ASSISTANT_FUSION_PHASE_STARTED: data = AssistantFusionPhaseStartedData.from_dict(data_obj) + case SessionEventType.ASSISTANT_FUSION_PHASE_ACTIVITY: data = AssistantFusionPhaseActivityData.from_dict(data_obj) case SessionEventType.ASSISTANT_FUSION_PHASE_COMPLETED: data = AssistantFusionPhaseCompletedData.from_dict(data_obj) case SessionEventType.ASSISTANT_FUSION_PHASE_FAILED: data = AssistantFusionPhaseFailedData.from_dict(data_obj) case SessionEventType.ASSISTANT_SERVER_TOOL_PROGRESS: data = AssistantServerToolProgressData.from_dict(data_obj) @@ -12478,6 +12939,8 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SESSION_CUSTOM_AGENTS_UPDATED: data = SessionCustomAgentsUpdatedData.from_dict(data_obj) case SessionEventType.SESSION_MCP_SERVERS_LOADED: data = SessionMcpServersLoadedData.from_dict(data_obj) case SessionEventType.SESSION_MCP_SERVER_STATUS_CHANGED: data = SessionMcpServerStatusChangedData.from_dict(data_obj) + case SessionEventType.SESSION_MCP_SERVER_REMOVED: data = SessionMcpServerRemovedData.from_dict(data_obj) + case SessionEventType.SESSION_MCP_SERVER_NEEDS_RECONNECT: data = SessionMcpServerNeedsReconnectData.from_dict(data_obj) case SessionEventType.MCP_TOOLS_LIST_CHANGED: data = McpToolsListChangedData.from_dict(data_obj) case SessionEventType.MCP_RESOURCES_LIST_CHANGED: data = McpResourcesListChangedData.from_dict(data_obj) case SessionEventType.MCP_PROMPTS_LIST_CHANGED: data = McpPromptsListChangedData.from_dict(data_obj) @@ -12529,6 +12992,8 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AgentInterruptedActivity", "AgentInterruptedCancelPhase", "AgentInterruptedData", + "AgentModelPolicy", + "AssistantFusionPhaseActivityData", "AssistantFusionPhaseCompletedData", "AssistantFusionPhaseFailedData", "AssistantFusionPhaseStartedData", @@ -12586,6 +13051,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "AutoModeSwitchRequestedData", "AutoModeSwitchResponse", "AutoTier", + "AutoTierSwitchFailureReason", "AutopilotObjectiveChangedOperation", "AutopilotObjectiveChangedStatus", "BinaryAssetReference", @@ -12613,6 +13079,10 @@ def session_event_to_dict(x: SessionEvent) -> Any: "CompactionCompleteCompactionTokensUsed", "CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail", "CompactionTrigger", + "CompletionReceiptEventRange", + "CompletionReceiptFinalTool", + "CompletionReceiptStopReason", + "CompletionReceiptToolStatus", "ContextTier", "CustomAgentsUpdatedAgent", "Data", @@ -12642,7 +13112,9 @@ def session_event_to_dict(x: SessionEvent) -> Any: "FusionFollowUpAction", "FusionFollowUpRecommendation", "FusionPattern", + "FusionPhaseActivityKind", "FusionPhaseKind", + "FusionPhasePlanStep", "FusionPhaseStatus", "FusionPhaseUsage", "FusionScores", @@ -12676,6 +13148,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "McpOauthWWWAuthenticateParams", "McpPromptsListChangedData", "McpResourcesListChangedData", + "McpServerMetadata", "McpServerSource", "McpServerStatus", "McpServerTransport", @@ -12752,11 +13225,13 @@ def session_event_to_dict(x: SessionEvent) -> Any: "PromptCacheBreakData", "RawSessionEventData", "ReasoningSummary", + "RemediationAction", "SamplingCompletedData", "SamplingRequestedData", "SandboxDecisionData", "ScheduleOrigin", "SessionAutoModeResolvedData", + "SessionAutoTierSwitchFailedData", "SessionAutopilotObjectiveChangedData", "SessionBackgroundTasksChangedData", "SessionBinaryAssetData", @@ -12768,6 +13243,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SessionCanvasUnavailableData", "SessionCompactionCompleteData", "SessionCompactionStartData", + "SessionCompletionReceiptData", "SessionContextChangedData", "SessionContextClearedData", "SessionCustomAgentsUpdatedData", @@ -12792,10 +13268,13 @@ def session_event_to_dict(x: SessionEvent) -> Any: "SessionLimitsExhaustedResponseAction", "SessionManagedSettingsEnforcedData", "SessionManagedSettingsResolvedData", + "SessionMcpServerNeedsReconnectData", + "SessionMcpServerRemovedData", "SessionMcpServerStatusChangedData", "SessionMcpServersLoadedData", "SessionMode", "SessionModeChangedData", + "SessionModeNoticeDeliveredData", "SessionModelChangeData", "SessionPermissionsChangedData", "SessionPlanChangedData", diff --git a/python/copilot/session.py b/python/copilot/session.py index 3c6d3d54a4..b5823923ee 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -19,6 +19,7 @@ from collections.abc import Awaitable, Callable from dataclasses import dataclass from datetime import UTC, datetime +from enum import Enum from types import TracebackType from typing import TYPE_CHECKING, Any, Literal, NotRequired, Required, TypedDict, cast @@ -26,6 +27,9 @@ from ._jsonrpc import JsonRpcError, ProcessExitedError from ._telemetry import get_trace_context, trace_context from .canvas import CanvasError, CanvasHandler, OpenCanvasInstance +from .generated.rpc import ( + AutoTier as _RpcAutoTier, +) from .generated.rpc import ( BuiltinToolInputSchemaType, CanvasProviderCloseRequest, @@ -39,6 +43,7 @@ LogRequest, MCPOauthHandlePendingRequest, MCPOauthPendingRequestResponse, + ModelSwitchAutoTierResult, ModelSwitchToRequest, PermissionDecision, PermissionDecisionApproveOnce, @@ -69,6 +74,7 @@ CapabilitiesChangedData, CommandExecuteData, ElicitationRequestedData, + ExternalToolCompletedData, ExternalToolRequestedData, McpOauthRequiredData, PermissionRequest, @@ -174,9 +180,37 @@ def _capabilities_to_dict(caps: ModelCapabilitiesOverride) -> dict: ReasoningEffort = Literal["low", "medium", "high", "xhigh", "max"] ReasoningSummary = Literal["none", "concise", "detailed"] ContextTier = Literal["default", "long_context"] +AutoTier = Literal["efficiency", "balance", "intelligence"] SessionFsConventions = Literal["posix", "windows"] +class _Unset: + """Sentinel distinguishing an omitted argument from an explicit ``None``. + + Auto routing treats ``None`` as a meaningful value: it means "return to the + provider's default routing". Omitting the argument instead means "leave the + current preference alone", so the two cases cannot share a default. + """ + + def __repr__(self) -> str: + return "UNSET" + + +_UNSET = _Unset() + + +def _auto_tier_to_wire(auto_tier: AutoTier | _RpcAutoTier | None) -> str | None: + """Normalize an Auto tier to its wire value. + + Callers may pass either the ``AutoTier`` string literal or the generated + ``AutoTier`` enum, which is the type the SDK hands back on results and + events. The JSON-RPC encoder only understands plain strings. + """ + if isinstance(auto_tier, Enum): + return str(auto_tier.value) + return auto_tier + + class SessionFsCapabilities(TypedDict, total=False): sqlite: bool @@ -1574,6 +1608,7 @@ def __init__( self._event_handlers_lock = threading.Lock() self._tool_handlers: dict[str, ToolHandler] = {} self._tool_handlers_lock = threading.Lock() + self._pending_external_tools: dict[str, asyncio.Task[None]] = {} self._permission_handler: _PermissionHandlerFn | None = None self._permission_handler_lock = threading.Lock() self._mcp_auth_handler: McpAuthHandler | None = None @@ -1602,6 +1637,7 @@ def __init__( self._open_canvases_lock = threading.Lock() self._rpc: SessionRpc | None = None self._destroyed = False + self._disconnect_lock = asyncio.Lock() self._on_disconnect = on_disconnect def _set_disconnect_callback(self, callback: Callable[[], None]) -> None: @@ -1614,6 +1650,19 @@ def _run_disconnect_callback(self) -> None: if callback is not None: callback() + def _cancel_pending_external_tools(self) -> None: + pending_external_tools = list(self._pending_external_tools.values()) + self._pending_external_tools.clear() + current_task = asyncio.current_task() + for task in pending_external_tools: + if task is not current_task: + task.cancel() + + def _mark_disconnected(self) -> None: + self._destroyed = True + self._cancel_pending_external_tools() + self._run_disconnect_callback() + @property def rpc(self) -> SessionRpc: """Typed session-scoped RPC methods.""" @@ -1932,7 +1981,7 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: case ExternalToolRequestedData() as data: request_id = data.request_id tool_name = data.tool_name - if not request_id or not tool_name: + if self._destroyed or not request_id or not tool_name: return handler = self._get_tool_handler(tool_name) @@ -1943,11 +1992,26 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: arguments = data.arguments tp = getattr(data, "traceparent", None) ts = getattr(data, "tracestate", None) - asyncio.ensure_future( + task = asyncio.create_task( self._execute_tool_and_respond( request_id, tool_name, tool_call_id, arguments, handler, tp, ts ) ) + if request_id in self._pending_external_tools: + task.cancel() + return + self._pending_external_tools[request_id] = task + task.add_done_callback( + lambda completed, rid=request_id: self._remove_pending_external_tool( + rid, completed + ) + ) + + case ExternalToolCompletedData() as data: + if data.request_id: + task = self._pending_external_tools.pop(data.request_id, None) + if task is not None: + task.cancel() case PermissionRequestedData() as data: if logger.isEnabledFor(logging.DEBUG): @@ -2155,6 +2219,8 @@ async def _execute_tool_and_respond( # standard "Failed to execute..." message. Deliberate user-returned # failures send the full structured result to preserve metadata. if tool_result._from_exception: + if not self._claim_external_tool(request_id): + return rpc_start = time.perf_counter() await self.rpc.tools.handle_pending_tool_call( HandlePendingToolCallRequest( @@ -2173,6 +2239,8 @@ async def _execute_tool_and_respond( tool_name=tool_name, ) else: + if not self._claim_external_tool(request_id): + return rpc_start = time.perf_counter() await self.rpc.tools.handle_pending_tool_call( HandlePendingToolCallRequest( @@ -2191,6 +2259,8 @@ async def _execute_tool_and_respond( tool_name=tool_name, ) except Exception as exc: + if not self._claim_external_tool(request_id): + return try: await self.rpc.tools.handle_pending_tool_call( HandlePendingToolCallRequest( @@ -2201,6 +2271,17 @@ async def _execute_tool_and_respond( except (JsonRpcError, ProcessExitedError, OSError): pass # Connection lost or RPC error — nothing we can do + def _remove_pending_external_tool(self, request_id: str, completed: asyncio.Task[None]) -> None: + if self._pending_external_tools.get(request_id) is completed: + self._pending_external_tools.pop(request_id, None) + + def _claim_external_tool(self, request_id: str) -> bool: + current = asyncio.current_task() + if self._destroyed or self._pending_external_tools.get(request_id) is not current: + return False + self._pending_external_tools.pop(request_id, None) + return True + async def _execute_permission_and_respond( self, request_id: str, @@ -2976,19 +3057,20 @@ async def disconnect(self) -> None: >>> # Clean up when done — session can still be resumed later >>> await session.disconnect() """ - # Ensure that the check and update of _destroyed are atomic so that - # only the first caller proceeds to send the destroy RPC. - with self._event_handlers_lock: - if self._destroyed: - return - self._destroyed = True + async with self._disconnect_lock: + with self._event_handlers_lock: + if self._destroyed: + return - try: - await self._client.request("session.destroy", {"sessionId": self.session_id}) - finally: + response = await self._client.request("session.detach", {"sessionId": self.session_id}) + if not response.get("success"): + detail = response.get("error") or "unknown error" + raise RuntimeError(f"Failed to detach session {self.session_id}: {detail}") + + self._cancel_pending_external_tools() self._run_disconnect_callback() - # Clear handlers even if the request fails. with self._event_handlers_lock: + self._destroyed = True self._event_handlers.clear() with self._tool_handlers_lock: self._tool_handlers.clear() @@ -3050,6 +3132,7 @@ async def set_model( reasoning_summary: ReasoningSummary | None = None, context_tier: ContextTier | None = None, model_capabilities: ModelCapabilitiesOverride | None = None, + auto_tier: AutoTier | _RpcAutoTier | None | _Unset = _UNSET, ) -> None: """ Change the model for this session. @@ -3067,6 +3150,14 @@ async def set_model( context_tier: Optional context window tier for supported models. Omit to use normal model behavior with no explicit tier. model_capabilities: Override individual model capabilities resolved by the runtime. + auto_tier: **Experimental.** Part of an experimental Auto routing + surface and may change or be removed in a future release. + Routing preference to apply when ``model`` is ``"auto"``. + Pass ``None`` to return to the provider's default Auto routing. + Omit the argument to leave the current preference alone. The + runtime rejects this option when ``model`` is anything other than + ``"auto"``; use :meth:`set_auto_tier` to change the preference + without changing the selected model. Raises: Exception: If the session has been destroyed or the connection fails. @@ -3074,23 +3165,79 @@ async def set_model( Example: >>> await session.set_model("gpt-5.4") >>> await session.set_model("claude-sonnet-4.6", reasoning_effort="high") + >>> await session.set_model("auto", auto_tier="intelligence") """ rpc_caps = None if model_capabilities is not None: rpc_caps = _RpcModelCapabilitiesOverride.from_dict( _capabilities_to_dict(model_capabilities) ) - await self.rpc.model.switch_to( - ModelSwitchToRequest( - model_id=model, - reasoning_effort=reasoning_effort, - reasoning_summary=( - _RpcReasoningSummary(reasoning_summary) - if reasoning_summary is not None - else None - ), - context_tier=(_RpcContextTier(context_tier) if context_tier is not None else None), - model_capabilities=rpc_caps, + request = ModelSwitchToRequest( + model_id=model, + reasoning_effort=reasoning_effort, + reasoning_summary=( + _RpcReasoningSummary(reasoning_summary) if reasoning_summary is not None else None + ), + context_tier=(_RpcContextTier(context_tier) if context_tier is not None else None), + model_capabilities=rpc_caps, + ) + if isinstance(auto_tier, _Unset): + await self.rpc.model.switch_to(request) + return + + # The generated wrapper drops null fields, which would silently turn a + # request for default Auto routing into "leave the preference alone", so + # send the payload directly to preserve an explicit null. + params = {k: v for k, v in request.to_dict().items() if v is not None} + params["autoTier"] = _auto_tier_to_wire(auto_tier) + params["sessionId"] = self.session_id + await self._client.request("session.model.switchTo", params) + + async def set_auto_tier( + self, auto_tier: AutoTier | _RpcAutoTier | None + ) -> ModelSwitchAutoTierResult: + """ + Change the Auto routing preference without changing the selected model. + + **Experimental.** Part of an experimental Auto routing surface and may + change or be removed in a future release. + + The runtime does not apply the preference immediately. It records the + request and commits it only when a later user turn using the ``auto`` + model successfully obtains a usable model from the provider. A + ``"pending"`` status therefore confirms that the request was accepted, + not that it took effect. + + Watch for the outcome through the ``session.model_change`` event on + success, or the ephemeral ``session.auto_tier_switch_failed`` event on + failure. You can also read the current committed and in-flight state at + any time with ``session.rpc.model.get_current()``. + + Only the most recent request survives: issuing a new request replaces any + earlier one that has not yet been claimed by a turn. + + Args: + auto_tier: Routing preference to activate, or ``None`` to return to + the provider's default Auto routing. + + Returns: + The runtime's immediate acknowledgement and Auto preference snapshot. + + Raises: + Exception: If the session has been destroyed or the connection fails. + + Example: + >>> result = await session.set_auto_tier("intelligence") + >>> if result.status == ModelSwitchAutoTierStatus.PENDING: + ... pass # Takes effect on a later turn that uses the `auto` model. + """ + # `autoTier` is a required field whose null value means "use provider + # default routing", so this cannot go through the generated wrapper, + # which omits null fields. + return ModelSwitchAutoTierResult.from_dict( + await self._client.request( + "session.model.switchAutoTier", + {"sessionId": self.session_id, "autoTier": _auto_tier_to_wire(auto_tier)}, ) ) diff --git a/python/e2e/_copilot_request_helpers.py b/python/e2e/_copilot_request_helpers.py index 2d91bc9bc2..d4073dd197 100644 --- a/python/e2e/_copilot_request_helpers.py +++ b/python/e2e/_copilot_request_helpers.py @@ -58,8 +58,8 @@ def _wants_stream(body: bytes) -> bool: def model_catalog(supported_endpoints: list[str] | None = None) -> dict: """The synthetic ``/models`` catalog payload.""" model: dict = { - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -67,7 +67,7 @@ def model_catalog(supported_endpoints: list[str] | None = None) -> dict: "model_picker_enabled": True, "capabilities": { "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": {"max_context_window_tokens": 200000, "max_output_tokens": 8192}, "supports": { @@ -191,7 +191,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "chatcmpl-stub-1", "object": "chat.completion.chunk", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", } chunks = [ { @@ -234,7 +234,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [], "stop_reason": None, "stop_sequence": None, @@ -283,7 +283,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [{"type": "text", "text": text}], "stop_reason": "end_turn", "stop_sequence": None, @@ -300,7 +300,7 @@ def build_inference_response(request: httpx.Request, text: str = SYNTHETIC_TEXT) "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "choices": [ { "index": 0, diff --git a/python/e2e/conftest.py b/python/e2e/conftest.py index a789b2b567..d61fe0d875 100644 --- a/python/e2e/conftest.py +++ b/python/e2e/conftest.py @@ -20,10 +20,8 @@ # Out-of-process children resolve auth in their own process where the token already # outranks HMAC. See https://github.com/github/copilot-sdk/issues/1934. if not cli_download.CLI_VERSION: - package_lock = json.loads( - (Path(__file__).parents[2] / "nodejs" / "package-lock.json").read_text() - ) - cli_download.CLI_VERSION = package_lock["packages"]["node_modules/@github/copilot"]["version"] + package_json = json.loads((Path(__file__).parents[2] / "nodejs" / "package.json").read_text()) + cli_download.CLI_VERSION = package_json["copilotCliVersion"] if is_inprocess_transport(): os.environ.pop("COPILOT_HMAC_KEY", None) diff --git a/python/e2e/test_auto_tier_e2e.py b/python/e2e/test_auto_tier_e2e.py new file mode 100644 index 0000000000..5a878c4655 --- /dev/null +++ b/python/e2e/test_auto_tier_e2e.py @@ -0,0 +1,81 @@ +""" +E2E coverage for Auto routing tier switching (snapshot category ``auto_tier``). + +The runtime stages an Auto routing preference instead of applying it immediately: a +request stays "unclaimed" until a later turn using the ``auto`` model mints a usable +model and token pair. These tests observe that staged state through +``model.get_current``, so they assert what the runtime actually recorded rather than +what the SDK serialized. +""" + +from __future__ import annotations + +import pytest + +from copilot.rpc import ModelSwitchAutoTierStatus +from copilot.session import PermissionHandler +from copilot.session_events import AutoTier + +from .testharness import E2ETestContext + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +async def pending_auto_tier(session) -> AutoTier | None: + return (await session.rpc.model.get_current()).pending_auto_tier + + +class TestAutoTier: + async def test_should_stage_and_reset_auto_tier_preference(self, ctx: E2ETestContext): + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="auto", + ) + try: + assert await pending_auto_tier(session) is None + + staged = await session.set_auto_tier("efficiency") + assert staged.status == ModelSwitchAutoTierStatus.PENDING + assert staged.pending_auto_tier == AutoTier.EFFICIENCY + assert await pending_auto_tier(session) == AutoTier.EFFICIENCY + + # A second request replaces the first and reports the one it displaced. + superseded = await session.set_auto_tier("intelligence") + assert superseded.status == ModelSwitchAutoTierStatus.PENDING + assert superseded.pending_auto_tier == AutoTier.INTELLIGENCE + assert superseded.superseded_auto_tier == AutoTier.EFFICIENCY + assert await pending_auto_tier(session) == AutoTier.INTELLIGENCE + + # Passing None returns the session to provider-default routing. The status is + # "unchanged" because provider-default was already the committed preference; + # the request's effect is cancelling the staged one. + reset = await session.set_auto_tier(None) + assert reset.status == ModelSwitchAutoTierStatus.UNCHANGED + assert reset.superseded_auto_tier == AutoTier.INTELLIGENCE + assert await pending_auto_tier(session) is None + finally: + await session.disconnect() + + async def test_should_preserve_auto_tier_when_set_model_omits_it(self, ctx: E2ETestContext): + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + model="auto", + ) + try: + await session.set_auto_tier("balance") + assert await pending_auto_tier(session) == AutoTier.BALANCE + + # Omitting the argument leaves the staged preference alone. + await session.set_model("auto") + assert await pending_auto_tier(session) == AutoTier.BALANCE + + # Supplying a tier replaces it. + await session.set_model("auto", auto_tier="intelligence") + assert await pending_auto_tier(session) == AutoTier.INTELLIGENCE + + # Supplying None clears it. Omission, a value, and None are three distinct + # outcomes, which is why the argument cannot collapse to a plain optional. + await session.set_model("auto", auto_tier=None) + assert await pending_auto_tier(session) is None + finally: + await session.disconnect() diff --git a/python/e2e/test_client_options_e2e.py b/python/e2e/test_client_options_e2e.py index fe1ed54820..b07e9a5402 100644 --- a/python/e2e/test_client_options_e2e.py +++ b/python/e2e/test_client_options_e2e.py @@ -168,6 +168,10 @@ def _get_available_port() -> int: writeResponse(message.id, { success: true }); return; } + if (message.method === "session.detach") { + writeResponse(message.id, { success: true }); + return; + } writeResponse(message.id, {}); } @@ -396,7 +400,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request( await client.start() session = await client.create_session( client_name="advanced-create-client", - model="claude-sonnet-4.5", + model="claude-sonnet-5", reasoning_effort="medium", reasoning_summary="detailed", context_tier="long_context", @@ -470,7 +474,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request( "provider": "create-provider", "id": "create-model", "name": "Create Model", - "model_id": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", "wire_model": "create-wire-model", "max_context_window_tokens": 12_000, "max_prompt_tokens": 10_000, @@ -482,7 +486,7 @@ async def test_should_forward_advanced_session_options_in_create_wire_request( try: params = _get_captured_request(capture_path, "session.create") assert params["clientName"] == "advanced-create-client" - assert params["model"] == "claude-sonnet-4.5" + assert params["model"] == "claude-sonnet-5" assert params["reasoningEffort"] == "medium" assert params["reasoningSummary"] == "detailed" assert params["contextTier"] == "long_context" @@ -538,7 +542,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request( try: await client.start() session = await client.create_session( - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider={ "type": "azure", "wire_api": "responses", @@ -548,7 +552,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request( "bearer_token": "provider-bearer-token", "azure": {"api_version": "2024-02-15-preview"}, "headers": {"X-Provider-Wire": "yes"}, - "model_id": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", "wire_model": "azure-deployment", "max_prompt_tokens": 8192, "max_output_tokens": 1024, @@ -565,7 +569,7 @@ async def test_should_forward_singular_provider_options_in_create_wire_request( assert provider["bearerToken"] == "provider-bearer-token" assert provider["azure"]["apiVersion"] == "2024-02-15-preview" assert provider["headers"]["X-Provider-Wire"] == "yes" - assert provider["modelId"] == "claude-sonnet-4.5" + assert provider["modelId"] == "claude-sonnet-5" assert provider["wireModel"] == "azure-deployment" assert provider["maxPromptTokens"] == 8192 assert provider["maxOutputTokens"] == 1024 diff --git a/python/e2e/test_copilot_request_session_id_e2e.py b/python/e2e/test_copilot_request_session_id_e2e.py index 81624d73d0..75bf15f2a4 100644 --- a/python/e2e/test_copilot_request_session_id_e2e.py +++ b/python/e2e/test_copilot_request_session_id_e2e.py @@ -105,14 +105,14 @@ async def test_threads_session_id_into_byok_session(self, session_id_client): baseline = len(handler.records) session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider={ "type": "openai", "wire_api": "responses", "base_url": "https://byok.invalid/v1", "api_key": "byok-secret", - "model_id": "claude-sonnet-4.5", - "wire_model": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", + "wire_model": "claude-sonnet-5", }, ) byok_session_id = session.session_id diff --git a/python/e2e/test_event_fidelity_e2e.py b/python/e2e/test_event_fidelity_e2e.py index 25b18407a0..6b14ab2486 100644 --- a/python/e2e/test_event_fidelity_e2e.py +++ b/python/e2e/test_event_fidelity_e2e.py @@ -46,7 +46,7 @@ async def test_should_emit_events_in_correct_order_for_tool_using_conversation( assert user_idx < assistant_idx idle_idx = len(types) - 1 - types[::-1].index("session.idle") - assert idle_idx == len(types) - 1 + assert assistant_idx < idle_idx finally: unsubscribe() await session.disconnect() diff --git a/python/e2e/test_external_tool_cancellation_e2e.py b/python/e2e/test_external_tool_cancellation_e2e.py new file mode 100644 index 0000000000..aca28ceadf --- /dev/null +++ b/python/e2e/test_external_tool_cancellation_e2e.py @@ -0,0 +1,62 @@ +""" +E2E tests for external tool cancellation. +""" + +from __future__ import annotations + +import asyncio + +import pytest + +from copilot.session import PermissionHandler +from copilot.tools import Tool, ToolInvocation, ToolResult + +from .testharness import E2ETestContext + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +class TestExternalToolCancellation: + async def test_should_cancel_tool_handler_when_session_disconnects(self, ctx: E2ETestContext): + tool_started = asyncio.Event() + tool_cancelled = asyncio.Event() + release_tool: asyncio.Future = asyncio.get_event_loop().create_future() + + async def slow_tool_handler(invocation: ToolInvocation) -> ToolResult: + _ = (invocation.arguments or {}).get("value", "") + tool_started.set() + try: + result = await asyncio.wait_for(release_tool, timeout=120.0) + return ToolResult(text_result_for_llm=str(result)) + except asyncio.CancelledError: + tool_cancelled.set() + raise + + session = await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + tools=[ + Tool( + name="slow_analysis", + description="A slow analysis tool that blocks until released", + parameters={ + "type": "object", + "properties": { + "value": {"type": "string", "description": "Value to analyze"} + }, + "required": ["value"], + }, + handler=slow_tool_handler, + ) + ], + ) + + try: + asyncio.ensure_future( + session.send("Use slow_analysis with value 'test_abort'. Wait for the result.") + ) + await asyncio.wait_for(tool_started.wait(), timeout=60.0) + await session.disconnect() + await asyncio.wait_for(tool_cancelled.wait(), timeout=60.0) + finally: + if not release_tool.done(): + release_tool.set_result("RELEASED") diff --git a/python/e2e/test_mcp_oauth_e2e.py b/python/e2e/test_mcp_oauth_e2e.py index 76f1cbf721..3502ccdf4a 100644 --- a/python/e2e/test_mcp_oauth_e2e.py +++ b/python/e2e/test_mcp_oauth_e2e.py @@ -128,6 +128,7 @@ def on_mcp_auth_request(request, _invocation): on_mcp_auth_request=on_mcp_auth_request, mcp_servers=mcp_servers, ) as session: + await session.rpc.mcp.reload() await _wait_for_mcp_server_status(session, server_name) tools = await session.rpc.mcp.list_tools( @@ -164,13 +165,11 @@ async def test_should_resolve_pending_mcp_oauth_request_with_direct_rpc( ): url, process = await _start_oauth_mcp_server() server_name = "oauth-direct-rpc-mcp" - loop = asyncio.get_running_loop() - observed_request = loop.create_future() + observed_requests = asyncio.Queue() release_handler = asyncio.Event() async def on_mcp_auth_request(request, _invocation): - if not observed_request.done(): - observed_request.set_result(request) + observed_requests.put_nowait(request) await release_handler.wait() return {"kind": "token", "accessToken": EXPECTED_TOKEN} @@ -190,9 +189,29 @@ async def on_mcp_auth_request(request, _invocation): mcp_servers=mcp_servers, enable_mcp_apps=True, ) as session: + # session.create can begin MCP startup before the SDK registers OAuth + # event interest. Reload after registration so this test cannot lose + # the initial challenge to that race. + reload_task = asyncio.create_task(session.rpc.mcp.reload()) connected = asyncio.create_task(_wait_for_mcp_server_status(session, server_name)) try: - request = await asyncio.wait_for(observed_request, timeout=30.0) + request = await asyncio.wait_for(observed_requests.get(), timeout=30.0) + while True: + handled = await session.rpc.mcp.oauth.handle_pending_request( + MCPOauthHandlePendingRequest( + request_id=request["requestId"], + result=MCPOauthPendingRequestResponse( + kind=GitHubTokenAcquireResultKind.TOKEN, + access_token=EXPECTED_TOKEN, + token_type="Bearer", + expires_in=3600, + ), + ) + ) + if handled.success: + break + request = await asyncio.wait_for(observed_requests.get(), timeout=30.0) + assert request["serverName"] == server_name assert request["serverUrl"] == f"{url}/mcp" assert request["reason"] == "initial" @@ -202,19 +221,8 @@ async def on_mcp_auth_request(request, _invocation): "error": "invalid_token", } - handled = await session.rpc.mcp.oauth.handle_pending_request( - MCPOauthHandlePendingRequest( - request_id=request["requestId"], - result=MCPOauthPendingRequestResponse( - kind=GitHubTokenAcquireResultKind.TOKEN, - access_token=EXPECTED_TOKEN, - token_type="Bearer", - expires_in=3600, - ), - ) - ) - assert handled.success is True - + release_handler.set() + await asyncio.wait_for(reload_task, timeout=60.0) connected_result = await asyncio.wait_for(connected, timeout=60.0) assert connected_result is None tools = await session.rpc.mcp.list_tools( @@ -225,6 +233,9 @@ async def on_mcp_auth_request(request, _invocation): release_handler.set() if not connected.done(): connected.cancel() + if not reload_task.done(): + reload_task.cancel() + await asyncio.gather(connected, reload_task, return_exceptions=True) finally: await _stop_process(process) @@ -270,7 +281,11 @@ def on_mcp_auth_request(request, _invocation): mcp_servers=mcp_servers, enable_mcp_apps=True, ) as session: + # Re-run startup after OAuth event interest is registered to avoid + # racing the initial challenge emitted during session.create. + await session.rpc.mcp.reload() await _wait_for_mcp_server_status(session, server_name) + refresh_count = 0 for scenario in ("refresh", "upscope", "reauth"): result = await session.rpc.mcp.apps.call_tool( @@ -283,8 +298,9 @@ def on_mcp_auth_request(request, _invocation): ) assert result["content"] == [{"type": "text", "text": "oauth-test-user"}] - assert [request["reason"] for request in observed_requests] == [ - "initial", + assert [ + request["reason"] for request in observed_requests if request["reason"] != "initial" + ] == [ "refresh", "upscope", "refresh", @@ -324,6 +340,7 @@ def on_mcp_auth_request(request, _invocation): on_mcp_auth_request=on_mcp_auth_request, mcp_servers=mcp_servers, ) as session: + await session.rpc.mcp.reload() await _wait_for_mcp_server_status(session, server_name, McpServerStatus.NEEDS_AUTH) # The MCP connection is kicked off by session.create, but the SDK only registers diff --git a/python/e2e/test_multi_client_e2e.py b/python/e2e/test_multi_client_e2e.py index 91beb2239e..1938ddfe89 100644 --- a/python/e2e/test_multi_client_e2e.py +++ b/python/e2e/test_multi_client_e2e.py @@ -345,7 +345,16 @@ async def test_one_client_rejects_permission_and_both_see_the_result( client2_completed = wait_for_event( session2, lambda event: event.type.value == "permission.completed" ) - waiters = [client1_requested, client2_requested, client1_completed, client2_completed] + session1_idle = wait_for_event( + session1, lambda event: event.type.value == "session.idle" + ) + waiters = [ + client1_requested, + client2_requested, + client1_completed, + client2_completed, + session1_idle, + ] # Create a file that the agent will try to edit test_file = os.path.join(mctx.work_dir, "protected.txt") @@ -353,12 +362,6 @@ async def test_one_client_rejects_permission_and_both_see_the_result( f.write("protected content") await session1.send("Edit protected.txt and replace 'protected' with 'hacked'.") - await get_final_assistant_message(session1) - - # Verify the file was NOT modified (permission was denied) - with open(test_file) as f: - content = f.read() - assert content == "protected content" # Both clients should have seen permission.requested and permission.completed await asyncio.gather(client1_requested, client2_requested) @@ -367,6 +370,13 @@ async def test_one_client_rejects_permission_and_both_see_the_result( completed_events = await asyncio.gather(client1_completed, client2_completed) for event in completed_events: assert event.data.result.kind == "denied-interactively-by-user" + + assert (await session1_idle).type.value == "session.idle" + + # Verify the file was NOT modified (permission was denied) + with open(test_file) as f: + content = f.read() + assert content == "protected content" finally: for waiter in waiters: if not waiter.done(): diff --git a/python/e2e/test_rewind_e2e.py b/python/e2e/test_rewind_e2e.py index fa87c08b64..cf7627c1a3 100644 --- a/python/e2e/test_rewind_e2e.py +++ b/python/e2e/test_rewind_e2e.py @@ -35,7 +35,7 @@ async def test_should_restore_tracked_file_and_conversation(self, ctx: E2ETestCo file_path = Path(ctx.work_dir) / FILE_NAME file_path.write_text(ORIGINAL_FILE_CONTENT, encoding="utf-8") session = await ctx.client.create_session( - model="claude-sonnet-4.5", + model="claude-sonnet-5", enable_file_change_tracking=True, on_permission_request=PermissionHandler.approve_all, ) diff --git a/python/e2e/test_rpc_e2e.py b/python/e2e/test_rpc_e2e.py index 4440635727..c9f08742cd 100644 --- a/python/e2e/test_rpc_e2e.py +++ b/python/e2e/test_rpc_e2e.py @@ -82,7 +82,7 @@ class TestSessionRpc: async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): """Test calling session.rpc.model.getCurrent""" session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" ) result = await session.rpc.model.get_current() @@ -96,7 +96,7 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext from copilot.rpc import ModelSwitchToRequest session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" ) # Get initial model diff --git a/python/e2e/test_rpc_server_e2e.py b/python/e2e/test_rpc_server_e2e.py index e7c4a446ce..fdff3b8004 100644 --- a/python/e2e/test_rpc_server_e2e.py +++ b/python/e2e/test_rpc_server_e2e.py @@ -183,7 +183,7 @@ async def test_should_call_rpc_models_list_with_typed_result(self, authed_ctx: E await client.start() result = await client.rpc.models.list(ModelsListRequest()) assert result.models is not None - assert any(model.id == "claude-sonnet-4.5" for model in result.models) + assert any(model.id == "claude-sonnet-5" for model in result.models) assert all((model.name or "").strip() for model in result.models) finally: try: diff --git a/python/e2e/test_rpc_server_misc_e2e.py b/python/e2e/test_rpc_server_misc_e2e.py index d5ade1aec1..2b6b7d9514 100644 --- a/python/e2e/test_rpc_server_misc_e2e.py +++ b/python/e2e/test_rpc_server_misc_e2e.py @@ -131,8 +131,11 @@ async def test_should_login_list_get_current_auth_and_logout_account(self, ctx: client, home = await _create_isolated_client(ctx, github_token=None) try: - initial = await client.rpc.account.get_current_auth() - assert initial.auth_info is None + assert client._client is not None + initial_current = await client._client.request("account.getCurrentAuth", {}) + initial_auth_info = initial_current.get("authInfo") + assert initial_auth_info is not None + assert initial_auth_info.get("login") != login login_result = await client.rpc.account.login( AccountLoginRequest(host="https://github.com", login=login, token=token) @@ -152,21 +155,26 @@ async def test_should_login_list_get_current_auth_and_logout_account(self, ctx: ( user for user in users - if user.auth_info.type == "user" - and getattr(user.auth_info, "login", None) == login + if user.get("authInfo", {}).get("type") == "user" + and user.get("authInfo", {}).get("login") == login ), None, ) if account is not None: - assert account.token == token + assert account.get("token") == token logout = await client.rpc.account.logout( AccountLogoutRequest(auth_info=current.auth_info) ) assert logout.has_more_users is False - after_logout = await client.rpc.account.get_current_auth() - assert after_logout.auth_info is None + users_after_logout = await client.rpc.account.get_all_users() + assert all( + user.get("authInfo", {}).get("login") != login for user in users_after_logout + ) + + current_after_logout = await client._client.request("account.getCurrentAuth", {}) + assert current_after_logout.get("authInfo") == initial_auth_info finally: await _dispose_isolated(client, home) diff --git a/python/e2e/test_rpc_session_state_e2e.py b/python/e2e/test_rpc_session_state_e2e.py index f4b03d2e65..622192cfe9 100644 --- a/python/e2e/test_rpc_session_state_e2e.py +++ b/python/e2e/test_rpc_session_state_e2e.py @@ -104,7 +104,7 @@ class TestRpcSessionState: async def test_should_call_session_rpc_model_get_current(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", ) try: result = await session.rpc.model.get_current() @@ -128,7 +128,7 @@ async def test_should_call_session_rpc_model_switchto(self, ctx: E2ETestContext) ) session = await isolated_ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", ) try: before = await session.rpc.model.get_current() @@ -264,13 +264,13 @@ async def test_should_call_metadata_snapshot_set_working_directory_and_record_co session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", working_directory=first_dir, ) try: snapshot = await session.rpc.metadata.snapshot() assert snapshot.session_id == session.session_id - assert snapshot.selected_model == "claude-sonnet-4.5" + assert snapshot.selected_model == "claude-sonnet-5" assert snapshot.is_remote is False assert snapshot.already_in_use is False assert _path_equals(first_dir, snapshot.working_directory) @@ -395,7 +395,7 @@ async def snapshot_updated() -> bool: async def test_should_set_reasoning_effort_and_auto_name(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", ) try: reasoning = await session.rpc.model.set_reasoning_effort( @@ -403,7 +403,7 @@ async def test_should_set_reasoning_effort_and_auto_name(self, ctx: E2ETestConte ) assert reasoning.reasoning_effort == "high" current = await session.rpc.model.get_current() - assert current.model_id == "claude-sonnet-4.5" + assert current.model_id == "claude-sonnet-5" assert current.reasoning_effort == "high" auto_name = f"Auto Session {uuid.uuid4().hex}" @@ -637,12 +637,12 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC MetadataContextInfoRequest( prompt_token_limit=128_000, output_token_limit=4_096, - selected_model="claude-sonnet-4.5", + selected_model="claude-sonnet-5", ) ) if context_info.context_info is not None: context = context_info.context_info - assert context.model_name == "claude-sonnet-4.5" + assert context.model_name == "claude-sonnet-5" assert context.prompt_token_limit == 128_000 assert context.limit >= context.prompt_token_limit assert context.total_tokens > 0 @@ -657,7 +657,7 @@ async def test_should_compact_session_history_after_messages(self, ctx: E2ETestC ) recomputed = await session.rpc.metadata.recompute_context_tokens( - MetadataRecomputeContextTokensRequest(model_id="claude-sonnet-4.5") + MetadataRecomputeContextTokensRequest(model_id="claude-sonnet-5") ) assert recomputed.system_token_count > 0 assert recomputed.messages_token_count > 0 diff --git a/python/e2e/test_rpc_session_state_extras_e2e.py b/python/e2e/test_rpc_session_state_extras_e2e.py index 7523059c7b..02ee0cd790 100644 --- a/python/e2e/test_rpc_session_state_extras_e2e.py +++ b/python/e2e/test_rpc_session_state_extras_e2e.py @@ -77,7 +77,7 @@ async def test_should_list_models_for_session(self, ctx: E2ETestContext): client = _make_authed_client(ctx, token) try: async with await client.create_session( - model="claude-sonnet-4.5", + model="claude-sonnet-5", on_permission_request=PermissionHandler.approve_all, github_token=token, ) as session: @@ -86,8 +86,7 @@ async def test_should_list_models_for_session(self, ctx: E2ETestContext): assert result.list is not None assert len(result.list) > 0 assert any( - "claude-sonnet-4.5" in json.dumps(model, sort_keys=True) - for model in result.list + "claude-sonnet-5" in json.dumps(model, sort_keys=True) for model in result.list ) finally: await _stop_client(client) @@ -126,7 +125,7 @@ async def test_should_add_byok_provider_and_model_at_runtime(self, ctx: E2ETestC provider=provider_name, id=model_id, name="SDK Runtime Model", - model_id="claude-sonnet-4.5", + model_id="claude-sonnet-5", wire_model="wire-sdk-runtime-model", max_context_window_tokens=4096, max_prompt_tokens=3072, diff --git a/python/e2e/test_session_config_e2e.py b/python/e2e/test_session_config_e2e.py index 62dc671893..4fc78e645d 100644 --- a/python/e2e/test_session_config_e2e.py +++ b/python/e2e/test_session_config_e2e.py @@ -167,8 +167,8 @@ def _create_anthropic_provider() -> dict: "type": "anthropic", "base_url": "https://anthropic-citations.invalid/v1", "api_key": "test-provider-key", - "model_id": "claude-sonnet-4.5", - "wire_model": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", + "wire_model": "claude-sonnet-5", } @@ -201,6 +201,7 @@ async def test_vision_disabled_then_enabled_via_setmodel(self, ctx: E2ETestConte session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, + model="claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=False) ), @@ -213,7 +214,7 @@ async def test_vision_disabled_then_enabled_via_setmodel(self, ctx: E2ETestConte # Switch vision on await session.set_model( - "claude-sonnet-4.5", + "claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=True) ), @@ -234,6 +235,7 @@ async def test_vision_enabled_then_disabled_via_setmodel(self, ctx: E2ETestConte session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, + model="claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=True) ), @@ -246,7 +248,7 @@ async def test_vision_enabled_then_disabled_via_setmodel(self, ctx: E2ETestConte # Switch vision off await session.set_model( - "claude-sonnet-4.5", + "claude-sonnet-5", model_capabilities=ModelCapabilitiesOverride( supports=ModelSupportsOverride(vision=False) ), @@ -295,7 +297,7 @@ async def test_should_forward_clientname_in_useragent(self, ctx: E2ETestContext) async def test_should_forward_custom_provider_headers_on_create(self, ctx: E2ETestContext): session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider=_make_proxy_provider(ctx.proxy_url, "create-provider-header"), ) @@ -319,7 +321,7 @@ async def test_should_forward_custom_provider_headers_on_resume(self, ctx: E2ETe session2 = await ctx.client.resume_session( session_id, on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider=_make_proxy_provider(ctx.proxy_url, "resume-provider-header"), ) @@ -345,7 +347,7 @@ async def test_should_forward_provider_wire_model(self, ctx: E2ETestContext): # it directly (see unit tests for serialization coverage). session = await ctx.client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", provider={ "type": "openai", "base_url": ctx.proxy_url, @@ -374,7 +376,7 @@ async def test_should_use_provider_model_id_as_wire_model(self, ctx: E2ETestCont "type": "openai", "base_url": ctx.proxy_url, "api_key": "test-provider-key", - "model_id": "claude-sonnet-4.5", + "model_id": "claude-sonnet-5", }, ) @@ -382,7 +384,7 @@ async def test_should_use_provider_model_id_as_wire_model(self, ctx: E2ETestCont exchanges = await ctx.get_exchanges() assert len(exchanges) == 1 - assert exchanges[0]["request"]["model"] == "claude-sonnet-4.5" + assert exchanges[0]["request"]["model"] == "claude-sonnet-5" await session.disconnect() @@ -473,7 +475,7 @@ async def test_should_enable_citations_for_anthropic_file_attachments_on_create( try: session = await client.create_session( on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", enable_citations=True, provider=_create_anthropic_provider(), ) @@ -521,7 +523,7 @@ async def test_should_enable_citations_for_anthropic_file_attachments_on_resume( session2 = await resume_client.resume_session( session1.session_id, on_permission_request=PermissionHandler.approve_all, - model="claude-sonnet-4.5", + model="claude-sonnet-5", enable_citations=True, provider=_create_anthropic_provider(), ) diff --git a/python/e2e/test_session_e2e.py b/python/e2e/test_session_e2e.py index 08413f228f..f57b9f5736 100644 --- a/python/e2e/test_session_e2e.py +++ b/python/e2e/test_session_e2e.py @@ -2,6 +2,7 @@ import base64 import os +import uuid from datetime import datetime import pytest @@ -25,7 +26,7 @@ class TestSessions: async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): session = await ctx.client.create_session( - on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-4.5" + on_permission_request=PermissionHandler.approve_all, model="claude-sonnet-5" ) assert session.session_id @@ -33,7 +34,7 @@ async def test_should_create_and_disconnect_sessions(self, ctx: E2ETestContext): assert len(messages) > 0 assert messages[0].type.value == "session.start" assert messages[0].data.session_id == session.session_id - assert messages[0].data.selected_model == "claude-sonnet-4.5" + assert messages[0].data.selected_model == "claude-sonnet-5" await session.disconnect() @@ -315,6 +316,57 @@ def on_mcp_auth_request(_request, _invocation): finally: await new_client.force_stop() + async def test_should_recover_marker_after_cold_resume_with_explicit_session_id( + self, ctx: E2ETestContext + ): + session_id = f"e2e-cold-resume-{uuid.uuid4()}" + github_token = DEFAULT_GITHUB_TOKEN if os.environ.get("GITHUB_ACTIONS") == "true" else None + + client1 = CopilotClient( + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, + ) + try: + session1 = await client1.create_session( + on_permission_request=PermissionHandler.approve_all, + session_id=session_id, + ) + assert session1.session_id == session_id + + answer = await session1.send_and_wait( + "Please remember this exact secret marker for later - MARKER-7f3ac21e. " + 'Reply with only the single word "Acknowledged".' + ) + assert answer is not None + assert "Acknowledged" in answer.data.content + + await session1.disconnect() + finally: + await client1.force_stop() + + client2 = CopilotClient( + connection=RuntimeConnection.for_stdio(path=ctx.cli_path), + working_directory=ctx.work_dir, + env=ctx.get_env(), + github_token=github_token, + ) + try: + session2 = await client2.resume_session( + session_id, on_permission_request=PermissionHandler.approve_all + ) + assert session2.session_id == session_id + + answer2 = await session2.send_and_wait( + "What was the exact secret marker I asked you to remember earlier? " + "Reply with only that marker value and nothing else." + ) + assert answer2 is not None + assert "MARKER-7f3ac21e" in answer2.data.content + finally: + await client2.force_stop() + async def test_should_throw_error_resuming_nonexistent_session(self, ctx: E2ETestContext): with pytest.raises(Exception): await ctx.client.resume_session( diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index 8eaecf6244..12eb9466fa 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -9,91 +9,47 @@ import os import re import shutil +import subprocess import tempfile import time -from collections.abc import Sequence from pathlib import Path from typing import Any from copilot import CopilotClient, RuntimeConnection -from copilot._cli_version import get_npm_platform from .proxy import CapiProxy -def _cli_platform_package_names(npm_platform: str | None = None) -> list[str]: - """Return candidate ``@github/copilot-*`` directory names, best match first. - - Mirrors ``getCliPlatformPackageNames()`` in ``nodejs/src/client.ts``: as of CLI - 1.0.64-1 the runnable ``index.js`` ships in a platform package such as - ``copilot-darwin-arm64``. On Linux both libc variants are listed (the detected - one first) because npm installs exactly one of them and musl probing can come up - empty in minimal containers. - """ - primary = npm_platform or get_npm_platform() - names = [f"copilot-{primary}"] - if primary.startswith("linux"): - arch = primary.rsplit("-", 1)[-1] - for variant in (f"linux-{arch}", f"linuxmusl-{arch}"): - name = f"copilot-{variant}" - if name not in names: - names.append(name) - return names - - -def _find_cli_in_node_modules(github_modules: Path, package_names: Sequence[str]) -> str | None: - """Return the resolved ``index.js`` of the first installed candidate package. - - Only exact package names are probed, so unrelated ``copilot-*`` directories - (e.g. ``copilot-language-server``) can never be mistaken for the CLI. - """ - for name in package_names: - candidate = github_modules / name / "index.js" - if candidate.exists(): - return str(candidate.resolve()) - return None - - -def _installed_cli_package_names(github_modules: Path) -> list[str]: - """Return the ``copilot-*`` directory names present, for error messages only. - - Selection never globs — that was the #2103 bug. This exists so a failure can - say what *is* installed, which is the difference between a dead-end "run npm - install" and a message that diagnoses itself on a mixed-architecture host. - """ - if not github_modules.is_dir(): - return [] - return sorted(path.name for path in github_modules.glob("copilot-*") if path.is_dir()) +def _prepare_pinned_cli(repo_root: Path) -> str: + npm = "npm.cmd" if os.name == "nt" else "npm" + result = subprocess.run( + [npm, "run", "--silent", "prepare:runtime", "--", "--print-path"], + cwd=repo_root / "nodejs", + capture_output=True, + text=True, + check=False, + ) + output = result.stdout.strip() + if result.returncode != 0 or not output: + detail = result.stderr.strip() or output or f"exit code {result.returncode}" + raise RuntimeError(f"Failed to prepare the pinned Copilot CLI: {detail}") + cli_path = Path(output.splitlines()[-1]) + if not cli_path.is_file(): + raise RuntimeError(f"Pinned Copilot CLI was not created at {cli_path}") + return str(cli_path.resolve()) def get_cli_path_for_tests() -> str: """Get CLI path for E2E tests. - Uses COPILOT_CLI_PATH env var if set, otherwise the platform-specific CLI - package in the sibling nodejs directory's node_modules. + Uses COPILOT_CLI_PATH env var if set, otherwise prepares the release pinned + by the sibling Node.js SDK. """ env_path = os.environ.get("COPILOT_CLI_PATH") if env_path and Path(env_path).exists(): return str(Path(env_path).resolve()) - # Look for CLI in sibling nodejs directory's node_modules. As of CLI 1.0.64-1 - # the @github/copilot package is a thin loader; the runnable index.js ships in - # the installed platform package (e.g. @github/copilot-linux-x64), so pick the - # one built for this host rather than whichever sorts first (#2103). - base_path = Path(__file__).parents[3] - github_modules = base_path / "nodejs" / "node_modules" / "@github" - package_names = _cli_platform_package_names() - found = _find_cli_in_node_modules(github_modules, package_names) - if found is not None: - return found - - installed = _installed_cli_package_names(github_modules) - raise RuntimeError( - f"CLI not found for tests under {github_modules} " - f"(tried: {', '.join(package_names)}; " - f"present: {', '.join(installed) or 'none'}). " - "Run 'npm install' in the nodejs directory, or set COPILOT_CLI_PATH." - ) + return _prepare_pinned_cli(Path(__file__).parents[3]) CLI_PATH = get_cli_path_for_tests() @@ -191,6 +147,7 @@ def _apply_inprocess_environment(self) -> None: { "GH_TOKEN": DEFAULT_GITHUB_TOKEN, "GITHUB_TOKEN": DEFAULT_GITHUB_TOKEN, + "COPILOT_CLI_PATH": self.cli_path, "COPILOT_HMAC_KEY": "", "CAPI_HMAC_KEY": "", } diff --git a/python/e2e/testharness/proxy.py b/python/e2e/testharness/proxy.py index 58584b831c..9d8fc73e63 100644 --- a/python/e2e/testharness/proxy.py +++ b/python/e2e/testharness/proxy.py @@ -5,15 +5,19 @@ It spawns the shared test harness server from test/harness/server.ts. """ +import asyncio import json import os import platform import re import subprocess +import warnings from typing import Any import httpx +PROCESS_SHUTDOWN_TIMEOUT_SECONDS = 5 + class CapiProxy: """Manages a replaying proxy server for E2E tests.""" @@ -46,15 +50,17 @@ async def start(self) -> str: cwd=os.path.dirname(server_path), shell=use_shell, ) + process = self._process + assert process.stdout is not None # Read until the server prints "Listening: http://..."; npm/npx may emit # wrapper output first on some platforms. line = "" match = None while True: - line = self._process.stdout.readline() + line = process.stdout.readline() if not line: - self._process.kill() + process.kill() raise RuntimeError("Failed to read proxy URL") match = re.search(r"Listening: (http://[^\s]+)", line.strip()) if match: @@ -63,17 +69,17 @@ async def start(self) -> str: self._proxy_url = match.group(1) metadata_match = re.search(r"(\{.*\})\s*$", line.strip()) if not metadata_match: - self._process.kill() + process.kill() raise RuntimeError(f"Proxy startup line missing CONNECT proxy metadata: {line}") try: metadata = json.loads(metadata_match.group(1)) except json.JSONDecodeError as exc: - self._process.kill() + process.kill() raise RuntimeError(f"Failed to parse proxy startup metadata: {line}") from exc self._connect_proxy_url = metadata.get("connectProxyUrl") self._ca_file_path = metadata.get("caFilePath") if not self._connect_proxy_url or not self._ca_file_path: - self._process.kill() + process.kill() raise RuntimeError(f"Proxy startup metadata missing CONNECT proxy details: {line}") return self._proxy_url @@ -97,10 +103,23 @@ async def stop(self, skip_writing_cache: bool = False): except Exception: pass # Best effort - # Wait for process to exit - self._process.wait() - self._process = None - self._proxy_url = None + try: + process = self._process + try: + await asyncio.to_thread(process.wait, timeout=PROCESS_SHUTDOWN_TIMEOUT_SECONDS) + except subprocess.TimeoutExpired: + _kill_process_tree(process) + try: + await asyncio.to_thread(process.wait, timeout=PROCESS_SHUTDOWN_TIMEOUT_SECONDS) + except subprocess.TimeoutExpired: + warnings.warn( + f"Proxy process {process.pid} did not exit after being killed", + RuntimeWarning, + stacklevel=2, + ) + finally: + self._process = None + self._proxy_url = None async def configure(self, file_path: str, work_dir: str): """Send configuration to the proxy.""" @@ -164,3 +183,28 @@ def get_proxy_env(self) -> dict[str, str]: "GH_ENTERPRISE_TOKEN": "", "GITHUB_ENTERPRISE_TOKEN": "", } + + +def _kill_process_tree(process: subprocess.Popen) -> None: + if process.poll() is not None: + return + + if platform.system() == "Windows": + try: + subprocess.run( + ["taskkill", "/PID", str(process.pid), "/T", "/F"], + check=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=PROCESS_SHUTDOWN_TIMEOUT_SECONDS, + ) + except subprocess.TimeoutExpired: + # Fall through to the direct-process kill below. + pass + + if process.poll() is None: + try: + process.kill() + except ProcessLookupError: + # The process exited between poll() and kill(). + pass diff --git a/python/scripts/inject-cli-version.mjs b/python/scripts/inject-cli-version.mjs index 359e7f680b..fef0f967b8 100644 --- a/python/scripts/inject-cli-version.mjs +++ b/python/scripts/inject-cli-version.mjs @@ -2,7 +2,7 @@ /** * inject-cli-version.mjs * - * Reads the pinned @github/copilot version from nodejs/package-lock.json and + * Reads the pinned Copilot CLI version from nodejs/package.json and * writes it into python/copilot/_cli_version.py, replacing the `CLI_VERSION = None` * sentinel with the concrete version string. * @@ -17,19 +17,13 @@ import { fileURLToPath } from "url"; const __dirname = dirname(fileURLToPath(import.meta.url)); const repoRoot = join(__dirname, "..", ".."); -// Read version from nodejs/package-lock.json -const lockPath = join(repoRoot, "nodejs", "package-lock.json"); -const lock = JSON.parse(readFileSync(lockPath, "utf-8")); - -// The version is in packages["node_modules/@github/copilot"].version -const copilotPkg = lock.packages?.["node_modules/@github/copilot"]; -if (!copilotPkg?.version) { - console.error( - "Error: Could not find @github/copilot version in nodejs/package-lock.json" - ); +const packagePath = join(repoRoot, "nodejs", "package.json"); +const packageJson = JSON.parse(readFileSync(packagePath, "utf-8")); +const version = packageJson.copilotCliVersion; +if (!version) { + console.error("Error: Could not find copilotCliVersion in nodejs/package.json"); process.exit(1); } -const version = copilotPkg.version; console.log(`Injecting CLI_VERSION = "${version}"`); // Patch _cli_version.py diff --git a/python/test_cli_download.py b/python/test_cli_download.py index a5a20dce0d..fcf65df41d 100644 --- a/python/test_cli_download.py +++ b/python/test_cli_download.py @@ -1,167 +1,320 @@ -"""Tests for the in-process runtime library download integrity checks.""" +"""Tests for unified Copilot release-package provisioning.""" from __future__ import annotations -import base64 import hashlib import io import os import tarfile -from unittest.mock import patch +from concurrent.futures import ThreadPoolExecutor +from http.client import IncompleteRead +from threading import Barrier +from unittest.mock import MagicMock, patch import pytest -from copilot import _cli_download +from copilot import _cli_download, _cli_version, _ffi_runtime_host -def _integrity(data: bytes, algo: str = "sha512") -> str: - digest = hashlib.new(algo, data).digest() - return f"{algo}-{base64.b64encode(digest).decode('ascii')}" - - -def _runtime_package(npm_platform: str) -> bytes: +def _release_package(runtime_platform: str) -> bytes: wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" members = { - f"package/prebuilds/{npm_platform}/{wrapper_name}": b"wrapper", - f"package/prebuilds/{npm_platform}/runtime.node": b"runtime", - "package/copilot": b"excluded", - "package/copilot.exe": b"excluded", - f"package/ripgrep/bin/{npm_platform}/rg": b"ripgrep", + f"package/prebuilds/{runtime_platform}/{wrapper_name}": b"wrapper", + f"package/prebuilds/{runtime_platform}/runtime.node": b"runtime", + f"package/ripgrep/bin/{runtime_platform}/rg": b"ripgrep", "package/definitions/future.json": b"{}", "package/app.js": b"excluded", "package/LICENSE.md": b"excluded", - "package/README.md": b"excluded", } buffer = io.BytesIO() with tarfile.open(fileobj=buffer, mode="w:gz") as archive: for name, content in members.items(): info = tarfile.TarInfo(name) + info.mode = 0o755 if name.endswith((wrapper_name, "/rg")) else 0o644 info.size = len(content) archive.addfile(info, io.BytesIO(content)) return buffer.getvalue() -class TestVerifyIntegrity: - def test_accepts_matching_checksum(self): - data = b"native-library-bytes" - _cli_download._verify_integrity(data, _integrity(data)) +def test_fetch_url_bytes_retries_truncated_response(): + truncated_response = MagicMock() + truncated_response.__enter__.return_value.read.side_effect = IncompleteRead(b"partial", 4) + complete_response = MagicMock() + complete_response.__enter__.return_value.read.return_value = b"complete" + + with ( + patch.object( + _cli_download, + "urlopen", + side_effect=[truncated_response, complete_response], + ) as urlopen, + patch.object(_cli_download.time, "sleep") as sleep, + ): + assert _cli_download._fetch_url_bytes("https://example/runtime", timeout=30) == b"complete" + + assert urlopen.call_count == 2 + sleep.assert_called_once_with(1) + + +def _release_fetches(version: str, runtime_platform: str, data: bytes): + asset_name = f"github-copilot-{version}-{runtime_platform}.tgz" + checksum = hashlib.sha256(data).hexdigest() + + def fetch(url: str, *, timeout: int) -> bytes: + del timeout + if url.endswith("/SHA256SUMS.txt"): + return f"{checksum} {asset_name}\n".encode() + assert url.endswith(f"/{asset_name}") + return data + + return fetch + + +def test_release_asset_uses_platform_package_name(monkeypatch): + monkeypatch.setenv("COPILOT_CLI_DOWNLOAD_BASE_URL", "https://mirror.example/releases/") + + name = _cli_version.get_release_asset_name("1.2.3-4", "linux-x64") + + assert name == "github-copilot-1.2.3-4-linux-x64.tgz" + assert ( + _cli_version.get_download_url("1.2.3-4", name) + == "https://mirror.example/releases/v1.2.3-4/github-copilot-1.2.3-4-linux-x64.tgz" + ) + + +@pytest.mark.parametrize( + "member_name", + ["package/../outside", r"package\..\outside"], +) +def test_hostless_runtime_path_rejects_traversal(member_name): + with pytest.raises(RuntimeError, match="Unsafe runtime package path"): + _cli_download._hostless_runtime_path(member_name, "linux-x64") + + +def test_rejects_release_package_checksum_mismatch(tmp_path): + runtime_platform = "linux-x64" + data = _release_package(runtime_platform) + asset_name = f"github-copilot-1.2.3-{runtime_platform}.tgz" + + def fetch(url: str, *, timeout: int) -> bytes: + del timeout + if url.endswith("/SHA256SUMS.txt"): + return f"{'0' * 64} {asset_name}\n".encode() + return data + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=tmp_path / "cache"), + patch.object(_cli_download, "get_runtime_platform", return_value=runtime_platform), + patch.object(_cli_download, "_fetch_url_bytes", side_effect=fetch), + ): + with pytest.raises(RuntimeError, match="Checksum mismatch"): + _cli_download.ensure_runtime_wrapper(version="1.2.3") + + +def test_rejects_release_package_without_checksum(tmp_path): + runtime_platform = "linux-x64" + + def fetch(url: str, *, timeout: int) -> bytes: + del timeout + assert url.endswith("/SHA256SUMS.txt") + return f"{'0' * 64} another-file.tgz\n".encode() + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=tmp_path / "cache"), + patch.object(_cli_download, "get_runtime_platform", return_value=runtime_platform), + patch.object(_cli_download, "_fetch_url_bytes", side_effect=fetch), + ): + with pytest.raises(RuntimeError, match="SHA256SUMS.txt does not contain"): + _cli_download.ensure_runtime_wrapper(version="1.2.3") + + +def test_cli_and_runtime_share_one_staged_bundle(tmp_path, monkeypatch): + version = "1.2.3" + runtime_platform = "linux-x64" + cli_name = "copilot.exe" if os.name == "nt" else "copilot" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + data = _release_package(runtime_platform) + cache_dir = tmp_path / "cache" + install_dir = cache_dir / "prebuilds" / runtime_platform + fetch = _release_fetches(version, runtime_platform, data) + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_runtime_platform", return_value=runtime_platform), + patch.object(_cli_download, "_fetch_url_bytes", side_effect=fetch) as fetch_mock, + ): + cli = _cli_download.download_cli(version) + monkeypatch.setenv("COPILOT_SKIP_CLI_DOWNLOAD", "1") + wrapper = _cli_download.ensure_runtime_wrapper(version) + assert _cli_download.get_cached_cli_path(version) == str(install_dir / cli_name) + + assert cli == str(install_dir / cli_name) + assert wrapper == str(install_dir / wrapper_name) + assert (install_dir / cli_name).read_bytes() == b"wrapper" + assert not (cache_dir / cli_name).exists() + assert not (cache_dir / "packages").exists() + assert (install_dir / wrapper_name).read_bytes() == b"wrapper" + assert (install_dir / "runtime.node").read_bytes() == b"runtime" + assert (install_dir / "ripgrep" / "bin" / runtime_platform / "rg").read_bytes() == b"ripgrep" + assert (install_dir / "definitions" / "future.json").read_bytes() == b"{}" + assert not (install_dir / "app.js").exists() + assert (install_dir / ".hostless-runtime-assets-v2").is_file() + assert fetch_mock.call_count == 2 + if os.name != "nt": + assert (install_dir / cli_name).stat().st_mode & 0o111 + assert (install_dir / wrapper_name).stat().st_mode & 0o111 + + +def test_concurrent_staging_materializes_one_complete_bundle(tmp_path): + version = "1.2.3" + runtime_platform = "linux-x64" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + data = _release_package(runtime_platform) + cache_dir = tmp_path / "cache" + fetch = _release_fetches(version, runtime_platform, data) + fetch_barrier = Barrier(2) + + def concurrent_fetch(url: str, *, timeout: int) -> bytes: + fetch_barrier.wait(timeout=10) + return fetch(url, timeout=timeout) - def test_rejects_mismatched_checksum(self): - with pytest.raises(RuntimeError, match="Integrity mismatch"): - _cli_download._verify_integrity(b"tampered", _integrity(b"original")) + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_runtime_platform", return_value=runtime_platform), + patch.object(_cli_download, "_fetch_url_bytes", side_effect=concurrent_fetch) as fetch_mock, + ThreadPoolExecutor(max_workers=2) as executor, + ): + futures = [executor.submit(_cli_download.ensure_runtime_wrapper, version) for _ in range(2)] + wrappers = [future.result() for future in futures] - def test_rejects_unsupported_algorithm(self): - # Fail closed rather than silently skipping verification of native code. - with pytest.raises(RuntimeError, match="Unsupported integrity algorithm"): - _cli_download._verify_integrity(b"bytes", "md5-deadbeef") + install_dir = cache_dir / "prebuilds" / runtime_platform + expected_wrapper = str(install_dir / wrapper_name) + assert wrappers == [expected_wrapper, expected_wrapper] + assert (install_dir / wrapper_name).read_bytes() == b"wrapper" + assert (install_dir / "runtime.node").read_bytes() == b"runtime" + assert (install_dir / ".hostless-runtime-assets-v2").is_file() + assert not list((cache_dir / "prebuilds").glob(".runtime-bundle-*")) + assert not (cache_dir / "packages").exists() + assert fetch_mock.call_count == 4 -class TestEnsureRuntimeLibraryFailsClosed: - def test_raises_when_integrity_unavailable(self, tmp_path): - """A missing npm integrity value must abort the download, not load unverified code.""" - cli_path = tmp_path / "copilot" - cli_path.write_bytes(b"#!/bin/sh\n") +def test_force_restages_complete_bundle_and_compatibility_alias(tmp_path): + version = "1.2.3" + runtime_platform = "linux-x64" + cli_name = "copilot.exe" if os.name == "nt" else "copilot" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + data = _release_package(runtime_platform) + cache_dir = tmp_path / "cache" + install_dir = cache_dir / "prebuilds" / runtime_platform + install_dir.mkdir(parents=True) + (install_dir / cli_name).write_bytes(b"old-alias") + (install_dir / wrapper_name).write_bytes(b"old-wrapper") + (install_dir / "runtime.node").write_bytes(b"old-runtime") + (install_dir / ".hostless-runtime-assets-v2").write_text("1\n", encoding="ascii") + fetch = _release_fetches(version, runtime_platform, data) + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_runtime_platform", return_value=runtime_platform), + patch.object(_cli_download, "_fetch_url_bytes", side_effect=fetch) as fetch_mock, + ): + cli = _cli_download.download_cli(version, force=True) + + assert cli == str(install_dir / cli_name) + assert (install_dir / cli_name).read_bytes() == b"wrapper" + assert (install_dir / wrapper_name).read_bytes() == b"wrapper" + assert (install_dir / "runtime.node").read_bytes() == b"runtime" + assert fetch_mock.call_count == 2 + + +def test_skip_download_returns_none_without_cached_bundle(tmp_path, monkeypatch): + monkeypatch.setenv("COPILOT_SKIP_CLI_DOWNLOAD", "true") + runtime_platform = "linux-x64" + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=tmp_path / "cache"), + patch.object(_cli_download, "get_runtime_platform", return_value=runtime_platform), + patch.object(_cli_download, "_fetch_url_bytes") as fetch_mock, + ): + assert _cli_download.get_or_download_cli("1.2.3") is None - with ( - patch("copilot._ffi_runtime_host.resolve_library_path", return_value=None), - patch.object(_cli_download, "_should_skip_download", return_value=False), - patch.object(_cli_download, "get_npm_platform", return_value="linux-x64"), - patch.object(_cli_download, "get_runtime_lib_url", return_value="https://example/lib"), - patch.object(_cli_download, "_fetch_url_bytes", return_value=b"tarball-bytes"), - patch.object(_cli_download, "_fetch_runtime_integrity", return_value=None), - patch.object(_cli_download, "_extract_runtime_node") as extract, - ): - with pytest.raises(RuntimeError, match="refusing to load unverified native code"): - _cli_download.ensure_runtime_library(str(cli_path), version="1.2.3") + fetch_mock.assert_not_called() - # The library bytes must never be extracted/written when verification is impossible. - extract.assert_not_called() +def test_cached_cli_rejects_alias_from_incomplete_bundle(tmp_path): + version = "1.2.3" + runtime_platform = "linux-x64" + cli_name = "copilot.exe" if os.name == "nt" else "copilot" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + cache_dir = tmp_path / "cache" + install_dir = cache_dir / "prebuilds" / runtime_platform + install_dir.mkdir(parents=True) + (install_dir / cli_name).write_bytes(b"stale-wrapper") + (install_dir / wrapper_name).write_bytes(b"wrapper") + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_runtime_platform", return_value=runtime_platform), + ): + assert _cli_download.get_cached_cli_path(version) is None + with pytest.raises(RuntimeError, match="Incomplete Copilot runtime bundle"): + _cli_download.download_cli(version) + + +def test_explicit_cli_reuses_library_from_canonical_staged_bundle(tmp_path): + version = "1.2.3" + runtime_platform = "linux-x64" + data = _release_package(runtime_platform) + cache_dir = tmp_path / "cache" + cli_dir = tmp_path / "external" + cli_dir.mkdir() + cli_path = cli_dir / ("copilot.exe" if os.name == "nt" else "copilot") + cli_path.write_bytes(b"external") + fetch = _release_fetches(version, runtime_platform, data) + + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_runtime_platform", return_value=runtime_platform), + patch.object(_cli_download, "_fetch_url_bytes", side_effect=fetch) as fetch_mock, + patch("copilot._ffi_runtime_host.resolve_library_path", return_value=None), + ): + library = _cli_download.ensure_runtime_library(str(cli_path), version) + wrapper = _cli_download.ensure_runtime_wrapper(version) + + assert library == str(cli_dir / _ffi_runtime_host._natural_library_name()) + assert (cli_dir / _ffi_runtime_host._natural_library_name()).read_bytes() == b"runtime" + assert (cache_dir / "prebuilds" / runtime_platform / "runtime.node").read_bytes() == b"runtime" + assert not (cache_dir / "packages").exists() + assert wrapper.endswith( + os.path.join( + "prebuilds", + runtime_platform, + "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime", + ) + ) + assert fetch_mock.call_count == 2 + + +def test_resolve_library_path_accepts_adjacent_runtime_node(tmp_path): + wrapper = tmp_path / ("copilot-runtime.exe" if os.name == "nt" else "copilot-runtime") + wrapper.write_bytes(b"wrapper") + runtime_node = tmp_path / "runtime.node" + runtime_node.write_bytes(b"runtime") + + assert _ffi_runtime_host.resolve_library_path(str(wrapper)) == str(runtime_node) + + +def test_rejects_cached_wrapper_without_runtime_node(tmp_path): + runtime_platform = "linux-x64" + wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" + cache_dir = tmp_path / "cache" + install_dir = cache_dir / "prebuilds" / runtime_platform + install_dir.mkdir(parents=True) + (install_dir / wrapper_name).write_bytes(b"wrapper") -class TestEnsureRuntimeWrapper: - def test_materializes_pair_from_absent_cache_with_stripped_environment( - self, tmp_path, monkeypatch + with ( + patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), + patch.object(_cli_download, "get_runtime_platform", return_value=runtime_platform), ): - npm_platform = "win32-x64" if os.name == "nt" else "linux-x64" - wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" - data = _runtime_package(npm_platform) - cache_dir = tmp_path / "cache" - empty_path = tmp_path / "empty-path" - empty_path.mkdir() - assert not cache_dir.exists() - - for name in ( - "COPILOT_CLI_PATH", - "COPILOT_RUNTIME_HOST_COMMAND", - "COPILOT_RUNTIME_PROVIDER_LIB", - ): - monkeypatch.delenv(name, raising=False) - monkeypatch.setenv("PATH", str(empty_path)) - - with ( - patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), - patch.object(_cli_download, "get_npm_platform", return_value=npm_platform), - patch.object(_cli_download, "_should_skip_download", return_value=False), - patch.object(_cli_download, "_fetch_url_bytes", return_value=data), - patch.object( - _cli_download, - "_fetch_runtime_integrity", - return_value=_integrity(data), - ), - ): - wrapper = _cli_download.ensure_runtime_wrapper(version="1.2.3") - - install_dir = cache_dir / "prebuilds" / npm_platform - assert wrapper == str(install_dir / wrapper_name) - assert (install_dir / wrapper_name).read_bytes() == b"wrapper" - assert (install_dir / "runtime.node").read_bytes() == b"runtime" - assert (install_dir / "ripgrep" / "bin" / npm_platform / "rg").read_bytes() == b"ripgrep" - assert (install_dir / "definitions" / "future.json").read_bytes() == b"{}" - assert not (install_dir / "app.js").exists() - assert not (install_dir / "copilot").exists() - assert not (install_dir / "copilot.exe").exists() - assert (install_dir / ".hostless-runtime-assets-v2").is_file() - if os.name != "nt": - assert (install_dir / wrapper_name).stat().st_mode & 0o111 - - def test_rejects_cached_wrapper_without_runtime_node(self, tmp_path): - npm_platform = "win32-x64" if os.name == "nt" else "linux-x64" - wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" - cache_dir = tmp_path / "cache" - install_dir = cache_dir / "prebuilds" / npm_platform - install_dir.mkdir(parents=True) - (install_dir / wrapper_name).write_bytes(b"wrapper") - - with ( - patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), - patch.object(_cli_download, "get_npm_platform", return_value=npm_platform), - ): - with pytest.raises(RuntimeError, match="Incomplete Copilot runtime bundle"): - _cli_download.ensure_runtime_wrapper(version="1.2.3") - - def test_upgrades_pair_only_cache_with_retained_assets(self, tmp_path): - npm_platform = "win32-x64" if os.name == "nt" else "linux-x64" - wrapper_name = "copilot-runtime.exe" if os.name == "nt" else "copilot-runtime" - cache_dir = tmp_path / "cache" - install_dir = cache_dir / "prebuilds" / npm_platform - install_dir.mkdir(parents=True) - (install_dir / wrapper_name).write_bytes(b"old-wrapper") - (install_dir / "runtime.node").write_bytes(b"old-runtime") - (install_dir / "copilot").write_bytes(b"legacy-sea") - (install_dir / ".hostless-runtime-assets-v1").write_text("1\n", encoding="ascii") - data = _runtime_package(npm_platform) - - with ( - patch.object(_cli_download, "get_cache_dir", return_value=cache_dir), - patch.object(_cli_download, "get_npm_platform", return_value=npm_platform), - patch.object(_cli_download, "_should_skip_download", return_value=False), - patch.object(_cli_download, "_fetch_url_bytes", return_value=data), - patch.object(_cli_download, "_fetch_runtime_integrity", return_value=_integrity(data)), - ): - wrapper = _cli_download.ensure_runtime_wrapper(version="1.2.3") - - assert wrapper == str(install_dir / wrapper_name) - assert (install_dir / wrapper_name).read_bytes() == b"wrapper" - assert not (install_dir / "copilot").exists() - assert (install_dir / ".hostless-runtime-assets-v2").is_file() - assert (install_dir / "ripgrep" / "bin" / npm_platform / "rg").is_file() + with pytest.raises(RuntimeError, match="Incomplete Copilot runtime bundle"): + _cli_download.ensure_runtime_wrapper(version="1.2.3") diff --git a/python/test_client.py b/python/test_client.py index e475dba0e2..2e3868ef1c 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -6,6 +6,7 @@ import asyncio import inspect +import json import os from datetime import UTC, datetime from tempfile import TemporaryDirectory @@ -21,6 +22,7 @@ ExtensionInfo, ModelBillingTokenPrices, ModelBillingTokenPricesLongContext, + ModelSwitchAutoTierStatus, RuntimeConnection, StdioRuntimeConnection, define_tool, @@ -38,7 +40,8 @@ ModelLimits, ModelSupports, ) -from copilot.session import PermissionHandler +from copilot.generated.rpc import AutoTier as AutoTierEnum +from copilot.session import CopilotSession, PermissionHandler from copilot.session_events import ( McpOauthRequestReason, McpOauthRequiredData, @@ -195,6 +198,67 @@ async def test_force_stop_external_server_clears_process_references(self): assert client._process is None assert client._cli_process is None + @pytest.mark.asyncio + async def test_force_stop_cancels_pending_external_tools(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + session = CopilotSession("session-1", Mock()) + cancelled = asyncio.Event() + + async def blocked(): + try: + await asyncio.Future() + finally: + cancelled.set() + + task = asyncio.create_task(blocked()) + session._pending_external_tools["request-1"] = task + client._sessions["session-1"] = session + await asyncio.sleep(0) + + await client.force_stop() + + await asyncio.wait_for(cancelled.wait(), timeout=1) + assert session._destroyed + + @pytest.mark.asyncio + async def test_connection_close_cancels_pending_external_tools(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + session = CopilotSession("session-1", Mock()) + cancelled = asyncio.Event() + + async def blocked(): + try: + await asyncio.Future() + finally: + cancelled.set() + + task = asyncio.create_task(blocked()) + session._pending_external_tools["request-1"] = task + client._sessions["session-1"] = session + await asyncio.sleep(0) + + client._client = Mock(_loop=asyncio.get_running_loop()) + await asyncio.to_thread(client._handle_connection_close) + + await asyncio.wait_for(cancelled.wait(), timeout=1) + assert not session._destroyed + assert client._sessions == {"session-1": session} + + def test_connection_close_tolerates_event_loop_close_race(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:1234")) + session = CopilotSession("session-1", Mock()) + loop = Mock() + loop.is_closed.return_value = False + loop.call_soon_threadsafe.side_effect = RuntimeError("Event loop is closed") + client._client = Mock(_loop=loop) + client._sessions["session-1"] = session + client._github_token_providers["registration-1"] = Mock() + + client._handle_connection_close() + + assert client._sessions == {"session-1": session} + assert client._github_token_providers == {} + class TestPermissionHandlerOptional: @pytest.mark.asyncio @@ -1147,7 +1211,56 @@ async def mock_request(method, params, **kwargs): await client.force_stop() @pytest.mark.asyncio - async def test_create_and_resume_session_forward_capi_options(self): + @pytest.mark.parametrize( + ("create_capi", "resume_capi", "expected_create", "expected_resume"), + [ + (None, None, None, None), + ({}, {}, {}, {}), + ( + {"enable_web_socket_responses": False}, + {"enable_web_socket_responses": True}, + {"enableWebSocketResponses": False}, + {"enableWebSocketResponses": True}, + ), + ( + {"enable_web_socket_responses": True}, + {"enable_web_socket_responses": False}, + {"enableWebSocketResponses": True}, + {"enableWebSocketResponses": False}, + ), + ( + {"auto_tier": "efficiency"}, + {"auto_tier": "efficiency"}, + {"autoTier": "efficiency"}, + {"autoTier": "efficiency"}, + ), + ( + {"auto_tier": "balance"}, + {"auto_tier": "balance"}, + {"autoTier": "balance"}, + {"autoTier": "balance"}, + ), + ( + {"auto_tier": "intelligence"}, + {"auto_tier": "intelligence"}, + {"autoTier": "intelligence"}, + {"autoTier": "intelligence"}, + ), + ( + {"auto_tier": "balance", "enable_web_socket_responses": False}, + {"auto_tier": "balance", "enable_web_socket_responses": True}, + {"autoTier": "balance", "enableWebSocketResponses": False}, + {"autoTier": "balance", "enableWebSocketResponses": True}, + ), + ], + ) + async def test_create_and_resume_session_forward_capi_options( + self, + create_capi: CapiSessionOptions | None, + resume_capi: CapiSessionOptions | None, + expected_create: dict[str, object] | None, + expected_resume: dict[str, object] | None, + ): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) await client.start() try: @@ -1164,11 +1277,9 @@ async def mock_request(method, params, **kwargs): return {} client._client.request = mock_request - create_capi: CapiSessionOptions = {"enable_web_socket_responses": False} - resume_capi: CapiSessionOptions = {"enable_web_socket_responses": True} - session = await client.create_session( on_permission_request=PermissionHandler.approve_all, + model="auto", capi=create_capi, ) await client.resume_session( @@ -1177,12 +1288,14 @@ async def mock_request(method, params, **kwargs): capi=resume_capi, ) - assert captured["session.create"]["capi"] == { - "enableWebSocketResponses": False, - } - assert captured["session.resume"]["capi"] == { - "enableWebSocketResponses": True, - } + for method, expected in ( + ("session.create", expected_create), + ("session.resume", expected_resume), + ): + if expected is None: + assert "capi" not in captured[method] + else: + assert captured[method]["capi"] == expected finally: await client.force_stop() @@ -1461,12 +1574,28 @@ def test_parse_host_port_url(self): assert client._actual_host == "127.0.0.1" assert client._is_external_server + def test_parse_bracketed_ipv6_host_port_url(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("[::1]:9000")) + assert client._runtime_port == 9000 + assert client._actual_host == "::1" + assert client._is_external_server + def test_parse_http_url(self): client = CopilotClient(connection=RuntimeConnection.for_uri("http://localhost:7000")) assert client._runtime_port == 7000 assert client._actual_host == "localhost" assert client._is_external_server + def test_parse_http_ipv6_url(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("http://[::1]:7000")) + assert client._runtime_port == 7000 + assert client._actual_host == "::1" + assert client._is_external_server + + def test_reject_bracketed_non_ipv6_host(self): + with pytest.raises(ValueError, match="Invalid cli_url format"): + CopilotClient(connection=RuntimeConnection.for_uri("[not-ipv6]:1234")) + def test_parse_https_url(self): client = CopilotClient(connection=RuntimeConnection.for_uri("https://example.com:443")) assert client._runtime_port == 443 @@ -1493,6 +1622,18 @@ def test_is_external_server_true(self): client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:8080")) assert client._is_external_server + @pytest.mark.asyncio + async def test_connect_via_tcp_uses_family_independent_resolution(self): + client = CopilotClient(connection=RuntimeConnection.for_uri("[::1]:9000")) + fake_socket = Mock() + fake_socket.makefile.return_value = Mock() + + with patch("socket.create_connection", return_value=fake_socket) as create_connection: + await client._connect_via_tcp() + + create_connection.assert_called_once_with(("::1", 9000), timeout=10) + client._process.terminate() + class TestSessionFsConfig: def test_missing_initial_cwd(self): @@ -2508,6 +2649,135 @@ async def mock_request(method, params, **kwargs): assert captured["session.model.switchTo"]["modelId"] == "gpt-4.1" assert captured["session.model.switchTo"]["reasoningSummary"] == "detailed" assert captured["session.model.switchTo"]["contextTier"] == "long_context" + assert "autoTier" not in captured["session.model.switchTo"] + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_set_model_sends_auto_tier(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.model.switchTo": + return {} + return await original_request(method, params, **kwargs) + + client._client.request = mock_request + await session.set_model("auto", auto_tier="intelligence") + assert captured["session.model.switchTo"]["sessionId"] == session.session_id + assert captured["session.model.switchTo"]["modelId"] == "auto" + assert captured["session.model.switchTo"]["autoTier"] == "intelligence" + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_set_model_sends_explicit_null_auto_tier(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.model.switchTo": + return {} + return await original_request(method, params, **kwargs) + + client._client.request = mock_request + await session.set_model("auto", auto_tier=None) + # An explicit null must survive to the wire; omitting it would mean + # "leave the preference alone" rather than "use default routing". + assert "autoTier" in captured["session.model.switchTo"] + assert captured["session.model.switchTo"]["autoTier"] is None + finally: + await client.force_stop() + + +class TestSetAutoTier: + @pytest.mark.asyncio + @pytest.mark.parametrize("auto_tier", ["efficiency", "balance", "intelligence", None]) + async def test_set_auto_tier_sends_correct_rpc(self, auto_tier): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.model.switchAutoTier": + return { + "status": "pending", + "effectiveAutoTier": "balance", + "pendingAutoTier": auto_tier, + "activatingAutoTier": None, + } + return await original_request(method, params, **kwargs) + + client._client.request = mock_request + result = await session.set_auto_tier(auto_tier) + + params = captured["session.model.switchAutoTier"] + assert params["sessionId"] == session.session_id + assert "autoTier" in params + assert params["autoTier"] == auto_tier + + assert result.status == ModelSwitchAutoTierStatus.PENDING + assert result.effective_auto_tier == AutoTierEnum.BALANCE + assert result.activating_auto_tier is None + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_set_auto_tier_accepts_the_enum_it_returns(self): + """The tier on a result or event is an enum, so it has to be valid input too.""" + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + + try: + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all + ) + + captured = {} + original_request = client._client.request + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method == "session.model.switchAutoTier": + return {"status": "pending", "effectiveAutoTier": "intelligence"} + return await original_request(method, params, **kwargs) + + client._client.request = mock_request + await session.set_auto_tier(AutoTierEnum.INTELLIGENCE) + + params = captured["session.model.switchAutoTier"] + # The value must be a plain string; the JSON-RPC encoder cannot + # serialize an enum. + assert params["autoTier"] == "intelligence" + assert isinstance(params["autoTier"], str) + json.dumps(params) finally: await client.force_stop() @@ -3101,6 +3371,106 @@ async def request(self, method, params, **kwargs): client._client = _FakeClient() await client._verify_protocol_version() assert "enableGitHubTelemetryForwarding" not in captured["connect"] + assert captured["connect"]["supportedTaskKinds"] == ["agent", "client", "shell"] + + @pytest.mark.asyncio + async def test_connect_forwards_client_info(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={ + "application_name": "acme-developer-portal", + "application_version": "2.4.0", + "integration_name": "copilot-assistant", + "integration_version": "1.5.0", + }, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert captured["connect"]["clientInfo"] == { + "editorName": "acme-developer-portal", + "editorVersion": "2.4.0", + "extensionName": "copilot-assistant", + "extensionVersion": "1.5.0", + } + + @pytest.mark.asyncio + async def test_connect_omits_client_info_when_unset(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert "clientInfo" not in captured["connect"] + + @pytest.mark.asyncio + async def test_connect_forwards_partial_client_info_with_forwarding(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={"application_name": "example-app"}, + on_github_telemetry=lambda _notification: None, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert captured["connect"]["clientInfo"] == {"editorName": "example-app"} + assert captured["connect"]["enableGitHubTelemetryForwarding"] is True + + @pytest.mark.asyncio + async def test_connect_drops_empty_client_info_fields(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={"application_name": "example-app", "application_version": ""}, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert captured["connect"]["clientInfo"] == {"editorName": "example-app"} + + @pytest.mark.asyncio + async def test_connect_omits_all_empty_client_info(self): + client = CopilotClient( + connection=RuntimeConnection.for_stdio(path=CLI_PATH), + client_info={ + "application_name": "", + "application_version": "", + "integration_name": "", + "integration_version": "", + }, + ) + captured = {} + + class _FakeClient: + async def request(self, method, params, **kwargs): + captured[method] = params + return {"ok": True, "protocolVersion": 3, "version": "test"} + + client._client = _FakeClient() + await client._verify_protocol_version() + assert "clientInfo" not in captured["connect"] @pytest.mark.asyncio async def test_event_routes_to_handler(self): diff --git a/python/test_e2e_harness_cli_path.py b/python/test_e2e_harness_cli_path.py index 8a50ba7a51..c81fb48d05 100644 --- a/python/test_e2e_harness_cli_path.py +++ b/python/test_e2e_harness_cli_path.py @@ -1,99 +1,12 @@ -"""Unit tests for the E2E harness's Copilot CLI platform-package resolution. - -Regression coverage for github/copilot-sdk#2103: the harness used to return the -first ``@github/copilot-*`` directory in alphabetical order instead of the package -built for the current platform. -""" +"""Unit tests for the E2E harness's pinned CLI preparation.""" from __future__ import annotations -from pathlib import Path - import pytest -from copilot._cli_version import get_npm_platform from e2e.testharness import context -def _make_package(github_modules: Path, name: str) -> Path: - """Create ``//index.js`` and return the entrypoint path.""" - package_dir = github_modules / name - package_dir.mkdir(parents=True, exist_ok=True) - index = package_dir / "index.js" - index.write_text("// fake CLI entrypoint\n") - return index - - -class TestCliPlatformPackageNames: - def test_non_linux_platform_yields_single_candidate(self): - assert context._cli_platform_package_names("darwin-arm64") == ["copilot-darwin-arm64"] - - def test_windows_platform_yields_single_candidate(self): - assert context._cli_platform_package_names("win32-x64") == ["copilot-win32-x64"] - - def test_glibc_linux_also_considers_musl_variant(self): - assert context._cli_platform_package_names("linux-x64") == [ - "copilot-linux-x64", - "copilot-linuxmusl-x64", - ] - - def test_musl_linux_prefers_musl_then_falls_back_to_glibc(self): - assert context._cli_platform_package_names("linuxmusl-arm64") == [ - "copilot-linuxmusl-arm64", - "copilot-linux-arm64", - ] - - def test_defaults_to_current_host_platform(self): - assert context._cli_platform_package_names()[0] == f"copilot-{get_npm_platform()}" - - -class TestFindCliInNodeModules: - def test_skips_alphabetically_earlier_foreign_package(self, tmp_path): - # The #2103 regression: "aardvark" sorts before every real platform name. - _make_package(tmp_path, "copilot-aardvark-x64") - expected = _make_package(tmp_path, "copilot-darwin-arm64") - found = context._find_cli_in_node_modules(tmp_path, ["copilot-darwin-arm64"]) - assert found == str(expected.resolve()) - - def test_returns_none_when_no_candidate_is_installed(self, tmp_path): - _make_package(tmp_path, "copilot-win32-x64") - assert context._find_cli_in_node_modules(tmp_path, ["copilot-darwin-arm64"]) is None - - def test_ignores_non_platform_copilot_packages(self, tmp_path): - _make_package(tmp_path, "copilot-language-server") - assert context._find_cli_in_node_modules(tmp_path, ["copilot-linux-x64"]) is None - - def test_prefers_earlier_candidate_when_both_libc_variants_exist(self, tmp_path): - expected = _make_package(tmp_path, "copilot-linuxmusl-x64") - _make_package(tmp_path, "copilot-linux-x64") - found = context._find_cli_in_node_modules( - tmp_path, ["copilot-linuxmusl-x64", "copilot-linux-x64"] - ) - assert found == str(expected.resolve()) - - def test_returns_none_when_package_dir_has_no_index_js(self, tmp_path): - (tmp_path / "copilot-linux-x64").mkdir() - assert context._find_cli_in_node_modules(tmp_path, ["copilot-linux-x64"]) is None - - def test_returns_none_when_github_modules_is_absent(self, tmp_path): - missing = tmp_path / "missing" - assert context._find_cli_in_node_modules(missing, ["copilot-linux-x64"]) is None - - -class TestInstalledCliPackageNames: - def test_lists_platform_directories_sorted(self, tmp_path): - _make_package(tmp_path, "copilot-win32-x64") - _make_package(tmp_path, "copilot-darwin-arm64") - (tmp_path / "not-copilot").mkdir() - assert context._installed_cli_package_names(tmp_path) == [ - "copilot-darwin-arm64", - "copilot-win32-x64", - ] - - def test_returns_empty_when_directory_is_absent(self, tmp_path): - assert context._installed_cli_package_names(tmp_path / "missing") == [] - - class TestGetCliPathForTests: def test_env_var_takes_precedence(self, tmp_path, monkeypatch): cli = tmp_path / "custom-cli.js" @@ -101,46 +14,43 @@ def test_env_var_takes_precedence(self, tmp_path, monkeypatch): monkeypatch.setenv("COPILOT_CLI_PATH", str(cli)) assert context.get_cli_path_for_tests() == str(cli.resolve()) - def test_error_names_the_packages_tried_and_the_remedy(self, monkeypatch): + def test_prepares_the_pinned_runtime(self, tmp_path, monkeypatch): monkeypatch.delenv("COPILOT_CLI_PATH", raising=False) - monkeypatch.setattr( - context, "_cli_platform_package_names", lambda *_: ["copilot-linux-x64"] - ) - monkeypatch.setattr(context, "_find_cli_in_node_modules", lambda *_: None) - with pytest.raises(RuntimeError) as excinfo: - context.get_cli_path_for_tests() - message = str(excinfo.value) - assert "copilot-linux-x64" in message - assert "npm install" in message - assert "COPILOT_CLI_PATH" in message + cli = tmp_path / "copilot" + cli.write_text("runtime\n") - def test_error_names_the_searched_directory(self, monkeypatch): - monkeypatch.delenv("COPILOT_CLI_PATH", raising=False) - seen: list[Path] = [] + class Result: + returncode = 0 + stdout = f"{cli}\n" + stderr = "" - def fake_find(github_modules, package_names): - seen.append(github_modules) - return None + monkeypatch.setattr(context.subprocess, "run", lambda *args, **kwargs: Result()) + assert context._prepare_pinned_cli(tmp_path) == str(cli.resolve()) - monkeypatch.setattr(context, "_cli_platform_package_names", lambda *_: ["copilot-nope-x64"]) - monkeypatch.setattr(context, "_find_cli_in_node_modules", fake_find) - with pytest.raises(RuntimeError) as excinfo: - context.get_cli_path_for_tests() - assert seen, "get_cli_path_for_tests must consult _find_cli_in_node_modules" - assert seen[0].name == "@github" - assert seen[0].parent.name == "node_modules" - assert seen[0].parent.parent.name == "nodejs" - assert str(seen[0]) in str(excinfo.value) + def test_preparation_failure_includes_command_error(self, tmp_path, monkeypatch): + class Result: + returncode = 1 + stdout = "" + stderr = "download failed" - def test_error_lists_the_packages_actually_installed(self, monkeypatch): - monkeypatch.delenv("COPILOT_CLI_PATH", raising=False) - monkeypatch.setattr(context, "_cli_platform_package_names", lambda *_: ["copilot-nope-x64"]) - monkeypatch.setattr(context, "_find_cli_in_node_modules", lambda *_: None) - monkeypatch.setattr( - context, "_installed_cli_package_names", lambda *_: ["copilot-darwin-arm64"] - ) + monkeypatch.setattr(context.subprocess, "run", lambda *args, **kwargs: Result()) with pytest.raises(RuntimeError) as excinfo: - context.get_cli_path_for_tests() - message = str(excinfo.value) - assert "present: copilot-darwin-arm64" in message - assert "copilot-nope-x64" in message + context._prepare_pinned_cli(tmp_path) + assert "download failed" in str(excinfo.value) + + +def test_inprocess_environment_reuses_prepared_runtime(tmp_path, monkeypatch): + cli = tmp_path / "copilot-runtime" + cli.write_text("runtime\n") + + test_context = context.E2ETestContext() + test_context.cli_path = str(cli) + test_context.work_dir = str(tmp_path) + monkeypatch.setattr(test_context, "get_env", lambda: {"HTTPS_PROXY": "https://proxy"}) + monkeypatch.delenv("COPILOT_CLI_PATH", raising=False) + + try: + test_context._apply_inprocess_environment() + assert context.os.environ["COPILOT_CLI_PATH"] == str(cli) + finally: + test_context._restore_inprocess_environment() diff --git a/python/test_event_forward_compatibility.py b/python/test_event_forward_compatibility.py index 2e8015a97d..65e39a80ba 100644 --- a/python/test_event_forward_compatibility.py +++ b/python/test_event_forward_compatibility.py @@ -14,6 +14,8 @@ from copilot.session_events import ( AttachmentGitHubReferenceType, + AutoTier, + AutoTierSwitchFailureReason, Data, ElicitationCompletedAction, ElicitationRequestedMode, @@ -22,8 +24,11 @@ PermissionPromptRequestMemory, PermissionRequestMemory, PermissionRequestMemoryAction, + SessionAutoTierSwitchFailedData, SessionEventType, SessionManagedSettingsResolvedData, + SessionResumeData, + SessionStartData, SessionTaskCompleteData, UserMessageAgentMode, session_event_from_dict, @@ -34,6 +39,87 @@ class TestEventForwardCompatibility: """Test forward compatibility for unknown event types.""" + @pytest.mark.parametrize("event_type", ["session.start", "session.resume"]) + @pytest.mark.parametrize("tier", ["efficiency", "balance", "intelligence", None]) + def test_auto_tier_lifecycle_events_round_trip(self, event_type, tier): + timestamp = "2026-08-28T00:00:00Z" + data = ( + { + "copilotVersion": "1.0.82-1", + "producer": "copilot-agent", + "sessionId": str(uuid4()), + "startTime": timestamp, + "version": 1, + } + if event_type == "session.start" + else {"eventCount": 1, "resumeTime": timestamp} + ) + if tier is not None: + data["autoTier"] = tier + event = session_event_from_dict( + { + "id": str(uuid4()), + "timestamp": timestamp, + "parentId": None, + "type": event_type, + "data": data, + } + ) + assert isinstance(event.data, (SessionStartData, SessionResumeData)) + assert event.data.auto_tier == (AutoTier(tier) if tier is not None else None) + serialized = session_event_to_dict(event)["data"] + if tier is None: + assert "autoTier" not in serialized + else: + assert serialized["autoTier"] == tier + + @pytest.mark.parametrize( + "reason", + ["policy_rejected", "request_failed", "setup_failed", "unsupported"], + ) + def test_auto_tier_switch_failed_event_decodes_every_reason(self, reason): + timestamp = "2026-08-28T00:00:00Z" + event = session_event_from_dict( + { + "id": str(uuid4()), + "timestamp": timestamp, + "parentId": None, + "type": "session.auto_tier_switch_failed", + "data": { + "effectiveAutoTier": "balance", + "requestedAutoTier": "intelligence", + "reason": reason, + }, + } + ) + assert isinstance(event.data, SessionAutoTierSwitchFailedData) + assert event.data.reason == AutoTierSwitchFailureReason(reason) + assert event.data.effective_auto_tier == AutoTier.BALANCE + assert event.data.requested_auto_tier == AutoTier.INTELLIGENCE + + def test_auto_tier_switch_failed_event_allows_null_requested_tier(self): + # A null requested tier means the attempt to return to provider-default + # Auto routing is what failed. + timestamp = "2026-08-28T00:00:00Z" + event = session_event_from_dict( + { + "id": str(uuid4()), + "timestamp": timestamp, + "parentId": None, + "type": "session.auto_tier_switch_failed", + "data": { + "effectiveAutoTier": "efficiency", + "requestedAutoTier": None, + "reason": "unsupported", + }, + } + ) + assert isinstance(event.data, SessionAutoTierSwitchFailedData) + assert event.data.requested_auto_tier is None + assert event.data.effective_auto_tier == AutoTier.EFFICIENCY + serialized = session_event_to_dict(event)["data"] + assert serialized["requestedAutoTier"] is None + def test_session_usage_info_is_recognized(self): """The session.usage_info event type should be in the enum.""" assert SessionEventType.SESSION_USAGE_INFO.value == "session.usage_info" @@ -144,6 +230,7 @@ def test_managed_settings_client_provenance_round_trips(self): "server", "device", "client", + "policyHelper", "mixed", "none", ] diff --git a/python/test_github_token_provider.py b/python/test_github_token_provider.py index 5b203f3e34..9566a4f817 100644 --- a/python/test_github_token_provider.py +++ b/python/test_github_token_provider.py @@ -31,8 +31,8 @@ async def request(self, method: str, params: dict[str, Any], **kwargs: Any) -> d if callback is not None: callback(response) return response - if method == "session.destroy": - return {} + if method == "session.detach": + return {"success": True} if method == "session.delete": return {"success": True} raise RuntimeError(f"Unexpected method: {method}") diff --git a/python/test_message_identity_generated.py b/python/test_message_identity_generated.py new file mode 100644 index 0000000000..024223ac96 --- /dev/null +++ b/python/test_message_identity_generated.py @@ -0,0 +1,41 @@ +from copilot.generated.rpc import QueuePendingItems +from copilot.generated.session_events import UserMessageData + + +def test_queue_pending_message_id_uses_camel_case_and_is_optional(): + item = QueuePendingItems.from_dict( + { + "id": "queue-1", + "messageId": "message-1", + "kind": "message", + "displayText": "hello", + "agentMode": "interactive", + } + ) + + assert item.message_id == "message-1" + assert item.to_dict()["messageId"] == "message-1" + + older_item = QueuePendingItems.from_dict( + { + "id": "queue-2", + "kind": "command", + "displayText": "/help", + "agentMode": "interactive", + } + ) + + assert older_item.message_id is None + assert "messageId" not in older_item.to_dict() + + +def test_user_message_id_uses_camel_case_and_is_optional(): + message = UserMessageData.from_dict({"content": "hello", "messageId": "message-1"}) + + assert message.message_id == "message-1" + assert message.to_dict()["messageId"] == "message-1" + + older_message = UserMessageData.from_dict({"content": "hello"}) + + assert older_message.message_id is None + assert "messageId" not in older_message.to_dict() diff --git a/python/test_rpc_generated.py b/python/test_rpc_generated.py index a23173727a..a21dcc0b6e 100644 --- a/python/test_rpc_generated.py +++ b/python/test_rpc_generated.py @@ -16,6 +16,7 @@ RemoteControlStatusOff, RemoteControlStatusResult, RemoteSessionMetadataValue, + SandboxConfig, SessionList, SlashCommandTextResult, TaskAgentInfo, @@ -23,6 +24,14 @@ ) +def test_sandbox_config_round_trips_allow_bypass_and_omits_when_absent(): + configured = SandboxConfig(enabled=True, allow_bypass=True) + + assert configured.to_dict() == {"enabled": True, "allowBypass": True} + assert SandboxConfig.from_dict(configured.to_dict()).allow_bypass is True + assert SandboxConfig(enabled=True).to_dict() == {"enabled": True} + + @pytest.mark.asyncio async def test_commands_invoke_deserializes_slash_command_result(): client = AsyncMock() diff --git a/python/test_session.py b/python/test_session.py index dd2d0a72f6..d58ce94091 100644 --- a/python/test_session.py +++ b/python/test_session.py @@ -10,11 +10,14 @@ from copilot.session import CopilotSession from copilot.session_events import ( AssistantMessageData, + ExternalToolCompletedData, + ExternalToolRequestedData, SessionEvent, SessionEventType, SessionIdleData, SessionMode, ) +from copilot.tools import Tool, ToolResult def _event(data, event_type: SessionEventType) -> SessionEvent: @@ -67,3 +70,59 @@ async def test_send_and_wait_skips_autopilot_continuation_idle(): assert result is not None assert isinstance(result.data, AssistantMessageData) assert result.data.content == "final" + + +@pytest.mark.asyncio +async def test_external_tool_completed_cancels_blocked_handler(): + client = Mock() + client.request = AsyncMock() + session = CopilotSession("session-1", client) + started = asyncio.Event() + cancelled = asyncio.Event() + + async def blocked_tool(_invocation): + started.set() + try: + await asyncio.Future() + except asyncio.CancelledError: + cancelled.set() + return ToolResult(text_result_for_llm="late result") + + session._register_tools([Tool("blocked_tool", "Blocks", blocked_tool)]) + session._dispatch_event( + _event( + ExternalToolRequestedData( + request_id="request-1", + session_id="session-1", + tool_call_id="tool-call-1", + tool_name="blocked_tool", + ), + SessionEventType.EXTERNAL_TOOL_REQUESTED, + ) + ) + await asyncio.wait_for(started.wait(), timeout=1) + + session._dispatch_event( + _event( + ExternalToolCompletedData(request_id="request-1"), + SessionEventType.EXTERNAL_TOOL_COMPLETED, + ) + ) + + await asyncio.wait_for(cancelled.wait(), timeout=1) + await asyncio.sleep(0) + client.request.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_disconnect_from_tool_task_does_not_cancel_detach_request(): + client = Mock() + client.request = AsyncMock(return_value={"success": True}) + session = CopilotSession("session-1", client) + current_task = asyncio.current_task() + assert current_task is not None + session._pending_external_tools["request-1"] = current_task + + await session.disconnect() + + client.request.assert_awaited_once_with("session.detach", {"sessionId": "session-1"}) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 8de6797989..b91eebd06c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -454,6 +454,7 @@ dependencies = [ "tracing", "ureq", "uuid", + "windows-sys 0.61.2", "zip", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index a28190cd1f..4495d3928c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -70,6 +70,12 @@ tokio-tungstenite = { version = "0.24", default-features = false, features = ["c [target.'cfg(windows)'.dependencies] zip = { version = "2", default-features = false, features = ["deflate"], optional = true } +windows-sys = { version = "0.61", default-features = false, features = [ + "Win32_Foundation", + "Win32_System_Diagnostics_ToolHelp", + "Win32_System_JobObjects", + "Win32_System_Threading", +] } [dev-dependencies] rusqlite = { version = "0.35", features = ["bundled"] } @@ -105,8 +111,17 @@ required-features = ["test-support"] test = false bench = false +[[bin]] +name = "copilot-host-crash-fixture" +path = "tests/fixtures/host_crash_fixture.rs" +required-features = ["test-support"] +test = false +bench = false + +[[test]] +name = "prepared_session_test" +required-features = ["test-support"] [build-dependencies] -base64 = "0.22" dirs = "5" flate2 = "1" serde_json = "1" diff --git a/rust/README.md b/rust/README.md index c05746a30e..2de8a88a2d 100644 --- a/rust/README.md +++ b/rust/README.md @@ -103,7 +103,7 @@ transports. | `transport` | `Transport` | `Default`, `Stdio`, `InProcess`, `Tcp`, or `External` | | `extension_launch_provider` | `Option>` | Connection-global extension launch resolver | -With the default `CliProgram::Resolve`, managed stdio and TCP transports resolve an explicit `CliProgram::Path(path)`, `COPILOT_CLI_PATH`, then the bundled `copilot-runtime` wrapper and adjacent `runtime.node`. In-process transport retains its CLI-entrypoint resolution. There is no PATH scanning. +With the default `CliProgram::Resolve`, managed stdio and TCP transports resolve an explicit `CliProgram::Path(path)`, `COPILOT_CLI_PATH`, then the bundled `copilot-runtime` wrapper and adjacent `runtime.node`. In-process transport loads the native runtime library adjacent to that resolved runtime bundle. There is no PATH scanning. #### Extension launch provider @@ -362,6 +362,51 @@ provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer. +### Auto routing tiers + +Use `CapiSessionOptions::with_auto_tier` to select `AutoTier::Efficiency`, +`AutoTier::Balance`, or `AutoTier::Intelligence`. This option is meaningful only +with model `auto` (Auto mode V2). +It requires a runtime version that supports `capi.autoTier`. + +```rust +use github_copilot_sdk::{AutoTier, CapiSessionOptions, SessionConfig}; + +let config = SessionConfig::default() + .with_model("auto") + .with_capi(CapiSessionOptions::new().with_auto_tier(AutoTier::Balance)); +``` + +The same options work with `ResumeSessionConfig::with_capi` and can be combined +with `with_enable_web_socket_responses(false)`. The SDK omits an unset tier: +the runtime chooses its default on create and preserves the persisted/current +tier on resume. An explicit tier overrides the persisted tier on cold resume. On +resident resume, a different tier requests a safe switch applied after the +resume succeeds; it cannot change a turn that is already in flight. The SDK does not choose a default or manage tier persistence. + +### Changing the Auto tier during a session + +Change the Auto routing preference without changing the selected model. The runtime does not apply the preference immediately: it records the request and commits it only when a later user turn using the `auto` model successfully obtains a usable model from the provider, so a `pending` status confirms acceptance rather than effect. Only the most recent request survives. + +Watch for the outcome through the `session.model_change` event on success or the ephemeral `session.auto_tier_switch_failed` event on failure. Read the authoritative committed, pending, and activating preferences at any time through the session's `model.getCurrent` RPC method. + +```rust,ignore +use github_copilot_sdk::{AutoTier, ModelSwitchAutoTierStatus}; + +let result = session.set_auto_tier(Some(AutoTier::Intelligence)).await?; +if result.status == ModelSwitchAutoTierStatus::Pending { + // Accepted, but not yet in effect. +} + +// Return to the provider's default Auto routing. +session.set_auto_tier(None).await?; +``` + +`set_model` accepts the same preference through `SetModelOptions::with_auto_tier`, which stages the tier atomically with selecting `auto`. Use `with_reset_auto_tier` instead to return to provider-default routing. + +See [Auto tier persistence](../docs/features/session-persistence.md#auto-tier-persistence) +for the lifecycle rules. + ### Session Hooks Hooks intercept CLI behavior at lifecycle points — tool use, prompt submission, session start/end, and errors. Install a `SessionHooks` impl with [`SessionConfig::with_hooks`] — the SDK auto-enables `hooks` in `SessionConfig` when one is set. @@ -672,6 +717,36 @@ while let Ok(event) = events.recv().await { When streaming is off (the default), only the final `assistant.message` and `assistant.reasoning` events fire. Delta events arrive in order; concatenating their `delta` text payloads reproduces the final message. +#### Subscribing before the session starts + +`session.subscribe()` can only be called once the session exists, so any event the runtime emits while `session.create` / `session.resume` is still in flight is broadcast with no receiver installed and is not delivered. Ephemeral events such as `session.idle` are not written to the session log either, so `get_messages` can't recover them afterwards. + +`Client::prepare_session` / `Client::prepare_resume_session` close that window. They return a `PreparedSession` that owns the session's broadcast channel up front: + +```rust,ignore +let prepared = client.prepare_session( + SessionConfig::default().with_event_buffer_capacity(2048), +)?; + +// Installed before any wire activity happens. +let mut events = prepared.subscribe(); +tokio::spawn(async move { + while let Ok(event) = events.recv().await { + println!("{}", event.event_type); + } +}); + +let session = prepared.start().await?; +``` + +`prepare_*` is synchronous and inert — it validates the buffer capacity, allocates a local channel and cancellation token, and touches neither the router nor the transport until `start()` is first polled. `start(self)` consumes the handle and `PreparedSession` is deliberately not `Clone`, so a prepared session can never spawn two event loops. Dropping an unstarted handle leaves no state and closes its subscriptions; dropping the `start()` future cancels the startup, unregisters the session, and lets a same-ID retry succeed. Cleanup removes only the exact registration that startup owned, so a retry started while an abandoned attempt is still unwinding is never evicted by it. + +The buffer is finite — `session::DEFAULT_EVENT_BUFFER_CAPACITY` (512) unless `event_buffer_capacity` overrides it, and `Some(0)` is rejected as `ErrorKind::InvalidConfig` rather than clamped. Subscribers that fall behind observe `RecvErrorKind::Lagged` with the skipped count instead of applying backpressure, so a consumer that needs a lossless view of a large startup burst must configure enough capacity or drain concurrently with `start()`. + +For cloud sessions where the server assigns the session ID, notifications can't be routed until the create response arrives; the guarantee is that *routed* events are never dropped for lack of a receiver. Pin `session_id` for full pre-response coverage. + +`create_session` / `resume_session` are unchanged wrappers over `prepare_*(...)?.start()`, with identical RPC sequences and error kinds. + ### Infinite Sessions Enable the SDK's session-store integration so conversations persist across CLI restarts and grow beyond the model's context window via automatic compaction: @@ -875,6 +950,12 @@ none of them are scheduled for removal. arg vectors for "prepend before subcommand" vs "append after the built-in flags", giving precise control over CLI invocation order without string-splicing. +- **`Client::prepare_session` / `prepare_resume_session`** — return an inert + `PreparedSession` whose `subscribe()` installs an event receiver before any + protocol activity, so startup events (including ephemeral `session.idle`) + aren't dropped. Other SDKs register callbacks on a config object instead, + which sidesteps the problem in a way Rust's broadcast-based `subscribe()` + cannot. ## Layout @@ -882,7 +963,7 @@ none of them are scheduled for removal. | ----------------- | -------------------------------------------------------------------------------------------------------------------------- | | `lib.rs` | `Client`, `ClientOptions`, `CliProgram`, `Transport`, `Error` | | `extension_launch_provider.rs` | Connection-global `ExtensionLaunchProvider` trait and launch profile DTOs | -| `session.rs` | `Session` struct, event loop, `send`/`send_and_wait`, `Client::create_session`/`resume_session` | +| `session.rs` | `Session` struct, `PreparedSession`, event loop, `send`/`send_and_wait`, `Client::create_session`/`resume_session`/`prepare_session`/`prepare_resume_session` | | `subscription.rs` | `EventSubscription` / `LifecycleSubscription` (`Stream`-able observer handles for `subscribe()` / `subscribe_lifecycle()`) | | `handler.rs` | `PermissionHandler`, `ElicitationHandler`, `UserInputHandler`, `ExitPlanModeHandler`, `AutoModeSwitchHandler` traits; `ApproveAllHandler`, `DenyAllHandler` | | `hooks.rs` | `SessionHooks` trait, `HookEvent`/`HookOutput` enums, typed hook inputs/outputs | @@ -896,15 +977,16 @@ none of them are scheduled for removal. ## Bundled runtime artifacts -The SDK provisions its runtime at build time. By default the `bundled-cli` -feature embeds the verified `copilot-runtime` wrapper and adjacent -`runtime.node` in your compiled crate. The compatible CLI artifact remains -available separately for `install_bundled_cli` and in-process hosting. -Enable `bundled-in-process` to additionally embed the native runtime library -and use `Transport::InProcess`: +The SDK provisions two verified artifacts at build time. By default the +`bundled-cli` feature embeds both the full Copilot CLI/Node SEA and a separate +runtime bundle containing `copilot-runtime`, adjacent `runtime.node`, and its +required assets. Managed transports use only the runtime bundle; the full CLI +is available through `install_bundled_cli` for diagnostics and version probes. +Enable `bundled-in-process` to additionally include the native runtime library +in the runtime bundle and use `Transport::InProcess`: ```toml -github-copilot-sdk = { version = "0.1", features = ["bundled-in-process"] } +github-copilot-sdk = { version = "1", features = ["bundled-in-process"] } ``` `CliProgram::Path` and raw `ClientOptions::extra_args` apply only to @@ -914,7 +996,7 @@ provisioned compatible runtime package with in-process transport. For builds that prefer a smaller artifact, disable the `bundled-cli` feature: ```toml -github-copilot-sdk = { version = "0.1", default-features = false } +github-copilot-sdk = { version = "1", default-features = false } ``` > **You become responsible for supplying the runtime at deployment.** With @@ -936,17 +1018,24 @@ github-copilot-sdk = { version = "0.1", default-features = false } ### How it works 1. **Version pin.** `build.rs` reads the CLI version from one of two sources: - - `cli-version.txt` at the crate root (present in published crate tarballs and vendored slots). - - Otherwise, `../nodejs/package-lock.json` (contributor build inside the github/copilot-sdk repo — matches the .NET and Go SDK conventions here). + - `cli-version.txt` and `cli-version-in-process.txt` at the crate root + (present in published crate tarballs and vendored slots). + - Otherwise, `../nodejs/package.json` (contributor build inside the github/copilot-sdk repo). The resolved version is baked into the crate via `cargo:rustc-env=COPILOT_SDK_CLI_VERSION` regardless of mode. The runtime resolver consumes it to recompute the on-disk path by convention, so no absolute paths leak into the rlib. -2. **Build time:** `build.rs` downloads the platform-specific npm package and - verifies its `sha512` integrity against the lockfile or publish snapshot. +2. **Build time:** `build.rs` downloads the platform-specific full CLI archive + and runtime package, then verifies both SHA-256 hashes against the release's + `SHA256SUMS.txt` or the publish snapshots. Then: - - **`bundled-cli` on (default):** creates and embeds a minimal archive containing the CLI executable, `copilot-runtime[.exe]`, and `runtime.node`. - - **`bundled-in-process` on:** the archive additionally contains the platform-native runtime library (`.dll`, `.so`, or `.dylib`). - - **`bundled-cli` off:** extracts the same artifacts directly into the platform cache using staging files and atomic renames. + - **`bundled-cli` on (default):** embeds the full CLI release archive and a + separately filtered runtime archive containing `copilot-runtime[.exe]`, + `runtime.node`, and required assets. + - **`bundled-in-process` on:** the runtime archive additionally contains the + platform-native runtime library (`.dll`, `.so`, or `.dylib`). + - **`bundled-cli` off:** downloads only the runtime package and extracts its + managed runtime artifacts directly into the platform cache using staging + files and atomic renames. 3. **Runtime:** in both modes the artifacts share one versioned directory: @@ -994,9 +1083,9 @@ For managed child-process transports, `Client::start` resolves the program in th 3. **`bundled-cli` on:** the embedded wrapper pair, lazily extracted on first call. 4. **`bundled-cli` off:** the build-time-extracted wrapper pair in the per-user cache. -In-process transport resolves the compatible CLI artifact from -`COPILOT_CLI_PATH`, the embedded archive, or the build-time cache. There is no -PATH scanning. +In-process transport loads the native runtime library adjacent to the runtime +wrapper selected from `COPILOT_CLI_PATH`, the embedded runtime archive, or the +build-time cache. There is no PATH scanning. ### Reaching the bundled binary without a `Client` @@ -1037,11 +1126,19 @@ returns the wrapper path. ### Download cache (build-time, embed mode) -In embed mode `build.rs` re-downloads on every clean build by default. Set `BUNDLED_CLI_CACHE_DIR=` to cache the verified archive between builds (CI keys this on `-` for ~zero-cost rebuilds on cache hits). With `bundled-cli` disabled there is no separate archive cache — the extracted binary itself is the cache. +In embed mode `build.rs` downloads both verified archives on every clean build +by default. Set `BUNDLED_CLI_CACHE_DIR=` to cache them between builds (CI +keys this on `-` for near-zero-cost rebuilds on cache hits). For +Copilot CLI 1.0.83-5, the two upstream archives total roughly 132-157 MB per +platform before the runtime package is filtered. With `bundled-cli` disabled +there is no separate archive cache: the extracted runtime bundle is the cache. ### Platforms -Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64`, `win32-arm64`. The target platform is auto-detected from `CARGO_CFG_TARGET_OS` and `CARGO_CFG_TARGET_ARCH` (cross-compilation works). +Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, +`linuxmusl-x64`, `linuxmusl-arm64`, `win32-x64`, and `win32-arm64`. The target +platform is auto-detected from `CARGO_CFG_TARGET_OS`, `CARGO_CFG_TARGET_ARCH`, +and `CARGO_CFG_TARGET_ENV` (cross-compilation works). ## Features @@ -1052,20 +1149,17 @@ Supported: `darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, `win32-x64` | `derive` | — | `schema_for::()` for generating JSON Schema from Rust types (adds `schemars`). | ```toml -# These examples use registry syntax for illustration; until the crate is -# published, use a path or git dependency instead. - # Default — bundles the Copilot CLI in your binary. -github-copilot-sdk = "0.1" +github-copilot-sdk = "1" # Enable the in-process transport and bundle its native runtime library. -github-copilot-sdk = { version = "0.1", features = ["bundled-in-process"] } +github-copilot-sdk = { version = "1", features = ["bundled-in-process"] } # Opt out of bundling — supply the CLI explicitly at runtime. -github-copilot-sdk = { version = "0.1", default-features = false } +github-copilot-sdk = { version = "1", default-features = false } # Derive JSON Schema for tool parameters (adds to default bundled-cli). -github-copilot-sdk = { version = "0.1", features = ["derive"] } +github-copilot-sdk = { version = "1", features = ["derive"] } ``` ## Development diff --git a/rust/build/in_process.rs b/rust/build/in_process.rs index f1e947b27a..c1aa583f9c 100644 --- a/rust/build/in_process.rs +++ b/rust/build/in_process.rs @@ -2,7 +2,6 @@ use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::time::Duration; -use base64::Engine; use sha2::Digest; pub(crate) fn main() { @@ -12,22 +11,23 @@ pub(crate) fn main() { println!("cargo:rerun-if-env-changed=BUNDLED_CLI_CACHE_DIR"); println!("cargo::rustc-check-cfg=cfg(has_bundled_cli)"); println!("cargo::rustc-check-cfg=cfg(has_extracted_cli)"); + println!("cargo:rerun-if-changed=cli-version.txt"); println!("cargo:rerun-if-changed=cli-version-in-process.txt"); - // Only declare the lockfile rerun when the lockfile actually exists. + // Only declare the package metadata rerun when it actually exists. // Cargo treats `rerun-if-changed` for a missing path as "always rerun" // — so unconditionally declaring this on consumers without a sibling // `nodejs/` (vendored slots, published crates) would force build.rs // to re-run on every `cargo build` even when nothing has changed. - // The lockfile path is only the source-of-truth in this repo's - // contributor builds; everywhere else `cli-version-in-process.txt` is canonical. + // The package file is only the source-of-truth in this repo's + // contributor builds; everywhere else the snapshot files are canonical. let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); - let lockfile = Path::new(&manifest_dir) + let package_json = Path::new(&manifest_dir) .join("..") .join("nodejs") - .join("package-lock.json"); - if lockfile.is_file() { - println!("cargo:rerun-if-changed={}", lockfile.display()); + .join("package.json"); + if package_json.is_file() { + println!("cargo:rerun-if-changed={}", package_json.display()); } // Hard opt-out: disable the entire download / bundle / cache mechanism @@ -60,17 +60,13 @@ pub(crate) fn main() { let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR is always set by cargo"); let out = Path::new(&out_dir); - // Resolve version + npm integrity from one of two sources, in order: + // Resolve version and, when available locally, the release SHA-256 from + // one of two sources, in order: // 1. `cli-version-in-process.txt` snapshot at the crate root (published-crate - // consumer; generated by the publish workflow). Combined format: - // `version=X` line + per-package integrity lines. Committing these - // makes the publish workflow the trust boundary — an attacker who - // later re-points the release tag can't silently poison consumer - // builds. - // 2. Sibling `../nodejs/package-lock.json` (contributor build inside - // the github/copilot-sdk repo), whose platform-package integrity is - // the same trust source npm uses. - let (version, expected_integrity) = resolve_version_and_integrity(platform.package_name); + // consumer; generated by the publish workflow from SHA256SUMS.txt). + // 2. Sibling `../nodejs/package.json` plus the release SHA256SUMS.txt + // (contributor build inside the github/copilot-sdk repo). + let (version, local_expected_hash) = resolve_version_and_optional_hash(platform.package_name); // Bake the version into the crate regardless of mode. This is the // single source of truth for "what CLI version did build.rs target", @@ -81,10 +77,13 @@ pub(crate) fn main() { // `target/` reuse stays cache-coherent. println!("cargo:rustc-env=COPILOT_SDK_CLI_VERSION={version}"); - let archive_name = format!("{}-{version}.tgz", platform.package_name); + let asset_platform = platform + .package_name + .strip_prefix("copilot-") + .expect("platform package names start with copilot-"); + let archive_name = format!("github-copilot-{version}-{asset_platform}.tgz"); let download_url = format!( - "https://registry.npmjs.org/@github/{}/-/{}", - platform.package_name, archive_name + "https://github.com/github/copilot-cli/releases/download/v{version}/{archive_name}" ); let cache_dir = std::env::var("BUNDLED_CLI_CACHE_DIR") .ok() @@ -94,9 +93,37 @@ pub(crate) fn main() { let include_runtime = std::env::var_os("CARGO_FEATURE_BUNDLED_IN_PROCESS").is_some(); if std::env::var_os("CARGO_FEATURE_BUNDLED_CLI").is_some() { - let archive = cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir); - verify_runtime_package(&archive, platform, &archive_name); - emit_embedded(out, &archive, platform, include_runtime); + let runtime_expected_hash = local_expected_hash + .clone() + .unwrap_or_else(|| fetch_in_process_release_hash(&version, platform.package_name)); + let runtime_package = cached_download( + &download_url, + &cache_key, + &runtime_expected_hash, + &cache_dir, + ); + verify_runtime_package(&runtime_package, platform, &archive_name); + + let cli_asset_name = platform.cli_asset_name(); + let cli_expected_hash = resolve_cli_hash(&version, &cli_asset_name); + let cli_archive = cached_download( + &format!( + "https://github.com/github/copilot-cli/releases/download/v{version}/{cli_asset_name}" + ), + &format!("v{version}-{cli_asset_name}"), + &cli_expected_hash, + &cache_dir, + ); + let cli_binary_size = verify_cli_archive(&cli_archive, platform, &cli_asset_name); + + emit_embedded( + out, + &cli_archive, + cli_binary_size, + &runtime_package, + platform, + include_runtime, + ); println!("cargo:rustc-cfg=has_bundled_cli"); } else { // With `bundled-cli` off the extracted runtime pair *is* the cache. @@ -112,8 +139,6 @@ pub(crate) fn main() { install_dir.join("runtime.node"), install_dir.join(".hostless-runtime-assets-v1"), ]; - let expected_marker = format!("{version}\n{expected_integrity}\n"); - // Invalidate build.rs whenever either cached artifact disappears (cache // GC, manual rm, OS reset, switching extract dir). Without this, cargo // replays the saved `has_extracted_cli` cfg from its build-script @@ -123,10 +148,20 @@ pub(crate) fn main() { println!("cargo:rerun-if-changed={}", path.display()); } + let marker = std::fs::read_to_string(&required_paths[2]).ok(); let cache_is_current = required_paths.iter().all(|path| path.is_file()) - && std::fs::read_to_string(&required_paths[2]).ok().as_deref() - == Some(expected_marker.as_str()); + && match local_expected_hash.as_deref() { + Some(expected_hash) => { + marker.as_deref() == Some(&format!("{version}\n{expected_hash}\n")) + } + None => marker + .as_deref() + .is_some_and(|contents| marker_matches_version(contents, &version)), + }; if !cache_is_current { + let expected_hash = local_expected_hash + .unwrap_or_else(|| fetch_in_process_release_hash(&version, platform.package_name)); + let expected_marker = format!("{version}\n{expected_hash}\n"); if install_dir.exists() { std::fs::remove_dir_all(&install_dir).unwrap_or_else(|e| { panic!( @@ -135,8 +170,7 @@ pub(crate) fn main() { ) }); } - let archive = - cached_download(&download_url, &cache_key, &expected_integrity, &cache_dir); + let archive = cached_download(&download_url, &cache_key, &expected_hash, &cache_dir); verify_runtime_package(&archive, platform, &archive_name); extract_to_cache( &archive, @@ -177,24 +211,38 @@ fn extracted_install_dir(version: &str) -> PathBuf { } } -/// Emit the `bundled_cli.rs` glue + `copilot_cli.archive` blob into `OUT_DIR` -/// for embed mode (`bundled-cli` cargo feature on). The version is exposed -/// crate-wide via the unconditional `cargo:rustc-env=COPILOT_SDK_CLI_VERSION` -/// emit; the binary name is OS-derived at runtime — so all we need to -/// generate here is the archive blob include. -fn emit_embedded(out: &Path, package: &[u8], platform: Platform, include_runtime: bool) { - let archive = build_embedded_archive(package, platform, include_runtime); - std::fs::write(out.join("copilot_cli.archive"), archive) +/// Emit separate full-CLI and runtime payloads into `OUT_DIR` for embed mode. +fn emit_embedded( + out: &Path, + cli_archive: &[u8], + cli_binary_size: u64, + runtime_package: &[u8], + platform: Platform, + include_runtime: bool, +) { + let runtime_archive = + build_embedded_runtime_archive(runtime_package, platform, include_runtime); + std::fs::write(out.join("copilot_cli.archive"), cli_archive) .expect("failed to write copilot_cli.archive"); + std::fs::write(out.join("copilot_runtime.archive"), runtime_archive) + .expect("failed to write copilot_runtime.archive"); - let generated = r#"// Auto-generated by github-copilot-sdk build.rs. Do not edit. + let generated = format!( + r#"// Auto-generated by github-copilot-sdk build.rs. Do not edit. pub(super) static CLI_ARCHIVE: &[u8] = include_bytes!("copilot_cli.archive"); -"#; +pub(super) static RUNTIME_ARCHIVE: &[u8] = include_bytes!("copilot_runtime.archive"); +pub(super) const CLI_BINARY_SIZE: u64 = {cli_binary_size}; +"# + ); std::fs::write(out.join("bundled_cli.rs"), generated).expect("failed to write bundled_cli.rs"); } -fn build_embedded_archive(package: &[u8], platform: Platform, include_runtime: bool) -> Vec { +fn build_embedded_runtime_archive( + package: &[u8], + platform: Platform, + include_runtime: bool, +) -> Vec { let encoder = flate2::GzBuilder::new() .mtime(0) .write(Vec::new(), flate2::Compression::default()); @@ -276,18 +324,15 @@ fn hostless_runtime_path(source: &str, platform: Platform) -> Option { "app.js", "assets", "changelog.json", - "copilot-sdk", "foundry-local-sdk", "index.js", "LICENSE.md", "napi-oop-runtime", "npm-loader.js", "package.json", - "preloads", "pvrecorder", "queries", "README.md", - "sdk", "sea-loader.js", "webview", ]; @@ -331,10 +376,10 @@ fn append_archive_file( .unwrap_or_else(|e| panic!("failed to add `{path}` to embedded CLI archive: {e}")); } -/// Resolve the CLI version and npm integrity for the current target's -/// platform package. Picks one of two sources in order. Panics with a clear -/// error if neither is available. -fn resolve_version_and_integrity(package_name: &str) -> (String, String) { +/// Resolve the CLI version and any locally snapshotted release hash for the +/// current target's platform package. Contributor builds defer fetching the +/// checksum until a download is actually required. +fn resolve_version_and_optional_hash(package_name: &str) -> (String, Option) { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); // 1. Snapshot file at the crate root (published-crate consumer, @@ -343,17 +388,19 @@ fn resolve_version_and_integrity(package_name: &str) -> (String, String) { if snapshot.is_file() { let contents = std::fs::read_to_string(&snapshot) .unwrap_or_else(|e| panic!("failed to read {}: {e}", snapshot.display())); - return parse_snapshot(&contents, package_name) + let (version, hash) = parse_snapshot(&contents, package_name) .unwrap_or_else(|e| panic!("invalid {}: {e}", snapshot.display())); + return (version, Some(hash)); } - // 2. Lockfile fallback (contributor build inside github/copilot-sdk). - let lockfile = Path::new(&manifest_dir) + // 2. Package version plus release checksums (contributor build). + let package_json = Path::new(&manifest_dir) .join("..") .join("nodejs") - .join("package-lock.json"); - if lockfile.is_file() { - return read_version_and_integrity_from_package_lock(&lockfile, package_name); + .join("package.json"); + if package_json.is_file() { + let version = read_version_from_package_json(&package_json); + return (version, None); } panic!( @@ -362,19 +409,55 @@ fn resolve_version_and_integrity(package_name: &str) -> (String, String) { - {} (missing)\n\ - {} (missing)\n\ In a published crate or vendored slot, `cli-version-in-process.txt` should be present.\n\ - Inside the github/copilot-sdk repo, `../nodejs/package-lock.json` is the source.", + Inside the github/copilot-sdk repo, `../nodejs/package.json` is the version source.", snapshot.display(), - lockfile.display(), + package_json.display(), ); } +fn fetch_in_process_release_hash(version: &str, package_name: &str) -> String { + let platform = package_name + .strip_prefix("copilot-") + .expect("platform package names start with copilot-"); + let asset_name = format!("github-copilot-{version}-{platform}.tgz"); + fetch_release_hash(version, &asset_name) +} + +fn resolve_cli_hash(version: &str, asset_name: &str) -> String { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); + let snapshot = Path::new(&manifest_dir).join("cli-version.txt"); + if snapshot.is_file() { + let contents = std::fs::read_to_string(&snapshot) + .unwrap_or_else(|e| panic!("failed to read {}: {e}", snapshot.display())); + let (snapshot_version, hash) = parse_snapshot(&contents, asset_name) + .unwrap_or_else(|e| panic!("invalid {}: {e}", snapshot.display())); + assert_eq!( + snapshot_version, + version, + "{} and the selected runtime version source must pin the same version", + snapshot.display() + ); + return hash; + } + fetch_release_hash(version, asset_name) +} + +fn marker_matches_version(contents: &str, version: &str) -> bool { + let mut lines = contents.lines(); + lines.next() == Some(version) + && lines.next().is_some_and(|hash| { + hash.len() == 64 && hash.bytes().all(|byte| byte.is_ascii_hexdigit()) + }) + && lines.next().is_none() +} + /// Parse the `cli-version-in-process.txt` snapshot file. Format is one `key=value` per /// line. The first non-comment line is `version=X.Y.Z`; subsequent lines map -/// platform package name to npm integrity. Blank lines and lines starting with `#` +/// platform package name to SHA-256. Blank lines and lines starting with `#` /// are skipped. fn parse_snapshot(contents: &str, package_name: &str) -> Result<(String, String), String> { let mut version: Option = None; - let mut integrity: Option = None; + let mut hash: Option = None; for (line_no, raw) in contents.lines().enumerate() { let line = raw.trim(); if line.is_empty() || line.starts_with('#') { @@ -388,33 +471,42 @@ fn parse_snapshot(contents: &str, package_name: &str) -> Result<(String, String) }; match key.trim() { "version" => version = Some(value.trim().to_string()), - k if k == package_name => integrity = Some(value.trim().to_string()), + k if k == package_name => hash = Some(value.trim().to_string()), _ => {} } } let version = version.ok_or("missing `version=` line")?; - let integrity = - integrity.ok_or_else(|| format!("missing integrity for package `{package_name}`"))?; - Ok((version, integrity)) + let hash = hash.ok_or_else(|| format!("missing hash for package `{package_name}`"))?; + Ok((version, hash)) } -fn read_version_and_integrity_from_package_lock( - path: &Path, - package_name: &str, -) -> (String, String) { +fn read_version_from_package_json(path: &Path) -> String { let contents = std::fs::read_to_string(path) .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())); - let lock: serde_json::Value = serde_json::from_str(&contents) + let package_json: serde_json::Value = serde_json::from_str(&contents) .unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display())); - let cli_key = "node_modules/@github/copilot"; - let version = lock["packages"][cli_key]["version"] - .as_str() - .unwrap_or_else(|| panic!("{cli_key} has no version in {}", path.display())); - let platform_key = format!("node_modules/@github/{package_name}"); - let integrity = lock["packages"][&platform_key]["integrity"] + package_json["copilotCliVersion"] .as_str() - .unwrap_or_else(|| panic!("{platform_key} has no integrity in {}", path.display())); - (version.to_string(), integrity.to_string()) + .unwrap_or_else(|| panic!("copilotCliVersion is missing in {}", path.display())) + .to_string() +} + +fn fetch_release_hash(version: &str, asset_name: &str) -> String { + let url = format!( + "https://github.com/github/copilot-cli/releases/download/v{version}/SHA256SUMS.txt" + ); + let checksums = download_with_retry(&url); + let checksums = std::str::from_utf8(&checksums).expect("SHA256SUMS.txt is not valid UTF-8"); + find_sha256_for_asset(checksums, asset_name) +} + +fn find_sha256_for_asset(sums: &str, asset_name: &str) -> String { + sums.lines() + .find_map(|line| { + let (hash, name) = line.split_once(char::is_whitespace)?; + (name.trim_start().trim_start_matches('*') == asset_name).then(|| hash.to_string()) + }) + .unwrap_or_else(|| panic!("SHA256SUMS.txt does not contain {asset_name}")) } #[derive(Clone, Copy)] @@ -424,6 +516,19 @@ struct Platform { } impl Platform { + fn cli_asset_name(&self) -> String { + let platform = self + .package_name + .strip_prefix("copilot-") + .expect("platform package name has copilot- prefix"); + let extension = if self.package_name.contains("win32") { + "zip" + } else { + "tar.gz" + }; + format!("copilot-{platform}.{extension}") + } + fn runtime_wrapper_name(&self) -> &'static str { if self.package_name.contains("win32") { "copilot-runtime.exe" @@ -574,6 +679,12 @@ fn install_cached_file_path( bytes: &[u8], executable: bool, ) { + // `executable` only affects file permissions on Unix (see the `#[cfg(unix)]` + // block below); explicitly mark it used elsewhere so non-Unix targets don't + // warn about an unused parameter under `-D warnings`. + #[cfg(not(unix))] + let _ = executable; + assert!( !relative_path.is_absolute() && !relative_path.components().any(|component| { @@ -689,20 +800,20 @@ fn sanitize_version(version: &str) -> String { } /// Read a file from the download cache, or download it (with retries) and save -/// to cache. Verifies npm integrity on every path. Evicts stale/corrupt cache entries +/// to cache. Verifies SHA-256 on every path. Evicts stale/corrupt cache entries /// automatically. Cache I/O failures are treated as cache misses — they never /// break the build. fn cached_download( url: &str, cache_key: &str, - expected_integrity: &str, + expected_hash: &str, cache_dir: &Option, ) -> Vec { if let Some(dir) = cache_dir { let cached_path = dir.join(cache_key); if cached_path.is_file() { match std::fs::read(&cached_path) { - Ok(data) if verify_integrity(&data, expected_integrity) => { + Ok(data) if verify_hash(&data, expected_hash) => { // Silent cache hit — nothing to surface. return data; } @@ -722,9 +833,9 @@ fn cached_download( println!("cargo:warning=Downloading {url}"); let data = download_with_retry(url); - if !verify_integrity(&data, expected_integrity) { + if !verify_hash(&data, expected_hash) { panic!( - "Archive integrity check failed for {url}!\n expected: {expected_integrity}\n \ + "Archive integrity check failed for {url}!\n expected: {expected_hash}\n \ This could indicate a corrupted download or a supply-chain attack." ); } @@ -826,11 +937,7 @@ fn try_download(url: &str) -> Result, DownloadError> { } fn verify_runtime_package(archive: &[u8], platform: Platform, package_name: &str) { - for file_name in [ - platform.binary_name, - "runtime.node", - platform.runtime_wrapper_name(), - ] { + for file_name in ["runtime.node", platform.runtime_wrapper_name()] { if archive_contains_tar_entry(archive, file_name) { continue; } @@ -840,11 +947,29 @@ fn verify_runtime_package(archive: &[u8], platform: Platform, package_name: &str } } +fn verify_cli_archive(archive: &[u8], platform: Platform, archive_name: &str) -> u64 { + let binary_size = if platform.package_name.contains("win32") { + archive_zip_entry_size(archive, platform.binary_name) + } else { + archive_tar_entry_size(archive, platform.binary_name) + }; + binary_size.unwrap_or_else(|| { + panic!( + "Copilot CLI archive `{archive_name}` does not contain an entry named `{}`", + platform.binary_name + ) + }) +} + fn archive_contains_tar_entry(targz: &[u8], binary_name: &str) -> bool { + archive_tar_entry_size(targz, binary_name).is_some() +} + +fn archive_tar_entry_size(targz: &[u8], binary_name: &str) -> Option { let gz = flate2::read::GzDecoder::new(targz); let mut archive = tar::Archive::new(gz); let Ok(entries) = archive.entries() else { - return false; + return None; }; for entry in entries.flatten() { let Ok(path) = entry.path() else { @@ -852,20 +977,27 @@ fn archive_contains_tar_entry(targz: &[u8], binary_name: &str) -> bool { }; let name = path.to_string_lossy(); if name == binary_name || name.ends_with(&format!("/{binary_name}")) { - return true; + return Some(entry.size()); } } - false + None } -fn verify_integrity(data: &[u8], integrity: &str) -> bool { - let Some(encoded) = integrity.strip_prefix("sha512-") else { - return false; - }; - let Ok(expected) = base64::engine::general_purpose::STANDARD.decode(encoded) else { - return false; +fn archive_zip_entry_size(zip_bytes: &[u8], binary_name: &str) -> Option { + let reader = std::io::Cursor::new(zip_bytes); + let Ok(mut archive) = zip::ZipArchive::new(reader) else { + return None; }; - let mut hasher = sha2::Sha512::new(); + (0..archive.len()).find_map(|index| { + archive.by_index(index).ok().and_then(|entry| { + (entry.name() == binary_name || entry.name().ends_with(&format!("/{binary_name}"))) + .then(|| entry.size()) + }) + }) +} + +fn verify_hash(data: &[u8], expected: &str) -> bool { + let mut hasher = sha2::Sha256::new(); hasher.update(data); - hasher.finalize().as_slice() == expected + format!("{:x}", hasher.finalize()) == expected } diff --git a/rust/build/out_of_process.rs b/rust/build/out_of_process.rs index b8cd3acc3f..c0a9dd3050 100644 --- a/rust/build/out_of_process.rs +++ b/rust/build/out_of_process.rs @@ -13,20 +13,20 @@ pub(crate) fn main() { println!("cargo::rustc-check-cfg=cfg(has_extracted_cli)"); println!("cargo:rerun-if-changed=cli-version.txt"); - // Only declare the lockfile rerun when the lockfile actually exists. + // Only declare the package metadata rerun when it actually exists. // Cargo treats `rerun-if-changed` for a missing path as "always rerun" // — so unconditionally declaring this on consumers without a sibling // `nodejs/` (vendored slots, published crates) would force build.rs // to re-run on every `cargo build` even when nothing has changed. - // The lockfile path is only the source-of-truth in this repo's + // The package file is only the source-of-truth in this repo's // contributor builds; everywhere else `cli-version.txt` is canonical. let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set"); - let lockfile = Path::new(&manifest_dir) + let package_json = Path::new(&manifest_dir) .join("..") .join("nodejs") - .join("package-lock.json"); - if lockfile.is_file() { - println!("cargo:rerun-if-changed={}", lockfile.display()); + .join("package.json"); + if package_json.is_file() { + println!("cargo:rerun-if-changed={}", package_json.display()); } // Hard opt-out: disable the entire download / bundle / cache mechanism @@ -61,15 +61,9 @@ pub(crate) fn main() { // Resolve version + per-asset SHA-256 from one of two sources, in order: // 1. `cli-version.txt` snapshot at the crate root (published-crate - // consumer; generated by the publish workflow). Combined format: - // `version=X` line + per-asset hash lines. Committing the hashes - // makes the publish workflow the trust boundary — an attacker who - // later re-points the release tag can't silently poison consumer - // builds. - // 2. Sibling `../nodejs/package-lock.json` (contributor build inside - // the github/copilot-sdk repo; live SHA256SUMS.txt fetch). Matches - // the .NET `_GetCopilotCliVersion` MSBuild target and the Go - // `cmd/bundler` tool. + // consumer; generated by the publish workflow from SHA256SUMS.txt). + // 2. Sibling `../nodejs/package.json` plus the release SHA256SUMS.txt + // (contributor build inside the github/copilot-sdk repo). let (version, expected_hash) = resolve_version_and_hash(platform.asset_name); // Bake the version into the crate regardless of mode. This is the @@ -194,15 +188,14 @@ fn resolve_version_and_hash(asset_name: &str) -> (String, String) { .unwrap_or_else(|e| panic!("invalid {}: {e}", snapshot.display())); } - // 2. Lockfile fallback (contributor build inside github/copilot-sdk) — - // read version, fetch live SHA256SUMS. - let lockfile = Path::new(&manifest_dir) + // 2. Package version plus release checksums (contributor build). + let package_json = Path::new(&manifest_dir) .join("..") .join("nodejs") - .join("package-lock.json"); - if lockfile.is_file() { - let version = read_version_from_package_lock(&lockfile); - let hash = fetch_live_sha256(&version, asset_name); + .join("package.json"); + if package_json.is_file() { + let version = read_version_from_package_json(&package_json); + let hash = fetch_release_hash(&version, asset_name); return (version, hash); } @@ -212,9 +205,9 @@ fn resolve_version_and_hash(asset_name: &str) -> (String, String) { - {} (missing)\n\ - {} (missing)\n\ In a published crate or vendored slot, `cli-version.txt` should be present.\n\ - Inside the github/copilot-sdk repo, `../nodejs/package-lock.json` is the source.", + Inside the github/copilot-sdk repo, `../nodejs/package.json` is the version source.", snapshot.display(), - lockfile.display(), + package_json.display(), ); } @@ -247,39 +240,33 @@ fn parse_snapshot(contents: &str, asset_name: &str) -> Result<(String, String), Ok((version, hash)) } -/// Read the `@github/copilot` version from `nodejs/package-lock.json`. -fn read_version_from_package_lock(path: &Path) -> String { +fn read_version_from_package_json(path: &Path) -> String { let contents = std::fs::read_to_string(path) .unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display())); - // Minimal JSON walk: find `"node_modules/@github/copilot"` object and - // its `"version"` field. Full JSON parsing keeps build.rs dep-light by - // using a regex; the file is generated by npm and we're matching an - // exact key path. - let key = "\"node_modules/@github/copilot\""; - let key_pos = contents - .find(key) - .unwrap_or_else(|| panic!("{} does not contain {key}", path.display())); - let after_key = &contents[key_pos + key.len()..]; - let version_key = "\"version\""; - let v_pos = after_key - .find(version_key) - .unwrap_or_else(|| panic!("no `version` field found near {key} in {}", path.display())); - let after_v = &after_key[v_pos + version_key.len()..]; - let q1 = after_v.find('"').expect("malformed version"); - let after_q1 = &after_v[q1 + 1..]; - let q2 = after_q1.find('"').expect("malformed version"); - after_q1[..q2].to_string() + let package_json: serde_json::Value = serde_json::from_str(&contents) + .unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display())); + package_json["copilotCliVersion"] + .as_str() + .unwrap_or_else(|| panic!("copilotCliVersion is missing in {}", path.display())) + .to_string() } -/// Fetch the live `SHA256SUMS.txt` for the given version from GitHub Releases -/// and pluck out the entry for `asset_name`. -fn fetch_live_sha256(version: &str, asset_name: &str) -> String { - let base_url = format!("https://github.com/github/copilot-cli/releases/download/v{version}"); - let checksums_url = format!("{base_url}/SHA256SUMS.txt"); - let checksums = download_with_retry(&checksums_url); - let checksums_text = - std::str::from_utf8(&checksums).expect("checksums file is not valid UTF-8"); - find_sha256_for_asset(checksums_text, asset_name) +fn fetch_release_hash(version: &str, asset_name: &str) -> String { + let url = + format!("https://github.com/github/copilot-cli/releases/download/v{version}/SHA256SUMS.txt"); + let checksums = download_with_retry(&url); + let checksums = + std::str::from_utf8(&checksums).expect("SHA256SUMS.txt is not valid UTF-8"); + find_sha256_for_asset(checksums, asset_name) +} + +fn find_sha256_for_asset(sums: &str, asset_name: &str) -> String { + sums.lines() + .find_map(|line| { + let (hash, name) = line.split_once(char::is_whitespace)?; + (name.trim_start().trim_start_matches('*') == asset_name).then(|| hash.to_string()) + }) + .unwrap_or_else(|| panic!("SHA256SUMS.txt does not contain {asset_name}")) } #[derive(Clone, Copy)] @@ -641,18 +628,6 @@ fn try_download(url: &str) -> Result, DownloadError> { } } -fn find_sha256_for_asset(sums: &str, asset_name: &str) -> String { - for line in sums.lines() { - // Format: " " (two spaces) - if let Some((hash, name)) = line.split_once(" ") - && name.trim() == asset_name - { - return hash.trim().to_string(); - } - } - panic!("SHA256SUMS.txt does not contain an entry for {asset_name}"); -} - fn sha256(data: &[u8]) -> [u8; 32] { let mut hasher = sha2::Sha256::new(); hasher.update(data); diff --git a/rust/scripts/snapshot-bundled-cli-version.sh b/rust/scripts/snapshot-bundled-cli-version.sh index 7f78d529b0..0045f5e6c8 100755 --- a/rust/scripts/snapshot-bundled-cli-version.sh +++ b/rust/scripts/snapshot-bundled-cli-version.sh @@ -5,10 +5,9 @@ # how .NET's _GenerateVersionProps BeforeTargets="Pack" target writes # GitHub.Copilot.SDK.props before NuGet packing. # -# Inputs: -# - ../nodejs/package-lock.json (sibling) - source of the pinned version. -# - https://github.com/github/copilot-cli/releases/v{version}/SHA256SUMS.txt - -# authoritative per-platform hashes. +# Input: +# - ../nodejs/package.json - pinned release version. +# - The release's SHA256SUMS.txt - per-platform hashes. # # Output: # - cli-version.txt (in the rust crate root). Gitignored. @@ -18,50 +17,49 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RUST_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${RUST_DIR}/.." && pwd)" -LOCKFILE="${REPO_ROOT}/nodejs/package-lock.json" +PACKAGE_FILE="${REPO_ROOT}/nodejs/package.json" OUTPUT="${RUST_DIR}/cli-version.txt" -if [[ ! -f "${LOCKFILE}" ]]; then - echo "error: ${LOCKFILE} not found" >&2 +if [[ ! -f "${PACKAGE_FILE}" ]]; then + echo "error: ${PACKAGE_FILE} not found" >&2 exit 1 fi -VERSION="$(node -e "console.log(require('${LOCKFILE}').packages['node_modules/@github/copilot'].version)")" +VERSION="$(node -e "console.log(require('${PACKAGE_FILE}').copilotCliVersion)")" if [[ -z "${VERSION}" ]]; then - echo "error: could not read @github/copilot version from ${LOCKFILE}" >&2 + echo "error: could not read copilotCliVersion from ${PACKAGE_FILE}" >&2 exit 1 fi - CHECKSUMS_URL="https://github.com/github/copilot-cli/releases/download/v${VERSION}/SHA256SUMS.txt" -echo "Fetching ${CHECKSUMS_URL}" -SHA256SUMS="$(curl -fsSL --retry 3 --retry-delay 2 "${CHECKSUMS_URL}")" +SHA256SUMS="$(curl --fail --silent --show-error --location --retry 3 "${CHECKSUMS_URL}")" ASSETS=( "copilot-darwin-arm64.tar.gz" "copilot-darwin-x64.tar.gz" "copilot-linux-arm64.tar.gz" "copilot-linux-x64.tar.gz" + "copilot-linuxmusl-arm64.tar.gz" + "copilot-linuxmusl-x64.tar.gz" "copilot-win32-arm64.zip" "copilot-win32-x64.zip" ) -declare -A HASHES -for asset in "${ASSETS[@]}"; do - hash="$(printf '%s\n' "${SHA256SUMS}" | awk -v a="${asset}" '$2 == a { print $1 }')" - if [[ -z "${hash}" ]]; then - echo "error: SHA256SUMS.txt missing entry for ${asset}" >&2 - exit 1 - fi - HASHES[$asset]="${hash}" -done - +TEMP_OUTPUT="${OUTPUT}.tmp.$$" +trap 'rm -f "${TEMP_OUTPUT}"' EXIT { echo "# Auto-generated by rust/scripts/snapshot-bundled-cli-version.sh" echo "# Do not edit. Regenerated by the publish workflow on every release." echo "version=${VERSION}" for asset in "${ASSETS[@]}"; do - echo "${asset}=${HASHES[$asset]}" + hash="$(printf '%s\n' "${SHA256SUMS}" | awk -v asset="${asset}" '$2 == asset || $2 == "*" asset { print $1; exit }')" + if [[ -z "${hash}" ]]; then + echo "error: SHA256SUMS.txt does not contain ${asset}" >&2 + exit 1 + fi + echo "${asset}=${hash}" done -} > "${OUTPUT}" +} > "${TEMP_OUTPUT}" +mv "${TEMP_OUTPUT}" "${OUTPUT}" +trap - EXIT echo "Wrote ${OUTPUT} (version=${VERSION}, ${#ASSETS[@]} hashes)" \ No newline at end of file diff --git a/rust/scripts/snapshot-bundled-in-process-version.sh b/rust/scripts/snapshot-bundled-in-process-version.sh index 8743f9d17a..9fe2298c78 100755 --- a/rust/scripts/snapshot-bundled-in-process-version.sh +++ b/rust/scripts/snapshot-bundled-in-process-version.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Snapshot the Copilot CLI version + per-platform npm integrity values for the +# Snapshot the Copilot CLI version + per-platform release hashes for the # rust crate's bundled-in-process build path. set -euo pipefail @@ -8,19 +8,21 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" RUST_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${RUST_DIR}/.." && pwd)" -LOCKFILE="${REPO_ROOT}/nodejs/package-lock.json" +PACKAGE_FILE="${REPO_ROOT}/nodejs/package.json" OUTPUT="${RUST_DIR}/cli-version-in-process.txt" -if [[ ! -f "${LOCKFILE}" ]]; then - echo "error: ${LOCKFILE} not found" >&2 +if [[ ! -f "${PACKAGE_FILE}" ]]; then + echo "error: ${PACKAGE_FILE} not found" >&2 exit 1 fi -VERSION="$(node -e "console.log(require('${LOCKFILE}').packages['node_modules/@github/copilot'].version)")" +VERSION="$(node -e "console.log(require('${PACKAGE_FILE}').copilotCliVersion)")" if [[ -z "${VERSION}" ]]; then - echo "error: could not read @github/copilot version from ${LOCKFILE}" >&2 + echo "error: could not read copilotCliVersion from ${PACKAGE_FILE}" >&2 exit 1 fi +CHECKSUMS_URL="https://github.com/github/copilot-cli/releases/download/v${VERSION}/SHA256SUMS.txt" +SHA256SUMS="$(curl --fail --silent --show-error --location --retry 3 "${CHECKSUMS_URL}")" PACKAGES=( "copilot-darwin-arm64" @@ -33,23 +35,24 @@ PACKAGES=( "copilot-win32-x64" ) -declare -A INTEGRITIES -for package in "${PACKAGES[@]}"; do - integrity="$(node -e "console.log(require('${LOCKFILE}').packages['node_modules/@github/${package}'].integrity)")" - if [[ -z "${integrity}" ]]; then - echo "error: package-lock.json missing integrity for @github/${package}" >&2 - exit 1 - fi - INTEGRITIES[$package]="${integrity}" -done - +TEMP_OUTPUT="${OUTPUT}.tmp.$$" +trap 'rm -f "${TEMP_OUTPUT}"' EXIT { echo "# Auto-generated by rust/scripts/snapshot-bundled-in-process-version.sh" echo "# Do not edit. Regenerated by the publish workflow on every release." echo "version=${VERSION}" for package in "${PACKAGES[@]}"; do - echo "${package}=${INTEGRITIES[$package]}" + platform="${package#copilot-}" + asset="github-copilot-${VERSION}-${platform}.tgz" + hash="$(printf '%s\n' "${SHA256SUMS}" | awk -v asset="${asset}" '$2 == asset || $2 == "*" asset { print $1; exit }')" + if [[ -z "${hash}" ]]; then + echo "error: SHA256SUMS.txt does not contain ${asset}" >&2 + exit 1 + fi + echo "${package}=${hash}" done -} > "${OUTPUT}" +} > "${TEMP_OUTPUT}" +mv "${TEMP_OUTPUT}" "${OUTPUT}" +trap - EXIT -echo "Wrote ${OUTPUT} (version=${VERSION}, ${#PACKAGES[@]} integrity values)" +echo "Wrote ${OUTPUT} (version=${VERSION}, ${#PACKAGES[@]} hashes)" diff --git a/rust/src/embeddedcli.rs b/rust/src/embeddedcli.rs index 3cc527a2e2..574ed42a6f 100644 --- a/rust/src/embeddedcli.rs +++ b/rust/src/embeddedcli.rs @@ -2,11 +2,11 @@ //! crate (gated on the `bundled-cli` cargo feature, which is in the default //! feature set). //! -//! Normal builds embed the platform release archive from GitHub Releases. -//! Builds with `bundled-in-process` instead embed a filtered archive from the -//! platform npm package containing the CLI executable, runtime wrapper, native -//! runtime artifacts, and auxiliary runtime assets. Extraction to a real -//! on-disk path is deferred until the relevant installer is called. +//! Builds embed two platform release payloads from GitHub Releases: the full +//! CLI archive and a filtered runtime archive containing the wrapper, +//! `runtime.node`, auxiliary runtime assets, and optionally the in-process +//! runtime library. Extraction to a real on-disk path is deferred until the +//! relevant installer is called. //! //! The embedded bytes are part of the consumer's signed binary and therefore //! trusted *as the source of truth* — but the bytes that land on disk are not. @@ -41,7 +41,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use tracing::{info, warn}; // When the `bundled-cli` cargo feature is enabled and the target platform is -// supported, build.rs generates `bundled_cli.rs` exposing the selected archive. +// supported, build.rs generates `bundled_cli.rs` exposing both selected archives. // The CLI version is exposed crate-wide via the // `cargo:rustc-env=COPILOT_SDK_CLI_VERSION` emit (see `build.rs`), and the // binary name is OS-derived — so no other generated constants are needed. @@ -101,7 +101,11 @@ pub(crate) fn path() -> Option { #[cfg(has_bundled_cli)] { let dir = default_install_dir(CLI_VERSION); - match install_cli_bundle(&dir, build_time::CLI_ARCHIVE) { + match install_cli( + &dir, + build_time::CLI_ARCHIVE, + build_time::CLI_BINARY_SIZE, + ) { Ok(path) => { info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed"); return Some(path); @@ -129,7 +133,11 @@ pub(crate) fn path() -> Option { pub(crate) fn install_at(extract_dir: &Path) -> Option { #[cfg(has_bundled_cli)] { - match install_cli_bundle(extract_dir, build_time::CLI_ARCHIVE) { + match install_cli( + extract_dir, + build_time::CLI_ARCHIVE, + build_time::CLI_BINARY_SIZE, + ) { Ok(path) => { info!(path = %path.display(), version = CLI_VERSION, "embedded CLI installed"); return Some(path); @@ -155,7 +163,7 @@ pub(crate) fn runtime_path() -> Option { #[cfg(has_bundled_cli)] { let dir = default_install_dir(CLI_VERSION); - match install_runtime(&dir, build_time::CLI_ARCHIVE) { + match install_runtime(&dir, build_time::RUNTIME_ARCHIVE) { Ok(path) => { info!(path = %path.display(), version = CLI_VERSION, "embedded runtime installed"); return Some(path); @@ -183,7 +191,7 @@ pub(crate) fn install_runtime_at(extract_dir: &Path) -> Option { return None; } }; - match install_runtime(&install_dir, build_time::CLI_ARCHIVE) { + match install_runtime(&install_dir, build_time::RUNTIME_ARCHIVE) { Ok(path) => { info!(path = %path.display(), version = CLI_VERSION, "embedded runtime installed"); return Some(path); @@ -264,23 +272,14 @@ const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.dylib"; ))] const RUNTIME_LIBRARY_NAME: &str = "libcopilot_runtime.so"; -#[cfg(has_bundled_cli)] -fn install_cli_bundle(install_dir: &Path, archive: &[u8]) -> Result { - install_cli(install_dir, archive)?; - install_hostless_assets(install_dir, archive)?; - #[cfg(feature = "bundled-in-process")] - { - install_runtime_library(install_dir, archive)?; - } - Ok(install_dir.join(CLI_BINARY_NAME)) -} - #[cfg(has_bundled_cli)] fn install_runtime(install_dir: &Path, archive: &[u8]) -> Result { fs::create_dir_all(install_dir) .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::CreateDir, e))?; install_hostless_assets(install_dir, archive)?; install_runtime_pair(install_dir, archive)?; + #[cfg(feature = "bundled-in-process")] + install_runtime_library(install_dir, archive)?; Ok(install_dir.join(RUNTIME_BINARY_NAME)) } @@ -414,7 +413,11 @@ fn install_adjacent_file( } #[cfg(has_bundled_cli)] -fn install_cli(install_dir: &Path, archive: &[u8]) -> Result { +fn install_cli( + install_dir: &Path, + archive: &[u8], + expected_binary_size: u64, +) -> Result { let verbose = std::env::var("COPILOT_CLI_INSTALL_VERBOSE").ok().as_deref() == Some("1"); fs::create_dir_all(install_dir) @@ -427,7 +430,7 @@ fn install_cli(install_dir: &Path, archive: &[u8]) -> Result Result PathBuf { /// modes (zero-length / truncated / quarantined-to-garbage) without re-reading /// the whole file. #[cfg(any(has_bundled_cli, test))] -fn existing_install_is_valid(final_path: &Path, marker_path: &Path) -> bool { +fn existing_install_is_valid( + final_path: &Path, + marker_path: &Path, + expected_binary_size: u64, +) -> bool { let Ok(meta) = fs::metadata(final_path) else { return false; }; @@ -503,7 +510,9 @@ fn existing_install_is_valid(final_path: &Path, marker_path: &Path) -> bool { return false; } match read_marker_len(marker_path) { - Some(expected) if expected == meta.len() => looks_like_valid_image(final_path), + Some(expected) if expected == expected_binary_size && expected == meta.len() => { + looks_like_valid_image(final_path) + } _ => false, } } @@ -694,6 +703,32 @@ fn read_marker_len(marker_path: &Path) -> Option { .ok() } +#[cfg(all(has_bundled_cli, not(windows)))] +fn extract_cli_binary(archive: &[u8]) -> Result, EmbeddedCliError> { + extract_binary(archive, CLI_BINARY_NAME) +} + +#[cfg(all(has_bundled_cli, windows))] +fn extract_cli_binary(archive: &[u8]) -> Result, EmbeddedCliError> { + let reader = std::io::Cursor::new(archive); + let mut zip = zip::ZipArchive::new(reader) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?; + for index in 0..zip.len() { + let mut entry = zip + .by_index(index) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?; + if entry.name() == CLI_BINARY_NAME || entry.name().ends_with(&format!("/{CLI_BINARY_NAME}")) + { + let mut bytes = Vec::with_capacity(entry.size() as usize); + entry + .read_to_end(&mut bytes) + .map_err(|e| EmbeddedCliError::new(EmbeddedCliErrorKind::Archive, e))?; + return Ok(bytes); + } + } + Err(EmbeddedCliErrorKind::BinaryNotFoundInArchive.into()) +} + #[cfg(has_bundled_cli)] fn extract_binary(archive: &[u8], binary_name: &str) -> Result, EmbeddedCliError> { let gz = flate2::read::GzDecoder::new(archive); @@ -858,8 +893,8 @@ mod tests { #[cfg(all(has_bundled_cli, feature = "bundled-in-process"))] #[test] - fn embedded_archive_contains_runtime_assets_and_excludes_cli_only_files() { - let gz = flate2::read::GzDecoder::new(build_time::CLI_ARCHIVE); + fn embedded_runtime_archive_contains_runtime_assets_and_excludes_cli() { + let gz = flate2::read::GzDecoder::new(build_time::RUNTIME_ARCHIVE); let mut archive = tar::Archive::new(gz); let mut names: Vec = archive .entries() @@ -875,12 +910,12 @@ mod tests { .collect(); names.sort(); - assert!(names.contains(&CLI_BINARY_NAME.to_string())); assert!(names.contains(&RUNTIME_LIBRARY_NAME.to_string())); assert!(names.contains(&RUNTIME_BINARY_NAME.to_string())); assert!(names.contains(&RUNTIME_NODE_NAME.to_string())); assert!(names.iter().any(|name| name.starts_with("ripgrep/"))); assert!(names.iter().any(|name| name.starts_with("definitions/"))); + assert!(!names.contains(&CLI_BINARY_NAME.to_string())); assert!(!names.contains(&"app.js".to_string())); } @@ -911,7 +946,11 @@ mod tests { assert!(final_path.is_file(), "binary should be published"); assert_eq!(fs::read(&final_path).expect("read"), bytes); assert_eq!(read_marker_len(&marker), Some(bytes.len() as u64)); - assert!(existing_install_is_valid(&final_path, &marker)); + assert!(existing_install_is_valid( + &final_path, + &marker, + bytes.len() as u64 + )); // No leftover temp files in the install dir. let leftovers: Vec<_> = fs::read_dir(dir.path()) @@ -945,28 +984,40 @@ mod tests { let bytes = fake_image(4096); // Missing binary entirely. - assert!(!existing_install_is_valid(&final_path, &marker)); + assert!(!existing_install_is_valid(&final_path, &marker, 1)); // Valid binary but no marker (e.g. installed by an older SDK). fs::write(&final_path, &bytes).expect("write binary"); assert!( - !existing_install_is_valid(&final_path, &marker), + !existing_install_is_valid(&final_path, &marker, bytes.len() as u64), "an install without a marker must not be trusted" ); // Marker present but the binary was later truncated (partial write / // antivirus). Marker still records the original full size. write_marker(&marker, bytes.len() as u64).expect("marker"); - assert!(existing_install_is_valid(&final_path, &marker)); + assert!(existing_install_is_valid( + &final_path, + &marker, + bytes.len() as u64 + )); + assert!( + !existing_install_is_valid(&final_path, &marker, bytes.len() as u64 + 1), + "a marker from the wrapper-as-CLI regression must not validate the full CLI" + ); fs::write(&final_path, &bytes[..bytes.len() / 2]).expect("truncate"); assert!( - !existing_install_is_valid(&final_path, &marker), + !existing_install_is_valid(&final_path, &marker, bytes.len() as u64), "a truncated binary must be detected via the size marker" ); // Zero-length binary (quarantined to empty). fs::write(&final_path, b"").expect("empty"); - assert!(!existing_install_is_valid(&final_path, &marker)); + assert!(!existing_install_is_valid( + &final_path, + &marker, + bytes.len() as u64 + )); } #[test] @@ -981,7 +1032,7 @@ mod tests { write_marker(&marker, garbage.len() as u64).expect("marker"); assert!( - !existing_install_is_valid(&final_path, &marker), + !existing_install_is_valid(&final_path, &marker, garbage.len() as u64), "a non-executable image must be rejected even with a matching marker" ); } @@ -1038,15 +1089,17 @@ mod tests { fs::write(dir.path().join(RUNTIME_NODE_NAME), b"stale runtime").expect("seed runtime"); fs::write(dir.path().join(RUNTIME_BINARY_NAME), b"stale wrapper").expect("seed wrapper"); - install_runtime(dir.path(), build_time::CLI_ARCHIVE).expect("install runtime"); + install_runtime(dir.path(), build_time::RUNTIME_ARCHIVE).expect("install runtime"); assert_eq!( fs::read(dir.path().join(RUNTIME_NODE_NAME)).expect("read runtime"), - extract_binary(build_time::CLI_ARCHIVE, RUNTIME_NODE_NAME).expect("extract runtime") + extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_NODE_NAME) + .expect("extract runtime") ); assert_eq!( fs::read(dir.path().join(RUNTIME_BINARY_NAME)).expect("read wrapper"), - extract_binary(build_time::CLI_ARCHIVE, RUNTIME_BINARY_NAME).expect("extract wrapper") + extract_binary(build_time::RUNTIME_ARCHIVE, RUNTIME_BINARY_NAME) + .expect("extract wrapper") ); } diff --git a/rust/src/errors.rs b/rust/src/errors.rs index 70f4c14ff1..3bf5becbda 100644 --- a/rust/src/errors.rs +++ b/rust/src/errors.rs @@ -152,6 +152,9 @@ pub enum SessionErrorKind { /// Session ID returned by the CLI. returned: SessionId, }, + + /// The CLI could not detach the session. + DetachFailed, } impl fmt::Display for SessionErrorKind { @@ -186,6 +189,7 @@ impl fmt::Display for SessionErrorKind { f, "CLI returned session ID {returned} after SDK registered {requested}" ), + SessionErrorKind::DetachFailed => write!(f, "failed to detach session"), } } } @@ -400,7 +404,7 @@ fn capture_backtrace() -> Option> { /// /// `Client::stop` performs cooperative shutdown across every active /// session before killing the CLI child process. Errors from any -/// per-session `session.destroy` RPC and from the terminal child-kill +/// per-session `session.detach` RPC and from the terminal child-kill /// step are collected here rather than short-circuiting on the first /// failure, so callers see the full picture of what went wrong during /// teardown. diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index 701ae61403..1bd83a50f7 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -10,11 +10,11 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; use super::session_events::{ - AbortReason, AutoTier, ContextTier, McpOauthHttpResponse, McpOauthWWWAuthenticateParams, - McpServerSource, McpServerStatus, ModelChangeSource, OmittedBinaryOmittedReason, - PermissionMode, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionLimitsConfig, - SessionMode, ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, - Verbosity, + AbortReason, AgentModelPolicy, AutoTier, ContextTier, McpOauthHttpResponse, + McpOauthWWWAuthenticateParams, McpServerMetadata, McpServerSource, McpServerStatus, + ModelChangeSource, OmittedBinaryOmittedReason, PermissionMode, PermissionPromptRequest, + PermissionRule, ReasoningSummary, RemediationAction, SessionLimitsConfig, SessionMode, + ShutdownType, SkillSource, TaskCompletionOutcome, UserToolSessionApproval, Verbosity, }; use crate::types::{RequestId, SessionEvent, SessionId}; @@ -24,6 +24,8 @@ pub mod rpc_methods { pub const PING: &str = "ping"; /// `connect` pub const CONNECT: &str = "connect"; + /// `hooks.discover` + pub const HOOKS_DISCOVER: &str = "hooks.discover"; /// `models.list` pub const MODELS_LIST: &str = "models.list"; /// `models.getBuiltInCatalog` @@ -122,6 +124,8 @@ pub mod rpc_methods { pub const USER_SETTINGS_SET: &str = "user.settings.set"; /// `managedSettings.read` pub const MANAGEDSETTINGS_READ: &str = "managedSettings.read"; + /// `managedSettings.clearCache` + pub const MANAGEDSETTINGS_CLEARCACHE: &str = "managedSettings.clearCache"; /// `runtime.shutdown` pub const RUNTIME_SHUTDOWN: &str = "runtime.shutdown"; /// `sessionFs.setProvider` @@ -142,6 +146,8 @@ pub mod rpc_methods { pub const SESSIONS_LIST: &str = "sessions.list"; /// `sessions.getMetadata` pub const SESSIONS_GETMETADATA: &str = "sessions.getMetadata"; + /// `sessions.readPersistedEvents` + pub const SESSIONS_READPERSISTEDEVENTS: &str = "sessions.readPersistedEvents"; /// `sessions.listNonEmptySessionIds` pub const SESSIONS_LISTNONEMPTYSESSIONIDS: &str = "sessions.listNonEmptySessionIds"; /// `sessions.findByTaskId` @@ -282,6 +288,8 @@ pub mod rpc_methods { pub const SESSION_MODEL_GETCURRENT: &str = "session.model.getCurrent"; /// `session.model.switchTo` pub const SESSION_MODEL_SWITCHTO: &str = "session.model.switchTo"; + /// `session.model.switchAutoTier` + pub const SESSION_MODEL_SWITCHAUTOTIER: &str = "session.model.switchAutoTier"; /// `session.model.applyStartupOverlay` pub const SESSION_MODEL_APPLYSTARTUPOVERLAY: &str = "session.model.applyStartupOverlay"; /// `session.model.setReasoningEffort` @@ -345,6 +353,8 @@ pub mod rpc_methods { pub const SESSION_WORKSPACES_SAVELARGEPASTE: &str = "session.workspaces.saveLargePaste"; /// `session.workspaces.diff` pub const SESSION_WORKSPACES_DIFF: &str = "session.workspaces.diff"; + /// `session.autopilotObjective.getState` + pub const SESSION_AUTOPILOTOBJECTIVE_GETSTATE: &str = "session.autopilotObjective.getState"; /// `session.completions.getTriggerCharacters` pub const SESSION_COMPLETIONS_GETTRIGGERCHARACTERS: &str = "session.completions.getTriggerCharacters"; @@ -370,6 +380,10 @@ pub mod rpc_methods { pub const SESSION_TASKS_STARTAGENT: &str = "session.tasks.startAgent"; /// `session.tasks.list` pub const SESSION_TASKS_LIST: &str = "session.tasks.list"; + /// `session.tasks.register` + pub const SESSION_TASKS_REGISTER: &str = "session.tasks.register"; + /// `session.tasks.update` + pub const SESSION_TASKS_UPDATE: &str = "session.tasks.update"; /// `session.tasks.refresh` pub const SESSION_TASKS_REFRESH: &str = "session.tasks.refresh"; /// `session.tasks.waitForPending` @@ -737,12 +751,18 @@ pub mod rpc_methods { pub const SESSION_SCHEDULE_REARMSELFPACED: &str = "session.schedule.rearmSelfPaced"; /// `session.schedule.stop` pub const SESSION_SCHEDULE_STOP: &str = "session.schedule.stop"; + /// `skillProvider.list` + pub const SKILLPROVIDER_LIST: &str = "skillProvider.list"; + /// `skillProvider.read` + pub const SKILLPROVIDER_READ: &str = "skillProvider.read"; /// `providerToken.getToken` pub const PROVIDERTOKEN_GETTOKEN: &str = "providerToken.getToken"; /// `factory.execute` pub const FACTORY_EXECUTE: &str = "factory.execute"; /// `factory.abort` pub const FACTORY_ABORT: &str = "factory.abort"; + /// `tasks.cancel` + pub const TASKS_CANCEL: &str = "tasks.cancel"; /// `sessionFs.readFile` pub const SESSIONFS_READFILE: &str = "sessionFs.readFile"; /// `sessionFs.writeFile` @@ -1541,7 +1561,7 @@ pub struct AgentDiscoveryPathList { pub paths: Vec, } -/// Agent metadata, including identifiers, display details, source, tools, model, MCP servers, skills, and file path. +/// Agent metadata, including identifiers, display details, source, tools, model, models, MCP servers, skills, and file path. /// ///

/// @@ -1571,6 +1591,12 @@ pub struct AgentInfo { /// Authored preferred model id for this agent. Runtime model selection may choose a different model; omitted means no authored preference. #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Whether authored models are preferences or required constraints. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_policy: Option, + /// Authored preferred model ids for this agent, in priority order. Runtime model selection chooses the first available model; omitted means no authored preference. + #[serde(skip_serializing_if = "Option::is_none")] + pub models: Option>, /// Name of the agent. Use `id` as the stable selection identifier. pub name: String, /// Absolute local file path of the agent definition. Only set for file-based agents loaded from disk; remote agents do not have a path. @@ -2492,6 +2518,73 @@ pub struct AuthValidationError { pub message: String, } +/// Current per-window credit limit and consumption for an autopilot objective. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutopilotObjectiveCreditLimit { + /// Configured AI-credit cap, when one is set. + #[serde(skip_serializing_if = "Option::is_none")] + pub credits: Option, + /// Window consumption in fractional AI credits, for display. + pub credits_used: f64, + /// Exact window consumption in non-negative integer nano-AIU, encoded as a decimal string. + pub credits_used_nano_aiu: String, +} + +/// Public, persistence-independent projection of an autopilot objective. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutopilotObjectiveState { + /// Optional summary recorded when the objective completed. + #[serde(skip_serializing_if = "Option::is_none")] + pub completion_summary: Option, + /// Exact lifetime AI-credit consumption in non-negative integer nano-AIU, encoded as a decimal string. + pub credit_count_nano_aiu: String, + /// Current per-window consumption and optional cap, when a credit-tracking window is present. + #[serde(skip_serializing_if = "Option::is_none")] + pub credit_limit: Option, + /// Session-local objective identifier. + pub id: i64, + /// User-provided objective text. + pub objective: String, + /// Optional reason the objective is paused. + #[serde(skip_serializing_if = "Option::is_none")] + pub pause_reason: Option, + /// Current normalized lifecycle status. + pub status: AutopilotObjectiveStatus, + /// Number of objective turns started. + pub turn_count: i64, +} + +/// Canonical runtime state for the session's current autopilot objective. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AutopilotObjectiveGetStateResult { + /// Current objective state, or `null` when the session has no objective. + pub state: Option, +} + /// A well-known model in the runtime's built-in catalog. /// ///
@@ -3022,7 +3115,7 @@ pub struct CanvasProviderUnregisterRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CapiSessionOptions { - /// Routing preference used when the session model is `auto`. The runtime persists the preference across cold resume. When omitted, the default routing behavior is used. Resuming an already-resident session cannot change its preference. + /// Routing preference for sessions whose model is `auto`. On create or cold resume, this establishes the preference sent as `tier` on CAPI `/auto` requests; when omitted on cold resume, the runtime restores the last committed preference. On resident resume, a different value requests a safe switch after resume succeeds and cannot change an in-flight turn. Successful switches are persisted for later cold resume. When no preference is supplied or restored, CAPI default routing is used. #[serde(skip_serializing_if = "Option::is_none")] pub auto_tier: Option, /// Whether to use WebSocket transport for the CAPI Responses API. Enabled by default when the model advertises `ws:/responses` support; set to `false` to force the HTTP Responses transport in environments where WebSockets are blocked (e.g. behind a proxy). Setting this to `false` is equivalent to the `COPILOT_CLI_DISABLE_WEBSOCKET_RESPONSES` environment variable. @@ -3365,6 +3458,9 @@ pub struct CatalogNetworkFailureError { pub message: String, /// Categorised failure, low cardinality so it can be aggregated without carrying a URL. pub reason: CatalogNetworkFailureReason, + /// Bounded cooldown in seconds before another catalog request should be attempted, when the authority supplied a numeric Retry-After value or the runtime applied its documented fallback. + #[serde(skip_serializing_if = "Option::is_none")] + pub retry_after_seconds: Option, /// HTTP status code, when the failure was a rejected response. #[serde(skip_serializing_if = "Option::is_none")] pub status_code: Option, @@ -3427,7 +3523,7 @@ pub struct CatalogSearchRequest { /// Maximum number of candidates to return. Defaults to 10 when omitted. #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, - /// Free-text search query. Never written to logs or telemetry. + /// Free-text search query. Persisted as tool input for session continuity, but omitted from telemetry. pub query: String, } @@ -3532,6 +3628,44 @@ pub struct CatalogUnavailableTransportError { pub reason: CatalogUnavailableTransportReason, } +/// Runtime-to-owner cancellation request for a client-owned task. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientTaskCancelRequest { + /// Opaque identifier shared by coalesced cancellation callers + pub cancellation_id: String, + /// Owner-scoped task key included for correlation + pub client_task_id: String, + /// Canonical runtime-generated task identifier + pub id: String, + /// Reason the runtime requests cancellation + pub reason: ClientTaskCancelReason, + /// Session that owns the client task + pub session_id: SessionId, +} + +/// Whether the client authoritatively confirmed its external work stopped. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientTaskCancelResult { + /// True only when the owner confirms that external work stopped before responding + pub cancelled: bool, +} + /// A literal choice the command input accepts, with a human-facing description /// ///
@@ -3978,6 +4112,9 @@ pub(crate) struct ConnectRequest { /// Opt this connection in to GitHub telemetry forwarding for its lifetime. When set, the runtime forwards every internal telemetry event it emits — across all sessions, plus sessionless events — to this connection over the `gitHubTelemetry.event` notification. Regular events are also written to the runtime's normal GitHub/CTS path (dual-write); host-only compatibility events are forward-only and intentionally skip that path. Intended for first-party hosts that re-emit the events into their own telemetry stores. Both unrestricted and restricted events are forwarded, each tagged with a `restricted` discriminator; a backstop drops restricted events when restricted telemetry is disabled — using the process-global gate for ordinary events and an explicit session-scoped decision for host-only events. #[serde(skip_serializing_if = "Option::is_none")] pub enable_git_hub_telemetry_forwarding: Option, + /// Task kinds this connection can decode when observing session tasks. Omit to retain agent and shell compatibility. + #[serde(skip_serializing_if = "Option::is_none")] + pub supported_task_kinds: Option>, /// Connection token; required when the server was started with COPILOT_CONNECTION_TOKEN #[serde(skip_serializing_if = "Option::is_none")] pub token: Option, @@ -3998,6 +4135,9 @@ pub(crate) struct ConnectResult { pub ok: bool, /// Server protocol version number pub protocol_version: i64, + /// Task kinds the server may return to this connection. + #[serde(skip_serializing_if = "Option::is_none")] + pub task_kinds: Option>, /// Server package version pub version: String, } @@ -4072,7 +4212,7 @@ pub struct ContextHeaviestMessage { pub tokens: i64, } -/// The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. +/// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. /// ///
/// @@ -4083,12 +4223,21 @@ pub struct ContextHeaviestMessage { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CurrentModel { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub activating_auto_tier: Option, + /// Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Context tier for models that support multiple context-window sizes. #[serde(skip_serializing_if = "Option::is_none")] pub context_tier: Option, /// Currently active model identifier #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_auto_tier: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -4376,6 +4525,36 @@ pub struct DiscoveredExtensionsEnableRequest { pub ids: Vec, } +/// One server-discovered hook action from user, repository, plugin, or managed-policy configuration. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DiscoveredHook { + /// Durable content hash used by hook enablement. Identical actions may intentionally share this key. Omitted when changing the user's disabled-hooks setting cannot change the action's current server-discovered state, including managed-policy hooks, session-start prompt actions, actions suppressed by disable-all settings, and projectless plugin actions that require project-directory expansion. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_key: Option, + /// Whether this action is enabled under the server-side discovery settings. Concrete sessions may differ because they can add session-specific directories, plugins, or trust. False when its disable key is present in the user's disabled-hooks setting or disable-all settings suppress the action. + pub enabled: bool, + /// Hook event that invokes this action. + pub hook_type: HookType, + /// Deterministic identifier for this server-discovered action row. It remains stable while the project, origin, source, event, action content, and duplicate ordinal are unchanged. This is row identity, not the key persisted in disabledHooks. + pub id: String, + /// Configuration tier that contributed this hook action. + pub origin: HookOrigin, + /// Input project path for which this server-side action was resolved. Set on every row returned for project-scoped discovery, including repeated user and policy actions. + #[serde(skip_serializing_if = "Option::is_none")] + pub project_path: Option, + /// Human-readable source label, such as a hook file path, settings source, or plugin name. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, +} + /// MCP server discovered by `mcp.discover`, with config source, optional plugin source, transport type, and enabled state. /// ///
@@ -5621,10 +5800,13 @@ pub struct FactoryResumeRequest { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FactoryRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -6453,8 +6635,7 @@ pub struct HistoryTruncateResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct HookInvokeRequest { - #[doc(hidden)] - pub(crate) hook_type: HookType, + pub hook_type: HookType, pub input: serde_json::Value, pub session_id: SessionId, } @@ -6467,6 +6648,44 @@ pub(crate) struct HookInvokeResponse { pub output: Option, } +/// Optional project paths and host-exclusion behavior for server-scoped hook discovery. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HooksDiscoverRequest { + /// When true, omit host-owned user and plugin hook rows and their diagnostics. Managed-policy hooks and trusted repository hooks remain visible, and host disabledHooks still contribute to each remaining row's effective enabled state. This filters sources rather than simulating a host with no settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub exclude_host_hooks: Option, + /// Optional project directory paths whose trusted repository and project-expanded plugin hooks should be discovered. When omitted or empty, user, managed-policy, and globally enabled installed or explicit plugin hooks are returned without project expansion. + #[serde(skip_serializing_if = "Option::is_none")] + pub project_paths: Option>, +} + +/// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct HooksDiscoverResult { + /// Errors for hook sources or actions that could not be loaded, making the result partially incomplete. Other valid actions are still returned. Project-resolution and repository-settings errors are prefixed with their project path. + pub errors: Vec, + /// All discovered hook actions. Byte-identical actions remain separate rows even when they share a disable key. + pub hooks: Vec, + /// Non-fatal source-loading warnings. Discovery remains complete for the affected source, although the source had a recoverable issue. Repository-settings warnings are prefixed with their project path when attribution is available. + pub warnings: Vec, +} + /// Installed plugin record from global state, with marketplace, version, install time, enabled state, cache path, and source. /// ///
@@ -7631,6 +7850,9 @@ pub struct McpConfigList { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpConfigRemoveRequest { + /// OAuth Client ID Metadata Document URL whose persisted credentials should also be removed. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_client_id_metadata_url: Option, /// Name of the MCP server to remove pub name: String, } @@ -9064,6 +9286,9 @@ pub struct McpServer { pub error: Option, /// Server name (config key) pub name: String, + /// Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + #[serde(skip_serializing_if = "Option::is_none")] + pub server_metadata: Option, /// Configuration source: user, workspace, plugin, or builtin #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, @@ -9886,6 +10111,9 @@ pub struct ModelBillingPromo { /// Human-readable promotion message. Does not include the expiry timestamp; consumers may format endsAt and append it when present. #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + /// Whether the service asked hosts to give this promotion a prominent surface, such as a dedicated banner, in addition to listing it with the model. `true` requests that surface and `false` asks for the model list only. Absent means the service expressed no preference — for example a response that predates the field — so hosts should apply their own default rather than read it as `false`. + #[serde(skip_serializing_if = "Option::is_none")] + pub show_banner: Option, } /// Long context tier pricing (available for models with extended context windows) @@ -10167,6 +10395,9 @@ pub struct Model { /// Informational notices the service published for this model, such as an upcoming change or a recommended alternative. Present only when the service published at least one notice. Hosts should surface these without implying anything is wrong with the model. #[serde(skip_serializing_if = "Option::is_none")] pub info_messages: Option>, + /// Provider-supplied model metadata. Keys and JSON-compatible values are preserved unchanged. This is factual metadata published by the model provider; it carries no picker or UX semantics. + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option>, /// Model capability category for grouping in the model picker #[serde(skip_serializing_if = "Option::is_none")] pub model_picker_category: Option, @@ -10212,6 +10443,9 @@ pub struct ModelApplyStartupOverlayRequest { /// Model required by device-managed policy, when configured. #[serde(skip_serializing_if = "Option::is_none")] pub device_managed_model: Option, + /// Startup default model from the enterprise policy helper, when configured. Weakest of the managed sources: it applies only when neither device nor server policy names a model, and an explicit user selection still wins. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_helper_model: Option, /// Context tier selected by repository settings, when configured. #[serde(skip_serializing_if = "Option::is_none")] pub repo_context_tier: Option, @@ -10443,6 +10677,51 @@ pub struct ModelsListRequest { pub selection_id: Option, } +/// An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelSwitchAutoTierRequest { + /// Auto preference to activate when a future user turn using the `auto` model safely mints a replacement model and token pair. Pass null to return to provider-default Auto routing. + pub auto_tier: Option, + /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelSwitchAutoTierResult { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub activating_auto_tier: Option, + /// Auto preference currently committed for the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub effective_auto_tier: Option, + /// Latest unclaimed Auto preference waiting for a future user turn. + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_auto_tier: Option, + /// Immediate request status. `pending` means accepted but not committed. + pub status: ModelSwitchAutoTierStatus, + /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + #[serde(skip_serializing_if = "Option::is_none")] + pub superseded_auto_tier: Option, +} + /// ///
/// @@ -10472,6 +10751,9 @@ pub struct ModelSwitchConfirmation { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModelSwitchToRequest { + /// Optional Auto routing preference to stage atomically with selecting `auto`. Pass null to return to provider-default Auto routing. This field is rejected when `modelId` is not `auto`. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Explicit response to a model-switch compaction preflight. Omit to request a confirmation projection when compaction is necessary. #[serde(skip_serializing_if = "Option::is_none")] pub compaction_decision: Option, @@ -10507,7 +10789,7 @@ pub struct ModelSwitchToRequest { /// When true, evaluate context-window compaction policy before applying the switch. #[serde(skip_serializing_if = "Option::is_none")] pub run_compaction_preflight: Option, - /// Origin to record on the effective `session.model_change` event. Defaults to `sdk` when omitted. + /// Origin to record on the effective `session.model_change` event for trusted in-process calls. Transport SDK calls are always recorded as `sdk`, regardless of this value. #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, /// Output verbosity level to request for supported models @@ -10541,6 +10823,9 @@ pub struct ModelSwitchToResult { /// Currently active model identifier after the switch #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_state: Option, /// Persistence failure encountered after applying the model switch. #[serde(skip_serializing_if = "Option::is_none")] pub persistence_error: Option, @@ -12639,6 +12924,9 @@ pub struct PluginInstallResult { pub post_install_message: Option, /// Number of skills discovered and installed from the plugin pub skills_installed: i64, + /// Where the completed plugin tree was staged before atomic promotion + #[serde(skip_serializing_if = "Option::is_none")] + pub staging_mode: Option, } /// Plugins installed for the session, with their enabled state and version metadata. @@ -13962,6 +14250,9 @@ pub struct QueuePendingItems { pub id: String, /// Whether this item is a queued user message or a queued slash command / model change pub kind: QueuePendingItemsKind, + /// Stable identity of the queued user message. Present for message rows and absent for slash commands and model changes. + #[serde(skip_serializing_if = "Option::is_none")] + pub message_id: Option, } /// Snapshot of the session's pending queued items and immediate-steering messages. @@ -14668,7 +14959,7 @@ pub struct SandboxConfigUserPolicyNetworkProxy { /// Optional password for proxy authentication, combined with the URL at spawn time. The persisted value may be a literal password, a `${secret:…}` reference resolved from the OS keychain, or a `${VAR}`/`$VAR` environment reference; it is resolved just before the sandboxed process routes through the proxy. The /sandbox dialog stores a real password in the OS keychain and persists only a `${secret:…}` placeholder (never plaintext in settings.json); the field is masked in the dialog and redacted by /settings show. #[serde(skip_serializing_if = "Option::is_none")] pub password: Option, - /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. + /// Proxy URL (e.g. http://proxy.example.com:8080). The port is optional and defaults to the scheme's standard port when omitted; an explicit port must be between 1 and 65535. Credentials must not be embedded here — a `user:pass@` authority is rejected; put them in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; loopback covers localhost and any *.localhost subdomain, the whole 127.0.0.0/8 range, ::1, and IPv4-mapped loopback (::ffff:127.0.0.1). An https:// URL, or one with a username/password set, is used as-is. pub url: String, /// Optional username for proxy authentication. Combined with the URL (and `password`) into `user:pass@host` when the sandboxed process routes through the proxy. #[serde(skip_serializing_if = "Option::is_none")] @@ -14692,7 +14983,7 @@ pub struct SandboxConfigUserPolicyNetwork { /// Whether outbound network traffic is allowed at all. #[serde(skip_serializing_if = "Option::is_none")] pub allow_outbound: Option, - /// HTTP proxy the sandboxed process routes traffic through. Enforced on Windows and cooperative (honored by well-behaved tools, not strictly enforced) on Linux and macOS. Credentials go in the separate `username`/`password` fields. A credential-free http:// loopback proxy URL is routed through the localhost proxy automatically; an https:// or authenticated loopback URL is used as-is. + /// HTTP proxy for sandboxed process traffic. Linux restricts egress to the proxy endpoint, requires that endpoint to be reachable over IPv4 (the `[::]` dual-stack wildcard is accepted and routed through the IPv4 gateway), and does not support proxy credentials. macOS relies on applications honoring proxy environment variables. Windows also configures a per-AppContainer WinHTTP proxy, but enforcement depends on the application's networking stack. Configure supported credentials in the separate `username` and `password` fields. A credential-free http:// loopback URL uses the localhost proxy form, while an https:// or authenticated loopback URL uses the URL form. #[serde(skip_serializing_if = "Option::is_none")] pub proxy: Option, } @@ -14752,6 +15043,9 @@ pub struct SandboxConfig { /// Whether to auto-add the current working directory to readwritePaths. Default: true. #[serde(skip_serializing_if = "Option::is_none")] pub add_current_working_directory: Option, + /// Whether the agent may request that an individual command run outside the sandbox, which the host then approves or denies through the usual permission flow. A host capability flag rather than part of the policy: it is stripped from the effective spawn policy and only has an effect while `enabled` is true. Fail-closed, unlike the opt-out flags on this object: omitting it offers no bypass. Default: false (opt-in). + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_bypass: Option, /// Whether to auto-grant read access to tool directories discovered on PATH and in toolchain environment variables (GOROOT, JAVA_HOME, VIRTUAL_ENV, and similar), and to common developer-tool caches, config, and toolchains. Writable grants cover scratch caches, the Unix GitHub CLI cache, and Cargo's registry, git store, and lock/tracker files. A relocated CARGO_HOME gets the same narrow split: registry and git are read-write; bin is read-only; the home root, config.toml, and credentials.toml stay ungranted. Set to false to disable every grant listed above; user-installed toolchains and caches then need explicit userPolicy.filesystem readonlyPaths and readwritePaths entries. The working directory (see addCurrentWorkingDirectory), temporary storage, session log paths, and system locations follow their own rules and stay granted. Default: true (enabled by default; set to false to opt out). #[serde(skip_serializing_if = "Option::is_none")] pub allow_dev_tool_access: Option, @@ -14760,6 +15054,20 @@ pub struct SandboxConfig { pub auth: Option, /// Whether sandboxing is enabled for the session. pub enabled: bool, + /// The `sandboxLspServers` counterpart of `managedMcpRoutingLocked`. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) managed_lsp_routing_locked: Option, + /// Set by the runtime when a managed policy forced `sandboxMcpServers` on and took the local opt-out away. Provenance rather than policy: it lets a sandbox startup failure point at the administrator instead of a setting the next managed merge would override, and it is ignored when comparing two configs for change. Only the managed merge may set it; a caller-supplied value is stripped. + #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) managed_mcp_routing_locked: Option, + /// Whether language servers the session launches are confined by the sandbox. Only an explicit `false` opts out. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_lsp_servers: Option, + /// Whether MCP servers the session launches are confined by the sandbox. Only an explicit `false` opts out; doing so also lets remote-MCP egress leave the sandbox, so the flag and `enabled` are always read together. Ignored while `enabled` is false. Default: true (enabled by default; set to false to opt out). + #[serde(skip_serializing_if = "Option::is_none")] + pub sandbox_mcp_servers: Option, /// User-managed sandbox policy fragment merged into the auto-discovered base policy. #[serde(skip_serializing_if = "Option::is_none")] pub user_policy: Option, @@ -16719,6 +17027,9 @@ pub struct SessionOpenOptions { /// Whether ask_user is explicitly disabled. #[serde(skip_serializing_if = "Option::is_none")] pub ask_user_disabled: Option, + /// OAuth Client ID Metadata Document URL used by this host for MCP authorization. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_client_id_metadata_url: Option, /// Initial authentication info for the session. #[serde(skip_serializing_if = "Option::is_none")] pub auth_info: Option, @@ -16786,6 +17097,9 @@ pub struct SessionOpenOptions { /// Whether shell-script safety heuristics are enabled. #[serde(skip_serializing_if = "Option::is_none")] pub enable_script_safety: Option, + /// Whether skill loading is enabled. When omitted, an SDK skill provider enables skills by default. + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_skills: Option, /// Whether model responses stream as delta events. #[serde(skip_serializing_if = "Option::is_none")] pub enable_streaming: Option, @@ -16811,6 +17125,17 @@ pub struct SessionOpenOptions { /// Feature-flag values resolved by the host. #[serde(skip_serializing_if = "Option::is_none")] pub feature_flags: Option>, + /// Whether the requesting SDK session has a skill provider. The provider remains ephemeral and is never persisted in session options or history. When enableSkills is false, it remains bound but dormant and receives no callbacks. Cloud, relay, handoff, and raw sessions.open flows reject it because they cannot safely pre-register the callback handler. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[doc(hidden)] + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) has_skill_provider: Option, /// Built-in subagent names to include in this session. When specified, only these built-ins are available, subject to runtime availability and exclusions. Custom agents with the same name remain available. #[serde(skip_serializing_if = "Option::is_none")] pub included_builtin_agents: Option>, @@ -17957,6 +18282,30 @@ pub struct SessionsPruneOldRequest { pub older_than_days: i64, } +/// Pagination options for reading an inactive or active local session's persisted event journal. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsReadPersistedEventsRequest { + /// Opaque cursor returned by a previous persisted-event read. Omit on the first call. + #[serde(skip_serializing_if = "Option::is_none")] + pub cursor: Option, + /// Direction to page through persisted history. Forward starts at the beginning; backward starts with the newest events. Events in each page remain chronological. + #[serde(skip_serializing_if = "Option::is_none")] + pub direction: Option, + /// Maximum number of events to return in this batch (1–1000, default 200). + #[serde(skip_serializing_if = "Option::is_none")] + pub max: Option, + /// Session ID whose persisted event journal should be read. + pub session_id: SessionId, +} + /// Session ID whose in-use lock should be released. /// ///
@@ -18232,7 +18581,7 @@ pub struct SessionUpdateOptionsParams { /// Whether to enable cross-session store writes and reads. #[serde(skip_serializing_if = "Option::is_none")] pub enable_session_store: Option, - /// Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. + /// Whether skill loading is enabled. Explicit false disables every source, including a bound SDK provider; changing the value invalidates the loaded skill snapshot. When omitted, creation falls back to enableConfigDiscovery unless an SDK skill provider is registered. #[serde(skip_serializing_if = "Option::is_none")] pub enable_skills: Option, /// Whether to stream model responses. @@ -18579,6 +18928,79 @@ pub struct SkillList { pub skills: Vec, } +/// Catalog-only metadata for one SDK-provided skill. The complete SKILL.md is fetched separately and lazily. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillProviderDescriptor { + /// Optional freeform argument hint used by slash-command catalogs. + #[serde(skip_serializing_if = "Option::is_none")] + pub argument_hint: Option, + /// Description used in skill catalogs without fetching content. + pub description: String, + /// Whether model invocation is disabled. Defaults to false. + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_model_invocation: Option, + /// Invocation and display name. + pub name: String, + /// Whether users may invoke the skill directly. Defaults to true. + #[serde(skip_serializing_if = "Option::is_none")] + pub user_invocable: Option, +} + +/// Catalog metadata returned by an SDK session's skill provider. Catalogs are limited to 1024 descriptors and 1 MiB of aggregate metadata. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SkillProviderListResult { + /// Skill descriptors in provider order. Invocation names must be unique under case-insensitive comparison. + pub skills: Vec, +} + +/// Identifies one SDK-provided skill by invocation name. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SkillProviderReadRequest { + /// Target session identifier + pub session_id: SessionId, + /// Invocation name of the skill to read. + pub name: String, +} + +/// Complete text-only SKILL.md content returned by an SDK session's skill provider. Related files and assets are not supported. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct SkillProviderReadResult { + /// Complete SKILL.md text. The runtime enforces a 1 MiB UTF-8 byte limit. + pub markdown: String, +} + /// Skill names to mark as disabled in global configuration, replacing any previous list. /// ///
@@ -18698,11 +19120,14 @@ pub struct SkillsInvokedSkill { pub allowed_tools: Option>, /// Full content of the skill file pub content: String, + /// Whether model invocation was disabled when this skill was invoked + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_model_invocation: Option, /// Turn number when the skill was invoked pub invoked_at_turn: i64, /// Unique identifier for the skill pub name: String, - /// Path to the SKILL.md file + /// Path to the SKILL.md file, or an empty string for an SDK-provided skill without a filesystem identity pub path: String, } @@ -18748,6 +19173,9 @@ pub struct SkillsLoadDiagnostics { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SlashCommandTimelineEntry { + /// What the user must do to recover, when the entry reports a failure the runtime knows an action for. The `text` never names a client affordance, so a client that offers one renders it from this value. + #[serde(skip_serializing_if = "Option::is_none")] + pub remediation: Option, /// Text displayed for the timeline entry. pub text: String, /// Timeline entry presentation type. @@ -18823,6 +19251,9 @@ pub struct SlashCommandCompletedResult { /// Optional user-facing message describing the completed command #[serde(skip_serializing_if = "Option::is_none")] pub message: Option, + /// Optional target session mode applied without submitting an agent prompt + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, /// True when the invocation mutated user runtime settings; consumers caching settings should refresh #[serde(skip_serializing_if = "Option::is_none")] pub runtime_settings_changed: Option, @@ -19016,6 +19447,9 @@ pub struct SubagentSettingsEntry { /// Model override for matching subagents #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Whether the configured model strategy is preferred or required + #[serde(skip_serializing_if = "Option::is_none")] + pub model_policy: Option, } /// Subagent settings to apply, or null to clear the live session override @@ -19145,6 +19579,199 @@ pub struct TaskAgentProgress { pub r#type: TaskAgentProgressType, } +/// Public owner attribution for a client-owned task. Identifiers are opaque and never authorize requests. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientOwner { + /// ISO 8601 timestamp when the bound join disconnected + #[serde(skip_serializing_if = "Option::is_none")] + pub disconnected_at: Option, + /// Display-only owner name + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Opaque identity of the currently or most recently bound session join + pub join_id: String, + /// Class of the task owner + pub kind: TaskClientOwnerKind, + /// Opaque session-scoped participant identity + pub participant_id: String, + /// Whether this task's bound join is currently connected + pub presence: TaskClientOwnerPresence, + /// Display-only owner source + #[serde(skip_serializing_if = "Option::is_none")] + pub source: Option, +} + +/// Tracked client-owned task metadata. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientInfo { + /// ISO 8601 timestamp when the current active segment started + #[serde(skip_serializing_if = "Option::is_none")] + pub active_started_at: Option, + /// Accumulated active execution time in milliseconds + pub active_time_ms: i64, + /// Whether the currently bound owner can receive a cancellation request + pub can_cancel: bool, + /// Human-readable reason for terminal cancellation + #[serde(skip_serializing_if = "Option::is_none")] + pub cancellation_reason: Option, + /// Owner-scoped registration and reclaim key + pub client_task_id: String, + /// ISO 8601 timestamp when the task reached a terminal status + #[serde(skip_serializing_if = "Option::is_none")] + pub completed_at: Option, + /// Task description + pub description: String, + /// Optional task display name + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Human-readable terminal failure message + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + /// Optional owner-supplied terminal failure code + #[serde(skip_serializing_if = "Option::is_none")] + pub error_code: Option, + /// Execution mode, which is always background for client-owned tasks + pub execution_mode: TaskClientExecutionMode, + /// Canonical runtime-generated task identifier + pub id: String, + /// ISO 8601 timestamp when the connected owner entered idle status + #[serde(skip_serializing_if = "Option::is_none")] + pub idle_since: Option, + /// ISO 8601 timestamp of the most recent orphan transition + #[serde(skip_serializing_if = "Option::is_none")] + pub orphaned_at: Option, + /// Public attribution and presence for the task owner + pub owner: TaskClientOwner, + /// ISO 8601 timestamp of the most recent successful reclaim + #[serde(skip_serializing_if = "Option::is_none")] + pub reclaimed_at: Option, + /// Opaque successful terminal result supplied by the task owner + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, + /// Sequence number of the latest accepted owner update + pub sequence: i64, + /// ISO 8601 timestamp when the task started + pub started_at: String, + /// Client task lifecycle status + pub status: TaskClientStatus, + /// Task kind + pub r#type: TaskClientType, + /// ISO 8601 timestamp of the latest accepted lifecycle change + pub updated_at: String, +} + +/// Generic progress for a client-owned task. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientProgress { + /// Most recent nonempty progress message + #[serde(skip_serializing_if = "Option::is_none")] + pub last_message: Option, + /// Current completion percentage from zero through one hundred + #[serde(skip_serializing_if = "Option::is_none")] + pub percentage: Option, + /// Current owner-defined progress phase + #[serde(skip_serializing_if = "Option::is_none")] + pub phase: Option, + /// Recent server-timestamped progress messages + pub recent_activity: Vec, + /// Sequence number of the latest accepted owner update + pub sequence: i64, + /// Current client task lifecycle status + pub status: TaskClientStatus, + /// Progress kind + pub r#type: TaskClientType, + /// ISO 8601 timestamp of the latest accepted lifecycle change + pub updated_at: String, +} + +/// Publishes nonterminal progress for a running or idle client task. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientUpdateProgress { + /// Client task update variant discriminator. + pub kind: TaskClientUpdateProgressKind, + /// Optional progress message appended to recent activity when nonempty + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Optional completion percentage; null clears the current percentage + #[serde(skip_serializing_if = "Option::is_none")] + pub percentage: Option, + /// Optional progress phase; null clears the current phase + #[serde(skip_serializing_if = "Option::is_none")] + pub phase: Option, + /// Optional active status transition + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option, +} + +/// Reports successful terminal completion. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientUpdateCompleted { + /// Client task update variant discriminator. + pub kind: TaskClientUpdateCompletedKind, + /// Optional final progress message + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Optional opaque successful terminal result + #[serde(skip_serializing_if = "Option::is_none")] + pub result: Option, +} + +/// Reports terminal failure. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientUpdateFailed { + /// Optional owner-supplied terminal failure code + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + /// Human-readable terminal failure message + pub error: String, + /// Client task update variant discriminator. + pub kind: TaskClientUpdateFailedKind, + /// Optional final progress message + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +/// Reports terminal cancellation after external work stopped. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskClientUpdateCancelled { + /// Client task update variant discriminator. + pub kind: TaskClientUpdateCancelledKind, + /// Optional final progress message + #[serde(skip_serializing_if = "Option::is_none")] + pub message: Option, + /// Optional human-readable cancellation reason + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + /// Task completion notification with summary from the agent /// ///
@@ -19293,7 +19920,7 @@ pub struct TasksGetProgressRequest { #[serde(rename_all = "camelCase")] pub struct TasksGetProgressResult { /// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. - pub progress: Option, + pub progress: serde_json::Value, } /// Tracked shell task metadata, including ID, command, status, timing, attachment/execution mode, log path, and PID. @@ -19416,6 +20043,52 @@ pub struct TasksPromoteToBackgroundResult { #[serde(rename_all = "camelCase")] pub struct TasksRefreshResult {} +/// Registers or reclaims a client-owned task. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TasksRegisterRequest { + /// Whether the owner supports runtime cancellation requests + pub cancellable: bool, + /// Owner-scoped idempotency key used for registration and reclaim + pub client_task_id: String, + /// Human-readable description of the external work + pub description: String, + /// Optional short display name for the external work + #[serde(skip_serializing_if = "Option::is_none")] + pub display_name: Option, + /// Expected current sequence for idempotent registration or orphan reclaim + #[serde(skip_serializing_if = "Option::is_none")] + pub expected_sequence: Option, + /// Task kind + pub r#type: TaskClientType, +} + +/// Result of registering or reclaiming a client-owned task. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TasksRegisterResult { + /// True only when this invocation created a new task + pub created: bool, + /// True only when this invocation reclaimed an orphaned task + pub reclaimed: bool, + /// Authoritative registered or reclaimed task + pub task: TaskClientInfo, +} + /// Identifier of the completed or cancelled task to remove from tracking. /// ///
@@ -19524,6 +20197,44 @@ pub struct TasksStartAgentResult { pub agent_id: String, } +/// Updates a client-owned task. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TasksUpdateRequest { + /// Canonical runtime-generated task identifier + pub id: String, + /// Owner update sequence to apply + pub sequence: i64, + /// Progress or terminal update payload + pub update: TaskClientUpdate, +} + +/// Result of publishing a client-owned task update. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TasksUpdateResult { + /// Whether this invocation changed task state + pub applied: bool, + /// Whether this invocation repeated the latest accepted update + pub duplicate: bool, + /// Authoritative task after processing the update + pub task: TaskClientInfo, +} + /// Wait until all in-flight background tasks (agents + shells) and any follow-up turns scheduled by their completions have settled. Returns when the runtime is fully drained or after an internal timeout (default 10 minutes; configurable via COPILOT_TASK_WAIT_TIMEOUT_SECONDS). /// ///
@@ -21490,6 +22201,9 @@ pub struct PluginsInstallResult { pub post_install_message: Option, /// Number of skills discovered and installed from the plugin pub skills_installed: i64, + /// Where the completed plugin tree was staged before atomic promotion + #[serde(skip_serializing_if = "Option::is_none")] + pub staging_mode: Option, } /// Result of updating a single plugin. @@ -21780,6 +22494,27 @@ pub struct SessionsListResult { pub sessions: Vec, } +/// Batch of session events returned by a read, with cursor and continuation metadata. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionsReadPersistedEventsResult { + /// Opaque cursor for the next read. Pass back unchanged in the next read.cursor to continue from where this read left off. Always present, even when no events were returned. For a backward read this cursor pages toward OLDER events; keep passing `direction: backward` with it (the cursor is also self-describing, so backward paging continues correctly). + pub cursor: String, + /// Cursor status: 'ok' means the cursor was applied successfully; 'expired' means the cursor referred to an event that no longer exists in history (e.g. truncated or compacted away) and the read fell back to a boundary of the remaining history. For a forward read the fallback starts from the beginning of the remaining history; for a backward read it falls back to the tail (the newest window). Because the fallback page is a fresh boundary snapshot rather than a continuation of the requested cursor, it may overlap events the consumer has already rendered — a backward fallback to the tail in particular can repeat the newest window. On 'expired', consumers should reset or rebase their local pagination state (or deduplicate by event id) before continuing from the returned cursor rather than blindly appending/prepending the fallback page. + pub cursor_status: EventsCursorStatus, + /// Session events for this batch, merged into a single stream in creation order: durable (persisted) events and ephemeral events interleave exactly as they were emitted. Set `includeEphemeral: false` to receive only durable events. Ephemeral events are never replayable once pruned from the in-memory ring, so a consumer that needs them should keep reading with a non-zero `waitMs`. For a backward (tail-first) read, the returned window contains persisted events only, still in chronological (oldest-to-newest) append order. + pub events: Vec, + /// True when more events are available in the read's direction. For a forward read, true means the batch returned `max` events and more are available immediately. For a backward read, true means older persisted events remain before the returned window. + pub has_more: bool, +} + /// ID of the local session bound to the given GitHub task, or omitted when none. /// ///
@@ -22388,10 +23123,13 @@ pub struct SessionCanvasActionInvokeResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -22437,10 +23175,13 @@ pub struct SessionFactoryResumeResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryRunFromToolResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -22486,10 +23227,13 @@ pub struct SessionFactoryResumeFromToolResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryGetRunResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -22627,10 +23371,13 @@ pub struct SessionFactoryGetRunProgressResult { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionFactoryCancelResult { + /// One-based execution attempt represented by this envelope. Absent before the first attempt starts or when returned by an older runtime. + #[serde(skip_serializing_if = "Option::is_none")] + pub attempt: Option, /// Error message for an errored run. #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Machine-readable failure details for an errored run. + /// Machine-readable failure details for a halted or errored run. #[serde(skip_serializing_if = "Option::is_none")] pub failure: Option, /// Reason for a halted or cancelled run. @@ -22721,7 +23468,7 @@ pub struct SessionModelGetCurrentParams { pub session_id: SessionId, } -/// The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. +/// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. /// ///
/// @@ -22732,12 +23479,21 @@ pub struct SessionModelGetCurrentParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionModelGetCurrentResult { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub activating_auto_tier: Option, + /// Auto preference currently committed for the session. This can remain available while another model is selected so a later switch to `auto` can reuse it. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Context tier for models that support multiple context-window sizes. #[serde(skip_serializing_if = "Option::is_none")] pub context_tier: Option, /// Currently active model identifier #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// Latest unclaimed Auto preference waiting for a future user turn. Null means the pending request is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_auto_tier: Option, /// Reasoning effort level currently applied to the active model, when one is set. Reads `Session.getReasoningEffort()` synchronously after `getSelectedModel()` resolves so the two values are reported as a snapshot. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_effort: Option, @@ -22769,6 +23525,9 @@ pub struct SessionModelSwitchToResult { /// Currently active model identifier after the switch #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_state: Option, /// Persistence failure encountered after applying the model switch. #[serde(skip_serializing_if = "Option::is_none")] pub persistence_error: Option, @@ -22780,6 +23539,33 @@ pub struct SessionModelSwitchToResult { pub warning: Option, } +/// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionModelSwitchAutoTierResult { + /// Auto preference currently claimed by an in-progress activation. Null means the activation is returning to provider-default routing. + #[serde(skip_serializing_if = "Option::is_none")] + pub activating_auto_tier: Option, + /// Auto preference currently committed for the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub effective_auto_tier: Option, + /// Latest unclaimed Auto preference waiting for a future user turn. + #[serde(skip_serializing_if = "Option::is_none")] + pub pending_auto_tier: Option, + /// Immediate request status. `pending` means accepted but not committed. + pub status: ModelSwitchAutoTierStatus, + /// Earlier unclaimed preference replaced by this request. This can be present with either status, including when selecting the effective preference cancels pending work. + #[serde(skip_serializing_if = "Option::is_none")] + pub superseded_auto_tier: Option, +} + /// The model identifier active on the session after the switch. /// ///
@@ -22806,6 +23592,9 @@ pub struct SessionModelApplyStartupOverlayResult { /// Currently active model identifier after the switch #[serde(skip_serializing_if = "Option::is_none")] pub model_id: Option, + /// Authoritative model and Auto preference state after an immediate switch. For deferred switches this remains the current state until the queued change drains. + #[serde(skip_serializing_if = "Option::is_none")] + pub model_state: Option, /// Persistence failure encountered after applying the model switch. #[serde(skip_serializing_if = "Option::is_none")] pub persistence_error: Option, @@ -23645,6 +24434,36 @@ pub struct SessionWorkspacesDiffResult { pub unavailable_reason: Option, } +/// Identifies the target session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionAutopilotObjectiveGetStateParams { + /// Target session identifier + pub session_id: SessionId, +} + +/// Canonical runtime state for the session's current autopilot objective. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionAutopilotObjectiveGetStateResult { + /// Current objective state, or `null` when the session has no objective. + pub state: Option, +} + /// Identifies the target session. /// ///
@@ -23885,6 +24704,44 @@ pub struct SessionTasksListResult { pub tasks: Vec, } +/// Result of registering or reclaiming a client-owned task. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionTasksRegisterResult { + /// True only when this invocation created a new task + pub created: bool, + /// True only when this invocation reclaimed an orphaned task + pub reclaimed: bool, + /// Authoritative registered or reclaimed task + pub task: TaskClientInfo, +} + +/// Result of publishing a client-owned task update. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionTasksUpdateResult { + /// Whether this invocation changed task state + pub applied: bool, + /// Whether this invocation repeated the latest accepted update + pub duplicate: bool, + /// Authoritative task after processing the update + pub task: TaskClientInfo, +} + /// Identifies the target session. /// ///
@@ -23951,7 +24808,7 @@ pub struct SessionTasksWaitForPendingResult {} #[serde(rename_all = "camelCase")] pub struct SessionTasksGetProgressResult { /// Progress information for the task, discriminated by type. Returns null when no task with this ID is currently tracked. - pub progress: Option, + pub progress: serde_json::Value, } /// Identifies the target session. @@ -27012,6 +27869,21 @@ pub struct SessionScheduleStopResult { pub entry: Option, } +/// Identifies the target session. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillProviderListParams { + /// Target session identifier + pub session_id: SessionId, +} + /// A bearer token supplied by the SDK client for a BYOK provider. The runtime sets it as `Authorization: Bearer ` on the outbound request and does no caching; the SDK consumer owns token caching and refresh. /// ///
@@ -27839,6 +28711,31 @@ pub enum AuthInfoType { Unknown, } +/// Current normalized autopilot objective lifecycle status. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AutopilotObjectiveStatus { + /// The objective is actively running. + #[serde(rename = "active")] + Active, + /// The objective is paused and may be resumed. + #[serde(rename = "paused")] + Paused, + /// The objective completed. + #[serde(rename = "completed")] + Completed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Custom input-format kind. /// ///
@@ -28378,7 +29275,16 @@ pub enum CatalogNetworkFailureReason { /// The connection was refused or reset. #[serde(rename = "connection-refused")] ConnectionRefused, - /// The authority returned a status the runtime treats as a failure. + /// The configured proxy returned 407 and requires authentication. + #[serde(rename = "proxy-authentication-required")] + ProxyAuthenticationRequired, + /// The authority rate-limited requests and supplied or implied a bounded cooldown. + #[serde(rename = "rate-limited")] + RateLimited, + /// The authority returned a transient 5xx response. + #[serde(rename = "service-unavailable")] + ServiceUnavailable, + /// The authority returned another status the runtime treats as a failure. #[serde(rename = "http-status")] HttpStatus, /// The response exceeded the permitted size. @@ -28613,6 +29519,28 @@ pub enum CatalogUnavailableTransportReason { Unknown, } +/// Why the runtime requests client-task cancellation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ClientTaskCancelReason { + /// A caller requested task cancellation. + #[serde(rename = "cancel_requested")] + CancelRequested, + /// The session is shutting down. + #[serde(rename = "session_shutdown")] + SessionShutdown, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Optional completion hint for the input (e.g. 'directory' for filesystem path completion) /// ///
@@ -28718,6 +29646,31 @@ pub enum ConnectedRemoteSessionMetadataKind { Unknown, } +/// Closed set of public task kinds a connection can negotiate. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskKind { + /// Runtime-owned background agent task. + #[serde(rename = "agent")] + Agent, + /// Runtime-owned shell task. + #[serde(rename = "shell")] + Shell, + /// Client-owned externally executed task. + #[serde(rename = "client")] + Client, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Controls how MCP tool result content is filtered: none leaves content unchanged, markdown sanitizes HTML while preserving Markdown-friendly output, and hidden_characters removes characters that can hide directives. /// ///
@@ -28915,6 +29868,101 @@ pub enum DiscoveredExtensionMode { Unknown, } +/// Hook event name. Discovery emits the file-configurable subset; SDK callbacks additionally support callback-only events. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HookType { + /// Runs before a tool is invoked. + #[serde(rename = "preToolUse")] + PreToolUse, + /// Runs before an MCP tool is invoked. + #[serde(rename = "preMcpToolCall")] + PreMcpToolCall, + /// Runs after a tool completes successfully. + #[serde(rename = "postToolUse")] + PostToolUse, + /// Runs after a tool fails. + #[serde(rename = "postToolUseFailure")] + PostToolUseFailure, + /// Runs after the user submits a prompt. + #[serde(rename = "userPromptSubmitted")] + UserPromptSubmitted, + /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. + #[serde(rename = "userPromptTransformed")] + UserPromptTransformed, + /// Runs when a session starts. + #[serde(rename = "sessionStart")] + SessionStart, + /// Runs when a session ends. + #[serde(rename = "sessionEnd")] + SessionEnd, + /// Runs after an agent result is produced. + #[serde(rename = "postResult")] + PostResult, + /// Runs before a pull request description is generated. + #[serde(rename = "prePRDescription")] + PrePRDescription, + /// Runs when the agent encounters an error. + #[serde(rename = "errorOccurred")] + ErrorOccurred, + /// Runs when the agent stops. + #[serde(rename = "agentStop")] + AgentStop, + /// Runs when a subagent starts. + #[serde(rename = "subagentStart")] + SubagentStart, + /// Runs when a subagent stops. + #[serde(rename = "subagentStop")] + SubagentStop, + /// Runs before conversation context is compacted. + #[serde(rename = "preCompact")] + PreCompact, + /// Runs when the agent requests permission. + #[serde(rename = "permissionRequest")] + PermissionRequest, + /// Runs when the agent emits a notification. + #[serde(rename = "notification")] + Notification, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Configuration tier that contributed a discovered hook action. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum HookOrigin { + /// Hook loaded from user settings or the user's hook directory. + #[serde(rename = "user")] + User, + /// Hook loaded from repository settings or the repository hook directory. + #[serde(rename = "repository")] + Repository, + /// Hook provided by an enabled installed or explicit plugin. Projectless rows omit projectPath and do not expand a project directory. + #[serde(rename = "plugin")] + Plugin, + /// Hook enforced by centrally managed policy. + #[serde(rename = "policy")] + Policy, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Server transport type: stdio, http, sse (deprecated), or memory /// ///
@@ -29529,66 +30577,6 @@ pub enum HistoryRewindOutcome { Unknown, } -/// Hook event name dispatched through the SDK callback transport. -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum HookType { - /// Runs before a tool is invoked. - #[serde(rename = "preToolUse")] - PreToolUse, - /// Runs before an MCP tool is invoked. - #[serde(rename = "preMcpToolCall")] - PreMcpToolCall, - /// Runs after a tool completes successfully. - #[serde(rename = "postToolUse")] - PostToolUse, - /// Runs after a tool fails. - #[serde(rename = "postToolUseFailure")] - PostToolUseFailure, - /// Runs after the user submits a prompt. - #[serde(rename = "userPromptSubmitted")] - UserPromptSubmitted, - /// Runs after the runtime transforms the submitted prompt for the model, before it is added to session history. - #[serde(rename = "userPromptTransformed")] - UserPromptTransformed, - /// Runs when a session starts. - #[serde(rename = "sessionStart")] - SessionStart, - /// Runs when a session ends. - #[serde(rename = "sessionEnd")] - SessionEnd, - /// Runs after an agent result is produced. - #[serde(rename = "postResult")] - PostResult, - /// Runs before a pull request description is generated. - #[serde(rename = "prePRDescription")] - PrePRDescription, - /// Runs when the agent encounters an error. - #[serde(rename = "errorOccurred")] - ErrorOccurred, - /// Runs when the agent stops. - #[serde(rename = "agentStop")] - AgentStop, - /// Runs when a subagent starts. - #[serde(rename = "subagentStart")] - SubagentStart, - /// Runs when a subagent stops. - #[serde(rename = "subagentStop")] - SubagentStop, - /// Runs before conversation context is compacted. - #[serde(rename = "preCompact")] - PreCompact, - /// Runs when the agent requests permission. - #[serde(rename = "permissionRequest")] - PermissionRequest, - /// Runs when the agent emits a notification. - #[serde(rename = "notification")] - Notification, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - /// Constant value. Always "github". #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum InstalledPluginSourceGitHubSource { @@ -30901,6 +31889,28 @@ pub enum ModelPolicyState { Unknown, } +/// Whether the requested preference was already effective or was accepted for later transactional activation. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ModelSwitchAutoTierStatus { + /// The requested preference is already effective. No activation is pending for it, although this request may have cancelled an earlier unclaimed preference reported in `supersededAutoTier`. + #[serde(rename = "unchanged")] + Unchanged, + /// The request was accepted but has not committed. A later user turn using the `auto` model must mint and validate the replacement before it becomes effective. + #[serde(rename = "pending")] + Pending, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Provider transport. Defaults to "http". /// ///
@@ -31808,6 +32818,28 @@ pub enum PermissionsSetApproveAllSource { Unknown, } +/// Where completed plugin content was staged before atomic promotion. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum PluginInstallStagingMode { + /// A sibling of the installed-plugins root, outside the recursively watched tree. + #[serde(rename = "external")] + External, + /// A sibling of the destination plugin directory, used when external staging is unavailable. + #[serde(rename = "destination_sibling")] + DestinationSibling, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Controls whether the runtime may defer loading an external tool definition. /// ///
@@ -33264,6 +34296,191 @@ pub enum TaskAgentProgressType { Agent, } +/// Active status a client owner may publish with a progress update. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientActiveStatus { + /// The external owner is actively working. + #[serde(rename = "running")] + Running, + /// The external owner is connected but waiting. + #[serde(rename = "idle")] + Idle, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Client-owned tasks always execute outside the runtime in background mode. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientExecutionMode { + #[serde(rename = "background")] + Background, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Connection class owning a client task. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientOwnerKind { + /// A discovered extension connection owns the task. + #[serde(rename = "extension")] + Extension, + /// A generic SDK connection owns the task. + #[serde(rename = "sdk")] + Sdk, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Presence of the task's bound join. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientOwnerPresence { + /// The bound session join is connected. + #[serde(rename = "connected")] + Connected, + /// The bound session join is disconnected. + #[serde(rename = "disconnected")] + Disconnected, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Lifecycle status of a client-owned task. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientStatus { + /// The external owner is actively working. + #[serde(rename = "running")] + Running, + /// The external owner is connected but waiting. + #[serde(rename = "idle")] + Idle, + /// The owner reported successful completion. + #[serde(rename = "completed")] + Completed, + /// The owner reported failure. + #[serde(rename = "failed")] + Failed, + /// The owner reported or confirmed cancellation. + #[serde(rename = "cancelled")] + Cancelled, + /// The bound owner join disappeared; external executor state is unknown. + #[serde(rename = "orphaned")] + Orphaned, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Discriminator for a client-owned task. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientType { + #[serde(rename = "client")] + Client, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Client task update variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientUpdateProgressKind { + #[serde(rename = "progress")] + #[default] + Progress, +} + +/// Client task update variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientUpdateCompletedKind { + #[serde(rename = "completed")] + #[default] + Completed, +} + +/// Client task update variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientUpdateFailedKind { + #[serde(rename = "failed")] + #[default] + Failed, +} + +/// Client task update variant discriminator. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum TaskClientUpdateCancelledKind { + #[serde(rename = "cancelled")] + #[default] + Cancelled, +} + +/// Progress or terminal update for a client-owned task. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum TaskClientUpdate { + Progress(TaskClientUpdateProgress), + Completed(TaskClientUpdateCompleted), + Failed(TaskClientUpdateFailed), + Cancelled(TaskClientUpdateCancelled), +} + /// Whether the shell runs inside a managed PTY session or as an independent background process /// ///
diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 6f5895d856..50eee0f1fb 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -64,6 +64,13 @@ impl<'a> ClientRpc<'a> { } } + /// `hooks.*` sub-namespace. + pub fn hooks(&self) -> ClientRpcHooks<'a> { + ClientRpcHooks { + client: self.client, + } + } + /// `instructions.*` sub-namespace. pub fn instructions(&self) -> ClientRpcInstructions<'a> { ClientRpcInstructions { @@ -656,6 +663,45 @@ impl<'a> ClientRpcExtensions<'a> { } } +/// `hooks.*` RPCs. +#[derive(Clone, Copy)] +pub struct ClientRpcHooks<'a> { + pub(crate) client: &'a Client, +} + +impl<'a> ClientRpcHooks<'a> { + /// Discovers hook actions enabled under server-side discovery settings from user, repository, plugin, and managed-policy sources. + /// + /// Wire method: `hooks.discover`. + /// + /// # Parameters + /// + /// * `params` - Optional project paths and host-exclusion behavior for server-scoped hook discovery. + /// + /// # Returns + /// + /// Server-discovered hook actions and partial-load diagnostics from user, repository, plugin, and managed-policy sources. Concrete sessions may include additional session-specific hook sources. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn discover( + &self, + params: HooksDiscoverRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::HOOKS_DISCOVER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `instructions.*` RPCs. #[derive(Clone, Copy)] pub struct ClientRpcInstructions<'a> { @@ -859,6 +905,26 @@ impl<'a> ClientRpcManagedSettings<'a> { .await?; Ok(serde_json::from_value(_value)?) } + + /// Force-refreshes enterprise managed settings for every account: wipes the persistent server-policy cache (the whole `/managed-settings` directory) and drops this runtime process's in-memory retained server policy. It does not itself fetch policy — the effect is that the next time a session resolves managed settings for an account, that resolution re-fetches the account's org policy from the network instead of serving a cached response. Note that `managedSettings.read` returns only device/MDM settings and never triggers the account server-policy fetch, so a host implementing "sync account policy" should start a fresh session resolution rather than treat a subsequent `managedSettings.read` as the refreshed org policy. Mirrors the invalidation a sign-out performs, broadened from the one signing-out account to all of them; device/MDM layers describe the machine, not the account, and are left untouched. Rejects if the on-disk cache cannot be removed. + /// + /// Wire method: `managedSettings.clearCache`. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn clear_cache(&self) -> Result<(), Error> { + let wire_params = serde_json::json!({}); + let _value = self + .client + .call(rpc_methods::MANAGEDSETTINGS_CLEARCACHE, Some(wire_params)) + .await?; + Ok(()) + } } /// `mcp.*` RPCs. @@ -1886,6 +1952,37 @@ impl<'a> ClientRpcSessions<'a> { Ok(serde_json::from_value(_value)?) } + /// Reads a page of durable events directly from a local session's persisted journal without creating, resuming, or activating the session. The initial backward read uses a bounded tail scan for fast first paint; cursor continuations preserve the session event-log paging semantics. Persisted events may omit payloads that are reconstructed only for an active session. + /// + /// Wire method: `sessions.readPersistedEvents`. + /// + /// # Parameters + /// + /// * `params` - Pagination options for reading an inactive or active local session's persisted event journal. + /// + /// # Returns + /// + /// Batch of session events returned by a read, with cursor and continuation metadata. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn read_persisted_events( + &self, + params: SessionsReadPersistedEventsRequest, + ) -> Result { + let wire_params = serde_json::to_value(params)?; + let _value = self + .client + .call(rpc_methods::SESSIONS_READPERSISTEDEVENTS, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Lists recent local session IDs that contain user-visible history, omitting housekeeping-only sessions. /// /// Wire method: `sessions.listNonEmptySessionIds`. @@ -3003,6 +3100,13 @@ impl<'a> SessionRpc<'a> { } } + /// `session.autopilotObjective.*` sub-namespace. + pub fn autopilot_objective(&self) -> SessionRpcAutopilotObjective<'a> { + SessionRpcAutopilotObjective { + session: self.session, + } + } + /// `session.canvas.*` sub-namespace. pub fn canvas(&self) -> SessionRpcCanvas<'a> { SessionRpcCanvas { @@ -3725,6 +3829,42 @@ impl<'a> SessionRpcAgent<'a> { } } +/// `session.autopilotObjective.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcAutopilotObjective<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcAutopilotObjective<'a> { + /// Reads the current canonical autopilot objective state for this session. + /// + /// Wire method: `session.autopilotObjective.getState`. + /// + /// # Returns + /// + /// Canonical runtime state for the session's current autopilot objective. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn get_state(&self) -> Result { + let wire_params = serde_json::json!({ "sessionId": self.session.id() }); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_AUTOPILOTOBJECTIVE_GETSTATE, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.canvas.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcCanvas<'a> { @@ -7357,13 +7497,13 @@ pub struct SessionRpcModel<'a> { } impl<'a> SessionRpcModel<'a> { - /// Gets the currently selected model for the session. + /// Gets the session's authoritative model snapshot, including the committed Auto preference and any newer unclaimed Auto preference waiting for a future user turn. /// /// Wire method: `session.model.getCurrent`. /// /// # Returns /// - /// The currently selected model, reasoning effort, and context tier for the session. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. + /// The session's authoritative model snapshot. Auto preference fields are configuration for the virtual `auto` model and do not change the selected model identifier. The context tier reflects `Session.getContextTier()`, restored from the session journal on resume. /// ///
/// @@ -7415,6 +7555,39 @@ impl<'a> SessionRpcModel<'a> { Ok(serde_json::from_value(_value)?) } + /// Requests an Auto preference change without changing the session's selected model. The latest unclaimed request wins; the runtime commits it only after a later prompt using the `auto` model mints a usable model and token pair. A `pending` response confirms that the request was accepted, not that it committed. Observe eventual success through `session.model_change`, failure through the ephemeral `session.auto_tier_switch_failed` event, or current unclaimed state through `session.model.getCurrent`. + /// + /// Wire method: `session.model.switchAutoTier`. + /// + /// # Parameters + /// + /// * `params` - An Auto preference request for the session. This updates Auto configuration only; it does not change the selected model to `auto`. + /// + /// # Returns + /// + /// Immediate acknowledgement and Auto preference snapshot after a switch request. This result never implies that a pending preference committed. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn switch_auto_tier( + &self, + params: ModelSwitchAutoTierRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_MODEL_SWITCHAUTOTIER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Resolves and applies organization-managed and repository model overlays. /// /// Wire method: `session.model.applyStartupOverlay`. @@ -10166,6 +10339,69 @@ impl<'a> SessionRpcTasks<'a> { Ok(serde_json::from_value(_value)?) } + /// Registers a client-owned task, or reclaims an orphaned task belonging to the same extension principal. + /// + /// Wire method: `session.tasks.register`. + /// + /// # Parameters + /// + /// * `params` - Registers or reclaims a client-owned task. + /// + /// # Returns + /// + /// Result of registering or reclaiming a client-owned task. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn register( + &self, + params: TasksRegisterRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_TASKS_REGISTER, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + + /// Publishes generic progress or a terminal outcome for a client-owned task. + /// + /// Wire method: `session.tasks.update`. + /// + /// # Parameters + /// + /// * `params` - Updates a client-owned task. + /// + /// # Returns + /// + /// Result of publishing a client-owned task update. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn update(&self, params: TasksUpdateRequest) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call(rpc_methods::SESSION_TASKS_UPDATE, Some(wire_params)) + .await?; + Ok(serde_json::from_value(_value)?) + } + /// Refreshes metadata for any detached background shells the runtime knows about. /// /// Wire method: `session.tasks.refresh`. diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index 4b04b03444..1d4cdc7e43 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -37,8 +37,12 @@ pub enum SessionEventType { SessionWarning, #[serde(rename = "session.model_change")] SessionModelChange, + #[serde(rename = "session.auto_tier_switch_failed")] + SessionAutoTierSwitchFailed, #[serde(rename = "session.mode_changed")] SessionModeChanged, + #[serde(rename = "session.mode_notice_delivered")] + SessionModeNoticeDelivered, #[serde(rename = "session.session_limits_changed")] SessionSessionLimitsChanged, /// @@ -85,6 +89,15 @@ pub enum SessionEventType { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "session.completion_receipt")] + SessionCompletionReceipt, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(rename = "session.fusion_route_started")] SessionFusionRouteStarted, /// @@ -142,6 +155,15 @@ pub enum SessionEventType { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "assistant.fusion_phase_activity")] + AssistantFusionPhaseActivity, + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(rename = "assistant.fusion_phase_completed")] AssistantFusionPhaseCompleted, /// @@ -359,6 +381,10 @@ pub enum SessionEventType { SessionMcpServersLoaded, #[serde(rename = "session.mcp_server_status_changed")] SessionMcpServerStatusChanged, + #[serde(rename = "session.mcp_server_removed")] + SessionMcpServerRemoved, + #[serde(rename = "session.mcp_server_needs_reconnect")] + SessionMcpServerNeedsReconnect, #[serde(rename = "mcp.tools.list_changed")] McpToolsListChanged, #[serde(rename = "mcp.resources.list_changed")] @@ -463,8 +489,12 @@ pub enum SessionEventData { SessionWarning(SessionWarningData), #[serde(rename = "session.model_change")] SessionModelChange(SessionModelChangeData), + #[serde(rename = "session.auto_tier_switch_failed")] + SessionAutoTierSwitchFailed(SessionAutoTierSwitchFailedData), #[serde(rename = "session.mode_changed")] SessionModeChanged(SessionModeChangedData), + #[serde(rename = "session.mode_notice_delivered")] + SessionModeNoticeDelivered(SessionModeNoticeDeliveredData), #[serde(rename = "session.session_limits_changed")] SessionSessionLimitsChanged(SessionSessionLimitsChangedData), /// @@ -511,6 +541,15 @@ pub enum SessionEventData { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "session.completion_receipt")] + SessionCompletionReceipt(SessionCompletionReceiptData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(rename = "session.fusion_route_started")] SessionFusionRouteStarted(SessionFusionRouteStartedData), /// @@ -568,6 +607,15 @@ pub enum SessionEventData { /// and may change or be removed in future SDK or CLI releases. /// ///
+ #[serde(rename = "assistant.fusion_phase_activity")] + AssistantFusionPhaseActivity(AssistantFusionPhaseActivityData), + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
#[serde(rename = "assistant.fusion_phase_completed")] AssistantFusionPhaseCompleted(AssistantFusionPhaseCompletedData), /// @@ -778,6 +826,10 @@ pub enum SessionEventData { SessionMcpServersLoaded(SessionMcpServersLoadedData), #[serde(rename = "session.mcp_server_status_changed")] SessionMcpServerStatusChanged(SessionMcpServerStatusChangedData), + #[serde(rename = "session.mcp_server_removed")] + SessionMcpServerRemoved(SessionMcpServerRemovedData), + #[serde(rename = "session.mcp_server_needs_reconnect")] + SessionMcpServerNeedsReconnect(SessionMcpServerNeedsReconnectData), #[serde(rename = "mcp.tools.list_changed")] McpToolsListChanged(McpToolsListChangedData), #[serde(rename = "mcp.resources.list_changed")] @@ -1058,6 +1110,9 @@ pub struct SessionErrorData { /// GitHub request tracing ID (x-github-request-id header) for correlating with server-side logs #[serde(skip_serializing_if = "Option::is_none")] pub provider_call_id: Option, + /// What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + #[serde(skip_serializing_if = "Option::is_none")] + pub remediation: Option, /// Copilot service request ID (x-copilot-service-request-id header) for CAPI log correlation #[serde(skip_serializing_if = "Option::is_none")] pub service_request_id: Option, @@ -1180,6 +1235,9 @@ pub struct SessionInfoData { pub struct SessionWarningData { /// Human-readable warning message for display in the timeline pub message: String, + /// What the user must do to recover, when the runtime knows of an action. The `message` never names a client affordance, so a client that offers one — a slash command, a settings pane, a link — renders it from this value. + #[serde(skip_serializing_if = "Option::is_none")] + pub remediation: Option, /// Optional URL associated with this warning that the user can open in a browser #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, @@ -1191,6 +1249,9 @@ pub struct SessionWarningData { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SessionModelChangeData { + /// Committed Auto preference after the model configuration change, when applicable. + #[serde(skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, /// Reason the change happened, when not user-initiated. `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path, or `"refusal_fallback"` when the active model declined a request (content refusal) and the runtime switched to the configured refusal-fallback model. UI clients can use this to render contextual copy. #[serde(skip_serializing_if = "Option::is_none")] pub cause: Option, @@ -1199,6 +1260,9 @@ pub struct SessionModelChangeData { pub context_tier: Option, /// Newly selected model identifier pub new_model: String, + /// Previously committed Auto preference, when one was explicitly selected. + #[serde(skip_serializing_if = "Option::is_none")] + pub previous_auto_tier: Option, /// Model that was previously selected, if any #[serde(skip_serializing_if = "Option::is_none")] pub previous_model: Option, @@ -1225,6 +1289,19 @@ pub struct SessionModelChangeData { pub verbosity: Option, } +/// Session event "session.auto_tier_switch_failed". A transient Auto preference failure emitted when the runtime cannot mint or accept a usable model and token pair. The previously effective preference remains active, so SDK clients can surface a non-blocking failure without changing their committed-tier state. This event is ephemeral and is not persisted or replayed on resume. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionAutoTierSwitchFailedData { + /// Auto preference that remains effective after the failed request. + #[serde(skip_serializing_if = "Option::is_none")] + pub effective_auto_tier: Option, + /// Low-cardinality failure outcome reported by Auto resolution. + pub reason: AutoTierSwitchFailureReason, + /// Auto preference that failed to activate, or null when returning to provider-default routing failed. + pub requested_auto_tier: Option, +} + /// Session event "session.mode_changed". Agent mode change details including previous and new modes #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1235,6 +1312,17 @@ pub struct SessionModeChangedData { pub previous_mode: SessionMode, } +/// Session event "session.mode_notice_delivered". Records that a mode transition notice reached the model so cache-stable mode tools can remain offered across resume. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionModeNoticeDeliveredData { + /// Model-visible transition notice persisted for a mid-turn delivery + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + /// Mode established by the delivered transition notice + pub mode: SessionMode, +} + /// Session event "session.session_limits_changed". Session limits update details. Null clears the limits. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1817,6 +1905,62 @@ pub struct SessionTaskCompleteData { pub summary: Option, } +/// Inclusive durable event range summarized by a completion receipt. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionReceiptEventRange { + /// Identifier of the assistant turn-end event that ends the covered exchange. Always equals the receipt's sourceEventId, so either field is a valid join key. + pub end_event_id: String, + /// Identifier of the user message that starts the covered exchange. + pub start_event_id: String, +} + +/// Final structured tool completion in the covered event range. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CompletionReceiptFinalTool { + /// Process exit code from a structured shell result, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub exit_code: Option, + /// Structured success or failure status from the tool completion event. + pub status: CompletionReceiptToolStatus, + /// Unique identifier of the completed tool call. + pub tool_call_id: String, + /// Tool name from the matching tool execution start event, when available. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_name: Option, +} + +/// Session event "session.completion_receipt". Behavior-neutral record of structured runtime facts present when an agent completion decision is accepted. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionCompletionReceiptData { + /// One-based accepted completion receipt ordinal in the durable session history. + pub attempt: i64, + /// Inclusive durable event range summarized by this receipt. + pub event_range: CompletionReceiptEventRange, + /// Number of failed structured tool completions in the covered range. + pub failed_tool_count: i64, + /// Final structured tool completion in the covered range, when one exists. + #[serde(skip_serializing_if = "Option::is_none")] + pub final_tool: Option, + /// Version of the completion receipt payload. + pub schema_version: i64, + /// Identifier of the assistant turn-end event that supplied the accepted completion boundary. This is the receipt's idempotency key, and always equals eventRange.endEventId. + pub source_event_id: String, + /// Runtime reason the completion decision was accepted. + pub stop_reason: CompletionReceiptStopReason, + /// Number of successful structured tool completions in the covered range. + pub successful_tool_count: i64, +} + /// Session event "session.fusion_route_started". Experimental transient signal that HydraFusion routing has started for an eligible turn. /// ///
@@ -1886,6 +2030,27 @@ pub struct FusionFollowUpRecommendation { pub user_turn: FusionFollowUpAction, } +/// Presentation-neutral phase planned for a HydraFusion turn. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FusionPhasePlanStep { + /// Whether the phase executes only when an earlier phase requests it. + pub conditional: bool, + /// Kind of phase that may execute. + pub kind: FusionPhaseKind, + /// Semantic role assigned to the phase. + pub role: String, + /// Conversation scope in which the phase executes. + pub scope: FusionConversationScope, +} + /// Validated HydraFusion routing capability scores. /// ///
@@ -1934,6 +2099,16 @@ pub struct SessionFusionResolvedData { pub model_universe_version: Option, /// Validated orchestration pattern selected for the turn. pub pattern: FusionPattern, + /// Presentation-neutral phase plan for clients that render workflow progress. + /// + ///
+ /// + /// **Experimental.** This type is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. + /// + ///
+ #[serde(skip_serializing_if = "Option::is_none")] + pub phase_plan: Option>, /// Version of the validated execution-plan format. #[serde(skip_serializing_if = "Option::is_none")] pub plan_version: Option, @@ -2041,6 +2216,9 @@ pub struct UserMessageData { /// True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. #[serde(skip_serializing_if = "Option::is_none")] pub is_autopilot_continuation: Option, + /// Stable identity of the logical user message, matching the ID returned by send and retained by pending queue snapshots + #[serde(skip_serializing_if = "Option::is_none")] + pub message_id: Option, /// Path-backed native document attachments that stayed on the tagged_files path flow because native upload could not read them or would exceed the request size limit #[serde(skip_serializing_if = "Option::is_none")] pub native_document_path_fallback_paths: Option>, @@ -2171,6 +2349,39 @@ pub struct AssistantFusionPhaseStartedData { pub role: String, } +/// Session event "assistant.fusion_phase_activity". Experimental content-safe activity signal for a running HydraFusion phase. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssistantFusionPhaseActivityData { + /// Kind of real activity observed. + pub activity: FusionPhaseActivityKind, + /// Conversation scope in which the phase executes. + pub conversation_scope: FusionConversationScope, + /// Identifier of the HydraFusion turn containing the phase. + pub fusion_id: String, + /// HydraFusion orchestration pattern containing the phase. + pub pattern: FusionPattern, + /// Stable identifier for the concrete phase. + pub phase_id: String, + /// Kind of phase currently executing. + pub phase_kind: FusionPhaseKind, + /// Semantic role assigned to the phase. + pub role: String, + /// Opaque hashed correlation token for matching tool-started and tool-completed activity within this Fusion activity stream. It is not the tool call identifier exposed by tool lifecycle events. + #[serde(skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, + /// Cumulative private response bytes observed for this model call. The event never includes response text. + #[serde(skip_serializing_if = "Option::is_none")] + pub total_response_size_bytes: Option, +} + /// Internal durable terminal request staged by a HydraFusion phase until an idempotent final commit selects it. /// ///
@@ -2499,7 +2710,7 @@ pub struct FusionAttribution { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AssistantMessageReasoningBlocks { - /// Provider-native reasoning content blocks (e.g. Anthropic `thinking` / `redacted_thinking`) preserved verbatim, in order. A single response can carry several, each signed over the content preceding it, so dropping or reordering any of them invalidates the rest. + /// Provider-native reasoning items or content blocks preserved verbatim, in order. A single response can carry several, and provider signatures or identifiers may depend on their exact content and ordering. #[serde(skip_serializing_if = "Option::is_none")] pub blocks: Option>, /// Model provider that produced these reasoning blocks. @@ -3330,6 +3541,9 @@ pub struct ToolExecutionCompleteError { pub code: Option, /// Human-readable error message pub message: String, + /// What the user must do to recover, when the runtime knows of an action. Set on sandbox policy denials, where `message` names the rule that blocked the call but never the client affordance that relaxes it. + #[serde(skip_serializing_if = "Option::is_none")] + pub remediation: Option, } /// Binary result returned by a tool for the model @@ -3860,12 +4074,15 @@ pub struct SkillInvokedData { /// Description of the skill from its SKILL.md frontmatter #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + /// Whether model invocation is disabled for this skill + #[serde(skip_serializing_if = "Option::is_none")] + pub disable_model_invocation: Option, /// Model identifier active when the skill was invoked, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// Name of the invoked skill pub name: String, - /// File path to the SKILL.md definition + /// File path to the SKILL.md definition, or an empty string for an SDK-provided skill without a filesystem identity pub path: String, /// Name of the plugin this skill originated from, when applicable #[serde(skip_serializing_if = "Option::is_none")] @@ -3873,7 +4090,7 @@ pub struct SkillInvokedData { /// Version of the plugin this skill originated from, when applicable #[serde(skip_serializing_if = "Option::is_none")] pub plugin_version: Option, - /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), and remote (org/enterprise skill) + /// Source identifier for where the skill was discovered. Known values include: project (workspace skill), inherited (parent-directory skill), personal-copilot (~/.copilot/skills), personal-agents (~/.agents/skills), custom (configured directory), plugin (installed plugin), builtin (bundled runtime skill), remote (org/enterprise skill), and sdk (SDK-provided skill) #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, /// What triggered the skill invocation: `user-invoked` (explicit user action, such as via a slash command or UI affordance), `agent-invoked` (agent requested the skill), or `context-load` (loaded as part of another context, such as preloading skills configured on a custom agent or subagent) @@ -3966,6 +4183,9 @@ pub struct SubagentCompletedData { /// Model used by the sub-agent #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Why an explicit task-call model did not become the effective model + #[serde(skip_serializing_if = "Option::is_none")] + pub model_override_reason: Option, /// Tool call ID of the parent tool invocation that spawned this sub-agent pub tool_call_id: String, /// Total tokens (input + output) consumed by the sub-agent @@ -4007,6 +4227,9 @@ pub struct SubagentFailedData { /// Model selected for the sub-agent, when known #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Why an explicit task-call model did not become the effective model + #[serde(skip_serializing_if = "Option::is_none")] + pub model_override_reason: Option, /// Tool call ID of the parent tool invocation that spawned this sub-agent pub tool_call_id: String, /// Total tokens (input + output) consumed before the sub-agent failed @@ -4213,10 +4436,10 @@ pub struct PermissionRequestShell { pub possible_paths: Vec, /// URLs that may be accessed by the command pub possible_urls: Vec, - /// True when the model has requested to run this command outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this command outside the sandbox, either because the command detaches and cannot be sandboxed at all, or because a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the command runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass: Option, - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass_reason: Option, /// Tool call ID that triggered this permission request @@ -4271,10 +4494,10 @@ pub struct PermissionRequestRead { pub managed_approval_required: Option, /// Path of the file or directory being read pub path: String, - /// True when the model has requested to run this search outside the sandbox (it set requestSandboxBypass: true and the host opted in via sandbox.allowBypass). This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to re-run this search outside the sandbox, after a sandboxed run looked blocked (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the search runs unsandboxed only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass: Option, - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass_reason: Option, /// Tool call ID that triggered this permission request @@ -4331,10 +4554,10 @@ pub struct PermissionRequestUrl { /// Immediately preceding URL when this request is for a redirect target #[serde(skip_serializing_if = "Option::is_none")] pub redirected_from: Option, - /// True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass: Option, - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass_reason: Option, /// Tool call ID that triggered this permission request @@ -4742,10 +4965,10 @@ pub struct PermissionPromptRequestUrl { /// Immediately preceding URL when this prompt is for a redirect target #[serde(skip_serializing_if = "Option::is_none")] pub redirected_from: Option, - /// True when this URL fetch is requesting to bypass the sandbox network policy: either the model set requestSandboxBypass: true, or the tool re-issued the request as an interactive bypass after the network policy denied the approved URL (host opted in via sandbox.allowBypass). This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. + /// True when the tool is asking to run this URL fetch outside the sandbox, after the network policy denied the approved URL or the sandbox proxy could not reach it (host opted in via sandbox.allowBypass). The model cannot ask for this; only the tool raises it. This is a request, not a grant: the fetch runs only if the user approves this permission request. Hosts should highlight the elevated risk in the approval UI. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass: Option, - /// Model-provided justification for the sandbox-bypass request. Only meaningful when requestSandboxBypass is true. + /// What the tool tells the user about the bypass on offer: which policy rule blocked the call, or why it cannot be sandboxed. Only meaningful when requestSandboxBypass is true. #[serde(skip_serializing_if = "Option::is_none")] pub request_sandbox_bypass_reason: Option, /// Tool call ID that triggered this permission request @@ -5015,6 +5238,9 @@ pub struct PermissionPromptRequestExtensionEnvAccess { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PermissionRequestedData { + /// Agent mode captured from the owning turn when permission evaluation began. + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_mode: Option, /// Details of the permission being requested pub permission_request: PermissionRequest, /// Derived user-facing permission prompt details for UI consumers @@ -5711,7 +5937,7 @@ pub struct SessionAutoModeResolvedData { pub sticky_override: Option, } -/// Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values per ordinary key, while permissions compose restrictively across device, server, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. +/// Session event "session.managed_settings_resolved". Enterprise managed-settings resolution: the effective managed settings the session applied and which channels contributed, so SDK clients can show users what is enterprise-managed. Fires whenever managed policy is (re)applied — at session start, on resume, and on account switch. This is an ephemeral live snapshot (delivered to subscribers but not persisted to the session event log), because at session start it resolves before `session.start` is emitted. Device values take precedence over server values, then the policy helper, per ordinary key, while permissions compose restrictively across device, server, policy-helper, and SDK-client layers. The account-scoped `getManagedSettings()` API does not include session-local client injection. Marked experimental while the managed-settings surface stabilizes. /// ///
/// @@ -5736,6 +5962,9 @@ pub struct SessionManagedSettingsResolvedData { /// Whether at least two managed sources supplied permission allowlists, so enforcement intersects them and the flattened settings payload omits `permissions.allow`. #[serde(skip_serializing_if = "Option::is_none")] pub permissions_allow_intersected: Option, + /// Whether the policy-helper managed-settings layer was present. The policy helper is the weakest channel: it fills keys no enterprise source set and can never replace one. + #[serde(skip_serializing_if = "Option::is_none")] + pub policy_helper_managed: Option, /// Whether the effective sandbox policy forces the sandbox on *only* because managed policy could not be determined, rather than because the policy requires it. Lets clients tell a user whose `--no-sandbox` was overridden that the sandbox stayed on as a fail-closed fallback, instead of attributing it to an administrator who set no such policy. #[serde(skip_serializing_if = "Option::is_none")] pub sandbox_enabled_by_undetermined_policy: Option, @@ -5744,7 +5973,7 @@ pub struct SessionManagedSettingsResolvedData { /// The effective (resolved) managed settings values, so clients can render exactly what is enforced. Absent when no managed policy is in force. #[serde(skip_serializing_if = "Option::is_none")] pub settings: Option, - /// Channel summary: `server`, `device`, or `client` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. + /// Channel summary: `server`, `device`, `client`, or `policyHelper` when exactly one channel contributed; `mixed` when multiple channels contributed; otherwise `none`. Consult the per-channel booleans for exact provenance. pub source: ManagedSettingsResolvedSource, } @@ -5948,7 +6177,7 @@ pub struct SkillsLoadedSkill { /// Absolute path to the skill file, if available #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, - /// Source location type (e.g., project, personal-copilot, plugin, builtin) + /// Source location type (e.g., project, personal-copilot, plugin, builtin, remote, sdk) pub source: SkillSource, /// Whether the skill can be invoked by the user as a slash command pub user_invocable: bool, @@ -5962,7 +6191,7 @@ pub struct SessionSkillsLoadedData { pub skills: Vec, } -/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and model override. +/// A single loaded custom agent in `session.custom_agents_updated`, with identity, source, tools, invocability, and authored model configuration. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CustomAgentsUpdatedAgent { @@ -5975,6 +6204,12 @@ pub struct CustomAgentsUpdatedAgent { /// Model override for this agent, if set #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, + /// Whether authored models are preferences or required constraints + #[serde(skip_serializing_if = "Option::is_none")] + pub model_policy: Option, + /// Authored model ids in priority order, if configured + #[serde(skip_serializing_if = "Option::is_none")] + pub models: Option>, /// Internal name of the agent pub name: String, /// Source location: user, project, inherited, remote, or plugin @@ -5997,6 +6232,14 @@ pub struct SessionCustomAgentsUpdatedData { pub warnings: Vec, } +/// Server-advertised metadata learned through modern discovery or legacy initialization. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpServerMetadata { + /// Non-empty natural-language guidance for using the server, or null when the server omitted instructions or advertised an empty string. + pub instructions: Option, +} + /// A single MCP server status summary in `session.mcp_servers_loaded`, including name, status, source, transport, and plugin metadata. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -6012,6 +6255,9 @@ pub struct McpServersLoadedServer { /// Version of the plugin that supplied the effective MCP server config, only when source is plugin #[serde(skip_serializing_if = "Option::is_none")] pub plugin_version: Option, + /// Server-advertised metadata for a connected server. Omitted when no live connection metadata is available, including while pending or when failed, disabled, stopped, or not configured. + #[serde(skip_serializing_if = "Option::is_none")] + pub server_metadata: Option, /// Configuration source: user, workspace, plugin, or builtin #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, @@ -6043,6 +6289,22 @@ pub struct SessionMcpServerStatusChangedData { pub status: McpServerStatus, } +/// Session event "session.mcp_server_removed". Payload of `session.mcp_server_removed` identifying an MCP server the graph no longer runs. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpServerRemovedData { + /// Name of the MCP server that was removed from the graph + pub server_name: String, +} + +/// Session event "session.mcp_server_needs_reconnect". Payload of `session.mcp_server_needs_reconnect` identifying an MCP server whose connection must be re-established. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpServerNeedsReconnectData { + /// Name of the MCP server that needs to reconnect + pub server_name: String, +} + /// Session event "mcp.tools.list_changed". Payload identifying the MCP server associated with a list change. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -6424,6 +6686,30 @@ pub enum Verbosity { Unknown, } +/// What the user must do to recover from a failure, named as an action rather than as one client's affordance. The runtime cannot know which affordance a client offers — a slash command, a settings pane, a link — so the accompanying message stays host-agnostic and each client renders its own copy from this value. Absent when the runtime knows of no action the user can take. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum RemediationAction { + /// Authenticate again with the Copilot backend. The current credential is absent, expired, or rejected. + #[serde(rename = "sign_in")] + SignIn, + /// Authenticate as a different account. The current account exists but lacks access to the requested resource. + #[serde(rename = "switch_account")] + SwitchAccount, + /// Inspect which account is currently authenticated before deciding what to change. + #[serde(rename = "show_account")] + ShowAccount, + /// Review or widen the sandbox policy. The blocked path or host is named by the accompanying message or by the tool result the action arrived with. + #[serde(rename = "review_sandbox_policy")] + ReviewSandboxPolicy, + /// Permit outbound network access in the sandbox policy. + #[serde(rename = "allow_sandbox_outbound")] + AllowSandboxOutbound, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The session mode the agent is operating in #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SessionMode { @@ -6538,6 +6824,27 @@ pub enum ModelChangeSource { Unknown, } +/// Terminal reason an Auto preference activation failed. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AutoTierSwitchFailureReason { + /// The candidate model was rejected by model policy. + #[serde(rename = "policy_rejected")] + PolicyRejected, + /// The Auto routing request failed or returned an unusable response. + #[serde(rename = "request_failed")] + RequestFailed, + /// The runtime could not prepare the Auto routing request. + #[serde(rename = "setup_failed")] + SetupFailed, + /// The provider does not support Auto routing. + #[serde(rename = "unsupported")] + Unsupported, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Permission mode for the session. /// ///
@@ -6668,6 +6975,48 @@ pub enum TaskCompletionOutcome { Unknown, } +/// Structured terminal status from a tool completion event. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CompletionReceiptToolStatus { + /// The tool completed successfully. + #[serde(rename = "success")] + Success, + /// The tool failed without a more specific structured status. + #[serde(rename = "failure")] + Failure, + /// The tool exceeded its time budget. + #[serde(rename = "timeout")] + Timeout, + /// The user rejected the tool call. + #[serde(rename = "rejected")] + Rejected, + /// The permissions service denied the tool call. + #[serde(rename = "denied")] + Denied, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Runtime reason the completion decision was accepted. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum CompletionReceiptStopReason { + /// The model reached a natural terminal response. + #[serde(rename = "natural")] + Natural, + /// A terminal tool ended the interaction. + #[serde(rename = "terminal_tool")] + TerminalTool, + /// The configured agentStop continuation limit was reached. + #[serde(rename = "agent_stop_block_limit")] + AgentStopBlockLimit, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Kind of turn for which HydraFusion routing is running. /// ///
@@ -6737,6 +7086,65 @@ pub enum FusionPattern { Unknown, } +/// HydraFusion phase kind. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionPhaseKind { + /// Primary solver phase. + #[serde(rename = "primary")] + Primary, + /// Read-only cascade judge phase. + #[serde(rename = "judge")] + Judge, + /// Cascade repair phase. + #[serde(rename = "repair")] + Repair, + /// Initial critique-pattern draft phase. + #[serde(rename = "draft")] + Draft, + /// Read-only critique phase. + #[serde(rename = "critic")] + Critic, + /// Critique-pattern revision phase. + #[serde(rename = "revision")] + Revision, + /// Follow-up phase continuing from the resolved model. + #[serde(rename = "follow_up")] + FollowUp, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Conversation scope in which a HydraFusion phase executes. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum FusionConversationScope { + /// Canonical root conversation history. + #[serde(rename = "root")] + Root, + /// Isolated read-only review history that does not enter the root conversation. + #[serde(rename = "review")] + Review, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The agent mode that was active when this message was sent #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum UserMessageAgentMode { @@ -6827,7 +7235,7 @@ pub enum ModelCallFailureTransport { Unknown, } -/// Conversation scope in which a HydraFusion phase executes. +/// Content-safe activity observed while a HydraFusion phase is running. /// ///
/// @@ -6836,50 +7244,16 @@ pub enum ModelCallFailureTransport { /// ///
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionConversationScope { - /// Canonical root conversation history. - #[serde(rename = "root")] - Root, - /// Isolated read-only review history that does not enter the root conversation. - #[serde(rename = "review")] - Review, - /// Unknown variant for forward compatibility. - #[default] - #[serde(other)] - Unknown, -} - -/// HydraFusion phase kind. -/// -///
-/// -/// **Experimental.** This type is part of an experimental wire-protocol surface -/// and may change or be removed in future SDK or CLI releases. -/// -///
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub enum FusionPhaseKind { - /// Primary solver phase. - #[serde(rename = "primary")] - Primary, - /// Read-only cascade judge phase. - #[serde(rename = "judge")] - Judge, - /// Cascade repair phase. - #[serde(rename = "repair")] - Repair, - /// Initial critique-pattern draft phase. - #[serde(rename = "draft")] - Draft, - /// Read-only critique phase. - #[serde(rename = "critic")] - Critic, - /// Critique-pattern revision phase. - #[serde(rename = "revision")] - Revision, - /// Follow-up phase continuing from the resolved model. - #[serde(rename = "follow_up")] - FollowUp, +pub enum FusionPhaseActivityKind { + /// The provider produced additional private output bytes. + #[serde(rename = "model_output")] + ModelOutput, + /// A tool began executing inside the phase. + #[serde(rename = "tool_started")] + ToolStarted, + /// A tool finished executing inside the phase. + #[serde(rename = "tool_completed")] + ToolCompleted, /// Unknown variant for forward compatibility. #[default] #[serde(other)] @@ -8158,7 +8532,10 @@ pub enum ManagedSettingsResolvedSource { /// Only session-local SDK-host injection contributed. #[serde(rename = "client")] Client, - /// More than one channel contributed. Ordinary keys resolve device over server per key, while permissions compose restrictively across all present layers. + /// A policy helper registered by device or server policy contributed. Device registration takes priority when present. + #[serde(rename = "policyHelper")] + PolicyHelper, + /// More than one channel contributed. Ordinary keys resolve device over server over policy helper per key, while permissions compose restrictively across all present layers. #[serde(rename = "mixed")] Mixed, /// No managed policy is in force (no channel contributed). @@ -8251,7 +8628,7 @@ pub enum FactoryRunSettledStatus { Unknown, } -/// Source location type (e.g., project, personal-copilot, plugin, builtin) +/// Source location type (e.g., project, personal-copilot, plugin, builtin, sdk) #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum SkillSource { /// Skill defined in the current project's skill directories. @@ -8275,6 +8652,24 @@ pub enum SkillSource { /// Skill bundled with the runtime. #[serde(rename = "builtin")] Builtin, + /// Pathless skill supplied lazily by an SDK skill provider. + #[serde(rename = "sdk")] + Sdk, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// Whether configured models are advisory preferences or required constraints +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum AgentModelPolicy { + /// Treat the authored models as advisory preferences that callers may override. + #[serde(rename = "preferred")] + Preferred, + /// Require subagent execution to use one of the authored models. + #[serde(rename = "required")] + Required, /// Unknown variant for forward compatibility. #[default] #[serde(other)] diff --git a/rust/src/jsonrpc.rs b/rust/src/jsonrpc.rs index 25a405080b..48e6090aed 100644 --- a/rust/src/jsonrpc.rs +++ b/rust/src/jsonrpc.rs @@ -9,6 +9,7 @@ use serde_json::Value; use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader}; use tokio::sync::{broadcast, mpsc, oneshot}; use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; use tracing::{Instrument, debug, error, warn}; use crate::{Error, ErrorKind, ProtocolErrorKind}; @@ -266,6 +267,7 @@ pub struct JsonRpcClient { pending_requests: Arc>>, notification_tx: broadcast::Sender, request_tx: mpsc::UnboundedSender, + connection_closed: CancellationToken, read_task: Mutex>>, write_task: Mutex>>, } @@ -294,6 +296,7 @@ impl JsonRpcClient { pending_requests: Arc::new(RwLock::new(HashMap::new())), notification_tx, request_tx, + connection_closed: CancellationToken::new(), read_task: Mutex::new(None), write_task: Mutex::new(Some(write_task)), }; @@ -301,6 +304,7 @@ impl JsonRpcClient { let pending_requests = client.pending_requests.clone(); let notification_tx_clone = client.notification_tx.clone(); let request_tx_clone = client.request_tx.clone(); + let connection_closed = client.connection_closed.clone(); let reader_span = tracing::error_span!("jsonrpc_read_loop"); let read_task = tokio::spawn( @@ -312,6 +316,7 @@ impl JsonRpcClient { request_tx_clone, ) .await; + connection_closed.cancel(); } .instrument(reader_span), ); @@ -321,6 +326,7 @@ impl JsonRpcClient { } pub(crate) fn force_close(&self) { + self.connection_closed.cancel(); if let Some(task) = self.read_task.lock().take() { task.abort(); } @@ -330,6 +336,10 @@ impl JsonRpcClient { self.pending_requests.write().clear(); } + pub(crate) fn connection_closed_token(&self) -> CancellationToken { + self.connection_closed.child_token() + } + /// Writer-actor task. Owns the `AsyncWrite`, drains the command queue, /// and writes each frame atomically (header + body + flush) before /// signaling the ack. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 1c20fd1837..c95ed2087a 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -33,6 +33,7 @@ pub mod hooks; mod jsonrpc; /// Permission-policy helpers that produce a [`handler::PermissionHandler`]. pub mod permission; +mod process_tree; /// BYOK bearer-token provider callbacks. pub mod provider_token; mod provider_token_dispatch; @@ -65,6 +66,12 @@ pub mod session_events; /// [`Client::rpc`] and [`session::Session::rpc`](crate::session::Session::rpc). pub mod rpc; +#[derive(serde::Deserialize)] +struct SessionDetachResponse { + success: bool, + error: Option, +} + // Auto-generated protocol-type modules. Crate-private so the only public // access path is via the `session_events` and `rpc` facade modules above — // callers can never depend on the implementation-detail layout under @@ -208,9 +215,9 @@ pub const HAS_BUNDLED_CLI: bool = cfg!(has_bundled_cli); /// Returns the path to the bundled Copilot CLI, extracting it from the /// embedded archive on first call. /// -/// This exposes the CLI artifact directly for callers such as health checks, -/// diagnostics, version probes, and in-process hosting. Managed child-process -/// transports resolve the bundled `copilot-runtime` wrapper instead. +/// This exposes the full CLI artifact directly for callers such as health +/// checks, diagnostics, and version probes. Managed child-process and +/// in-process transports resolve the bundled runtime artifacts instead. /// /// Subsequent calls return the cached result. Extraction is skipped when /// an already-published binary passes a cheap integrity re-check; a @@ -400,6 +407,102 @@ pub struct ClientOptions { /// (the default) or are stripped to a minimal/safe baseline. See /// [`ClientMode`] for the contract and trade-offs. pub mode: ClientMode, + /// Declares the integrating application's identity, forwarded to the runtime on + /// the `server.connect` handshake. Declaring it lets the telemetry the + /// runtime emits on this connection be attributed to a consistent surface + /// (the application and its Copilot integration) instead of the runtime's own + /// build. All fields are optional; leave it `None` to keep the runtime's + /// default attribution. + pub client_info: Option, +} + +/// Identity of the integrating application, declared on the `server.connect` +/// handshake. +/// +/// Declaring it lets the telemetry the runtime emits on the connection be +/// attributed to a single, consistent surface instead of the runtime's own +/// build. All fields are optional; an empty field is omitted from the +/// handshake. +/// +/// The struct is `#[non_exhaustive]`, so construct it with [`ClientInfo::new`] +/// and the `with_*` builder methods rather than a struct literal. This lets the +/// SDK add identity fields in future releases without a breaking change. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[non_exhaustive] +pub struct ClientInfo { + /// Name of the application using the SDK. + pub application_name: Option, + /// Version of the application using the SDK. + pub application_version: Option, + /// Optional name of a specific integration within the application, such as an + /// extension or plugin. + pub integration_name: Option, + /// Optional version of the integration identified by [`Self::integration_name`]. + pub integration_version: Option, +} + +impl ClientInfo { + /// Create an empty `ClientInfo`. Populate fields with the `with_*` builder + /// methods; every field is optional. + pub fn new() -> Self { + Self::default() + } + + /// Set the name of the application using the SDK. + pub fn with_application_name(mut self, application_name: impl Into) -> Self { + self.application_name = Some(application_name.into()); + self + } + + /// Set the version of the application using the SDK. + pub fn with_application_version(mut self, application_version: impl Into) -> Self { + self.application_version = Some(application_version.into()); + self + } + + /// Set the name of a specific integration within the application, such as an + /// extension or plugin. + pub fn with_integration_name(mut self, integration_name: impl Into) -> Self { + self.integration_name = Some(integration_name.into()); + self + } + + /// Set the version of the integration identified by + /// [`Self::with_integration_name`]. + pub fn with_integration_version(mut self, integration_version: impl Into) -> Self { + self.integration_version = Some(integration_version.into()); + self + } + + /// Returns `true` when no field carries a non-empty value, in which case the + /// SDK omits `clientInfo` from the handshake and the runtime keeps its + /// default attribution. + fn is_empty(&self) -> bool { + Self::non_empty(&self.application_name).is_none() + && Self::non_empty(&self.application_version).is_none() + && Self::non_empty(&self.integration_name).is_none() + && Self::non_empty(&self.integration_version).is_none() + } + + /// Clone the field only when it holds a non-empty string, so empty fields are + /// dropped from the handshake. + fn non_empty(value: &Option) -> Option { + value.as_ref().filter(|s| !s.is_empty()).cloned() + } + + /// Map onto the generated connect wire shape, dropping empty fields. Returns + /// `None` when no field carries a non-empty value. + fn to_wire(&self) -> Option { + if self.is_empty() { + return None; + } + Some(crate::generated::api_types::ConnectClientInfo { + editor_name: Self::non_empty(&self.application_name), + editor_version: Self::non_empty(&self.application_version), + extension_name: Self::non_empty(&self.integration_name), + extension_version: Self::non_empty(&self.integration_version), + }) + } } impl std::fmt::Debug for ClientOptions { @@ -451,6 +554,7 @@ impl std::fmt::Debug for ClientOptions { .field("base_directory", &self.base_directory) .field("enable_remote_sessions", &self.enable_remote_sessions) .field("bundled_cli_extract_dir", &self.bundled_cli_extract_dir) + .field("client_info", &self.client_info) .finish() } } @@ -700,6 +804,7 @@ impl Default for ClientOptions { enable_remote_sessions: false, bundled_cli_extract_dir: None, mode: ClientMode::default(), + client_info: None, } } } @@ -930,6 +1035,14 @@ impl ClientOptions { self.mode = mode; self } + + /// Declare the integrating application's identity, forwarded to the runtime on + /// the `server.connect` handshake so its telemetry is attributed to a + /// consistent surface. See [`Self::client_info`]. + pub fn with_client_info(mut self, client_info: ClientInfo) -> Self { + self.client_info = Some(client_info); + self + } } /// Validate a [`SessionFsConfig`] before sending `sessionFs.setProvider`. @@ -1066,6 +1179,7 @@ impl std::fmt::Debug for Client { struct ClientInner { child: parking_lot::Mutex>, + process_tree: parking_lot::Mutex>, #[cfg(feature = "bundled-in-process")] /// In-process FFI runtime host, set only for [`Transport::InProcess`]. /// Closing it tears down the native runtime connection. @@ -1098,6 +1212,10 @@ struct ClientInner { /// `None` for stdio and for external-server transport without an /// explicit token. effective_connection_token: Option, + /// Application identity forwarded on the `connect` handshake, set from + /// [`ClientOptions::client_info`]. `None` keeps the runtime's default + /// telemetry attribution. + client_info: Option, /// SDK [`ClientMode`] captured at start time. Drives empty-mode safe /// defaults inside `create_session` / `resume_session`. pub(crate) mode: ClientMode, @@ -1302,6 +1420,7 @@ impl Client { reader, writer, None, + None, working_directory, options.on_list_models, extension_launch_provider.clone(), @@ -1311,13 +1430,14 @@ impl Client { options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )? } Transport::Tcp { port, connection_token: _, } => { - let (mut child, actual_port, spawn_elapsed, port_wait_elapsed) = + let (mut child, tree, actual_port, spawn_elapsed, port_wait_elapsed) = Self::spawn_tcp(&program, &options, &working_directory, port).await?; timings.process_spawn_ms = Some(StartupTimings::millis(spawn_elapsed)); timings.port_wait_ms = Some(StartupTimings::millis(port_wait_elapsed)); @@ -1334,6 +1454,7 @@ impl Client { reader, writer, Some(child), + tree, working_directory, options.on_list_models, extension_launch_provider.clone(), @@ -1343,10 +1464,11 @@ impl Client { options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )? } Transport::Stdio => { - let (mut child, spawn_elapsed) = + let (mut child, tree, spawn_elapsed) = Self::spawn_stdio(&program, &options, &working_directory)?; timings.process_spawn_ms = Some(StartupTimings::millis(spawn_elapsed)); let stdin = child.stdin.take().expect("stdin is piped"); @@ -1356,6 +1478,7 @@ impl Client { stdout, stdin, Some(child), + tree, working_directory, options.on_list_models, extension_launch_provider.clone(), @@ -1365,6 +1488,7 @@ impl Client { options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )? } Transport::InProcess => { @@ -1422,6 +1546,7 @@ impl Client { reader, writer, None, + None, working_directory, options.on_list_models, extension_launch_provider.clone(), @@ -1431,6 +1556,7 @@ impl Client { options.on_github_telemetry, effective_connection_token.clone(), options.mode, + options.client_info, )?; *client.inner.ffi_host.lock() = Some(shared); client @@ -1563,6 +1689,7 @@ impl Client { reader, writer, None, + None, cwd, None, None, @@ -1572,6 +1699,7 @@ impl Client { None, None, ClientMode::default(), + None, ) } @@ -1589,6 +1717,7 @@ impl Client { reader, writer, None, + None, cwd, None, Some(provider), @@ -1598,6 +1727,7 @@ impl Client { None, None, ClientMode::default(), + None, ) } @@ -1619,6 +1749,7 @@ impl Client { reader, writer, None, + None, cwd, None, None, @@ -1628,6 +1759,7 @@ impl Client { None, None, ClientMode::default(), + None, ) } @@ -1645,6 +1777,7 @@ impl Client { reader, writer, None, + None, cwd, None, None, @@ -1654,6 +1787,7 @@ impl Client { None, token, ClientMode::default(), + None, ) } @@ -1671,6 +1805,7 @@ impl Client { reader, writer, None, + None, cwd, None, None, @@ -1680,6 +1815,7 @@ impl Client { Some(on_github_telemetry), None, ClientMode::default(), + None, ) } @@ -1693,11 +1829,41 @@ impl Client { generate_connection_token() } + /// Construct a [`Client`] from raw streams with a preset + /// [`ClientInfo`], for integration testing the `connect` handshake's + /// application-identity forwarding path. + #[doc(hidden)] + #[cfg(any(test, feature = "test-support"))] + pub fn from_streams_with_client_info( + reader: impl AsyncRead + Unpin + Send + 'static, + writer: impl AsyncWrite + Unpin + Send + 'static, + cwd: PathBuf, + client_info: Option, + ) -> Result { + Self::from_transport( + reader, + writer, + None, + None, + cwd, + None, + None, + false, + false, + None, + None, + None, + ClientMode::default(), + client_info, + ) + } + #[allow(clippy::too_many_arguments)] fn from_transport( reader: impl AsyncRead + Unpin + Send + 'static, writer: impl AsyncWrite + Unpin + Send + 'static, child: Option, + process_tree: Option, cwd: PathBuf, on_list_models: Option>, extension_launch_provider: Option< @@ -1709,6 +1875,7 @@ impl Client { on_github_telemetry: Option, effective_connection_token: Option, mode: ClientMode, + client_info: Option, ) -> Result { let setup_start = Instant::now(); let (request_tx, request_rx) = mpsc::unbounded_channel::(); @@ -1732,6 +1899,7 @@ impl Client { let client = Self { inner: Arc::new(ClientInner { child: parking_lot::Mutex::new(child), + process_tree: parking_lot::Mutex::new(process_tree), #[cfg(feature = "bundled-in-process")] ffi_host: parking_lot::Mutex::new(None), rpc, @@ -1753,6 +1921,7 @@ impl Client { on_get_trace_context, effective_connection_token, mode, + client_info, startup_timings: OnceLock::new(), }), }; @@ -1870,13 +2039,6 @@ impl Client { .stdout(Stdio::piped()) .stderr(Stdio::piped()); - #[cfg(windows)] - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - command.as_std_mut().creation_flags(CREATE_NO_WINDOW); - } - command } @@ -1933,7 +2095,7 @@ impl Client { program: &Path, options: &ClientOptions, working_directory: &Path, - ) -> Result<(Child, Duration)> { + ) -> Result<(Child, Option, Duration)> { info!(cwd = ?working_directory, program = %program.display(), "spawning copilot CLI (stdio)"); let mut command = Self::build_command(program, options, working_directory); command @@ -1945,13 +2107,13 @@ impl Client { .args(&options.extra_args) .stdin(Stdio::piped()); let spawn_start = Instant::now(); - let child = command.spawn()?; + let (child, tree) = process_tree::spawn(&mut command)?; let spawn_elapsed = spawn_start.elapsed(); debug!( elapsed_ms = spawn_elapsed.as_millis(), "Client::spawn_stdio subprocess spawned" ); - Ok((child, spawn_elapsed)) + Ok((child, tree, spawn_elapsed)) } async fn spawn_tcp( @@ -1959,7 +2121,13 @@ impl Client { options: &ClientOptions, working_directory: &Path, port: u16, - ) -> Result<(Child, u16, Duration, Duration)> { + ) -> Result<( + Child, + Option, + u16, + Duration, + Duration, + )> { info!(cwd = ?working_directory, program = %program.display(), port = %port, "spawning copilot CLI (tcp)"); let mut command = Self::build_command(program, options, working_directory); command @@ -1971,7 +2139,7 @@ impl Client { .args(&options.extra_args) .stdin(Stdio::null()); let spawn_start = Instant::now(); - let mut child = command.spawn()?; + let (mut child, tree) = process_tree::spawn(&mut command)?; let spawn_elapsed = spawn_start.elapsed(); debug!( elapsed_ms = spawn_elapsed.as_millis(), @@ -2018,7 +2186,7 @@ impl Client { "Client::spawn_tcp TCP port wait complete" ); info!(port = %actual_port, "CLI server listening"); - Ok((child, actual_port, spawn_elapsed, port_wait_elapsed)) + Ok((child, tree, actual_port, spawn_elapsed, port_wait_elapsed)) } fn drain_stderr(child: &mut Child) { @@ -2097,6 +2265,25 @@ impl Client { self.call_with_inline_callback(method, params, None).await } + pub(crate) async fn detach_session(&self, session_id: &str) -> Result<()> { + let value = self + .call( + "session.detach", + Some(serde_json::json!({ "sessionId": session_id })), + ) + .await?; + let response: SessionDetachResponse = serde_json::from_value(value)?; + if response.success { + return Ok(()); + } + Err(Error::with_message( + ErrorKind::Session(SessionErrorKind::DetachFailed), + response + .error + .unwrap_or_else(|| "unknown error".to_string()), + )) + } + /// Same as [`call`](Self::call), but installs an `inline_callback` /// that runs synchronously on the JSON-RPC read task the instant the /// successful response is parsed, before it is delivered to this @@ -2162,15 +2349,18 @@ impl Client { /// Register a session to receive filtered events and requests. /// - /// Returns per-session channels for notifications and requests, routed - /// by `sessionId`. Starts the internal router on first call. + /// Returns the per-session channels plus a + /// [`RegistrationToken`](crate::router::RegistrationToken) identifying + /// *this* registration. Registering an ID that is already registered + /// replaces the previous registration. /// - /// When done, call [`unregister_session`](Self::unregister_session) to - /// clean up (typically on session destroy). + /// When done, call + /// [`unregister_session_owned`](Self::unregister_session_owned) with + /// that token to clean up (typically on session destroy). pub(crate) fn register_session( &self, session_id: &SessionId, - ) -> crate::router::SessionChannels { + ) -> crate::router::SessionRegistration { self.inner.router.ensure_started( &self.inner.notification_tx, &self.inner.request_rx, @@ -2182,9 +2372,30 @@ impl Client { self.inner.router.register(session_id) } - /// Unregister a session, dropping its per-session channels. - pub(crate) fn unregister_session(&self, session_id: &SessionId) { - self.inner.router.unregister(session_id); + /// Unregister a session only if `token` still identifies the live + /// registration. + /// + /// Session IDs can be reused: a caller may retry a cancelled startup + /// with the same pinned ID while the previous owner is still being torn + /// down. Compare-and-remove keeps a stale owner from unregistering the + /// live session that replaced it. + pub(crate) fn unregister_session_owned( + &self, + session_id: &SessionId, + token: crate::router::RegistrationToken, + ) { + self.inner.router.unregister_owned(session_id, token); + } + + /// Snapshot the session IDs currently registered on the router. + /// + /// Crate-internal so in-crate unit tests can assert registration + /// lifecycle without depending on the `test-support` feature, which + /// only gates the equivalent *public* test helper. Compiled only for + /// those two configurations — a default-feature build has no caller. + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn registered_session_ids(&self) -> Vec { + self.inner.router.session_ids() } pub(crate) fn register_github_token_provider( @@ -2315,7 +2526,20 @@ impl Client { .on_github_telemetry .is_some() .then_some(true), - ..Default::default() + supported_task_kinds: Some(vec![ + crate::generated::api_types::TaskKind::Agent, + crate::generated::api_types::TaskKind::Client, + crate::generated::api_types::TaskKind::Shell, + ]), + // Declare the integrating application's identity so the runtime attributes + // the telemetry it emits on this connection to a consistent surface + // instead of its own build. `None` when the app didn't supply it, and + // empty fields are dropped. + client_info: self + .inner + .client_info + .as_ref() + .and_then(ClientInfo::to_wire), }; let value = self .call( @@ -2421,6 +2645,24 @@ impl Client { ); } + #[cfg(feature = "test-support")] + #[doc(hidden)] + /// Snapshot the session IDs currently registered on this client's + /// notification router. This is test-harness plumbing, not part of the + /// supported SDK API. + pub fn registered_session_ids_for_test(&self) -> Vec { + self.registered_session_ids() + } + + #[cfg(feature = "test-support")] + #[doc(hidden)] + /// Count the sessions currently registered on this client's notification + /// router. Deliberately never materialises the session IDs themselves so + /// they cannot leak into test diagnostics. + pub fn registered_session_count_for_test(&self) -> usize { + self.inner.router.session_count() + } + #[cfg(feature = "test-support")] #[doc(hidden)] /// Disconnect and delete every session owned by this test client's isolated @@ -2429,12 +2671,7 @@ impl Client { let mut first_error = None; for session_id in self.inner.router.session_ids() { - if let Err(error) = self - .call( - "session.destroy", - Some(serde_json::json!({ "sessionId": session_id })), - ) - .await + if let Err(error) = self.detach_session(&session_id).await && first_error.is_none() { first_error = Some(error); @@ -2560,12 +2797,13 @@ impl Client { /// Cooperatively shut down the client and the CLI child process. /// - /// Walks every still-registered session and sends `session.destroy` - /// for each one, asks SDK-owned runtimes to shut down, then kills the - /// CLI child. Errors from per-session destroys, runtime shutdown, and - /// the final child-kill are collected into - /// [`StopErrors`] rather than short-circuiting on the first failure - /// — so callers see the full picture of teardown. + /// Walks every still-registered session and sends `session.detach` + /// for each one, asks SDK-owned runtimes to shut down, terminates the + /// Windows-owned CLI Job Object when present, and reaps the root process. + /// Errors from per-session detaches, runtime shutdown, and final process + /// termination are collected into [`StopErrors`] rather than + /// short-circuiting on the first failure — so callers see the full picture + /// of teardown. /// /// If you have already called [`Session::disconnect`] on every /// session this client created, the per-session destroy step is a @@ -2591,21 +2829,15 @@ impl Client { self.inner.extension_launch_provider.clear(); // Snapshot the registered session IDs without holding the router - // lock across the destroy RPCs. + // lock across the detach RPCs. for session_id in self.inner.router.session_ids() { - match self - .call( - "session.destroy", - Some(serde_json::json!({ "sessionId": session_id })), - ) - .await - { + match self.detach_session(&session_id).await { Ok(_) => {} Err(e) => { warn!( session_id = %session_id, error = %e, - "session.destroy failed during Client::stop", + "session.detach failed during Client::stop", ); errors.push(e); } @@ -2654,8 +2886,14 @@ impl Client { } let child = self.inner.child.lock().take(); + let process_tree = self.inner.process_tree.lock().take(); *self.inner.state.lock() = ConnectionState::Disconnected; *self.inner.models_cache.lock() = Arc::new(tokio::sync::OnceCell::new()); + if let Some(process_tree) = process_tree + && let Err(error) = process_tree.terminate() + { + errors.push(error.into()); + } if let Some(mut child) = child { match child.try_wait() { Ok(Some(_status)) => {} @@ -2696,10 +2934,9 @@ impl Client { /// /// Synchronous fallback when [`stop`](Self::stop) is unsuitable — for /// example when the awaiting tokio runtime is shutting down or the - /// process is wedged on I/O. Sends a kill signal without awaiting - /// reaper completion and immediately drops all per-session router - /// state so dependent tasks observe a closed channel rather than a - /// hang. + /// process is wedged on I/O. Terminates the Windows-owned CLI Job Object + /// when present and immediately drops all per-session router state so + /// dependent tasks observe a closed channel rather than a hang. /// /// # Cancel safety /// @@ -2725,6 +2962,11 @@ impl Client { let pid = self.pid(); info!(pid = ?pid, "force-stopping CLI process"); self.inner.extension_launch_provider.clear(); + if let Some(process_tree) = self.inner.process_tree.lock().take() + && let Err(error) = process_tree.terminate() + { + error!(pid = ?pid, %error, "failed to terminate CLI process tree"); + } if let Some(mut child) = self.inner.child.lock().take() && let Err(e) = child.start_kill() { @@ -2786,8 +3028,13 @@ impl Client { impl Drop for ClientInner { fn drop(&mut self) { + let pid = self.child.lock().as_ref().and_then(Child::id); + if let Some(process_tree) = self.process_tree.lock().take() + && let Err(error) = process_tree.terminate() + { + error!(pid = ?pid, %error, "failed to terminate CLI process tree on drop"); + } if let Some(ref mut child) = *self.child.lock() { - let pid = child.id(); if let Err(e) = child.start_kill() { error!(pid = ?pid, error = %e, "failed to kill CLI process on drop"); } else { @@ -3447,6 +3694,7 @@ mod tests { client_read, client_write, Some(child), + None, temp.path().to_path_buf(), None, None, @@ -3456,6 +3704,7 @@ mod tests { None, None, ClientMode::default(), + None, ) .unwrap(); @@ -3536,6 +3785,7 @@ mod tests { Client { inner: Arc::new(ClientInner { child: parking_lot::Mutex::new(None), + process_tree: parking_lot::Mutex::new(None), #[cfg(feature = "bundled-in-process")] ffi_host: parking_lot::Mutex::new(None), rpc: { @@ -3565,6 +3815,7 @@ mod tests { on_get_trace_context: None, effective_connection_token: None, mode: ClientMode::default(), + client_info: None, startup_timings: OnceLock::new(), }), } diff --git a/rust/src/process_tree.rs b/rust/src/process_tree.rs new file mode 100644 index 0000000000..8dc7a451c5 --- /dev/null +++ b/rust/src/process_tree.rs @@ -0,0 +1,195 @@ +//! Windows crash-safe ownership of an SDK-spawned CLI process. +//! +//! A Job Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` lets Windows +//! terminate the CLI when the SDK-hosting process exits abruptly, even when +//! Rust cleanup code never runs. Other platforms retain Tokio's direct-child +//! ownership because no equivalent product failure has been demonstrated. + +use std::io; + +use tokio::process::{Child, Command}; + +pub(crate) fn spawn(command: &mut Command) -> io::Result<(Child, Option)> { + #[cfg(windows)] + { + platform::spawn(command).map(|(child, tree)| (child, Some(ProcessTree(Some(tree))))) + } + #[cfg(not(windows))] + { + command.spawn().map(|child| (child, None)) + } +} + +pub(crate) struct ProcessTree(Option); + +impl ProcessTree { + pub(crate) fn terminate(mut self) -> io::Result<()> { + self.0.take().expect("process tree is armed").terminate() + } +} + +impl Drop for ProcessTree { + fn drop(&mut self) { + if let Some(tree) = self.0.take() { + let _ = tree.terminate(); + } + } +} + +#[cfg(not(windows))] +mod platform { + pub(super) struct Tree; + + impl Tree { + pub(super) fn terminate(&self) -> std::io::Result<()> { + unreachable!("process-tree ownership is Windows-only") + } + } +} + +#[cfg(windows)] +mod platform { + use std::mem::size_of; + use std::os::windows::process::CommandExt; + use std::{io, ptr}; + + use tokio::process::{Child, Command}; + use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE}; + use windows_sys::Win32::System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, TH32CS_SNAPTHREAD, THREADENTRY32, Thread32First, Thread32Next, + }; + use windows_sys::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, + SetInformationJobObject, TerminateJobObject, + }; + use windows_sys::Win32::System::Threading::{ + CREATE_NO_WINDOW, CREATE_SUSPENDED, OpenThread, ResumeThread, THREAD_SUSPEND_RESUME, + }; + + struct OwnedHandle(HANDLE); + + // SAFETY: Win32 handles may be used and closed from any thread. + unsafe impl Send for OwnedHandle {} + unsafe impl Sync for OwnedHandle {} + + impl Drop for OwnedHandle { + fn drop(&mut self) { + // SAFETY: this value uniquely owns a valid handle. + unsafe { + CloseHandle(self.0); + } + } + } + + pub(super) struct Tree { + job: OwnedHandle, + } + + pub(super) fn spawn(command: &mut Command) -> io::Result<(Child, Tree)> { + // The root cannot run or create descendants before Job assignment. + command + .as_std_mut() + .creation_flags(CREATE_NO_WINDOW | CREATE_SUSPENDED); + let mut child = command.spawn()?; + match attach_and_resume(&child) { + Ok(tree) => Ok((child, tree)), + Err(error) => { + let _ = child.start_kill(); + Err(error) + } + } + } + + fn attach_and_resume(child: &Child) -> io::Result { + // SAFETY: null security attributes and name create a private, + // non-inheritable Job Object. + let raw_job = unsafe { CreateJobObjectW(ptr::null(), ptr::null()) }; + if raw_job.is_null() { + return Err(io::Error::last_os_error()); + } + let job = OwnedHandle(raw_job); + + let mut limits = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default(); + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + // SAFETY: `limits` has the layout required by the selected info class. + if unsafe { + SetInformationJobObject( + job.0, + JobObjectExtendedLimitInformation, + ptr::from_ref(&limits).cast(), + size_of::() as u32, + ) + } == 0 + { + return Err(io::Error::last_os_error()); + } + + let process = child.raw_handle().ok_or_else(|| { + io::Error::new( + io::ErrorKind::NotFound, + "CLI exited before Job Object assignment", + ) + })?; + // SAFETY: both handles are valid and the child is still suspended. + if unsafe { AssignProcessToJobObject(job.0, process.cast()) } == 0 { + return Err(io::Error::last_os_error()); + } + + resume_initial_thread(child.id().ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, "CLI exited before thread resume") + })?)?; + Ok(Tree { job }) + } + + fn resume_initial_thread(pid: u32) -> io::Result<()> { + // SAFETY: the returned snapshot handle is owned and closed below. + let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) }; + if snapshot == INVALID_HANDLE_VALUE { + return Err(io::Error::last_os_error()); + } + let snapshot = OwnedHandle(snapshot); + let mut entry = THREADENTRY32 { + dwSize: size_of::() as u32, + ..Default::default() + }; + + // SAFETY: `entry` has the documented size and remains live throughout + // enumeration. + let mut found = unsafe { Thread32First(snapshot.0, &mut entry) } != 0; + while found { + if entry.th32OwnerProcessID == pid { + // SAFETY: the thread id came from the live system snapshot. + let raw_thread = + unsafe { OpenThread(THREAD_SUSPEND_RESUME, 0, entry.th32ThreadID) }; + if raw_thread.is_null() { + return Err(io::Error::last_os_error()); + } + let thread = OwnedHandle(raw_thread); + // SAFETY: this is the root's suspended initial thread. + if unsafe { ResumeThread(thread.0) } == u32::MAX { + return Err(io::Error::last_os_error()); + } + return Ok(()); + } + // SAFETY: same valid snapshot and initialized entry as above. + found = unsafe { Thread32Next(snapshot.0, &mut entry) } != 0; + } + + Err(io::Error::new( + io::ErrorKind::NotFound, + "CLI initial thread was not found", + )) + } + + impl Tree { + pub(super) fn terminate(&self) -> io::Result<()> { + // SAFETY: the handle is a live Job Object owned by this value. + if unsafe { TerminateJobObject(self.job.0, 1) } != 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } + } + } +} diff --git a/rust/src/router.rs b/rust/src/router.rs index 4815d0e1c5..2b09713723 100644 --- a/rust/src/router.rs +++ b/rust/src/router.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use parking_lot::Mutex; use tokio::sync::{broadcast, mpsc}; @@ -8,6 +9,24 @@ use tracing::warn; use crate::jsonrpc::{JsonRpcNotification, JsonRpcRequest}; use crate::types::{SessionEventNotification, SessionId}; +/// Identity of one specific registration of a session ID. +/// +/// Session IDs are not unique over time: a caller can retry a cancelled +/// startup with the same pinned ID, and the retry replaces the previous +/// registration. Removal is therefore compare-and-remove against this +/// token, so a stale owner (an aborted startup future or a superseded +/// [`Session`](crate::session::Session)) can never unregister the live +/// registration that replaced it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct RegistrationToken(u64); + +/// Per-session channels plus the identity of the registration that owns +/// them. Returned by [`SessionRouter::register`]. +pub(crate) struct SessionRegistration { + pub(crate) channels: SessionChannels, + pub(crate) token: RegistrationToken, +} + /// Per-session channels created by the router during session registration. pub(crate) struct SessionChannels { /// Filtered `session.event` notifications for this session. @@ -19,6 +38,7 @@ pub(crate) struct SessionChannels { struct SessionSenders { notifications: mpsc::UnboundedSender, requests: mpsc::UnboundedSender, + token: RegistrationToken, } /// Routes notifications and requests by sessionId to per-session channels. @@ -26,6 +46,7 @@ struct SessionSenders { /// Internal to the SDK — consumers interact via `Client::register_session()`. pub(crate) struct SessionRouter { sessions: Arc>>, + next_token: AtomicU64, started: Mutex, } @@ -33,32 +54,69 @@ impl SessionRouter { pub(crate) fn new() -> Self { Self { sessions: Arc::new(Mutex::new(HashMap::new())), + next_token: AtomicU64::new(0), started: Mutex::new(false), } } /// Register a session to receive filtered events and requests. - pub(crate) fn register(&self, session_id: &SessionId) -> SessionChannels { + /// + /// Replaces any existing registration for the same ID and returns a + /// fresh [`RegistrationToken`] identifying this registration. + pub(crate) fn register(&self, session_id: &SessionId) -> SessionRegistration { let (notif_tx, notif_rx) = mpsc::unbounded_channel(); let (req_tx, req_rx) = mpsc::unbounded_channel(); + let token = RegistrationToken(self.next_token.fetch_add(1, Ordering::Relaxed)); self.sessions.lock().insert( session_id.clone(), SessionSenders { notifications: notif_tx, requests: req_tx, + token, }, ); - SessionChannels { - notifications: notif_rx, - requests: req_rx, + SessionRegistration { + channels: SessionChannels { + notifications: notif_rx, + requests: req_rx, + }, + token, } } /// Unregister a session, dropping its channels. + /// + /// Unconditional: removes whichever registration currently holds the + /// ID. Only for client-wide teardown, where every session is going away + /// regardless of owner. Owners of a specific registration must use + /// [`unregister_owned`](Self::unregister_owned). pub(crate) fn unregister(&self, session_id: &SessionId) { self.sessions.lock().remove(session_id.as_str()); } + /// Unregister a session only if it is still the registration identified + /// by `token`. + /// + /// Returns `true` when the entry was removed. A `false` result means + /// the registration had already been replaced by a newer one, which the + /// caller does not own and must leave alone. + pub(crate) fn unregister_owned( + &self, + session_id: &SessionId, + token: RegistrationToken, + ) -> bool { + let mut sessions = self.sessions.lock(); + if sessions + .get(session_id.as_str()) + .is_some_and(|senders| senders.token == token) + { + sessions.remove(session_id.as_str()); + true + } else { + false + } + } + /// Snapshot every currently-registered session ID. /// /// Used by [`Client::stop`](crate::Client::stop) to iterate active @@ -68,6 +126,12 @@ impl SessionRouter { self.sessions.lock().keys().cloned().collect() } + /// Count the currently-registered sessions without exposing their IDs. + #[cfg(any(test, feature = "test-support"))] + pub(crate) fn session_count(&self) -> usize { + self.sessions.lock().len() + } + /// Drop all registered session channels. /// /// Used by [`Client::force_stop`](crate::Client::force_stop) to release @@ -238,3 +302,37 @@ impl SessionRouter { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn session_id() -> SessionId { + SessionId::new("router-ownership") + } + + #[test] + fn each_registration_gets_a_distinct_token() { + let router = SessionRouter::new(); + let first = router.register(&session_id()); + let second = router.register(&session_id()); + assert_ne!(first.token, second.token); + } + + #[test] + fn unregister_owned_removes_only_the_matching_registration() { + let router = SessionRouter::new(); + let stale = router.register(&session_id()); + let live = router.register(&session_id()); + + // The stale owner must not evict the registration that replaced it. + assert!(!router.unregister_owned(&session_id(), stale.token)); + assert_eq!(router.session_ids(), vec![session_id()]); + + assert!(router.unregister_owned(&session_id(), live.token)); + assert!(router.session_ids().is_empty()); + + // Removing twice is a no-op rather than evicting a future tenant. + assert!(!router.unregister_owned(&session_id(), live.token)); + } +} diff --git a/rust/src/session.rs b/rust/src/session.rs index b9d2173055..0e64d6061c 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -1,19 +1,22 @@ use std::collections::HashMap; +use std::panic::AssertUnwindSafe; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::{Duration, Instant}; +use futures_util::FutureExt; use parking_lot::Mutex as ParkingLotMutex; use serde_json::Value; use tokio::sync::oneshot; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; -use tracing::{Instrument, warn}; +use tracing::{Instrument, error, warn}; use crate::canvas::CanvasHandler; use crate::generated::api_types::{ - LogRequest, ModelSwitchToRequest, OpenCanvasInstance, PermissionDecisionRequest, - RegisterEventInterestParams, ToolsGetCurrentMetadataResult, rpc_methods, + LogRequest, ModelSwitchAutoTierRequest, ModelSwitchAutoTierResult, ModelSwitchToRequest, + OpenCanvasInstance, PermissionDecisionRequest, RegisterEventInterestParams, + ToolsGetCurrentMetadataResult, rpc_methods, }; use crate::generated::session_events::{ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData, @@ -30,12 +33,12 @@ use crate::session_fs::SessionFsProvider; use crate::trace_context::inject_trace_context; use crate::transforms::SystemMessageTransform; use crate::types::{ - CommandContext, CommandDefinition, CommandHandler, CreateSessionResult, ElicitationRequest, - ElicitationResult, ExitPlanModeData, GetMessagesResponse, MessageOptions, - PermissionRequestData, RequestId, ResumeSessionConfig, ResumeSessionResult, SectionOverride, - SessionCapabilities, SessionConfig, SessionEvent, SessionId, SetModelOptions, - SystemMessageConfig, ToolInvocation, ToolResult, ToolResultExpanded, TraceContext, - UiInputOptions, ensure_attachment_display_names, + AutoTier, AutoTierPreference, CommandContext, CommandDefinition, CommandHandler, + CreateSessionResult, ElicitationRequest, ElicitationResult, ExitPlanModeData, + GetMessagesResponse, MessageOptions, PermissionRequestData, RequestId, ResumeSessionConfig, + ResumeSessionResult, SectionOverride, SessionCapabilities, SessionConfig, SessionEvent, + SessionId, SetModelOptions, SystemMessageConfig, ToolInvocation, ToolResult, + ToolResultExpanded, TraceContext, UiInputOptions, ensure_attachment_display_names, }; use crate::{ Client, Error, ErrorKind, JsonRpcResponse, SessionErrorKind, SessionEventNotification, @@ -47,6 +50,31 @@ use crate::{ /// `overrides_built_in_tool` set to `true`. const TOOL_SEARCH_TOOL_NAME: &str = "tool_search_tool"; +/// Default capacity of the per-session event broadcast buffer backing +/// [`Session::subscribe`] and [`PreparedSession::subscribe`]. +/// +/// Override per session with +/// [`SessionConfig::event_buffer_capacity`](crate::types::SessionConfig::event_buffer_capacity) +/// or +/// [`ResumeSessionConfig::event_buffer_capacity`](crate::types::ResumeSessionConfig::event_buffer_capacity). +pub const DEFAULT_EVENT_BUFFER_CAPACITY: usize = 512; + +/// Validate a caller-supplied event buffer capacity and resolve the default. +/// +/// Zero is rejected rather than clamped: a zero-capacity broadcast channel +/// cannot exist, and silently substituting a different capacity would hide a +/// caller bug. +fn resolve_event_buffer_capacity(capacity: Option) -> Result { + match capacity { + Some(0) => Err(Error::with_message( + ErrorKind::InvalidConfig, + "event_buffer_capacity must be greater than zero", + )), + Some(capacity) => Ok(capacity), + None => Ok(DEFAULT_EVENT_BUFFER_CAPACITY), + } +} + /// Bundle of the per-session callbacks the SDK dispatches to. Built from a /// [`SessionConfig`] / [`ResumeSessionConfig`] at /// [`Client::create_session`] / [`Client::resume_session`] time. Each @@ -66,6 +94,41 @@ pub(crate) struct SessionHandlers { pub tools: Arc>>, } +type PendingExternalTools = Arc>>>; + +struct PendingExternalToolGuard { + request_id: RequestId, + token: Arc, + pending: PendingExternalTools, +} + +impl Drop for PendingExternalToolGuard { + fn drop(&mut self) { + let mut pending = self.pending.lock(); + if pending + .get(&self.request_id) + .is_some_and(|token| Arc::ptr_eq(token, &self.token)) + { + pending.remove(&self.request_id); + } + } +} + +impl PendingExternalToolGuard { + fn claim(&self) -> bool { + let mut pending = self.pending.lock(); + if pending + .get(&self.request_id) + .is_some_and(|token| Arc::ptr_eq(token, &self.token)) + { + pending.remove(&self.request_id); + true + } else { + false + } + } +} + fn has_managed_settings( enable_managed_settings: Option, managed_settings: Option<&crate::types::ManagedSettings>, @@ -104,25 +167,88 @@ impl Drop for WaiterGuard { struct PendingSessionRegistration { client: Client, - session_id: SessionId, + session_id: PendingSessionId, shutdown: CancellationToken, + external_tools_shutdown: CancellationToken, disarmed: bool, } +/// Which session ID a [`PendingSessionRegistration`] should unregister on +/// cleanup. +/// +/// `session.create` for cloud sessions without a caller-pinned ID does not +/// know the ID until the response arrives, at which point the inline +/// response callback registers it and stashes it. The guard therefore reads +/// the stash at cleanup time instead of capturing an ID up front. +enum PendingSessionId { + /// The ID was known before the RPC was issued (resume, and create with a + /// client- or caller-supplied ID). + Known(SessionId, crate::router::RegistrationToken), + /// Server-assigned ID, populated by the `session.create` inline response + /// callback. `None` in the stash means nothing was ever registered. + Deferred(Arc>>), +} + impl PendingSessionRegistration { - fn new(client: Client, session_id: SessionId, shutdown: CancellationToken) -> Self { + fn new( + client: Client, + session_id: SessionId, + token: crate::router::RegistrationToken, + shutdown: CancellationToken, + external_tools_shutdown: CancellationToken, + ) -> Self { Self { client, - session_id, + session_id: PendingSessionId::Known(session_id, token), shutdown, + external_tools_shutdown, disarmed: false, } } + /// Guard for a registration whose session ID is assigned by the server. + fn deferred( + client: Client, + stash: Arc>>, + shutdown: CancellationToken, + external_tools_shutdown: CancellationToken, + ) -> Self { + Self { + client, + session_id: PendingSessionId::Deferred(stash), + shutdown, + external_tools_shutdown, + disarmed: false, + } + } + + fn registered_id(&self) -> Option { + match &self.session_id { + PendingSessionId::Known(id, _) => Some(id.clone()), + PendingSessionId::Deferred(stash) => stash.lock().as_ref().map(|(id, _)| id.clone()), + } + } + + /// Re-target the guard at a now-known session ID. Used by + /// `session.create` once the response has been parsed and the stash has + /// been drained into the event loop. + fn resolve_to(&mut self, session_id: SessionId, token: crate::router::RegistrationToken) { + self.session_id = PendingSessionId::Known(session_id, token); + } + async fn cleanup(mut self, event_loop: JoinHandle<()>) { + self.external_tools_shutdown.cancel(); self.shutdown.cancel(); let _ = event_loop.await; - self.client.unregister_session(&self.session_id); + if let Some(id) = self.registered_id() { + if let PendingSessionId::Known(_, token) = self.session_id { + self.client.unregister_session_owned(&id, token); + } else if let PendingSessionId::Deferred(stash) = &self.session_id + && let Some((id, registration)) = stash.lock().as_ref() + { + self.client.unregister_session_owned(id, registration.token); + } + } self.disarmed = true; } @@ -134,8 +260,17 @@ impl PendingSessionRegistration { impl Drop for PendingSessionRegistration { fn drop(&mut self) { if !self.disarmed { + self.external_tools_shutdown.cancel(); self.shutdown.cancel(); - self.client.unregister_session(&self.session_id); + if let Some(id) = self.registered_id() { + if let PendingSessionId::Known(_, token) = self.session_id { + self.client.unregister_session_owned(&id, token); + } else if let PendingSessionId::Deferred(stash) = &self.session_id + && let Some((id, registration)) = stash.lock().as_ref() + { + self.client.unregister_session_owned(id, registration.token); + } + } } } } @@ -177,6 +312,9 @@ pub struct Session { /// via [`Session::cancellation_token`] to bind their own work to /// the session lifetime. shutdown: CancellationToken, + /// Cancels only host-owned external tool callbacks. Disconnect signals this + /// before the destroy RPC without stopping unrelated event delivery. + external_tools_shutdown: CancellationToken, /// Only populated while a `send_and_wait` call is in flight. /// /// Sync `parking_lot::Mutex` because the lock is never held across an @@ -192,6 +330,8 @@ pub struct Session { event_tx: tokio::sync::broadcast::Sender, github_token_registration: ParkingLotMutex>, + /// Identity of this session's router registration. + registration_token: crate::router::RegistrationToken, } impl Session { @@ -323,10 +463,15 @@ impl Session { /// Stop the internal event loop. Called automatically on [`destroy`](Self::destroy). /// /// Cooperative: signals shutdown via the session's [`CancellationToken`] - /// and awaits the loop's natural exit rather than aborting the task. - /// Any in-flight handler (permission callback, tool call, elicitation - /// response) completes before the loop exits, so the CLI never sees a - /// half-handled request. See RFD-400 review finding #3. + /// and awaits the loop's natural exit rather than aborting the task, so + /// the loop always stops between iterations instead of at an arbitrary + /// await point. See RFD-400 review finding #3. + /// + /// Inbound requests are dispatched to their own spawned tasks, which this + /// call does not await. A handler (permission callback, tool call, + /// elicitation response) still running at teardown may therefore outlive + /// the loop, and its response can be lost if the connection closes first. + /// Await your own handler work before calling this if it must complete. pub async fn stop_event_loop(&self) { self.shutdown.cancel(); let handle = self.event_loop.lock().take(); @@ -541,7 +686,12 @@ impl Session { /// Pass `None` for `opts` if no extra configuration is needed. pub async fn set_model(&self, model: &str, opts: Option) -> Result<(), Error> { let opts = opts.unwrap_or_default(); + let auto_tier = opts.auto_tier.clone(); let request = ModelSwitchToRequest { + auto_tier: match &auto_tier { + Some(AutoTierPreference::Tier(tier)) => Some(tier.clone()), + _ => None, + }, compaction_decision: None, context_tier: opts.context_tier, defer_if_model_change_queued: None, @@ -557,13 +707,68 @@ impl Session { source: None, verbosity: None, }; + + if matches!(auto_tier, Some(AutoTierPreference::Reset)) { + // The generated request skips a `None` tier, which the runtime reads + // as "leave the preference alone" rather than "use provider-default + // routing", so send an explicit null instead. + let mut wire_params = serde_json::to_value(request)?; + wire_params["sessionId"] = serde_json::Value::String(self.id.to_string()); + wire_params["autoTier"] = serde_json::Value::Null; + self.client + .call("session.model.switchTo", Some(wire_params)) + .await?; + return Ok(()); + } + self.rpc().model().switch_to(request).await?; Ok(()) } + /// Change the Auto routing preference without changing the selected model. + /// + /// The runtime does not apply the preference immediately. It records the + /// request and commits it only when a later user turn using the `auto` + /// model successfully obtains a usable model from the provider. A + /// [`ModelSwitchAutoTierStatus::Pending`] status therefore confirms that the + /// request was accepted, not that it took effect. + /// + /// Watch for the outcome through the `session.model_change` event on + /// success, or the ephemeral `session.auto_tier_switch_failed` event on + /// failure. You can also read the current committed and in-flight state at + /// any time through `session.rpc().model().get_current()`. + /// + /// Only the most recent request survives: issuing a new request replaces any + /// earlier one that has not yet been claimed by a turn. + /// + /// Pass `None` to return to the provider's default Auto routing. + /// + /// **Experimental.** Part of an experimental Auto routing surface and may + /// change or be removed in a future release. + /// + /// # Cancel safety + /// + /// **Cancel-safe.** Single `session.model.switchAutoTier` RPC; the + /// underlying [`Client::call`](crate::Client::call) is cancel-safe via the + /// writer-actor. + /// + /// [`ModelSwitchAutoTierStatus::Pending`]: crate::generated::api_types::ModelSwitchAutoTierStatus::Pending + pub async fn set_auto_tier( + &self, + auto_tier: Option, + ) -> Result { + self.rpc() + .model() + .switch_auto_tier(ModelSwitchAutoTierRequest { + auto_tier, + source: None, + }) + .await + } + /// Disconnect this session from the CLI. /// - /// Sends the `session.destroy` RPC, stops the event loop, and unregisters + /// Sends the `session.detach` RPC, stops the event loop, and unregisters /// the session from the client. **Session state on disk** (conversation /// history, planning state, artifacts) is **preserved**, so the /// conversation can be resumed later via [`Client::resume_session`] @@ -578,21 +783,16 @@ impl Session { /// [`Client::delete_session`]: crate::Client::delete_session /// [`send_and_wait`]: Self::send_and_wait pub async fn disconnect(&self) -> Result<(), Error> { - self.client - .call( - "session.destroy", - Some(serde_json::json!({ "sessionId": self.id })), - ) - .await?; + self.client.detach_session(&self.id).await?; + self.external_tools_shutdown.cancel(); self.stop_event_loop().await; - self.client.unregister_session(&self.id); self.github_token_registration.lock().take(); + self.client + .unregister_session_owned(&self.id, self.registration_token); Ok(()) } - /// Deprecated alias for [`disconnect`](Self::disconnect). The - /// underlying wire RPC happens to be named `session.destroy`, but it - /// only severs the connection — on-disk session state is preserved. + /// Deprecated alias for [`disconnect`](Self::disconnect). /// Prefer `disconnect` in new code. #[deprecated(since = "0.1.0", note = "Use `disconnect()` instead")] pub async fn destroy(&self) -> Result<(), Error> { @@ -653,18 +853,20 @@ impl Drop for Session { fn drop(&mut self) { // Cooperative shutdown: cancel the event loop's token to signal // exit between iterations. The loop will see the cancellation on - // its next select poll and break cleanly without interrupting an - // in-flight handler. We do NOT abort the JoinHandle — that would - // land at any await point in the loop body, potentially leaving - // the CLI with an unanswered request id. RFD-400 review finding - // #3. + // its next select poll and break cleanly. We do NOT abort the + // JoinHandle — that would land at any await point in the loop body, + // potentially leaving the CLI with an unanswered request id. + // RFD-400 review finding #3. Requests already dispatched to their + // own tasks are not tracked here and may outlive the session. // // The handle itself is left in `event_loop` to be reaped by the // tokio runtime when it next polls; we intentionally don't await // it here because Drop is sync. self.shutdown.cancel(); - self.client.unregister_session(&self.id); + self.external_tools_shutdown.cancel(); self.github_token_registration.lock().take(); + self.client + .unregister_session_owned(&self.id, self.registration_token); } } @@ -806,6 +1008,102 @@ impl<'a> SessionUi<'a> { } impl Client { + /// Prepare a new session without touching the transport. + /// + /// Returns a [`PreparedSession`] that owns the session's event broadcast + /// channel, so callers can install an + /// [`EventSubscription`](crate::subscription::EventSubscription) via + /// [`PreparedSession::subscribe`] *before* any protocol activity starts. + /// Call [`PreparedSession::start`] to actually create the session. + /// + /// This is the loss-free entry point for consumers that must observe + /// every *routed* event a session emits, including events the CLI emits + /// while `session.create` is still in flight and ephemeral events (such + /// as `session.idle`) that cannot be recovered from + /// [`Session::get_messages`]. [`create_session`](Self::create_session) + /// is a thin wrapper over `prepare_session(...)?.start()` and cannot + /// offer the same guarantee, because the subscription can only be + /// installed after the returned `Session` exists. + /// + /// Routing requires a known session ID. When the server assigns the ID, + /// the SDK cannot register the session on its notification router until + /// the `session.create` response arrives, so notifications emitted + /// before that point are not routable and stay unobservable. Pin + /// [`SessionConfig::session_id`](crate::types::SessionConfig::session_id) + /// for complete pre-response coverage — see the "Server-assigned session + /// IDs" section on [`PreparedSession`]. + /// + /// # Inertness + /// + /// `prepare_session` performs no router registration, spawns no task, + /// and writes nothing to the wire. It only validates + /// [`event_buffer_capacity`](SessionConfig::event_buffer_capacity), + /// allocates a local broadcast channel and cancellation token, and + /// stores the config. Dropping the returned handle without starting it + /// leaves no client-side or server-side state behind and closes every + /// subscription taken from it. + /// + /// # Errors + /// + /// Returns [`ErrorKind::InvalidConfig`] if + /// [`event_buffer_capacity`](SessionConfig::event_buffer_capacity) is + /// `Some(0)`. All other configuration and protocol errors surface from + /// [`PreparedSession::start`], with the same + /// [`ErrorKind`]s [`create_session`](Self::create_session) has always + /// returned. + /// + /// # Example + /// + /// ```no_run + /// # use github_copilot_sdk::{Client, SessionConfig}; + /// # async fn example(client: Client) -> Result<(), github_copilot_sdk::Error> { + /// let prepared = client.prepare_session(SessionConfig::default())?; + /// let mut events = prepared.subscribe(); + /// let drain = tokio::spawn(async move { + /// while let Ok(event) = events.recv().await { + /// println!("{}", event.event_type); + /// } + /// }); + /// let session = prepared.start().await?; + /// # let _ = (session, drain); + /// # Ok(()) + /// # } + /// ``` + pub fn prepare_session(&self, config: SessionConfig) -> Result { + let capacity = resolve_event_buffer_capacity(config.event_buffer_capacity)?; + Ok(PreparedSession::new( + self.clone(), + PreparedKind::Create(Box::new(config)), + capacity, + )) + } + + /// Prepare a session resume without touching the transport. + /// + /// The resume counterpart of [`prepare_session`](Self::prepare_session); + /// see that method for the inertness guarantee, error semantics, and + /// rationale. Particularly relevant on resume with + /// [`continue_pending_work`](ResumeSessionConfig::continue_pending_work), + /// where the runtime can start emitting events (and reach + /// `session.idle`) while `session.resume` is still in flight. + /// + /// # Errors + /// + /// Returns [`ErrorKind::InvalidConfig`] if + /// [`event_buffer_capacity`](ResumeSessionConfig::event_buffer_capacity) + /// is `Some(0)`. + pub fn prepare_resume_session( + &self, + config: ResumeSessionConfig, + ) -> Result { + let capacity = resolve_event_buffer_capacity(config.event_buffer_capacity)?; + Ok(PreparedSession::new( + self.clone(), + PreparedKind::Resume(Box::new(config)), + capacity, + )) + } + /// Create a new session on the CLI. /// /// Sends `session.create`, registers the session on the router, @@ -827,7 +1125,48 @@ impl Client { /// Each per-event handler is independently optional. If a handler is /// not installed, the SDK signals the runtime not to emit the matching /// broadcast (and silently skips dispatch if one arrives anyway). - pub async fn create_session(&self, mut config: SessionConfig) -> Result { + /// + /// # Event delivery + /// + /// Equivalent to `prepare_session(config)?.start().await`. Because the + /// first subscription can only be taken from the returned [`Session`], + /// events the runtime emits before this call returns are broadcast with + /// no receiver installed and are therefore not delivered to + /// [`Session::subscribe`]. Use + /// [`prepare_session`](Self::prepare_session) when startup events + /// matter. + pub async fn create_session(&self, config: SessionConfig) -> Result { + self.prepare_session(config)?.start().await + } + + /// Resume an existing session on the CLI. + /// + /// Sends `session.resume` and `session.skills.reload`, registers the + /// session on the router, and spawns the event loop. + /// + /// All callbacks (event handler, hooks, transform) are configured + /// via [`ResumeSessionConfig`] using its `with_*` builder methods. + /// + /// See [`Self::create_session`] for the defaults applied when callback + /// fields are unset. + /// + /// # Event delivery + /// + /// Equivalent to `prepare_resume_session(config)?.start().await`, and + /// carries the same startup-event caveat documented on + /// [`create_session`](Self::create_session). Use + /// [`prepare_resume_session`](Self::prepare_resume_session) when + /// startup events matter. + pub async fn resume_session(&self, config: ResumeSessionConfig) -> Result { + self.prepare_resume_session(config)?.start().await + } + + async fn start_prepared_create( + &self, + mut config: SessionConfig, + event_tx: tokio::sync::broadcast::Sender, + shutdown: CancellationToken, + ) -> Result { let total_start = Instant::now(); // For cloud sessions, let the CLI/server assign the session id and // register the session lazily once the response arrives. For non-cloud @@ -968,8 +1307,7 @@ impl Client { let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default())); let idle_waiter = Arc::new(ParkingLotMutex::new(None)); let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new())); - let shutdown = CancellationToken::new(); - let (event_tx, _) = tokio::sync::broadcast::channel(512); + let external_tools_shutdown = self.inner.rpc.connection_closed_token(); // For cloud sessions (use_server_generated_id), defer session // registration to the inline callback so the read task registers @@ -977,7 +1315,7 @@ impl Client { // For non-cloud sessions, register up-front so the CLI can issue // session-scoped requests during session.create processing. let inline_stash: Arc< - ParkingLotMutex>, + ParkingLotMutex>, > = Arc::new(ParkingLotMutex::new(None)); let inline_callback: Option = if let Some(ref sid) = @@ -1005,45 +1343,62 @@ impl Client { }) .into()); } - let channels = client.register_session(&parsed.session_id); - *stash.lock() = Some((parsed.session_id, channels)); + // Register and stash under a single stash-lock hold. The + // cancellation guard identifies the session to unregister by + // peeking this stash, so registering outside the lock would + // leave a window where a concurrent guard drop (caller + // cancellation) sees `None` and leaks the registration. + // `register_session` takes the router lock, never the stash + // lock, so there is no lock-order inversion here. + let mut stashed = stash.lock(); + let registration = client.register_session(&parsed.session_id); + *stashed = Some((parsed.session_id, registration)); Ok(()) })) }; - let rpc_start = Instant::now(); - let result = match self - .call_with_inline_callback("session.create", Some(params), inline_callback) - .await - { - Ok(result) => result, - Err(error) => { - if let Some((id, _channels)) = inline_stash.lock().take() { - self.unregister_session(&id); - } - return Err(error); + // Armed for the whole startup sequence: any early return, and any + // drop of this future (caller cancellation), cancels the session + // token and unregisters whatever was registered on the router. For + // the cloud path the ID is only known once the inline callback has + // run, so the guard reads the stash at cleanup time. + let mut pending_registration = match local_session_id { + Some(ref sid) => { + let token = inline_stash + .lock() + .as_ref() + .expect("session registration must exist") + .1 + .token; + PendingSessionRegistration::new( + self.clone(), + sid.clone(), + token, + shutdown.clone(), + external_tools_shutdown.clone(), + ) } + None => PendingSessionRegistration::deferred( + self.clone(), + inline_stash.clone(), + shutdown.clone(), + external_tools_shutdown.clone(), + ), }; + + let rpc_start = Instant::now(); + let result = self + .call_with_inline_callback("session.create", Some(params), inline_callback) + .await?; tracing::debug!( elapsed_ms = rpc_start.elapsed().as_millis(), "Client::create_session session creation request completed successfully" ); - let create_result: CreateSessionResult = match serde_json::from_value(result) { - Ok(result) => result, - Err(error) => { - if let Some((id, _channels)) = inline_stash.lock().take() { - self.unregister_session(&id); - } - return Err(error.into()); - } - }; + let create_result: CreateSessionResult = serde_json::from_value(result)?; if let Some(ref requested) = local_session_id && create_result.session_id != *requested { - if let Some((id, _channels)) = inline_stash.lock().take() { - self.unregister_session(&id); - } return Err(ErrorKind::Session(SessionErrorKind::SessionIdMismatch { requested: requested.clone(), returned: create_result.session_id.clone(), @@ -1051,10 +1406,13 @@ impl Client { .into()); } - let (session_id, channels) = inline_stash + let (session_id, registration) = inline_stash .lock() .take() .expect("session registration must have populated stash on success"); + let channels = registration.channels; + let registration_token = registration.token; + pending_registration.resolve_to(session_id.clone(), registration_token); let event_loop = spawn_event_loop( session_id.clone(), self.clone(), @@ -1071,6 +1429,7 @@ impl Client { open_canvases.clone(), event_tx.clone(), shutdown.clone(), + external_tools_shutdown.clone(), ); tracing::debug!( elapsed_ms = setup_start.elapsed().as_millis(), @@ -1081,8 +1440,11 @@ impl Client { "Client::create_session local setup complete" ); *capabilities.write() = create_result.capabilities.unwrap_or_default(); - if has_mcp_auth_handler { - register_mcp_auth_interest(self, &session_id).await?; + if has_mcp_auth_handler + && let Err(error) = register_mcp_auth_interest(self, &session_id).await + { + pending_registration.cleanup(event_loop).await; + return Err(error); } tracing::debug!( @@ -1090,6 +1452,7 @@ impl Client { session_id = %session_id, "Client::create_session complete" ); + pending_registration.disarm(); let session = Session { id: session_id, cwd: self.cwd().clone(), @@ -1098,11 +1461,13 @@ impl Client { client: self.clone(), event_loop: ParkingLotMutex::new(Some(event_loop)), shutdown, + external_tools_shutdown, idle_waiter, capabilities, open_canvases, event_tx, github_token_registration: ParkingLotMutex::new(github_token_registration), + registration_token, }; apply_mode_post_create_patch( &session, @@ -1132,7 +1497,12 @@ impl Client { /// /// See [`Self::create_session`] for the defaults applied when callback /// fields are unset. - pub async fn resume_session(&self, mut config: ResumeSessionConfig) -> Result { + async fn start_prepared_resume( + &self, + mut config: ResumeSessionConfig, + event_tx: tokio::sync::broadcast::Sender, + shutdown: CancellationToken, + ) -> Result { let total_start = Instant::now(); let session_id = config.session_id.clone(); if config.hooks_handler.is_some() && config.hooks.is_none() { @@ -1254,11 +1624,12 @@ impl Client { let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default())); let setup_start = Instant::now(); - let channels = self.register_session(&session_id); + let registration = self.register_session(&session_id); + let registration_token = registration.token; + let channels = registration.channels; let idle_waiter = Arc::new(ParkingLotMutex::new(None)); let open_canvases = Arc::new(parking_lot::RwLock::new(Vec::new())); - let shutdown = CancellationToken::new(); - let (event_tx, _) = tokio::sync::broadcast::channel(512); + let external_tools_shutdown = self.inner.rpc.connection_closed_token(); let event_loop = spawn_event_loop( session_id.clone(), self.clone(), @@ -1275,9 +1646,15 @@ impl Client { open_canvases.clone(), event_tx.clone(), shutdown.clone(), + external_tools_shutdown.clone(), + ); + let mut registration = PendingSessionRegistration::new( + self.clone(), + session_id.clone(), + registration_token, + shutdown.clone(), + external_tools_shutdown.clone(), ); - let mut registration = - PendingSessionRegistration::new(self.clone(), session_id.clone(), shutdown.clone()); tracing::debug!( elapsed_ms = setup_start.elapsed().as_millis(), session_id = %session_id, @@ -1320,10 +1697,12 @@ impl Client { }) .into()); } - if has_mcp_auth_handler { - register_mcp_auth_interest(self, &session_id).await?; + if has_mcp_auth_handler + && let Err(error) = register_mcp_auth_interest(self, &session_id).await + { + registration.cleanup(event_loop).await; + return Err(error); } - // Reload skills after resume (best-effort). let skills_reload_start = Instant::now(); if let Err(e) = self @@ -1373,11 +1752,13 @@ impl Client { client: self.clone(), event_loop: ParkingLotMutex::new(Some(event_loop)), shutdown, + external_tools_shutdown, idle_waiter, capabilities, open_canvases, event_tx, github_token_registration: ParkingLotMutex::new(github_token_registration), + registration_token, }; apply_mode_post_create_patch( &session, @@ -1398,6 +1779,149 @@ impl Client { } } +/// A session that has been configured but not yet created on the CLI. +/// +/// Returned by [`Client::prepare_session`] and +/// [`Client::prepare_resume_session`]. Its purpose is to make the session's +/// event stream observable *before* any protocol activity starts: +/// [`subscribe`](Self::subscribe) installs a receiver on the same broadcast +/// channel the eventual [`Session`] uses, so events the runtime emits while +/// `session.create` / `session.resume` is still in flight are delivered +/// rather than dropped for lack of a receiver. +/// +/// # Lifecycle +/// +/// A prepared handle is inert. It holds only a broadcast sender, a +/// cancellation token, the client handle, and the config — it performs no +/// router registration, spawns no task, and writes nothing to the wire +/// until [`start`](Self::start) is first polled. +/// +/// * Dropping it without starting leaves no client-side or server-side +/// state, and closes every subscription taken from it. +/// * Dropping the [`start`](Self::start) future mid-flight cancels the +/// session token, unregisters the session from the router if it was +/// registered, and closes early subscriptions. A retry with the same +/// session ID succeeds. Cleanup of already-spawned tasks is signalled, +/// not awaited: `Drop` is synchronous and cannot await, so the event loop +/// terminates promptly but not synchronously. +/// * A startup error from [`start`](Self::start) performs the same cleanup +/// and preserves the [`ErrorKind`] the equivalent +/// [`Client::create_session`] / [`Client::resume_session`] call has always +/// returned. +/// +/// [`start`](Self::start) consumes `self` and the type is deliberately not +/// [`Clone`], so a prepared session can be started at most once and can +/// never produce two event loops. +/// +/// # Buffering +/// +/// The broadcast buffer is finite — +/// [`DEFAULT_EVENT_BUFFER_CAPACITY`] unless +/// [`SessionConfig::event_buffer_capacity`] / +/// [`ResumeSessionConfig::event_buffer_capacity`] overrides it. Subscribers +/// that fall behind observe +/// [`Lagged`](crate::subscription::Lagged) instead of applying backpressure +/// to the event loop. Consumers that need a lossless view of a large +/// startup burst must either configure a capacity that covers it or drain +/// the subscription concurrently with [`start`](Self::start). +/// +/// # Server-assigned session IDs +/// +/// For cloud sessions without a caller-supplied session ID, the CLI assigns +/// the ID and the SDK can only register the session on its notification +/// router once the `session.create` response arrives. Notifications the +/// server emits before that point are not routable to any session and are +/// therefore not observable. The guarantee this type provides is narrower +/// and precise: **routed** events are never dropped for lack of an +/// installed receiver. Pin +/// [`SessionConfig::session_id`](crate::types::SessionConfig::session_id) +/// to get registration before the RPC and full pre-response coverage. +#[must_use = "a PreparedSession does nothing until started"] +pub struct PreparedSession { + client: Client, + kind: PreparedKind, + event_tx: tokio::sync::broadcast::Sender, + shutdown: CancellationToken, +} + +/// Which startup path a [`PreparedSession`] runs when started. Boxed +/// because the two config types are large and differently sized. +enum PreparedKind { + Create(Box), + Resume(Box), +} + +impl PreparedSession { + fn new(client: Client, kind: PreparedKind, event_buffer_capacity: usize) -> Self { + let (event_tx, _) = tokio::sync::broadcast::channel(event_buffer_capacity); + Self { + client, + kind, + event_tx, + shutdown: CancellationToken::new(), + } + } + + /// Subscribe to this session's events before it starts. + /// + /// The returned [`EventSubscription`](crate::subscription::EventSubscription) + /// is backed by the same broadcast channel + /// [`Session::subscribe`] returns after [`start`](Self::start) + /// succeeds, so a subscription taken here observes the full event + /// stream from the session's first routed event onward — including + /// ephemeral events such as `session.idle` that + /// [`Session::get_messages`] cannot recover. + /// + /// May be called any number of times, and each subscriber receives its + /// own copy of the stream — subject to the buffering contract above. A + /// subscriber that falls further behind than the configured capacity + /// observes [`Lagged`](crate::subscription::Lagged) and skips the + /// events it missed, rather than stalling the session's event loop. + /// Subscriptions taken here close if the prepared session is dropped + /// without starting, or if startup fails. + pub fn subscribe(&self) -> crate::subscription::EventSubscription { + crate::subscription::EventSubscription::new(self.event_tx.subscribe()) + } + + /// Create or resume the session on the CLI. + /// + /// This is where all protocol activity happens: config validation, + /// router registration, the `session.create` / `session.resume` RPC, + /// and the event loop spawn. Nothing observable occurs until this + /// future is first polled. + /// + /// # Errors + /// + /// Returns the same errors as [`Client::create_session`] / + /// [`Client::resume_session`] — including + /// [`ErrorKind::InvalidConfig`] for invalid configs, transport and RPC + /// failures, and + /// [`SessionIdMismatch`](crate::SessionErrorKind::SessionIdMismatch) + /// when the CLI returns a different session ID than the one requested. + /// Every error path unregisters the session and closes subscriptions + /// taken from this handle. + pub async fn start(self) -> Result { + let Self { + client, + kind, + event_tx, + shutdown, + } = self; + match kind { + PreparedKind::Create(config) => { + client + .start_prepared_create(*config, event_tx, shutdown) + .await + } + PreparedKind::Resume(config) => { + client + .start_prepared_resume(*config, event_tx, shutdown) + .await + } + } + } +} + type CommandHandlerMap = HashMap>; async fn apply_mode_post_create_patch( @@ -1530,11 +2054,14 @@ fn spawn_event_loop( open_canvases: Arc>>, event_tx: tokio::sync::broadcast::Sender, shutdown: CancellationToken, + external_tools_shutdown: CancellationToken, ) -> JoinHandle<()> { let crate::router::SessionChannels { mut notifications, mut requests, } = channels; + let pending_external_tools: PendingExternalTools = + Arc::new(ParkingLotMutex::new(HashMap::new())); let span = tracing::error_span!("session_event_loop", session_id = %session_id); tokio::spawn( @@ -1567,7 +2094,7 @@ fn spawn_event_loop( _ = shutdown.cancelled() => break, Some(notification) = notifications.recv() => { handle_notification( - &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &open_canvases, &event_tx, + &session_id, &client, &handlers, &command_handlers, notification, &idle_waiter, &capabilities, &open_canvases, &event_tx, &shutdown, &external_tools_shutdown, &pending_external_tools, ).await; } Some(request) = requests.recv() => { @@ -1583,6 +2110,8 @@ fn spawn_event_loop( let canvas_handler = canvas_handler.clone(); let session_fs_provider = session_fs_provider.clone(); let bearer_token_providers = bearer_token_providers.clone(); + let request_id = request.id; + let method = request.method.clone(); tokio::spawn( async move { let ctx = RequestDispatchContext { @@ -1594,7 +2123,19 @@ fn spawn_event_loop( session_fs_provider: session_fs_provider.as_ref(), bearer_token_providers: &bearer_token_providers, }; - handle_request(&session_id, ctx, request).await; + let dispatch = handle_request(&session_id, ctx, request); + if AssertUnwindSafe(dispatch).catch_unwind().await.is_err() { + // Tokio isolates the panic to this task, so without a + // reply the CLI waits out its own timeout on this id. + error!(method = %method, "request handler panicked"); + let _ = send_error_response( + &client, + request_id, + error_codes::INTERNAL_ERROR, + "request handler panicked", + ) + .await; + } } .instrument(span), ); @@ -1719,6 +2260,9 @@ async fn handle_notification( capabilities: &Arc>, open_canvases: &Arc>>, event_tx: &tokio::sync::broadcast::Sender, + shutdown: &CancellationToken, + external_tools_shutdown: &CancellationToken, + pending_external_tools: &PendingExternalTools, ) { let dispatch_start = Instant::now(); let event = notification.event.clone(); @@ -1832,6 +2376,13 @@ async fn handle_notification( // Notification-based permission/tool/elicitation requests require a // separate RPC callback. Spawn concurrently since the CLI doesn't block. match event_type { + SessionEventType::ExternalToolCompleted => { + if let Some(request_id) = extract_request_id(¬ification.event.data) + && let Some(token) = pending_external_tools.lock().remove(&request_id) + { + token.cancel(); + } + } SessionEventType::PermissionRequested => { let Some(request_id) = extract_request_id(¬ification.event.data) else { return; @@ -1856,6 +2407,7 @@ async fn handle_notification( }; let client = client.clone(); let sid = session_id.clone(); + let shutdown = shutdown.clone(); let data = permission_request_data( ¬ification.event.data, handlers.managed_settings_enabled, @@ -1885,18 +2437,39 @@ async fn handle_notification( return; }; let rpc_start = Instant::now(); - let _ = client - .call( - rpc_methods::SESSION_PERMISSIONS_HANDLEPENDINGPERMISSIONREQUEST, - Some(params), - ) - .await; - tracing::debug!( - elapsed_ms = rpc_start.elapsed().as_millis(), - session_id = %sid, - request_id = %request_id, - "Session::handle_notification response sent successfully" - ); + let method = + rpc_methods::SESSION_PERMISSIONS_HANDLEPENDINGPERMISSIONREQUEST; + tokio::select! { + biased; + response = client.call(method, Some(params)) => { + match response { + Ok(_) => tracing::debug!( + elapsed_ms = rpc_start.elapsed().as_millis(), + session_id = %sid, + request_id = %request_id, + method, + "Session::handle_notification response sent successfully" + ), + Err(error) => warn!( + error = %error, + session_id = %sid, + request_id = %request_id, + method, + "failed to deliver permission decision back to the runtime" + ), + } + } + _ = shutdown.cancelled() => { + warn!( + elapsed_ms = rpc_start.elapsed().as_millis(), + session_id = %sid, + request_id = %request_id, + method, + delivery_outcome = "unknown", + "permission confirmation acknowledgement wait cancelled during session shutdown" + ); + } + } } .instrument(span), ); @@ -1953,8 +2526,19 @@ async fn handle_notification( let Some(tool_handler) = tool_handler else { return; }; + let cancellation = Arc::new(external_tools_shutdown.child_token()); + { + let mut pending = pending_external_tools.lock(); + if external_tools_shutdown.is_cancelled() || pending.contains_key(&request_id) { + return; + } + pending.insert(request_id.clone(), cancellation.clone()); + } let client = client.clone(); let sid = session_id.clone(); + let pending_external_tools = pending_external_tools.clone(); + let guard_request_id = request_id.clone(); + let guard_cancellation = cancellation.clone(); let span = tracing::error_span!( "external_tool_handler", session_id = %sid, @@ -1962,11 +2546,22 @@ async fn handle_notification( ); tokio::spawn( async move { + let guard = PendingExternalToolGuard { + request_id: guard_request_id, + token: guard_cancellation, + pending: pending_external_tools, + }; + if cancellation.is_cancelled() { + return; + } // `tool_name.is_empty()` would have produced a `None` // lookup in `handlers.tools` and short-circuited at the // outer guard above, so only the tool_call_id check is // reachable here. if data.tool_call_id.is_empty() { + if !guard.claim() { + return; + } let error_msg = "Missing toolCallId"; let rpc_start = Instant::now(); let _ = client @@ -1996,13 +2591,15 @@ async fn handle_notification( // call; a failed fetch leaves the snapshot `None` rather than // failing the tool. let available_tools = if tool_name == TOOL_SEARCH_TOOL_NAME { - match client - .call( + let metadata_result = tokio::select! { + biased; + _ = cancellation.cancelled() => return, + result = client.call( rpc_methods::SESSION_TOOLS_GETCURRENTMETADATA, Some(serde_json::json!({ "sessionId": sid })), - ) - .await - { + ) => result, + }; + match metadata_result { Ok(value) => { serde_json::from_value::(value) .ok() @@ -2025,9 +2622,13 @@ async fn handle_notification( tracestate: data.tracestate, }; let handler_start = Instant::now(); - let tool_result = match tool_handler.call(invocation).await { - Ok(r) => r, - Err(e) => tool_failure_result(e.to_string()), + let tool_result = tokio::select! { + biased; + _ = cancellation.cancelled() => return, + result = tool_handler.call(invocation) => match result { + Ok(r) => r, + Err(e) => tool_failure_result(e.to_string()), + }, }; tracing::debug!( elapsed_ms = handler_start.elapsed().as_millis(), @@ -2037,6 +2638,9 @@ async fn handle_notification( tool_name = %tool_name, "ToolHandler::call dispatch" ); + if !guard.claim() { + return; + } let result_value = serde_json::to_value(tool_result).unwrap_or(Value::Null); let rpc_start = Instant::now(); let _ = client diff --git a/rust/src/types.rs b/rust/src/types.rs index 89906f9627..a99f00a19f 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -21,6 +21,10 @@ pub use crate::copilot_request_handler::{ CopilotWebSocketResponse, WebSocketTransform, forward_http, }; use crate::generated::api_types::{CurrentToolMetadata, OpenCanvasInstance}; +/// Acknowledgement and Auto preference snapshot returned by an Auto tier switch. +pub use crate::generated::api_types::{ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus}; +/// Routing tier for the `auto` model with Auto mode V2. +pub use crate::generated::session_events::AutoTier; use crate::generated::session_events::ReasoningSummary; /// Context window tier for models that support tiered context windows. pub use crate::generated::session_events::{ContextTier, SessionLimitsConfig}; @@ -1417,6 +1421,19 @@ impl ProviderConfig { #[serde(rename_all = "camelCase")] #[non_exhaustive] pub struct CapiSessionOptions { + /// Routing tier, meaningful only with model `auto` (Auto mode V2). + /// Requires a runtime version that supports `capi.autoTier`. + /// + /// When omitted, the runtime chooses its default on create and restores + /// the last committed tier on cold resume. On resident resume, a different + /// tier requests a safe switch that takes effect after resume succeeds and + /// never disturbs a turn that is already running. + /// + /// To change the preference on a live session, use + /// [`Session::set_auto_tier`](crate::session::Session::set_auto_tier). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub auto_tier: Option, + /// Whether to use WebSocket transport for CAPI Responses API calls. /// /// When `Some(false)`, the runtime uses HTTP Responses transport even if @@ -1432,6 +1449,12 @@ impl CapiSessionOptions { Self::default() } + /// Set the routing tier for the `auto` model (Auto mode V2). + pub fn with_auto_tier(mut self, auto_tier: AutoTier) -> Self { + self.auto_tier = Some(auto_tier); + self + } + /// Set whether to use WebSocket transport for CAPI Responses API calls. pub fn with_enable_web_socket_responses(mut self, enable: bool) -> Self { self.enable_web_socket_responses = Some(enable); @@ -2248,6 +2271,23 @@ pub struct SessionConfig { /// `session.options.update` after create/resume. Defaults to `false` in /// [`crate::ClientMode::Empty`] when unset. pub manage_schedule_enabled: Option, + /// Capacity of the per-session broadcast buffer backing + /// [`Session::subscribe`](crate::session::Session::subscribe) and + /// [`PreparedSession::subscribe`](crate::session::PreparedSession::subscribe). + /// + /// Runtime-only — never sent on the wire. Defaults to + /// [`DEFAULT_EVENT_BUFFER_CAPACITY`](crate::session::DEFAULT_EVENT_BUFFER_CAPACITY) + /// when unset. Must be non-zero; + /// `Some(0)` is rejected with + /// [`ErrorKind::InvalidConfig`](crate::ErrorKind::InvalidConfig) by + /// [`Client::prepare_session`](crate::Client::prepare_session). + /// + /// The buffer is finite: subscribers that fall behind observe + /// [`Lagged`](crate::subscription::Lagged) rather than applying + /// backpressure to the event loop. Raise this when a consumer needs a + /// lossless view of a large startup burst without draining + /// concurrently. + pub event_buffer_capacity: Option, } impl std::fmt::Debug for SessionConfig { @@ -2383,6 +2423,7 @@ impl std::fmt::Debug for SessionConfig { "system_message_transform", &self.system_message_transform.as_ref().map(|_| ""), ) + .field("event_buffer_capacity", &self.event_buffer_capacity) .finish() } } @@ -2479,6 +2520,7 @@ impl Default for SessionConfig { enable_experimental_mode: None, coauthor_enabled: None, manage_schedule_enabled: None, + event_buffer_capacity: None, } } } @@ -3282,6 +3324,18 @@ impl SessionConfig { self } + /// Set [`Self::event_buffer_capacity`]. + /// + /// A capacity of `0` is rejected with + /// [`ErrorKind::InvalidConfig`](crate::ErrorKind::InvalidConfig) by + /// [`Client::prepare_session`](crate::Client::prepare_session) and + /// [`Client::create_session`](crate::Client::create_session); the value + /// is never clamped. + pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self { + self.event_buffer_capacity = Some(capacity); + self + } + /// Inject ExP assignment ("flight") data for this session, in the same /// JSON shape the Copilot CLI fetches from the experimentation service /// (`CopilotExpAssignmentResponse`). The runtime feeds it into the same @@ -3584,6 +3638,8 @@ pub struct ResumeSessionConfig { pub coauthor_enabled: Option, /// See [`SessionConfig::manage_schedule_enabled`]. pub manage_schedule_enabled: Option, + /// See [`SessionConfig::event_buffer_capacity`]. + pub event_buffer_capacity: Option, } impl std::fmt::Debug for ResumeSessionConfig { @@ -3717,6 +3773,7 @@ impl std::fmt::Debug for ResumeSessionConfig { ) .field("suppress_resume_event", &self.suppress_resume_event) .field("continue_pending_work", &self.continue_pending_work) + .field("event_buffer_capacity", &self.event_buffer_capacity) .finish() } } @@ -3968,6 +4025,7 @@ impl ResumeSessionConfig { enable_experimental_mode: None, coauthor_enabled: None, manage_schedule_enabled: None, + event_buffer_capacity: None, } } @@ -4579,6 +4637,18 @@ impl ResumeSessionConfig { self } + /// Set [`Self::event_buffer_capacity`]. + /// + /// A capacity of `0` is rejected with + /// [`ErrorKind::InvalidConfig`](crate::ErrorKind::InvalidConfig) by + /// [`Client::prepare_resume_session`](crate::Client::prepare_resume_session) + /// and [`Client::resume_session`](crate::Client::resume_session); the + /// value is never clamped. + pub fn with_event_buffer_capacity(mut self, capacity: usize) -> Self { + self.event_buffer_capacity = Some(capacity); + self + } + /// Inject ExP assignment ("flight") data on resume. See /// [`SessionConfig::with_exp_assignments`]. Re-supply the assignments on /// resume so the runtime re-applies them after a CLI process restart. @@ -4773,6 +4843,30 @@ pub struct SetModelOptions { /// fields set on the override are applied; the rest fall back to the /// runtime-resolved values for the model. pub model_capabilities: Option, + /// Auto routing preference to stage atomically with selecting the `auto` + /// model. + /// + /// Leave as `None` to leave the current preference alone. The runtime + /// rejects this option when the model is anything other than `auto`; use + /// [`Session::set_auto_tier`](crate::session::Session::set_auto_tier) to + /// change the preference without changing the selected model. + pub auto_tier: Option, +} + +/// Auto routing preference requested alongside a model switch. +/// +/// **Experimental.** Part of an experimental Auto routing surface and may change +/// or be removed in a future release. +/// +/// This is a three-state choice. Leaving [`SetModelOptions::auto_tier`] as +/// `None` leaves the current preference alone, which is different from +/// [`AutoTierPreference::Reset`], which actively resets it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum AutoTierPreference { + /// Route using a specific tier. + Tier(AutoTier), + /// Return to the provider's default Auto routing. + Reset, } impl SetModelOptions { @@ -4802,6 +4896,19 @@ impl SetModelOptions { self.model_capabilities = Some(caps); self } + + /// Set [`auto_tier`](Self::auto_tier) to a specific routing tier. + pub fn with_auto_tier(mut self, tier: AutoTier) -> Self { + self.auto_tier = Some(AutoTierPreference::Tier(tier)); + self + } + + /// Set [`auto_tier`](Self::auto_tier) to return to the provider's default + /// Auto routing. + pub fn with_reset_auto_tier(mut self) -> Self { + self.auto_tier = Some(AutoTierPreference::Reset); + self + } } /// Response from the top-level `ping` RPC. @@ -6034,9 +6141,9 @@ mod tests { use super::{ AgentMode, Attachment, AttachmentLineRange, AttachmentSelectionPosition, - AttachmentSelectionRange, AzureProviderOptions, CapiSessionOptions, ConnectionState, - CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, ExpConfigEntry, - ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, + AttachmentSelectionRange, AutoTier, AzureProviderOptions, CapiSessionOptions, + ConnectionState, CopilotExpAssignmentResponse, CustomAgentConfig, DeliveryMode, + ExpConfigEntry, ExpFlagValue, ExtensionInfo, GitHubMcpToolConfig, GitHubReferenceType, InfiniteSessionConfig, LargeToolOutputConfig, McpServerConfig, McpStdioServerConfig, MemoryConfiguration, NamedProviderConfig, PermissionResponseCapability, ProviderConfig, ProviderModelConfig, ReasoningSummary, ResumeSessionConfig, SessionConfig, SessionEvent, @@ -7322,6 +7429,56 @@ mod tests { let unset = CapiSessionOptions::new(); let wire_unset = serde_json::to_value(&unset).unwrap(); assert!(wire_unset.get("enableWebSocketResponses").is_none()); + assert!(wire_unset.get("autoTier").is_none()); + assert_eq!(wire_unset, json!({})); + } + + #[test] + fn capi_auto_tier_canonical_values_round_trip_and_forward() { + for (tier, value) in [ + (AutoTier::Efficiency, "efficiency"), + (AutoTier::Balance, "balance"), + (AutoTier::Intelligence, "intelligence"), + ] { + let exported: crate::AutoTier = tier.clone(); + let capi = CapiSessionOptions::new().with_auto_tier(exported); + assert_eq!(capi.auto_tier, Some(tier)); + assert_eq!( + serde_json::to_value(&capi).unwrap(), + json!({"autoTier": value}) + ); + assert_eq!( + serde_json::from_value::(json!({"autoTier": value})).unwrap(), + capi + ); + + let capi = capi.with_enable_web_socket_responses(false); + let expected = json!({"autoTier": value, "enableWebSocketResponses": false}); + let (create, _) = SessionConfig::default() + .with_model("auto") + .with_capi(capi.clone()) + .into_wire(Some(SessionId::from("capi-create"))) + .unwrap(); + assert_eq!(serde_json::to_value(create).unwrap()["capi"], expected); + + let (resume, _) = ResumeSessionConfig::new(SessionId::from("capi-resume")) + .with_capi(capi) + .into_wire() + .unwrap(); + assert_eq!(serde_json::to_value(resume).unwrap()["capi"], expected); + } + } + + #[test] + fn capi_auto_tier_accepts_unknown_values_for_forward_compatibility() { + for value in ["balanced", "Balance", "unknown"] { + assert_eq!( + serde_json::from_value::(json!(value)).unwrap(), + AutoTier::Unknown + ); + } + let capi: CapiSessionOptions = serde_json::from_value(json!({})).unwrap(); + assert_eq!(capi.auto_tier, None); } #[test] diff --git a/rust/tests/api_types_test.rs b/rust/tests/api_types_test.rs index 9b86b1367a..9429a2bb6f 100644 --- a/rust/tests/api_types_test.rs +++ b/rust/tests/api_types_test.rs @@ -5,9 +5,53 @@ use github_copilot_sdk::rpc::{ Extension, ExtensionList, ExtensionSource, ExtensionStatus, ExtensionsDisableRequest, - ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, TasksStartAgentRequest, + ExtensionsEnableRequest, FleetStartRequest, FleetStartResult, ModelSwitchAutoTierRequest, + ModelSwitchAutoTierResult, ModelSwitchAutoTierStatus, QueuePendingItems, QueuePendingItemsKind, + SandboxConfig, SendAgentMode, TasksStartAgentRequest, }; -use github_copilot_sdk::session_events::{PermissionRequest, PermissionRequestedData}; +use github_copilot_sdk::session_events::{ + PermissionRequest, PermissionRequestedData, SessionEventData, TypedSessionEvent, +}; +use github_copilot_sdk::{AutoTier, AutoTierPreference, SetModelOptions}; + +#[test] +fn session_events_deserialize_auto_tier() { + for event_type in ["session.start", "session.resume"] { + for (tier, wire_tier) in [ + (Some(AutoTier::Efficiency), Some("efficiency")), + (Some(AutoTier::Balance), Some("balance")), + (Some(AutoTier::Intelligence), Some("intelligence")), + (None, None), + ] { + let mut wire = serde_json::json!({ + "id": "11111111-1111-1111-1111-111111111111", + "timestamp": "2026-08-28T00:00:00Z", + "parentId": null, + "type": event_type, + "data": { + "sessionId": "test-session", "version": 1, + "producer": "copilot", "copilotVersion": "1.0.82-1", + "startTime": "2026-08-28T00:00:00Z", + "resumeTime": "2026-08-28T00:00:00Z", "eventCount": 1 + } + }); + if let Some(wire_tier) = wire_tier { + wire["data"]["autoTier"] = serde_json::json!(wire_tier); + } + let event: TypedSessionEvent = serde_json::from_value(wire).unwrap(); + let actual: Option = match event.payload { + SessionEventData::SessionStart(data) if event_type == "session.start" => { + data.auto_tier + } + SessionEventData::SessionResume(data) if event_type == "session.resume" => { + data.auto_tier + } + _ => panic!("expected {event_type}"), + }; + assert_eq!(actual, tier); + } + } +} #[test] fn extension_running_has_expected_status_and_source() { @@ -104,6 +148,67 @@ fn permission_event_exposes_managed_approval_required() { assert_eq!(request.managed_approval_required, Some(true)); } +#[test] +fn queue_pending_message_id_uses_camel_case_wire_name() { + let item = QueuePendingItems { + agent_mode: SendAgentMode::Interactive, + display_text: "second message".to_string(), + id: "batch-1".to_string(), + kind: QueuePendingItemsKind::Message, + message_id: Some("message-2".to_string()), + }; + + let serialized = serde_json::to_value(&item).unwrap(); + assert_eq!(serialized["id"], "batch-1"); + assert_eq!(serialized["messageId"], "message-2"); + + let deserialized: QueuePendingItems = serde_json::from_value(serialized).unwrap(); + assert_eq!(deserialized.message_id.as_deref(), Some("message-2")); +} + +#[test] +fn queue_pending_message_id_is_optional_for_older_hosts() { + let item: QueuePendingItems = serde_json::from_value(serde_json::json!({ + "agentMode": "interactive", + "displayText": "/model gpt-5", + "id": "command-1", + "kind": "command" + })) + .unwrap(); + + assert_eq!(item.message_id, None); + assert!( + serde_json::to_value(item) + .unwrap() + .get("messageId") + .is_none() + ); +} + +#[test] +fn sandbox_allow_bypass_round_trips_as_optional_camel_case() { + let mut enabled = SandboxConfig::default(); + enabled.enabled = true; + enabled.allow_bypass = Some(true); + let value = serde_json::to_value(enabled).unwrap(); + assert_eq!( + value, + serde_json::json!({ + "allowBypass": true, + "enabled": true, + }) + ); + let round_tripped: SandboxConfig = serde_json::from_value(value).unwrap(); + assert_eq!(round_tripped.allow_bypass, Some(true)); + + let mut omitted = SandboxConfig::default(); + omitted.enabled = true; + assert_eq!( + serde_json::to_value(omitted).unwrap(), + serde_json::json!({ "enabled": true }) + ); +} + fn running_extension(id: &str, name: &str) -> Extension { Extension { id: id.to_string(), @@ -117,3 +222,65 @@ fn running_extension(id: &str, name: &str) -> Extension { status: ExtensionStatus::Running, } } + +#[test] +fn switch_auto_tier_request_serializes_explicit_null_tier() { + // `autoTier` is a required field whose null value means "use provider-default + // routing", so it must survive serialization rather than being skipped. + let request = ModelSwitchAutoTierRequest { + auto_tier: None, + source: None, + }; + let wire = serde_json::to_value(&request).unwrap(); + + assert_eq!(wire.get("autoTier"), Some(&serde_json::Value::Null)); + assert!(wire.get("source").is_none()); +} + +#[test] +fn switch_auto_tier_request_serializes_each_tier() { + for (tier, expected) in [ + (AutoTier::Efficiency, "efficiency"), + (AutoTier::Balance, "balance"), + (AutoTier::Intelligence, "intelligence"), + ] { + let request = ModelSwitchAutoTierRequest { + auto_tier: Some(tier), + source: None, + }; + let wire = serde_json::to_value(&request).unwrap(); + assert_eq!(wire["autoTier"], serde_json::json!(expected)); + } +} + +#[test] +fn switch_auto_tier_result_deserializes_full_snapshot() { + let result: ModelSwitchAutoTierResult = serde_json::from_value(serde_json::json!({ + "status": "pending", + "effectiveAutoTier": "balance", + "pendingAutoTier": "intelligence", + "activatingAutoTier": null, + "supersededAutoTier": null + })) + .unwrap(); + + assert_eq!(result.status, ModelSwitchAutoTierStatus::Pending); + assert_eq!(result.effective_auto_tier, Some(AutoTier::Balance)); + assert_eq!(result.pending_auto_tier, Some(AutoTier::Intelligence)); + assert_eq!(result.activating_auto_tier, None); +} + +#[test] +fn set_model_options_distinguishes_unset_tier_from_reset() { + let untouched = SetModelOptions::default(); + assert_eq!(untouched.auto_tier, None); + + let explicit = SetModelOptions::default().with_auto_tier(AutoTier::Intelligence); + assert_eq!( + explicit.auto_tier, + Some(AutoTierPreference::Tier(AutoTier::Intelligence)) + ); + + let cleared = SetModelOptions::default().with_reset_auto_tier(); + assert_eq!(cleared.auto_tier, Some(AutoTierPreference::Reset)); +} diff --git a/rust/tests/cli_resolution_test.rs b/rust/tests/cli_resolution_test.rs index 75773a0c0d..846fbacd27 100644 --- a/rust/tests/cli_resolution_test.rs +++ b/rust/tests/cli_resolution_test.rs @@ -199,42 +199,33 @@ async fn extract_dir_runtime_override_is_honored() { /// Build-time version pins, when present, must match the selected bundling /// implementation's checksum format. -/// When absent, build.rs falls through to `../nodejs/package-lock.json` — +/// When absent, build.rs falls through to `../nodejs/package.json` and +/// the release's `SHA256SUMS.txt` — /// both are accepted, this test only checks the pin file's format if it's /// there. #[test] fn pin_file_when_present_is_well_formed() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); - let (filename, value_prefix) = if cfg!(feature = "bundled-in-process") { - ("cli-version-in-process.txt", Some("sha512-")) - } else { - ("cli-version.txt", None) - }; - let pin = PathBuf::from(manifest_dir).join(filename); - if !pin.is_file() { - // Contributor build path — no assertion needed. - return; - } - let contents = std::fs::read_to_string(&pin).expect("read CLI version snapshot"); - let mut saw_version = false; - let mut package_count = 0; - for raw in contents.lines() { - let line = raw.trim(); - if line.is_empty() || line.starts_with('#') { + for filename in ["cli-version.txt", "cli-version-in-process.txt"] { + let pin = PathBuf::from(manifest_dir).join(filename); + if !pin.is_file() { + // Contributor build path — no assertion needed. continue; } - let (key, value) = line - .split_once('=') - .unwrap_or_else(|| panic!("malformed line: {raw:?}")); - assert!(!value.trim().is_empty(), "empty value for key {key:?}"); - if key.trim() == "version" { - saw_version = true; - } else { - if let Some(prefix) = value_prefix { - assert!( - value.trim().starts_with(prefix), - "invalid npm integrity for key {key:?}" - ); + let contents = std::fs::read_to_string(&pin).expect("read CLI version snapshot"); + let mut saw_version = false; + let mut package_count = 0; + for raw in contents.lines() { + let line = raw.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + let (key, value) = line + .split_once('=') + .unwrap_or_else(|| panic!("malformed line: {raw:?}")); + assert!(!value.trim().is_empty(), "empty value for key {key:?}"); + if key.trim() == "version" { + saw_version = true; } else { assert_eq!( value.trim().len(), @@ -245,12 +236,15 @@ fn pin_file_when_present_is_well_formed() { value.trim().bytes().all(|byte| byte.is_ascii_hexdigit()), "invalid SHA-256 hash for key {key:?}" ); + package_count += 1; } - package_count += 1; } + assert!(saw_version, "{filename} missing `version=` line"); + assert_eq!( + package_count, 8, + "{filename} has incomplete platform hashes" + ); } - assert!(saw_version, "{filename} missing `version=` line"); - assert_eq!(package_count, 6); } /// With `bundled-cli` on AND a supported target, `install_bundled_cli` @@ -280,26 +274,38 @@ fn install_bundled_cli_returns_extracted_path() { first, second, "install_bundled_cli must be idempotent across calls" ); +} - #[cfg(feature = "bundled-in-process")] - { - let runtime_name = if cfg!(windows) { - "copilot_runtime.dll" - } else if cfg!(target_os = "macos") { - "libcopilot_runtime.dylib" - } else { - "libcopilot_runtime.so" - }; - let runtime = first - .parent() - .expect("install directory") - .join(runtime_name); - assert!( - runtime.is_file(), - "bundled runtime library was not installed: {}", - runtime.display() - ); - } +#[cfg(all(feature = "bundled-cli", has_bundled_cli))] +#[test] +fn bundled_cli_is_distinct_from_runtime_and_supports_version_probe() { + let cli = install_bundled_cli().expect("bundled CLI should install"); + let runtime = install_bundled_runtime().expect("bundled runtime should install"); + + assert_ne!(cli, runtime); + assert_ne!( + std::fs::metadata(&cli).expect("CLI metadata").len(), + std::fs::metadata(&runtime) + .expect("runtime wrapper metadata") + .len(), + "the full CLI must not alias the runtime wrapper bytes" + ); + + let output = std::process::Command::new(&cli) + .arg("--binary-version") + .output() + .expect("run bundled CLI version probe"); + assert!( + output.status.success(), + "bundled CLI version probe failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + String::from_utf8_lossy(&output.stdout).contains(env!("COPILOT_SDK_CLI_VERSION")), + "bundled CLI version output did not contain {}: {}", + env!("COPILOT_SDK_CLI_VERSION"), + String::from_utf8_lossy(&output.stdout) + ); } /// With `bundled-cli` off (or the target unsupported), the public API @@ -336,6 +342,24 @@ fn install_bundled_runtime_returns_wrapper_bundle() { "runtime.node was not installed: {}", runtime_node.display() ); + #[cfg(feature = "bundled-in-process")] + { + let runtime_library = first + .parent() + .expect("install directory") + .join(if cfg!(windows) { + "copilot_runtime.dll" + } else if cfg!(target_os = "macos") { + "libcopilot_runtime.dylib" + } else { + "libcopilot_runtime.so" + }); + assert!( + runtime_library.is_file(), + "bundled runtime library was not installed: {}", + runtime_library.display() + ); + } let second = install_bundled_runtime().expect("second call should also succeed"); assert_eq!(first, second); } diff --git a/rust/tests/e2e.rs b/rust/tests/e2e.rs index 03723dfb1b..9d1c868fe9 100644 --- a/rust/tests/e2e.rs +++ b/rust/tests/e2e.rs @@ -5,6 +5,8 @@ mod abort; #[path = "e2e/ask_user.rs"] mod ask_user; +#[path = "e2e/auto_tier.rs"] +mod auto_tier; #[path = "e2e/builtin_tools.rs"] mod builtin_tools; #[path = "e2e/byok_bearer_token_provider.rs"] @@ -31,6 +33,8 @@ mod elicitation; mod error_resilience; #[path = "e2e/event_fidelity.rs"] mod event_fidelity; +#[path = "e2e/external_tool_cancellation.rs"] +mod external_tool_cancellation; #[path = "e2e/github_telemetry.rs"] mod github_telemetry; #[path = "e2e/hooks.rs"] diff --git a/rust/tests/e2e/auto_tier.rs b/rust/tests/e2e/auto_tier.rs new file mode 100644 index 0000000000..85c70dd460 --- /dev/null +++ b/rust/tests/e2e/auto_tier.rs @@ -0,0 +1,140 @@ +use github_copilot_sdk::SetModelOptions; +use github_copilot_sdk::rpc::ModelSwitchAutoTierStatus; +use github_copilot_sdk::session::Session; +use github_copilot_sdk::session_events::AutoTier; + +use super::support::with_dedicated_e2e_context; + +const MODEL_ID: &str = "auto"; + +/// End-to-end coverage for staging and resetting an Auto routing preference +/// (snapshot category "auto_tier"). +/// +/// The runtime stages an Auto routing preference instead of applying it immediately: a +/// request stays unclaimed until a later turn using the `auto` model mints a usable model +/// and token pair. These tests observe that staged state through `model().get_current()`, +/// so they assert what the runtime actually recorded rather than what the SDK serialized. +async fn pending_auto_tier(session: &Session) -> Option { + session + .rpc() + .model() + .get_current() + .await + .expect("get current model") + .pending_auto_tier +} + +#[tokio::test] +async fn should_stage_and_reset_auto_tier_preference() { + with_dedicated_e2e_context( + "auto_tier", + "should_stage_and_reset_auto_tier_preference", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let session = client + .create_session(ctx.approve_all_session_config().with_model(MODEL_ID)) + .await + .expect("create session"); + + assert_eq!(pending_auto_tier(&session).await, None); + + let staged = session + .set_auto_tier(Some(AutoTier::Efficiency)) + .await + .expect("stage efficiency"); + assert_eq!(staged.status, ModelSwitchAutoTierStatus::Pending); + assert_eq!(staged.pending_auto_tier, Some(AutoTier::Efficiency)); + assert_eq!( + pending_auto_tier(&session).await, + Some(AutoTier::Efficiency) + ); + + // A second request replaces the first and reports the one it displaced. + let superseded = session + .set_auto_tier(Some(AutoTier::Intelligence)) + .await + .expect("stage intelligence"); + assert_eq!(superseded.status, ModelSwitchAutoTierStatus::Pending); + assert_eq!(superseded.pending_auto_tier, Some(AutoTier::Intelligence)); + assert_eq!(superseded.superseded_auto_tier, Some(AutoTier::Efficiency)); + assert_eq!( + pending_auto_tier(&session).await, + Some(AutoTier::Intelligence) + ); + + // `None` returns the session to provider-default routing. The status is + // `Unchanged` because provider-default was already the committed + // preference; the request's effect is cancelling the staged one. + let reset = session.set_auto_tier(None).await.expect("reset tier"); + assert_eq!(reset.status, ModelSwitchAutoTierStatus::Unchanged); + assert_eq!(reset.superseded_auto_tier, Some(AutoTier::Intelligence)); + assert_eq!(pending_auto_tier(&session).await, None); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +#[tokio::test] +async fn should_preserve_auto_tier_when_set_model_omits_it() { + with_dedicated_e2e_context( + "auto_tier", + "should_preserve_auto_tier_when_set_model_omits_it", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let session = client + .create_session(ctx.approve_all_session_config().with_model(MODEL_ID)) + .await + .expect("create session"); + + session + .set_auto_tier(Some(AutoTier::Balance)) + .await + .expect("stage balance"); + assert_eq!(pending_auto_tier(&session).await, Some(AutoTier::Balance)); + + // Omitting the preference leaves the staged one alone. + session + .set_model(MODEL_ID, None) + .await + .expect("set model without a tier"); + assert_eq!(pending_auto_tier(&session).await, Some(AutoTier::Balance)); + + // Supplying a tier replaces it. + session + .set_model( + MODEL_ID, + Some(SetModelOptions::default().with_auto_tier(AutoTier::Intelligence)), + ) + .await + .expect("set model with a tier"); + assert_eq!( + pending_auto_tier(&session).await, + Some(AutoTier::Intelligence) + ); + + // Requesting a reset clears it. Omission, a tier, and a reset are three + // distinct outcomes, which `AutoTierPreference` makes explicit. + session + .set_model( + MODEL_ID, + Some(SetModelOptions::default().with_reset_auto_tier()), + ) + .await + .expect("set model with a reset"); + assert_eq!(pending_auto_tier(&session).await, None); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} diff --git a/rust/tests/e2e/client.rs b/rust/tests/e2e/client.rs index 880c1a28c4..3abedce1a8 100644 --- a/rust/tests/e2e/client.rs +++ b/rust/tests/e2e/client.rs @@ -120,7 +120,7 @@ async fn should_list_models_when_authenticated() { let models = client.list_models().await.expect("list models"); assert!( - models.iter().any(|model| model.id == "claude-sonnet-4.5"), + models.iter().any(|model| model.id == "claude-sonnet-5"), "expected default replay model in {models:?}" ); diff --git a/rust/tests/e2e/client_lifecycle.rs b/rust/tests/e2e/client_lifecycle.rs index 75646b4860..92bfa6ff6d 100644 --- a/rust/tests/e2e/client_lifecycle.rs +++ b/rust/tests/e2e/client_lifecycle.rs @@ -1,3 +1,5 @@ +#[cfg(windows)] +use github_copilot_sdk::CliProgram; use github_copilot_sdk::SessionLifecycleEventType; use serde_json::json; @@ -135,6 +137,151 @@ async fn dispose_disconnects_client_and_disposes_rpc_surface_drop() { .await; } +// This test represents github/app#2303: the SDK-hosting GitHub Copilot app +// process exits abruptly, so Client cleanup never runs. The helper starts a +// real CLI client, is terminated through `TerminateProcess`, and relies only +// on Job Object kill-on-close behavior to terminate the CLI. +#[cfg(windows)] +#[tokio::test] +async fn abrupt_host_termination_still_kills_cli_via_job_object() { + with_e2e_context( + "client_lifecycle", + "abrupt_host_termination_still_kills_cli_via_job_object", + |ctx| { + Box::pin(async move { + let options = ctx.client_options(); + let program = match &options.program { + CliProgram::Path(path) => path + .to_str() + .expect("CLI program path is valid UTF-8") + .to_owned(), + CliProgram::Resolve => { + panic!("E2E client options should resolve to an explicit CLI path") + } + }; + let prefix_args: Vec = options + .prefix_args + .iter() + .map(|arg| arg.to_str().expect("prefix arg is valid UTF-8").to_owned()) + .collect(); + let env_pairs: Vec<(String, String)> = options + .env + .iter() + .map(|(k, v)| { + ( + k.to_str().expect("env key is valid UTF-8").to_owned(), + v.to_str().expect("env value is valid UTF-8").to_owned(), + ) + }) + .collect(); + let cwd = options + .working_directory + .to_str() + .expect("cwd is valid UTF-8") + .to_owned(); + let pid_file = ctx.work_dir().join("host-crash-fixture-cli.pid"); + + let mut host = + std::process::Command::new(env!("CARGO_BIN_EXE_copilot-host-crash-fixture")) + .env("HOST_CRASH_FIXTURE_PROGRAM", &program) + .env( + "HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON", + serde_json::to_string(&prefix_args).expect("serialize prefix args"), + ) + .env("HOST_CRASH_FIXTURE_CWD", &cwd) + .env( + "HOST_CRASH_FIXTURE_ENV_JSON", + serde_json::to_string(&env_pairs).expect("serialize env pairs"), + ) + .env("HOST_CRASH_FIXTURE_PID_FILE", &pid_file) + .spawn() + .expect("spawn host-crash fixture process"); + + let cli_pid = wait_for_pid_file_windows(&pid_file).await; + assert!( + process_alive_windows(cli_pid), + "CLI should be alive before its host process is terminated" + ); + + // `Child::kill` maps to `TerminateProcess`, which runs none + // of the target process's cleanup code. + host.kill().expect("terminate host-crash fixture process"); + host.wait().expect("reap host-crash fixture process"); + + let cli_exited = wait_for_process_exit_windows(cli_pid).await; + if !cli_exited { + kill_process_windows(cli_pid); + } + assert!( + cli_exited, + "CLI survived its abruptly terminated host process; Job Object \ + kill-on-close did not terminate it" + ); + }) + }, + ) + .await; +} + +#[cfg(windows)] +async fn wait_for_pid_file_windows(path: &std::path::Path) -> u32 { + super::support::wait_for_condition("host-crash fixture CLI pid file", || async { + path.exists() + }) + .await; + std::fs::read_to_string(path) + .expect("read host-crash fixture CLI pid") + .trim() + .parse() + .expect("parse host-crash fixture CLI pid") +} + +#[cfg(windows)] +async fn wait_for_process_exit_windows(pid: u32) -> bool { + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15); + while process_alive_windows(pid) { + if std::time::Instant::now() >= deadline { + return false; + } + tokio::time::sleep(std::time::Duration::from_millis(25)).await; + } + true +} + +#[cfg(windows)] +fn process_alive_windows(pid: u32) -> bool { + use windows_sys::Win32::Foundation::{CloseHandle, WAIT_TIMEOUT}; + use windows_sys::Win32::System::Threading::{ + OpenProcess, PROCESS_SYNCHRONIZE, WaitForSingleObject, + }; + + // SAFETY: the process handle is closed before returning. + unsafe { + let process = OpenProcess(PROCESS_SYNCHRONIZE, 0, pid); + if process.is_null() { + return false; + } + let alive = WaitForSingleObject(process, 0) == WAIT_TIMEOUT; + CloseHandle(process); + alive + } +} + +#[cfg(windows)] +fn kill_process_windows(pid: u32) { + use windows_sys::Win32::Foundation::CloseHandle; + use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess}; + + // SAFETY: the pid came from this test's controlled fixture-spawned CLI. + unsafe { + let process = OpenProcess(PROCESS_TERMINATE, 0, pid); + if !process.is_null() { + TerminateProcess(process, 1); + CloseHandle(process); + } + } +} + #[tokio::test] async fn should_receive_session_updated_lifecycle_event_for_non_ephemeral_activity() { with_e2e_context( diff --git a/rust/tests/e2e/client_options.rs b/rust/tests/e2e/client_options.rs index fc1ceebb83..51880803d3 100644 --- a/rust/tests/e2e/client_options.rs +++ b/rust/tests/e2e/client_options.rs @@ -27,7 +27,7 @@ async fn should_forward_advanced_session_creation_options_to_the_cli() { SessionConfig::default() .with_session_id("advanced-session-id") .with_client_name("rust-sdk-e2e-client") - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_reasoning_effort("low") .with_reasoning_summary(ReasoningSummary::None) .with_context_tier("long_context") @@ -90,7 +90,7 @@ async fn should_forward_advanced_session_creation_options_to_the_cli() { [ ("sessionId", json!("advanced-session-id")), ("clientName", json!("rust-sdk-e2e-client")), - ("model", json!("claude-sonnet-4.5")), + ("model", json!("claude-sonnet-5")), ("reasoningEffort", json!("low")), ("reasoningSummary", json!("none")), ("contextTier", json!("long_context")), @@ -491,6 +491,10 @@ function handleMessage(message) { writeResponse(message.id, { success: true }); return; } + if (message.method === "session.detach") { + writeResponse(message.id, { success: true }); + return; + } writeResponse(message.id, {}); } diff --git a/rust/tests/e2e/copilot_request_handler.rs b/rust/tests/e2e/copilot_request_handler.rs index 46b4e510cd..478845f48e 100644 --- a/rust/tests/e2e/copilot_request_handler.rs +++ b/rust/tests/e2e/copilot_request_handler.rs @@ -101,8 +101,8 @@ fn sse(event_type: &str, data: &Value) -> String { fn model_catalog(supported_endpoints: Option<&[&str]>) -> String { let mut model = json!({ - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -110,7 +110,7 @@ fn model_catalog(supported_endpoints: Option<&[&str]>) -> String { "model_picker_enabled": true, "capabilities": { "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": { "max_context_window_tokens": 200000, @@ -232,7 +232,7 @@ fn synth_inference_response(url: &str, body: &[u8], text: &str) -> CopilotHttpRe "id": "chatcmpl-stub-1", "object": "chat.completion.chunk", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", }) }; let mut c1 = base(); @@ -257,7 +257,7 @@ fn synth_inference_response(url: &str, body: &[u8], text: &str) -> CopilotHttpRe "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "choices": [{ "index": 0, "message": { "role": "assistant", "content": text }, @@ -668,14 +668,14 @@ async fn threads_session_id_into_inference() { let before = handler.inference_records().len(); let byok_config = SessionConfig::default() .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_provider( ProviderConfig::new("https://byok.invalid/v1") .with_provider_type("openai") .with_wire_api("responses") .with_api_key("byok-secret") - .with_model_id("claude-sonnet-4.5") - .with_wire_model("claude-sonnet-4.5"), + .with_model_id("claude-sonnet-5") + .with_wire_model("claude-sonnet-5"), ); let byok_session = client .create_session(byok_config) diff --git a/rust/tests/e2e/external_tool_cancellation.rs b/rust/tests/e2e/external_tool_cancellation.rs new file mode 100644 index 0000000000..eb9b1b6650 --- /dev/null +++ b/rust/tests/e2e/external_tool_cancellation.rs @@ -0,0 +1,123 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use github_copilot_sdk::handler::ApproveAllHandler; +use github_copilot_sdk::tool::ToolHandler; +use github_copilot_sdk::{Error, SessionConfig, Tool, ToolInvocation, ToolResult}; +use serde_json::json; +use tokio::sync::{Mutex, mpsc, oneshot}; +use tokio::time::{Duration, timeout}; + +use super::support::DEFAULT_TEST_TOKEN; + +#[tokio::test] +async fn should_cancel_tool_handler_when_session_disconnects() { + super::support::with_dedicated_e2e_context( + "external_tool_cancellation", + "should_cancel_tool_handler_when_session_disconnects", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let client = ctx.start_client().await; + let (started_tx, mut started_rx) = mpsc::unbounded_channel(); + let (release_tx, release_rx) = oneshot::channel(); + let (cancelled_tx, cancelled_rx) = oneshot::channel(); + let tool = Arc::new(CancelAwareSlowTool { + started_tx, + release_rx: Mutex::new(Some(release_rx)), + cancelled_tx: Mutex::new(Some(cancelled_tx)), + }); + + let session = client + .create_session( + SessionConfig::default() + .with_github_token(DEFAULT_TEST_TOKEN) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_tools(vec![ + Tool::new("slow_analysis") + .with_description( + "A slow analysis tool that blocks until released", + ) + .with_parameters(json!({ + "type": "object", + "properties": { + "value": { + "type": "string", + "description": "Value to analyze" + } + }, + "required": ["value"] + })) + .with_handler(tool), + ]), + ) + .await + .expect("create session"); + + session + .send("Use slow_analysis with value 'test_abort'. Wait for the result.") + .await + .expect("send tool turn"); + + let started_value = timeout(Duration::from_secs(60), started_rx.recv()) + .await + .expect("tool start wait timed out") + .expect("tool start channel closed"); + assert_eq!(started_value, "test_abort"); + + session.disconnect().await.expect("disconnect session"); + timeout(Duration::from_secs(60), cancelled_rx) + .await + .expect("tool cancellation wait timed out") + .expect("tool cancellation sender dropped"); + + let _ = release_tx.send("RELEASED".to_string()); + client.stop().await.expect("stop client"); + }) + }, + ) + .await; +} + +struct CancelAwareSlowTool { + started_tx: mpsc::UnboundedSender, + release_rx: Mutex>>, + cancelled_tx: Mutex>>, +} + +struct CancelSignalGuard { + cancelled_tx: Option>, +} + +impl Drop for CancelSignalGuard { + fn drop(&mut self) { + if let Some(sender) = self.cancelled_tx.take() { + let _ = sender.send(()); + } + } +} + +#[async_trait] +impl ToolHandler for CancelAwareSlowTool { + async fn call(&self, invocation: ToolInvocation) -> Result { + let value = invocation + .arguments + .get("value") + .and_then(serde_json::Value::as_str) + .unwrap_or_default() + .to_string(); + let _ = self.started_tx.send(value); + + let cancelled_tx = self.cancelled_tx.lock().await.take(); + let _guard = CancelSignalGuard { cancelled_tx }; + + let release_rx = self + .release_rx + .lock() + .await + .take() + .expect("slow tool called once"); + let released = release_rx.await.unwrap_or_else(|_| "released".to_string()); + Ok(ToolResult::Text(released)) + } +} diff --git a/rust/tests/e2e/mcp_oauth.rs b/rust/tests/e2e/mcp_oauth.rs index fb202536c7..0930721b88 100644 --- a/rust/tests/e2e/mcp_oauth.rs +++ b/rust/tests/e2e/mcp_oauth.rs @@ -53,6 +53,12 @@ async fn should_satisfy_mcp_oauth_using_host_provided_token() { .await .expect("create session"); + session + .rpc() + .mcp() + .reload() + .await + .expect("reload MCP servers"); wait_for_mcp_server_status(&session, server_name, McpServerStatus::Connected).await; let tools = session .rpc() @@ -142,15 +148,27 @@ async fn should_request_replacement_tokens_across_mcp_oauth_lifecycle() { .await .expect("create session"); + session + .rpc() + .mcp() + .reload() + .await + .expect("reload MCP servers"); wait_for_mcp_server_status(&session, server_name, McpServerStatus::Connected).await; call_whoami(&session, server_name, "refresh").await; call_whoami(&session, server_name, "upscope").await; call_whoami(&session, server_name, "reauth").await; + let replacement_reasons = handler + .reasons + .lock() + .iter() + .filter(|reason| **reason != McpOauthRequestReason::Initial) + .cloned() + .collect::>(); assert_eq!( - handler.reasons.lock().as_slice(), + replacement_reasons, [ - McpOauthRequestReason::Initial, McpOauthRequestReason::Refresh, McpOauthRequestReason::Upscope, McpOauthRequestReason::Refresh, @@ -207,15 +225,14 @@ async fn should_cancel_pending_mcp_oauth_request() { .await .expect("create session"); + session + .rpc() + .mcp() + .reload() + .await + .expect("reload MCP servers"); wait_for_mcp_server_status(&session, server_name, McpServerStatus::NeedsAuth).await; - // The MCP connection is kicked off by session.create, but the SDK only registers its - // `mcp.oauth_required` event interest once create returns. If the server's initial 401 - // wins that race, the runtime records `needs-auth` WITHOUT invoking the host callback, - // so `handler.request` is briefly `None` even after `needs-auth` is observed. A later - // auth retry (now that interest is registered) invokes the callback with the same - // `Initial` reason. Wait for the callback rather than sampling it the instant - // `needs-auth` first appears, which is what made this test flaky. wait_for_condition("MCP OAuth request reaching the host callback", || async { handler.request.lock().is_some() }) @@ -239,6 +256,11 @@ async fn should_cancel_pending_mcp_oauth_request() { #[tokio::test] async fn should_resolve_pending_mcp_oauth_request_through_rpc() { + if super::support::skip_inprocess( + "blocked on github/copilot-agent-runtime#18961 MCP OAuth connection stall", + ) { + return; + } with_e2e_context_no_snapshot(|ctx| { Box::pin(async move { ctx.set_default_copilot_user(); diff --git a/rust/tests/e2e/rewind.rs b/rust/tests/e2e/rewind.rs index 44d6a39404..485c389ece 100644 --- a/rust/tests/e2e/rewind.rs +++ b/rust/tests/e2e/rewind.rs @@ -28,7 +28,7 @@ async fn should_restore_tracked_file_and_conversation() { let session = client .create_session( ctx.approve_all_session_config() - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_enable_file_change_tracking(true), ) .await diff --git a/rust/tests/e2e/rpc_additional_edge_cases.rs b/rust/tests/e2e/rpc_additional_edge_cases.rs index 56b9198ab7..26eb591f4f 100644 --- a/rust/tests/e2e/rpc_additional_edge_cases.rs +++ b/rust/tests/e2e/rpc_additional_edge_cases.rs @@ -1,6 +1,6 @@ use github_copilot_sdk::rpc::{ ModeSetRequest, NameSetRequest, PermissionsResetSessionApprovalsRequest, - PermissionsSetApproveAllRequest, PlanUpdateRequest, ShellExecRequest, + PermissionsSetApproveAllRequest, PlanUpdateRequest, ShellExecRequest, ShellKillRequest, WorkspacesCreateFileRequest, WorkspacesReadFileRequest, }; use github_copilot_sdk::session_events::SessionMode; @@ -39,6 +39,16 @@ async fn shell_exec_with_zero_timeout_does_not_kill_long_running_command() { marker_path.exists() }) .await; + let killed = session + .rpc() + .shell() + .kill(ShellKillRequest { + process_id: result.process_id, + signal: None, + }) + .await + .expect("kill zero-timeout shell process"); + assert!(killed.killed); session.disconnect().await.expect("disconnect session"); client.stop().await.expect("stop client"); @@ -547,7 +557,7 @@ async fn workspaces_getworkspace_returns_stable_result_across_calls() { #[cfg(windows)] fn delayed_marker_command(marker_path: &std::path::Path) -> String { format!( - "powershell -NoLogo -NoProfile -Command \"Start-Sleep -Seconds 2; Set-Content -LiteralPath '{}' -Value done\"", + "ping 127.0.0.1 -n 3 >nul & echo done>\"{}\" & ping 127.0.0.1 -n 61 >nul", marker_path.display() ) } @@ -555,7 +565,7 @@ fn delayed_marker_command(marker_path: &std::path::Path) -> String { #[cfg(not(windows))] fn delayed_marker_command(marker_path: &std::path::Path) -> String { format!( - "sh -c \"sleep 2; printf done > '{}'\"", + "sh -c \"sleep 2; printf done > '{}'; sleep 60\"", marker_path.display() ) } diff --git a/rust/tests/e2e/rpc_mcp_config.rs b/rust/tests/e2e/rpc_mcp_config.rs index 591d7d247c..29f5f42d39 100644 --- a/rust/tests/e2e/rpc_mcp_config.rs +++ b/rust/tests/e2e/rpc_mcp_config.rs @@ -18,6 +18,7 @@ async fn should_call_server_mcp_config_rpcs() { let _ = config .remove(McpConfigRemoveRequest { name: server_name.to_string(), + auth_client_id_metadata_url: None, }) .await; @@ -74,6 +75,7 @@ async fn should_call_server_mcp_config_rpcs() { config .remove(McpConfigRemoveRequest { name: server_name.to_string(), + auth_client_id_metadata_url: None, }) .await .expect("remove"); @@ -102,6 +104,7 @@ async fn should_round_trip_http_mcp_oauth_config_rpc() { let _ = config .remove(McpConfigRemoveRequest { name: server_name.to_string(), + auth_client_id_metadata_url: None, }) .await; @@ -197,6 +200,7 @@ async fn should_round_trip_http_mcp_oauth_config_rpc() { config .remove(McpConfigRemoveRequest { name: server_name.to_string(), + auth_client_id_metadata_url: None, }) .await .expect("remove"); diff --git a/rust/tests/e2e/rpc_server.rs b/rust/tests/e2e/rpc_server.rs index 8df662ea8e..2e80ae1d70 100644 --- a/rust/tests/e2e/rpc_server.rs +++ b/rust/tests/e2e/rpc_server.rs @@ -70,7 +70,7 @@ async fn should_call_rpc_models_list_with_typed_result() { result .models .iter() - .any(|model| model.id == "claude-sonnet-4.5") + .any(|model| model.id == "claude-sonnet-5") ); assert!(result.models.iter().all(|model| !model.name.is_empty())); client.stop().await.expect("stop client"); diff --git a/rust/tests/e2e/rpc_session_state.rs b/rust/tests/e2e/rpc_session_state.rs index c723d10f5e..e6d6a2b546 100644 --- a/rust/tests/e2e/rpc_session_state.rs +++ b/rust/tests/e2e/rpc_session_state.rs @@ -27,7 +27,7 @@ use super::support::{ assistant_message_content, recv_with_timeout, wait_for_condition, wait_for_event, }; -const MODEL_ID: &str = "claude-sonnet-4.5"; +const MODEL_ID: &str = "claude-sonnet-5"; #[tokio::test] async fn should_call_session_rpc_model_getcurrent() { diff --git a/rust/tests/e2e/rpc_session_state_extras.rs b/rust/tests/e2e/rpc_session_state_extras.rs index 81901d06ae..e764c53933 100644 --- a/rust/tests/e2e/rpc_session_state_extras.rs +++ b/rust/tests/e2e/rpc_session_state_extras.rs @@ -12,7 +12,7 @@ use github_copilot_sdk::session_events::PermissionMode; use super::support::{assistant_message_content, with_e2e_context}; -const MODEL_ID: &str = "claude-sonnet-4.5"; +const MODEL_ID: &str = "claude-sonnet-5"; #[tokio::test] async fn should_list_models_for_session() { @@ -495,6 +495,7 @@ async fn should_update_and_clear_live_subagent_settings() { ), effort_level: Some("low".to_string()), model: Some("gpt-5-mini".to_string()), + model_policy: None, }, )])), disabled_subagents: Some(vec!["legacy-agent".to_string()]), diff --git a/rust/tests/e2e/rpc_shell_edge_cases.rs b/rust/tests/e2e/rpc_shell_edge_cases.rs index df5ddb1dc7..3d1717723a 100644 --- a/rust/tests/e2e/rpc_shell_edge_cases.rs +++ b/rust/tests/e2e/rpc_shell_edge_cases.rs @@ -308,9 +308,9 @@ async fn wait_for_file_text(path: &Path, expected: &'static str) { async fn wait_for_process_cleanup( session: &github_copilot_sdk::session::Session, process_id: String, - _scenario: &'static str, + scenario: &'static str, ) { - tokio::time::sleep(std::time::Duration::from_secs(1)).await; + tokio::time::sleep(std::time::Duration::from_secs(5)).await; let result = session .rpc() .shell() @@ -320,7 +320,10 @@ async fn wait_for_process_cleanup( }) .await .expect("probe process cleanup"); - assert!(!result.killed); + assert!( + !result.killed, + "{scenario} should have already exited and been removed from the runtime process map" + ); } #[cfg(windows)] @@ -386,7 +389,7 @@ fn nonexistent_command() -> String { #[cfg(windows)] fn stderr_command(marker_path: &Path) -> String { format!( - "powershell -NoLogo -NoProfile -Command \"[Console]::Error.WriteLine('boom'); Set-Content -LiteralPath '{}' -Value done; exit 2\"", + "powershell -NoLogo -NoProfile -Command \"[Console]::Error.WriteLine('boom'); exit 2\" & echo done > \"{}\" & exit /b 2", marker_path.display() ) } @@ -402,7 +405,7 @@ fn stderr_command(marker_path: &Path) -> String { #[cfg(windows)] fn large_stdout_command(marker_path: &Path) -> String { format!( - "powershell -NoLogo -NoProfile -Command \"Write-Host ('x' * 204800); Set-Content -LiteralPath '{}' -Value done\"", + "powershell -NoLogo -NoProfile -Command \"Write-Host ('x' * 71680); Set-Content -LiteralPath '{}' -Value done\"", marker_path.display() ) } @@ -410,7 +413,7 @@ fn large_stdout_command(marker_path: &Path) -> String { #[cfg(not(windows))] fn large_stdout_command(marker_path: &Path) -> String { format!( - "sh -c \"python3 - <<'PY'\nprint('x' * 204800)\nPY\nprintf done > '{}'\"", + "sh -c \"python3 - <<'PY'\nprint('x' * 71680)\nPY\nprintf done > '{}'\"", marker_path.display() ) } diff --git a/rust/tests/e2e/rpc_tasks_and_handlers.rs b/rust/tests/e2e/rpc_tasks_and_handlers.rs index 540238ddd0..0b8e7d0642 100644 --- a/rust/tests/e2e/rpc_tasks_and_handlers.rs +++ b/rust/tests/e2e/rpc_tasks_and_handlers.rs @@ -66,7 +66,7 @@ async fn should_list_task_state_and_return_false_for_missing_task_operations() { .await .expect("progress missing") .progress - .is_none() + .is_null() ); assert!( session diff --git a/rust/tests/e2e/session.rs b/rust/tests/e2e/session.rs index 8c2463a5fa..02a13e7810 100644 --- a/rust/tests/e2e/session.rs +++ b/rust/tests/e2e/session.rs @@ -37,7 +37,7 @@ async fn shouldcreateanddisconnectsessions() { let session = client .create_session( ctx.approve_all_session_config() - .with_model("claude-sonnet-4.5"), + .with_model("claude-sonnet-5"), ) .await .expect("create session"); @@ -627,6 +627,76 @@ async fn should_resume_a_session_using_a_new_client() { .await; } +#[tokio::test] +async fn should_recover_marker_after_cold_resume_with_explicit_session_id() { + super::support::with_dedicated_e2e_context( + "session", + "should_recover_marker_after_cold_resume_with_explicit_session_id", + |ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + + let session_id = SessionId::from(format!( + "e2e-cold-resume-{}", + uuid::Uuid::new_v4().simple() + )); + + let client1 = ctx.start_client().await; + let session1 = client1 + .create_session( + ctx.approve_all_session_config() + .with_session_id(session_id.clone()), + ) + .await + .expect("create session"); + assert_eq!(session1.id(), &session_id); + + let first = session1 + .send_and_wait( + "Please remember this exact secret marker for later - MARKER-7f3ac21e. Reply with only the single word \"Acknowledged\".", + ) + .await + .expect("send") + .expect("assistant message"); + assert!(assistant_message_content(&first).contains("Acknowledged")); + + session1 + .disconnect() + .await + .expect("disconnect first session"); + client1.stop().await.expect("stop first client"); + + let new_client = ctx.start_client().await; + let resumed = new_client + .resume_session( + ResumeSessionConfig::new(session_id.clone()) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_github_token(super::support::DEFAULT_TEST_TOKEN), + ) + .await + .expect("resume session"); + assert_eq!(resumed.id(), &session_id); + + let second = resumed + .send_and_wait( + "What was the exact secret marker I asked you to remember earlier? Reply with only that marker value and nothing else.", + ) + .await + .expect("send after resume") + .expect("assistant message"); + assert!(assistant_message_content(&second).contains("MARKER-7f3ac21e")); + + resumed + .disconnect() + .await + .expect("disconnect resumed session"); + new_client.stop().await.expect("stop new client"); + }) + }, + ) + .await; +} + #[tokio::test] async fn resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured() { super::support::with_dedicated_e2e_context( diff --git a/rust/tests/e2e/session_config.rs b/rust/tests/e2e/session_config.rs index c3f6b57aea..2c844a8415 100644 --- a/rust/tests/e2e/session_config.rs +++ b/rust/tests/e2e/session_config.rs @@ -362,7 +362,7 @@ fn anthropic_message_stream_body(text: &str) -> String { "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [], "stop_reason": null, "stop_sequence": null, @@ -414,8 +414,8 @@ fn synth_non_inference_response(url: &str) -> CopilotHttpResponse { json_headers(), json!({ "data": [{ - "id": "claude-sonnet-4.5", - "name": "Claude Sonnet 4.5", + "id": "claude-sonnet-5", + "name": "Claude Sonnet 5", "object": "model", "vendor": "Anthropic", "version": "1", @@ -423,7 +423,7 @@ fn synth_non_inference_response(url: &str) -> CopilotHttpResponse { "model_picker_enabled": true, "capabilities": { "type": "chat", - "family": "claude-sonnet-4.5", + "family": "claude-sonnet-5", "tokenizer": "o200k_base", "limits": { "max_context_window_tokens": 200000, @@ -459,7 +459,7 @@ fn synth_inference_response(url: &str, body: &[u8]) -> CopilotHttpResponse { "id": "msg_stub_1", "type": "message", "role": "assistant", - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "content": [{ "type": "text", "text": SYNTHETIC_TEXT }], "stop_reason": "end_turn", "stop_sequence": null, @@ -474,7 +474,7 @@ fn synth_inference_response(url: &str, body: &[u8]) -> CopilotHttpResponse { "id": "chatcmpl-stub-1", "object": "chat.completion", "created": 1, - "model": "claude-sonnet-4.5", + "model": "claude-sonnet-5", "choices": [{ "index": 0, "message": { "role": "assistant", "content": SYNTHETIC_TEXT }, @@ -489,8 +489,8 @@ fn anthropic_provider() -> ProviderConfig { ProviderConfig::new("https://anthropic-citations.invalid/v1") .with_provider_type("anthropic") .with_api_key("test-provider-key") - .with_model_id("claude-sonnet-4.5") - .with_wire_model("claude-sonnet-4.5") + .with_model_id("claude-sonnet-5") + .with_wire_model("claude-sonnet-5") } fn pdf_attachment() -> Attachment { @@ -532,7 +532,7 @@ async fn should_enable_citations_for_anthropic_file_attachments_on_create() { .create_session( SessionConfig::default() .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_enable_citations(true) .with_provider(anthropic_provider()), ) @@ -592,7 +592,7 @@ async fn should_enable_citations_for_anthropic_file_attachments_on_resume() { .resume_session( ResumeSessionConfig::new(session1.id().clone()) .with_permission_handler(Arc::new(ApproveAllHandler)) - .with_model("claude-sonnet-4.5") + .with_model("claude-sonnet-5") .with_enable_citations(true) .with_provider(anthropic_provider()), ) diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index 8a4161efef..0b7bbae7a7 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -31,6 +31,7 @@ static SHARED_E2E_RUNTIME: LazyLock = LazyLock::new(|| .expect("create shared E2E runtime") }); const SHARED_E2E_CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); +const PROXY_STARTUP_TIMEOUT: Duration = Duration::from_secs(30); pub const DEFAULT_TEST_TOKEN: &str = "rust-e2e-token"; @@ -673,7 +674,7 @@ impl E2eContext { impl SharedE2eState { async fn prepare_test(&mut self, category: &str, snapshot_name: &str) -> std::io::Result<()> { self.cleanup_sessions().await?; - clear_directory_contents(self.context.work_dir())?; + clear_directory_contents(self.context.work_dir()).await?; self.context.configure(category, snapshot_name)?; self.context.set_default_copilot_user(); Ok(()) @@ -681,7 +682,7 @@ impl SharedE2eState { async fn cleanup_after_test(&mut self) -> std::io::Result<()> { self.cleanup_sessions().await?; - clear_directory_contents(self.context.work_dir()) + clear_directory_contents(self.context.work_dir()).await } async fn cleanup_sessions(&self) -> std::io::Result<()> { @@ -781,19 +782,36 @@ fn is_filtered_test_run() -> bool { }) } -fn clear_directory_contents(directory: &Path) -> std::io::Result<()> { +async fn clear_directory_contents(directory: &Path) -> std::io::Result<()> { for entry in std::fs::read_dir(directory)? { let entry = entry?; let path = entry.path(); - if entry.file_type()?.is_dir() { - std::fs::remove_dir_all(path)?; - } else { - std::fs::remove_file(path)?; + let is_directory = entry.file_type()?.is_dir(); + + for attempt in 1..=20 { + let result = if is_directory { + std::fs::remove_dir_all(&path) + } else { + std::fs::remove_file(&path) + }; + + match result { + Ok(()) => break, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => break, + Err(error) if is_transient_windows_file_lock(&error) && attempt < 20 => { + tokio::time::sleep(Duration::from_millis(50)).await; + } + Err(error) => return Err(error), + } } } Ok(()) } +fn is_transient_windows_file_lock(error: &std::io::Error) -> bool { + cfg!(windows) && matches!(error.raw_os_error(), Some(5 | 32 | 145)) +} + impl Drop for E2eContext { fn drop(&mut self) { if let Some(mut proxy) = self.proxy.take() { @@ -1174,29 +1192,23 @@ fn cli_path(repo_root: &Path) -> std::io::Result { } } - // The `@github/copilot` package is a thin loader; the runnable `index.js` - // ships in a platform-specific `@github/copilot--` package, - // exactly one of which is installed. Resolve whichever one is present. - let github_dir = repo_root - .join("nodejs") - .join("node_modules") - .join("@github"); - if let Ok(entries) = std::fs::read_dir(&github_dir) { - for entry in entries.flatten() { - if entry.file_name().to_string_lossy().starts_with("copilot-") { - let candidate = entry.path().join("index.js"); - if candidate.exists() { - return Ok(candidate); - } - } + let npm = if cfg!(windows) { "npm.cmd" } else { "npm" }; + let output = std::process::Command::new(npm) + .args(["run", "--silent", "prepare:runtime", "--", "--print-path"]) + .current_dir(repo_root.join("nodejs")) + .output()?; + if output.status.success() { + let path = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim()); + if path.is_file() { + return Ok(path); } } Err(std::io::Error::new( std::io::ErrorKind::NotFound, format!( - "CLI not found under {}; run npm install in nodejs first", - github_dir.display() + "failed to prepare the pinned Copilot CLI: {}", + String::from_utf8_lossy(&output.stderr).trim() ), )) } @@ -1274,7 +1286,7 @@ impl CapiProxy { } }); let re = regex::Regex::new(r"Listening: (http://[^\s]+)\s+(\{.*\})$").unwrap(); - let deadline = Instant::now() + SHARED_E2E_CLEANUP_TIMEOUT; + let deadline = Instant::now() + PROXY_STARTUP_TIMEOUT; while let Some(remaining) = deadline.checked_duration_since(Instant::now()) { let line = match line_rx.recv_timeout(remaining) { Ok(Ok(line)) => line, @@ -1341,7 +1353,7 @@ impl CapiProxy { kill_and_wait_child(&mut child); Err(std::io::Error::other(format!( - "timed out after {SHARED_E2E_CLEANUP_TIMEOUT:?} waiting for proxy startup" + "timed out after {PROXY_STARTUP_TIMEOUT:?} waiting for proxy startup" ))) } diff --git a/rust/tests/fixtures/host_crash_fixture.rs b/rust/tests/fixtures/host_crash_fixture.rs new file mode 100644 index 0000000000..c688cf3e87 --- /dev/null +++ b/rust/tests/fixtures/host_crash_fixture.rs @@ -0,0 +1,60 @@ +//! Test-only binary that hosts a single [`github_copilot_sdk::Client`] and then +//! blocks forever, so an external test can terminate *this* process abruptly +//! (simulating an SDK-embedding app process crashing) without ever running any +//! of this process's own cleanup code (`Client::stop`, `force_stop`, or +//! `Drop`). +//! +//! Configuration is passed entirely through environment variables so the +//! caller doesn't need this crate's non-`pub` types: +//! - `HOST_CRASH_FIXTURE_PROGRAM`: CLI program path. +//! - `HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON`: JSON array of prefix args. +//! - `HOST_CRASH_FIXTURE_CWD`: working directory for the spawned CLI. +//! - `HOST_CRASH_FIXTURE_ENV_JSON`: JSON array of `[key, value]` pairs to set +//! on the spawned CLI's environment. +//! - `HOST_CRASH_FIXTURE_PID_FILE`: path this process writes the CLI child's +//! OS process id to, once the client finishes starting. + +use std::path::PathBuf; + +use github_copilot_sdk::{CliProgram, Client, ClientOptions, Transport}; + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let program = std::env::var("HOST_CRASH_FIXTURE_PROGRAM").expect("HOST_CRASH_FIXTURE_PROGRAM"); + let prefix_args: Vec = serde_json::from_str( + &std::env::var("HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON") + .expect("HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON"), + ) + .expect("parse HOST_CRASH_FIXTURE_PREFIX_ARGS_JSON"); + let cwd = std::env::var("HOST_CRASH_FIXTURE_CWD").expect("HOST_CRASH_FIXTURE_CWD"); + let env_pairs: Vec<(String, String)> = serde_json::from_str( + &std::env::var("HOST_CRASH_FIXTURE_ENV_JSON").expect("HOST_CRASH_FIXTURE_ENV_JSON"), + ) + .expect("parse HOST_CRASH_FIXTURE_ENV_JSON"); + let pid_file = PathBuf::from( + std::env::var("HOST_CRASH_FIXTURE_PID_FILE").expect("HOST_CRASH_FIXTURE_PID_FILE"), + ); + + let options = ClientOptions::new() + .with_program(CliProgram::Path(PathBuf::from(program))) + .with_prefix_args(prefix_args) + .with_cwd(PathBuf::from(cwd)) + .with_env(env_pairs) + .with_use_logged_in_user(false) + .with_transport(Transport::Stdio); + + let client = Client::start(options).await.expect("start CLI client"); + let pid = client.pid().expect("client reports spawned CLI pid"); + std::fs::write(&pid_file, pid.to_string()).expect("write pid file"); + + // Deliberately leak the client so nothing in this process — including its + // `Drop` impls — ever runs cleanup code. The external test process + // terminates this process abruptly (e.g. `TerminateProcess` on Windows) + // to simulate an SDK-embedding host crashing, and asserts that the CLI + // still dies via the OS containment primitive alone. + std::mem::forget(client); + + loop { + std::thread::sleep(std::time::Duration::from_secs(3600)); + } +} diff --git a/rust/tests/prepared_session_test.rs b/rust/tests/prepared_session_test.rs new file mode 100644 index 0000000000..3fefccabb6 --- /dev/null +++ b/rust/tests/prepared_session_test.rs @@ -0,0 +1,1242 @@ +//! Early event subscription via `Client::prepare_session` / +//! `Client::prepare_resume_session`. +//! +//! Every test drives the SDK over an in-memory duplex transport and a +//! hand-rolled JSON-RPC peer, so event ordering is deterministic. Timeouts +//! are failure backstops only — no test sleeps to "let things settle". + +#![allow(clippy::unwrap_used)] + +use std::marker::PhantomData; +use std::sync::Arc; +use std::time::Duration; + +use async_trait::async_trait; +use github_copilot_sdk::handler::{McpAuthHandler, McpAuthRequest, McpAuthResult}; +use github_copilot_sdk::session::PreparedSession; +use github_copilot_sdk::subscription::{EventSubscription, RecvErrorKind}; +use github_copilot_sdk::types::{ + CloudSessionOptions, CloudSessionRepository, RequestId, ResumeSessionConfig, SessionConfig, + SessionId, +}; +use github_copilot_sdk::{Client, ErrorKind, SessionErrorKind}; +use serde_json::{Value, json}; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, duplex}; +use tokio::time::timeout; + +/// Failure backstop for operations that must complete promptly. +const TIMEOUT: Duration = Duration::from_secs(5); +/// Backstop for asserting that something does *not* happen. +const QUIET: Duration = Duration::from_millis(150); +/// Size of the pre-response event burst. Mirrors the copilot-host startup +/// burst that motivated the API. +const BURST: usize = 600; + +// --------------------------------------------------------------------------- +// Transport harness +// --------------------------------------------------------------------------- + +async fn write_framed(writer: &mut (impl AsyncWrite + Unpin), body: &[u8]) { + let header = format!("Content-Length: {}\r\n\r\n", body.len()); + writer.write_all(header.as_bytes()).await.unwrap(); + writer.write_all(body).await.unwrap(); + writer.flush().await.unwrap(); +} + +async fn read_framed(reader: &mut (impl AsyncRead + Unpin)) -> Value { + let mut header = String::new(); + loop { + let mut byte = [0u8; 1]; + tokio::io::AsyncReadExt::read_exact(reader, &mut byte) + .await + .unwrap(); + header.push(byte[0] as char); + if header.ends_with("\r\n\r\n") { + break; + } + } + let length: usize = header + .trim() + .strip_prefix("Content-Length: ") + .unwrap() + .parse() + .unwrap(); + let mut buf = vec![0u8; length]; + tokio::io::AsyncReadExt::read_exact(reader, &mut buf) + .await + .unwrap(); + serde_json::from_slice(&buf).unwrap() +} + +struct FakeServer { + read: tokio::io::DuplexStream, + write: tokio::io::DuplexStream, +} + +impl FakeServer { + async fn read_request(&mut self) -> Value { + timeout(TIMEOUT, read_framed(&mut self.read)).await.unwrap() + } + + async fn expect_quiet(&mut self) { + assert!( + timeout(QUIET, read_framed(&mut self.read)).await.is_err(), + "expected no wire traffic" + ); + } + + async fn respond(&mut self, request: &Value, result: Value) { + let id = request["id"].as_u64().unwrap(); + let response = json!({ "jsonrpc": "2.0", "id": id, "result": result }); + write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; + } + + async fn respond_error(&mut self, request: &Value, code: i64, message: &str) { + let id = request["id"].as_u64().unwrap(); + let response = json!({ + "jsonrpc": "2.0", + "id": id, + "error": { "code": code, "message": message }, + }); + write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; + } + + async fn send_event(&mut self, session_id: &str, id: &str, event_type: &str, ephemeral: bool) { + let notification = json!({ + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": session_id, + "event": { + "id": id, + "timestamp": "2025-01-01T00:00:00Z", + "ephemeral": ephemeral, + "type": event_type, + "data": {}, + }, + }, + }); + write_framed(&mut self.write, &serde_json::to_vec(¬ification).unwrap()).await; + } + + /// Emit the startup burst the host cares about: `BURST` ordered events + /// followed by an ephemeral `session.idle` that `getMessages` could + /// never recover. + async fn send_startup_burst(&mut self, session_id: &str) { + for i in 0..BURST { + self.send_event( + session_id, + &format!("evt-{i}"), + "assistant.message_delta", + false, + ) + .await; + } + self.send_event(session_id, "evt-idle", "session.idle", true) + .await; + } + + /// Answer the best-effort `session.skills.reload` that follows a resume. + async fn answer_skills_reload(&mut self) { + let request = self.read_request().await; + assert_eq!(request["method"], "session.skills.reload"); + self.respond(&request, json!({})).await; + } +} + +/// Minimal MCP-auth handler: its presence is what makes the SDK register +/// `mcp.oauth_required` interest after create/resume, which is the branch +/// under test. It is never invoked by these tests. +struct CancelMcpAuthHandler; + +#[async_trait] +impl McpAuthHandler for CancelMcpAuthHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _request: McpAuthRequest, + ) -> McpAuthResult { + McpAuthResult::Cancelled + } +} + +fn make_client() -> (Client, FakeServer) { + let (client_write, server_read) = duplex(1 << 20); + let (server_write, client_read) = duplex(1 << 20); + let client = Client::from_streams(client_read, client_write, std::env::temp_dir()).unwrap(); + ( + client, + FakeServer { + read: server_read, + write: server_write, + }, + ) +} + +fn cloud_options() -> CloudSessionOptions { + CloudSessionOptions::with_repository(CloudSessionRepository::new("octocat", "hello-world")) +} + +fn create_result(session_id: &str) -> Value { + json!({ "sessionId": session_id, "workspacePath": "/tmp/workspace" }) +} + +/// Collect the startup burst, asserting each event arrives exactly once and +/// in emission order. +async fn expect_startup_burst(events: &mut EventSubscription) { + for i in 0..BURST { + let event = timeout(TIMEOUT, events.recv()) + .await + .unwrap_or_else(|_| panic!("timed out waiting for event {i}")) + .unwrap_or_else(|error| panic!("event {i} not delivered: {error}")); + assert_eq!(event.id.as_str(), format!("evt-{i}"), "out-of-order event"); + } + let idle = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!(idle.id.as_str(), "evt-idle"); + assert_eq!(idle.event_type, "session.idle"); + assert_eq!(idle.ephemeral, Some(true)); +} + +/// Unwrap the error arm of a result whose `Ok` type is not `Debug`. +fn expect_error(result: Result) -> github_copilot_sdk::Error { + match result { + Ok(_) => panic!("expected an error"), + Err(error) => error, + } +} + +/// Assert the router holds exactly one registration, and that it is +/// `session_id`. +/// +/// The failure message is a fixed string: session IDs are never written to +/// test output. +fn assert_only_registration(client: &Client, session_id: &SessionId, context: &str) { + let registered = client.registered_session_ids_for_test(); + let matches_expected = registered.len() == 1 && registered[0] == *session_id; + assert!(matches_expected, "{context}"); +} + +/// Poll (bounded) until the client's router has no registered sessions. +/// +/// Diagnostics report how many registrations are outstanding rather than +/// which ones: session IDs are not written to test output. +async fn await_no_registrations(client: &Client) { + let deadline = tokio::time::Instant::now() + TIMEOUT; + loop { + let outstanding = client.registered_session_count_for_test(); + if outstanding == 0 { + return; + } + assert!( + tokio::time::Instant::now() < deadline, + "{outstanding} session registration(s) were never cleaned up" + ); + tokio::task::yield_now().await; + } +} + +/// Assert the subscription is closed (producer gone), tolerating any events +/// buffered before the close. +async fn expect_closed(events: &mut EventSubscription) { + loop { + match timeout(TIMEOUT, events.recv()).await.unwrap() { + Ok(_) => continue, + Err(error) => { + assert!( + matches!(error.kind(), RecvErrorKind::Closed), + "expected Closed, got {:?}", + error.kind() + ); + return; + } + } + } +} + +// --------------------------------------------------------------------------- +// 1 + 4. Loss-free startup events on create +// --------------------------------------------------------------------------- + +/// Subscription installed before `start()` is polled, drained concurrently: +/// the full pre-response burst plus the ephemeral `session.idle` arrives. +#[tokio::test] +async fn prepared_create_delivers_pre_response_burst_to_concurrent_drain() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-create-concurrent"); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(2048), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let drain = tokio::spawn(async move { + expect_startup_burst(&mut events).await; + }); + + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + server.send_startup_burst(session_id.as_str()).await; + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + timeout(TIMEOUT, drain).await.unwrap().unwrap(); + drop(session); +} + +/// A large configured buffer retains the whole burst even when the consumer +/// does not read anything until `start()` has returned. +#[tokio::test] +async fn prepared_create_retains_burst_for_deferred_consumer() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-create-deferred"); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(2048), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + server.send_startup_burst(session_id.as_str()).await; + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + // Only now does the consumer start reading. + expect_startup_burst(&mut events).await; + drop(session); +} + +// --------------------------------------------------------------------------- +// 2. Loss-free startup events on resume +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn prepared_resume_delivers_pre_response_burst() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-resume"); + + let prepared = client + .prepare_resume_session( + ResumeSessionConfig::new(session_id.clone()) + .with_continue_pending_work(true) + .with_event_buffer_capacity(2048), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let resume_req = server.read_request().await; + assert_eq!(resume_req["method"], "session.resume"); + assert_eq!(resume_req["params"]["continuePendingWork"], true); + server.send_startup_burst(session_id.as_str()).await; + server + .respond(&resume_req, json!({ "sessionId": session_id.as_str() })) + .await; + server.answer_skills_reload().await; + + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + expect_startup_burst(&mut events).await; + drop(session); +} + +// --------------------------------------------------------------------------- +// 3. Lag is observable, never silent +// --------------------------------------------------------------------------- + +/// An undersized buffer with a consumer that does not drain surfaces +/// `Lagged` rather than silently losing events, and the live tail stays +/// consumable afterwards. +#[tokio::test] +async fn undersized_buffer_reports_lag_and_keeps_live_tail() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-lag"); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(8), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + server.send_startup_burst(session_id.as_str()).await; + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + + // Drain until lag is reported. Every delivered event is still in order, + // and the loss is explicit rather than silent. + let mut lagged = None; + let mut last_index: Option = None; + while lagged.is_none() { + match timeout(TIMEOUT, events.recv()).await.unwrap() { + Ok(event) => { + if let Some(index) = event.id.as_str().strip_prefix("evt-") + && let Ok(index) = index.parse::() + { + if let Some(previous) = last_index { + assert!(index > previous, "delivered events must stay ordered"); + } + last_index = Some(index); + } + } + Err(error) => match error.kind() { + RecvErrorKind::Lagged(lag) => lagged = Some(lag.skipped()), + other => panic!("expected lag, got {other:?}"), + }, + } + } + assert!(lagged.unwrap() > 0, "lag must report the skipped count"); + + // The live tail is still consumable after a lag. + server + .send_event(session_id.as_str(), "evt-live", "assistant.message", false) + .await; + let live = loop { + match timeout(TIMEOUT, events.recv()).await.unwrap() { + Ok(event) if event.id.as_str() == "evt-live" => break event, + Ok(_) => continue, + Err(error) => match error.kind() { + RecvErrorKind::Lagged(_) => continue, + other => panic!("subscription ended before the live tail: {other:?}"), + }, + } + }; + assert_eq!(live.event_type, "assistant.message"); + drop(session); +} + +// --------------------------------------------------------------------------- +// 5 + 6. Inertness before start, and drop of an unstarted handle +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn prepare_is_inert_until_start_is_polled() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-inert"); + + let tasks_before = tokio::runtime::Handle::current() + .metrics() + .num_alive_tasks(); + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap(); + let _events = prepared.subscribe(); + + // No wire traffic, no router registration, no spawned task. + server.expect_quiet().await; + assert!(client.registered_session_ids_for_test().is_empty()); + assert_eq!( + tokio::runtime::Handle::current() + .metrics() + .num_alive_tasks(), + tasks_before, + "prepare must not spawn a task" + ); + + // Constructing the future is still inert; only polling it does work. + let start = prepared.start(); + server.expect_quiet().await; + assert!(client.registered_session_ids_for_test().is_empty()); + + let start = tokio::spawn(start); + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + drop(session); +} + +#[tokio::test] +async fn dropping_unstarted_prepared_session_leaves_no_state() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-dropped"); + + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + drop(prepared); + + assert!(matches!( + timeout(TIMEOUT, events.recv()) + .await + .unwrap() + .unwrap_err() + .kind(), + RecvErrorKind::Closed + )); + assert!(client.registered_session_ids_for_test().is_empty()); + server.expect_quiet().await; +} + +// --------------------------------------------------------------------------- +// 7. Cancelling a polled startup +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn cancelled_prepared_create_cleans_up_and_allows_retry() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-create-cancel"); + + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + // The request is on the wire; cancel before responding. + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + start.abort(); + let _ = start.await; + + await_no_registrations(&client).await; + expect_closed(&mut events).await; + + // A retry with the same session ID succeeds. + let retry = tokio::spawn( + client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap() + .start(), + ); + let retry_req = server.read_request().await; + assert_eq!(retry_req["method"], "session.create"); + server + .respond(&retry_req, create_result(session_id.as_str())) + .await; + let session = timeout(TIMEOUT, retry).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id(), &session_id); + drop(session); +} + +#[tokio::test] +async fn cancelled_prepared_resume_cleans_up_and_allows_retry() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-resume-cancel"); + + let prepared = client + .prepare_resume_session(ResumeSessionConfig::new(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let resume_req = server.read_request().await; + assert_eq!(resume_req["method"], "session.resume"); + start.abort(); + let _ = start.await; + + await_no_registrations(&client).await; + expect_closed(&mut events).await; + + let retry = tokio::spawn( + client + .prepare_resume_session(ResumeSessionConfig::new(session_id.clone())) + .unwrap() + .start(), + ); + let retry_req = server.read_request().await; + assert_eq!(retry_req["method"], "session.resume"); + server + .respond(&retry_req, json!({ "sessionId": session_id.as_str() })) + .await; + server.answer_skills_reload().await; + let session = timeout(TIMEOUT, retry).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id(), &session_id); + drop(session); +} + +// --------------------------------------------------------------------------- +// 8. Startup failures preserve error kinds and clean up +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn create_rpc_error_preserves_kind_and_cleans_up() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-create-rpc-error"); + + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + server + .respond_error(&create_req, -32000, "session create failed") + .await; + + let error = expect_error(timeout(TIMEOUT, start).await.unwrap().unwrap()); + assert!( + matches!(error.kind(), ErrorKind::Rpc { code: -32000 }), + "unexpected error kind: {:?}", + error.kind() + ); + await_no_registrations(&client).await; + expect_closed(&mut events).await; +} + +#[tokio::test] +async fn create_session_id_mismatch_preserves_kind_and_cleans_up() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-mismatch"); + + let prepared = client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + server + .respond(&create_req, create_result("some-other-id")) + .await; + + let error = expect_error(timeout(TIMEOUT, start).await.unwrap().unwrap()); + let ErrorKind::Session(SessionErrorKind::SessionIdMismatch { + requested, + returned, + }) = error.kind() + else { + panic!("unexpected error kind: {:?}", error.kind()); + }; + assert_eq!(requested, &session_id); + assert_eq!(returned.as_str(), "some-other-id"); + + await_no_registrations(&client).await; + expect_closed(&mut events).await; +} + +#[tokio::test] +async fn resume_session_id_mismatch_preserves_kind_and_cleans_up() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-resume-mismatch"); + + let prepared = client + .prepare_resume_session(ResumeSessionConfig::new(session_id.clone())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let resume_req = server.read_request().await; + server + .respond(&resume_req, json!({ "sessionId": "another-session" })) + .await; + + let error = expect_error(timeout(TIMEOUT, start).await.unwrap().unwrap()); + assert!( + matches!( + error.kind(), + ErrorKind::Session(SessionErrorKind::SessionIdMismatch { .. }) + ), + "unexpected error kind: {:?}", + error.kind() + ); + await_no_registrations(&client).await; + expect_closed(&mut events).await; +} + +/// The MCP-auth interest registration that follows a successful +/// `session.create` is the last fallible step before the session handle is +/// handed out. When it fails, the startup must unwind exactly like any +/// other create failure: original error kind preserved, router +/// registration removed, and subscriptions taken before `start()` closed. +#[tokio::test] +async fn create_mcp_auth_interest_error_preserves_kind_and_cleans_up() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-create-interest-error"); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_mcp_auth_handler(Arc::new(CancelMcpAuthHandler)), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + + let interest_req = server.read_request().await; + assert_eq!(interest_req["method"], "session.eventLog.registerInterest"); + assert_eq!(interest_req["params"]["eventType"], "mcp.oauth_required"); + server + .respond_error(&interest_req, -32003, "interest registration failed") + .await; + + let error = expect_error(timeout(TIMEOUT, start).await.unwrap().unwrap()); + assert!( + matches!(error.kind(), ErrorKind::Rpc { code: -32003 }), + "unexpected error kind: {:?}", + error.kind() + ); + expect_closed(&mut events).await; + await_no_registrations(&client).await; +} + +/// The resume counterpart. Interest registration runs before the +/// best-effort `session.skills.reload`, so a failure must abort the +/// startup without issuing the reload. +#[tokio::test] +async fn resume_mcp_auth_interest_error_preserves_kind_and_cleans_up() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-resume-interest-error"); + + let prepared = client + .prepare_resume_session( + ResumeSessionConfig::new(session_id.clone()) + .with_mcp_auth_handler(Arc::new(CancelMcpAuthHandler)), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let resume_req = server.read_request().await; + assert_eq!(resume_req["method"], "session.resume"); + server + .respond(&resume_req, json!({ "sessionId": session_id.as_str() })) + .await; + + let interest_req = server.read_request().await; + assert_eq!(interest_req["method"], "session.eventLog.registerInterest"); + server + .respond_error(&interest_req, -32004, "interest registration failed") + .await; + + let error = expect_error(timeout(TIMEOUT, start).await.unwrap().unwrap()); + assert!( + matches!(error.kind(), ErrorKind::Rpc { code: -32004 }), + "unexpected error kind: {:?}", + error.kind() + ); + server.expect_quiet().await; + expect_closed(&mut events).await; + await_no_registrations(&client).await; +} + +#[tokio::test] +async fn zero_event_buffer_capacity_is_invalid_config() { + let (client, _server) = make_client(); + + let error = expect_error( + client.prepare_session(SessionConfig::default().with_event_buffer_capacity(0)), + ); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig)); + + let error = expect_error(client.prepare_resume_session( + ResumeSessionConfig::new(SessionId::new("zero")).with_event_buffer_capacity(0), + )); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig)); + + // The compatibility wrappers surface the same error. + let error = expect_error( + client + .create_session(SessionConfig::default().with_event_buffer_capacity(0)) + .await, + ); + assert!(matches!(error.kind(), ErrorKind::InvalidConfig)); +} + +// --------------------------------------------------------------------------- +// 9. Early and late subscribers share one event loop +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn early_and_late_subscribers_share_one_event_loop() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("prepared-two-subscribers"); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(2048), + ) + .unwrap(); + let mut early = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + server + .send_event(session_id.as_str(), "evt-early", "assistant.message", false) + .await; + server + .respond(&create_req, create_result(session_id.as_str())) + .await; + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + + let mut late = session.subscribe(); + server + .send_event(session_id.as_str(), "evt-late", "assistant.message", false) + .await; + + // The early subscriber sees both events, once each. + assert_eq!( + timeout(TIMEOUT, early.recv()) + .await + .unwrap() + .unwrap() + .id + .as_str(), + "evt-early" + ); + assert_eq!( + timeout(TIMEOUT, early.recv()) + .await + .unwrap() + .unwrap() + .id + .as_str(), + "evt-late" + ); + // The late subscriber only sees what was emitted after it subscribed — + // exactly once, which would be twice if a second event loop existed. + assert_eq!( + timeout(TIMEOUT, late.recv()) + .await + .unwrap() + .unwrap() + .id + .as_str(), + "evt-late" + ); + assert!( + timeout(QUIET, late.recv()).await.is_err(), + "duplicate delivery implies more than one event loop" + ); + assert!( + timeout(QUIET, early.recv()).await.is_err(), + "duplicate delivery implies more than one event loop" + ); + drop(session); +} + +// --------------------------------------------------------------------------- +// 10. Compatibility wrappers +// --------------------------------------------------------------------------- + +#[tokio::test] +async fn create_session_wrapper_keeps_rpc_sequence() { + let (client, mut server) = make_client(); + + let start = tokio::spawn({ + let client = client.clone(); + async move { client.create_session(SessionConfig::default()).await } + }); + + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + let session_id = create_req["params"]["sessionId"] + .as_str() + .unwrap() + .to_string(); + server + .respond(&create_req, create_result(&session_id)) + .await; + + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id().as_str(), session_id); + server.expect_quiet().await; + drop(session); +} + +#[tokio::test] +async fn resume_session_wrapper_keeps_rpc_sequence() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("wrapper-resume"); + + let start = tokio::spawn({ + let client = client.clone(); + let session_id = session_id.clone(); + async move { + client + .resume_session(ResumeSessionConfig::new(session_id)) + .await + } + }); + + let resume_req = server.read_request().await; + assert_eq!(resume_req["method"], "session.resume"); + assert_eq!(resume_req["params"]["sessionId"], session_id.as_str()); + server + .respond(&resume_req, json!({ "sessionId": session_id.as_str() })) + .await; + server.answer_skills_reload().await; + + let session = timeout(TIMEOUT, start).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id(), &session_id); + server.expect_quiet().await; + drop(session); +} + +// --------------------------------------------------------------------------- +// 11. Type-level guarantees +// --------------------------------------------------------------------------- + +/// Detects `Clone` without requiring it: the inherent method wins whenever +/// `T: Clone`, otherwise the blanket trait method is selected. +struct CloneProbe(PhantomData); + +impl CloneProbe { + fn is_clone(&self) -> bool { + true + } +} + +trait MaybeClone { + fn is_clone(&self) -> bool { + false + } +} + +impl MaybeClone for CloneProbe {} + +#[test] +fn prepared_session_is_send_static_and_not_clone() { + fn assert_send_static() {} + assert_send_static::(); + + // Sanity-check the probe against a type that is `Clone` ... + assert!(CloneProbe::(PhantomData).is_clone()); + // ... then assert `PreparedSession` deliberately is not, so a prepared + // session can never be started twice. + assert!(!CloneProbe::(PhantomData).is_clone()); +} + +// --------------------------------------------------------------------------- +// Registration ownership: a stale startup guard must never unregister a +// newer registration that reused the same session ID. +// --------------------------------------------------------------------------- + +/// Drive a startup future until it parks awaiting its RPC response. +/// +/// The duration is a bound, not a correctness sleep: whether the future +/// actually reached the wire is asserted afterwards by reading the request, +/// which fails loudly on its own timeout if it did not. +const DRIVE: Duration = Duration::from_millis(50); + +#[tokio::test] +async fn stale_create_guard_does_not_unregister_same_id_retry() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("stale-create-guard"); + + // First attempt: registers, sends `session.create`, then parks. + let mut first = Box::pin( + client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap() + .start(), + ); + let _ = timeout(DRIVE, &mut first).await; + let first_req = server.read_request().await; + assert_eq!(first_req["method"], "session.create"); + + // Second attempt with the same pinned ID, started before the first is + // dropped, so it replaces the first attempt's router registration. + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(64), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let mut second = Box::pin(prepared.start()); + let _ = timeout(DRIVE, &mut second).await; + let second_req = server.read_request().await; + assert_eq!(second_req["method"], "session.create"); + + // The stale guard runs now. It must not touch the live registration. + drop(first); + assert_only_registration( + &client, + &session_id, + "a stale startup guard unregistered the live retry", + ); + + server + .respond(&second_req, create_result(session_id.as_str())) + .await; + let session = timeout(TIMEOUT, &mut second).await.unwrap().unwrap(); + + // Events must still route to the surviving registration. + server + .send_event( + session_id.as_str(), + "evt-after-stale", + "assistant.message", + false, + ) + .await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!(event.id.as_str(), "evt-after-stale"); + drop(session); +} + +#[tokio::test] +async fn stale_resume_guard_does_not_unregister_same_id_retry() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("stale-resume-guard"); + + let mut first = Box::pin( + client + .prepare_resume_session(ResumeSessionConfig::new(session_id.clone())) + .unwrap() + .start(), + ); + let _ = timeout(DRIVE, &mut first).await; + let first_req = server.read_request().await; + assert_eq!(first_req["method"], "session.resume"); + + let prepared = client + .prepare_resume_session( + ResumeSessionConfig::new(session_id.clone()).with_event_buffer_capacity(64), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let mut second = Box::pin(prepared.start()); + let _ = timeout(DRIVE, &mut second).await; + let second_req = server.read_request().await; + assert_eq!(second_req["method"], "session.resume"); + + drop(first); + assert_only_registration( + &client, + &session_id, + "a stale startup guard unregistered the live retry", + ); + + // Hand the surviving startup to a task: resume issues a follow-up + // `session.skills.reload` that only makes progress while it is polled. + let second = tokio::spawn(second); + server + .respond(&second_req, json!({ "sessionId": session_id.as_str() })) + .await; + server.answer_skills_reload().await; + let session = timeout(TIMEOUT, second).await.unwrap().unwrap().unwrap(); + + server + .send_event( + session_id.as_str(), + "evt-after-stale", + "assistant.message", + false, + ) + .await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!(event.id.as_str(), "evt-after-stale"); + drop(session); +} + +/// A disconnected session must not unregister a same-ID session that +/// replaced it. +#[tokio::test] +async fn dropping_superseded_session_does_not_unregister_its_replacement() { + let (client, mut server) = make_client(); + let session_id = SessionId::new("superseded-session"); + + let first = tokio::spawn( + client + .prepare_session(SessionConfig::default().with_session_id(session_id.clone())) + .unwrap() + .start(), + ); + let first_req = server.read_request().await; + server + .respond(&first_req, create_result(session_id.as_str())) + .await; + let first_session = timeout(TIMEOUT, first).await.unwrap().unwrap().unwrap(); + + let prepared = client + .prepare_session( + SessionConfig::default() + .with_session_id(session_id.clone()) + .with_event_buffer_capacity(64), + ) + .unwrap(); + let mut events = prepared.subscribe(); + let second = tokio::spawn(prepared.start()); + let second_req = server.read_request().await; + server + .respond(&second_req, create_result(session_id.as_str())) + .await; + let second_session = timeout(TIMEOUT, second).await.unwrap().unwrap().unwrap(); + + // The superseded handle goes away; the live session must survive. + drop(first_session); + assert_only_registration( + &client, + &session_id, + "dropping a superseded Session unregistered its replacement", + ); + + server + .send_event( + session_id.as_str(), + "evt-survivor", + "assistant.message", + false, + ) + .await; + let event = timeout(TIMEOUT, events.recv()).await.unwrap().unwrap(); + assert_eq!(event.id.as_str(), "evt-survivor"); + drop(second_session); +} + +// --------------------------------------------------------------------------- +// Deferred (server-assigned ID) create cancellation +// --------------------------------------------------------------------------- + +/// Cancelling a cloud create before the response arrives must leave no +/// registration behind, even though the session ID is only known to the +/// inline response callback. +#[tokio::test] +async fn cancelled_deferred_create_leaves_no_registration() { + let (client, mut server) = make_client(); + + let prepared = client + .prepare_session(SessionConfig::default().with_cloud(cloud_options())) + .unwrap(); + let mut events = prepared.subscribe(); + let start = tokio::spawn(prepared.start()); + + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + assert!(create_req["params"]["sessionId"].is_null()); + + // Cancel before the server answers, then answer: the response carries + // the server-assigned ID the inline callback would register. + start.abort(); + let _ = start.await; + server + .respond(&create_req, create_result("server-assigned-id")) + .await; + + expect_closed(&mut events).await; + await_no_registrations(&client).await; + // Nothing may appear after the response has been fully processed. + server.expect_quiet().await; + assert!( + client.registered_session_ids_for_test().is_empty(), + "a cancelled deferred create left a registration behind" + ); + + // A fresh cloud create still works afterwards. + let retry = tokio::spawn( + client + .prepare_session(SessionConfig::default().with_cloud(cloud_options())) + .unwrap() + .start(), + ); + let retry_req = server.read_request().await; + server + .respond(&retry_req, create_result("server-assigned-retry")) + .await; + let session = timeout(TIMEOUT, retry).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id().as_str(), "server-assigned-retry"); + drop(session); +} + +/// Poll (bounded) until `session_id` shows up on the client's router. +/// +/// The failure message names the expectation, not the ID: session IDs are +/// not written to test output. +async fn await_registered(client: &Client, session_id: &str) { + let deadline = tokio::time::Instant::now() + TIMEOUT; + while !client + .registered_session_ids_for_test() + .iter() + .any(|id| id.as_str() == session_id) + { + assert!( + tokio::time::Instant::now() < deadline, + "inline callback never registered the expected session" + ); + tokio::task::yield_now().await; + } +} + +/// The other half of the deferred-create window: cancellation lands *after* +/// the inline response callback has already registered the server-assigned +/// ID. The startup guard owns that registration and must remove it. +/// +/// Deterministic by construction — the start future is parked on its +/// response and never polled again, so the callback (which runs on the +/// JSON-RPC read task, independently of the caller) is guaranteed to have +/// registered before the future is dropped. +#[tokio::test] +async fn deferred_create_cancelled_after_callback_registered_is_cleaned_up() { + let (client, mut server) = make_client(); + + let prepared = client + .prepare_session(SessionConfig::default().with_cloud(cloud_options())) + .unwrap(); + let mut events = prepared.subscribe(); + let mut start = Box::pin(prepared.start()); + + // Drive to the wire, then park. + let _ = timeout(DRIVE, &mut start).await; + let create_req = server.read_request().await; + assert_eq!(create_req["method"], "session.create"); + + // The read task runs the inline callback and registers the ID while the + // caller's future stays unpolled. + server + .respond(&create_req, create_result("registered-then-cancelled")) + .await; + await_registered(&client, "registered-then-cancelled").await; + + // Cancellation now lands on a slot that already owns a registration. + drop(start); + + expect_closed(&mut events).await; + await_no_registrations(&client).await; + server.expect_quiet().await; + + // The same server-assigned ID can be handed out again without the dead + // attempt's cleanup interfering. + let retry = tokio::spawn( + client + .prepare_session(SessionConfig::default().with_cloud(cloud_options())) + .unwrap() + .start(), + ); + let retry_req = server.read_request().await; + server + .respond(&retry_req, create_result("registered-then-cancelled")) + .await; + let session = timeout(TIMEOUT, retry).await.unwrap().unwrap().unwrap(); + assert_eq!(session.id().as_str(), "registered-then-cancelled"); + assert_eq!( + client.registered_session_count_for_test(), + 1, + "retry must hold exactly one registration" + ); + drop(session); +} diff --git a/rust/tests/protocol_version_test.rs b/rust/tests/protocol_version_test.rs index 0d1268c59e..cd8563f87b 100644 --- a/rust/tests/protocol_version_test.rs +++ b/rust/tests/protocol_version_test.rs @@ -239,3 +239,153 @@ async fn connect_handshake_forwards_auto_generated_token() { .unwrap() .unwrap(); } + +/// Positive coverage for application-identity forwarding on the `connect` +/// handshake. A client constructed with a [`ClientInfo`] MUST serialize it +/// (camelCase) into the outbound `connect` request's `clientInfo` param so +/// the runtime attributes this connection's telemetry to the application. +#[tokio::test] +async fn connect_handshake_forwards_client_info() { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_client_info( + client_read, + client_write, + std::env::temp_dir(), + Some( + github_copilot_sdk::ClientInfo::new() + .with_application_name("acme-developer-portal") + .with_application_version("2.4.0") + .with_integration_name("copilot-assistant") + .with_integration_version("1.5.0"), + ), + ) + .unwrap(); + + let mut server_read = server_read; + let mut server_write = server_write; + + let verify_handle = tokio::spawn({ + let client = client.clone(); + async move { client.verify_protocol_version().await } + }); + + let req = read_framed(&mut server_read).await; + assert_eq!(req["method"], "connect"); + let client_info = &req["params"]["clientInfo"]; + assert_eq!(client_info["editorName"], "acme-developer-portal"); + assert_eq!(client_info["editorVersion"], "2.4.0"); + assert_eq!(client_info["extensionName"], "copilot-assistant"); + assert_eq!(client_info["extensionVersion"], "1.5.0"); + + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": req["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test-1.0.0" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + tokio::time::timeout(std::time::Duration::from_secs(2), verify_handle) + .await + .unwrap() + .unwrap() + .unwrap(); +} + +/// A [`ClientInfo`] with only some fields set must omit the empty ones from +/// the wire, and a fully-empty one must drop `clientInfo` entirely so the +/// runtime keeps its default attribution. +#[tokio::test] +async fn connect_handshake_omits_empty_client_info_fields() { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_client_info( + client_read, + client_write, + std::env::temp_dir(), + Some( + github_copilot_sdk::ClientInfo::new() + .with_application_name("example-app") + .with_application_version(""), + ), + ) + .unwrap(); + + let mut server_read = server_read; + let mut server_write = server_write; + + let verify_handle = tokio::spawn({ + let client = client.clone(); + async move { client.verify_protocol_version().await } + }); + + let req = read_framed(&mut server_read).await; + assert_eq!(req["method"], "connect"); + let client_info = &req["params"]["clientInfo"]; + assert_eq!(client_info["editorName"], "example-app"); + assert!(client_info.get("editorVersion").is_none()); + assert!(client_info.get("extensionName").is_none()); + assert!(client_info.get("extensionVersion").is_none()); + + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": req["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test-1.0.0" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + tokio::time::timeout(std::time::Duration::from_secs(2), verify_handle) + .await + .unwrap() + .unwrap() + .unwrap(); +} + +/// A [`ClientInfo`] whose every field is empty must drop `clientInfo` from the +/// handshake entirely so the runtime keeps its default attribution. +#[tokio::test] +async fn connect_handshake_omits_all_empty_client_info() { + let (client_write, server_read) = duplex(8192); + let (server_write, client_read) = duplex(8192); + let client = Client::from_streams_with_client_info( + client_read, + client_write, + std::env::temp_dir(), + Some( + github_copilot_sdk::ClientInfo::new() + .with_application_name("") + .with_application_version("") + .with_integration_name("") + .with_integration_version(""), + ), + ) + .unwrap(); + + let mut server_read = server_read; + let mut server_write = server_write; + + let verify_handle = tokio::spawn({ + let client = client.clone(); + async move { client.verify_protocol_version().await } + }); + + let req = read_framed(&mut server_read).await; + assert_eq!(req["method"], "connect"); + assert!( + req["params"].get("clientInfo").is_none(), + "an all-empty clientInfo must be omitted from the handshake" + ); + + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": req["id"], + "result": { "ok": true, "protocolVersion": 3, "version": "test-1.0.0" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&response).unwrap()).await; + + tokio::time::timeout(std::time::Duration::from_secs(2), verify_handle) + .await + .unwrap() + .unwrap() + .unwrap(); +} diff --git a/rust/tests/session_events_test.rs b/rust/tests/session_events_test.rs new file mode 100644 index 0000000000..437be9861f --- /dev/null +++ b/rust/tests/session_events_test.rs @@ -0,0 +1,36 @@ +// Unit tests for generated session-event payloads. + +#![allow(clippy::unwrap_used)] + +use github_copilot_sdk::session_events::UserMessageData; + +#[test] +fn user_message_id_uses_camel_case_wire_name() { + let data = UserMessageData { + content: "queued message".to_string(), + message_id: Some("message-123".to_string()), + ..Default::default() + }; + + let serialized = serde_json::to_value(&data).unwrap(); + assert_eq!(serialized["messageId"], "message-123"); + + let deserialized: UserMessageData = serde_json::from_value(serialized).unwrap(); + assert_eq!(deserialized.message_id.as_deref(), Some("message-123")); +} + +#[test] +fn user_message_id_is_optional_for_older_hosts() { + let data: UserMessageData = serde_json::from_value(serde_json::json!({ + "content": "legacy message" + })) + .unwrap(); + + assert_eq!(data.message_id, None); + assert!( + serde_json::to_value(data) + .unwrap() + .get("messageId") + .is_none() + ); +} diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index fb16a0f674..e7bafc683c 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -1,8 +1,10 @@ #![allow(clippy::unwrap_used)] +use std::collections::HashMap; +use std::fmt; use std::path::Path; use std::sync::Arc; -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; use std::time::Duration; use async_trait::async_trait; @@ -34,9 +36,151 @@ use github_copilot_sdk::types::{ use github_copilot_sdk::{Client, ContextTier, ErrorKind, ProtocolErrorKind, tool}; use serde_json::Value; use tokio::io::{AsyncWrite, AsyncWriteExt, duplex}; +use tokio::sync::Notify; use tokio::time::timeout; +use tracing::field::{Field, Visit}; +use tracing::span::{Attributes, Id, Record}; +use tracing::{Event, Metadata, Subscriber}; const TIMEOUT: Duration = Duration::from_secs(2); +const PERMISSION_CONFIRMATION_METHOD: &str = "session.permissions.handlePendingPermissionRequest"; + +#[derive(Clone, Debug)] +struct CapturedTraceEvent { + fields: HashMap, +} + +impl CapturedTraceEvent { + fn message_contains(&self, expected: &str) -> bool { + self.fields + .get("message") + .is_some_and(|message| message.contains(expected)) + } + + fn field_is(&self, name: &str, expected: &str) -> bool { + self.fields.get(name).is_some_and(|value| value == expected) + } +} + +#[derive(Clone, Default)] +struct TraceCapture { + events: Arc>>, +} + +impl TraceCapture { + fn permission_outcome(&self, request_id: &str) -> Option { + self.events + .lock() + .unwrap() + .iter() + .find(|event| { + event.field_is("request_id", request_id) + && (event.message_contains( + "Session::handle_notification response sent successfully", + ) || event.message_contains( + "failed to deliver permission decision back to the runtime", + ) || event + .message_contains("permission confirmation acknowledgement wait cancelled")) + }) + .cloned() + } + + async fn wait_for_permission_outcome(&self, request_id: &str) -> CapturedTraceEvent { + timeout(TIMEOUT, async { + loop { + if let Some(event) = self.permission_outcome(request_id) { + return event; + } + tokio::task::yield_now().await; + } + }) + .await + .expect("timed out waiting for permission confirmation diagnostic") + } +} + +#[derive(Default)] +struct TraceFieldVisitor { + fields: HashMap, +} + +impl Visit for TraceFieldVisitor { + fn record_bool(&mut self, field: &Field, value: bool) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_i64(&mut self, field: &Field, value: i64) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_u64(&mut self, field: &Field, value: u64) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_str(&mut self, field: &Field, value: &str) { + self.fields + .insert(field.name().to_string(), value.to_string()); + } + + fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) { + self.fields + .insert(field.name().to_string(), format!("{value:?}")); + } +} + +struct CaptureSubscriber { + capture: TraceCapture, + next_span_id: AtomicU64, +} + +impl CaptureSubscriber { + fn new(capture: TraceCapture) -> Self { + Self { + capture, + next_span_id: AtomicU64::new(1), + } + } +} + +impl Subscriber for CaptureSubscriber { + fn enabled(&self, _metadata: &Metadata<'_>) -> bool { + true + } + + fn new_span(&self, _span: &Attributes<'_>) -> Id { + Id::from_u64(self.next_span_id.fetch_add(1, Ordering::Relaxed)) + } + + fn record(&self, _span: &Id, _values: &Record<'_>) {} + + fn record_follows_from(&self, _span: &Id, _follows: &Id) {} + + fn event(&self, event: &Event<'_>) { + let mut visitor = TraceFieldVisitor::default(); + event.record(&mut visitor); + self.capture + .events + .lock() + .unwrap() + .push(CapturedTraceEvent { + fields: visitor.fields, + }); + } + + fn enter(&self, _span: &Id) {} + + fn exit(&self, _span: &Id) {} +} + +fn capture_traces() -> (TraceCapture, tracing::dispatcher::DefaultGuard) { + let capture = TraceCapture::default(); + let dispatch = tracing::Dispatch::new(CaptureSubscriber::new(capture.clone())); + let guard = tracing::dispatcher::set_default(&dispatch); + (capture, guard) +} struct TestCanvasHandler; @@ -44,6 +188,25 @@ struct CancelMcpAuthHandler; struct ContextualApproveHandler; +struct GatedApproveHandler { + entered: Arc, + release: Arc, +} + +#[async_trait] +impl PermissionHandler for GatedApproveHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _data: github_copilot_sdk::PermissionRequestData, + ) -> PermissionResult { + self.entered.notify_one(); + self.release.notified().await; + PermissionResult::approve_once() + } +} + #[async_trait] impl PermissionHandler for ContextualApproveHandler { async fn handle( @@ -158,6 +321,16 @@ impl FakeServer { write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; } + async fn respond_error(&mut self, request: &Value, code: i64, message: &str) { + let id = request["id"].as_u64().unwrap(); + let response = serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "error": { "code": code, "message": message }, + }); + write_framed(&mut self.write, &serde_json::to_vec(&response).unwrap()).await; + } + async fn send_notification(&mut self, method: &str, params: Value) { let notification = serde_json::json!({ "jsonrpc": "2.0", @@ -598,6 +771,108 @@ async fn create_session_registers_mcp_auth_interest_only_with_handler() { let _session = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); } +#[tokio::test] +async fn create_session_mcp_auth_registration_failure_cancels_external_tools() { + struct DropProbe(Option>); + + impl Drop for DropProbe { + fn drop(&mut self) { + if let Some(sender) = self.0.take() { + let _ = sender.send(()); + } + } + } + + struct BlockingTool { + started: parking_lot::Mutex>>, + cancelled: parking_lot::Mutex>>, + } + + #[async_trait] + impl tool::ToolHandler for BlockingTool { + async fn call( + &self, + _invocation: ToolInvocation, + ) -> Result { + if let Some(sender) = self.started.lock().take() { + let _ = sender.send(()); + } + let _probe = DropProbe(self.cancelled.lock().take()); + std::future::pending().await + } + } + + let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let (cancelled_tx, cancelled_rx) = tokio::sync::oneshot::channel(); + let (client, mut server_read, mut server_write) = make_client(); + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_mcp_auth_handler(Arc::new(CancelMcpAuthHandler)) + .with_tools(vec![ + Tool::new("blocked_tool") + .with_description("Blocks") + .with_parameters(serde_json::json!({"type":"object"})) + .with_handler(Arc::new(BlockingTool { + started: parking_lot::Mutex::new(Some(started_tx)), + cancelled: parking_lot::Mutex::new(Some(cancelled_tx)), + })), + ]), + ) + .await + } + }); + + let create_req = read_framed(&mut server_read).await; + let session_id = requested_session_id(&create_req).to_string(); + server_respond_create(&mut server_write, &create_req, &session_id).await; + let interest_req = read_framed(&mut server_read).await; + assert_eq!(interest_req["method"], "session.eventLog.registerInterest"); + + let event = serde_json::json!({ + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": session_id, + "event": { + "id": "evt-registration-failure", + "timestamp": "2025-01-01T00:00:00Z", + "type": "external_tool.requested", + "data": { + "requestId": "request-registration-failure", + "sessionId": session_id, + "toolCallId": "tool-call-registration-failure", + "toolName": "blocked_tool", + "arguments": {}, + }, + }, + }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&event).unwrap()).await; + timeout(TIMEOUT, started_rx).await.unwrap().unwrap(); + + let interest_id = interest_req["id"].as_u64().unwrap(); + let error = serde_json::json!({ + "jsonrpc": "2.0", + "id": interest_id, + "error": { "code": -32603, "message": "registration failed" }, + }); + write_framed(&mut server_write, &serde_json::to_vec(&error).unwrap()).await; + + assert!( + timeout(TIMEOUT, create_handle) + .await + .unwrap() + .unwrap() + .is_err() + ); + timeout(TIMEOUT, cancelled_rx).await.unwrap().unwrap(); +} + #[tokio::test] async fn cloud_create_session_registers_mcp_auth_interest_after_create_only_with_handler() { let cloud = || { @@ -1711,7 +1986,7 @@ async fn session_rpc_methods_send_correct_method_names() { let cases: Vec<(&str, Option<&str>)> = vec![ ("session.abort", None), ("session.log", Some("message")), - ("session.destroy", None), + ("session.detach", None), ]; for (expected_method, extra_param_key) in cases { @@ -1720,7 +1995,7 @@ async fn session_rpc_methods_send_correct_method_names() { match expected_method { "session.abort" => s.abort().await.map(|_| ()), "session.log" => s.log("test msg", None).await, - "session.destroy" => s.disconnect().await, + "session.detach" => s.disconnect().await, _ => unreachable!(), } }); @@ -1738,6 +2013,7 @@ async fn session_rpc_methods_send_correct_method_names() { "session.log" => { serde_json::json!({ "eventId": "00000000-0000-0000-0000-000000000000" }) } + "session.detach" => serde_json::json!({ "success": true }), _ => serde_json::json!({}), }; server.respond(&request, response).await; @@ -2792,7 +3068,8 @@ async fn user_input_requested_notification_does_not_double_dispatch() { } #[tokio::test] -async fn approve_all_handler_approves_permission() { +async fn permission_confirmation_success_behavior_is_unchanged() { + let (capture, _guard) = capture_traces(); let (_session, mut server) = create_session_pair_with_config(|cfg| { cfg.with_permission_handler(Arc::new(ApproveAllHandler)) }) @@ -2816,6 +3093,207 @@ async fn approve_all_handler_approves_permission() { ); assert_eq!(request["params"]["requestId"], "perm-auto"); assert_eq!(request["params"]["result"]["kind"], "approve-once"); + server.respond(&request, serde_json::json!({})).await; + + let outcome = capture.wait_for_permission_outcome("perm-auto").await; + assert!(outcome.message_contains("Session::handle_notification response sent successfully")); + assert!(outcome.field_is("session_id", &server.session_id)); + assert!(outcome.field_is("request_id", "perm-auto")); +} + +#[tokio::test] +async fn permission_confirmation_json_rpc_error_is_observable_and_connection_stays_responsive() { + let (capture, _guard) = capture_traces(); + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + let session = Arc::new(session); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-rpc-error", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + + let confirmation = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(confirmation["method"], PERMISSION_CONFIRMATION_METHOD); + server + .respond_error(&confirmation, -32603, "permission response rejected") + .await; + + let get_events = tokio::spawn({ + let session = session.clone(); + async move { session.get_events().await } + }); + let follow_up = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(follow_up["method"], "session.getMessages"); + server + .respond(&follow_up, serde_json::json!({ "events": [] })) + .await; + assert!(timeout(TIMEOUT, get_events).await.unwrap().unwrap().is_ok()); + + let outcome = capture.wait_for_permission_outcome("perm-rpc-error").await; + assert!(outcome.message_contains("failed to deliver permission decision back to the runtime")); + assert!(outcome.field_is("session_id", &server.session_id)); + assert!(outcome.field_is("request_id", "perm-rpc-error")); + assert!(outcome.field_is("method", PERMISSION_CONFIRMATION_METHOD)); +} + +#[tokio::test] +async fn permission_confirmation_write_failure_is_observable_and_events_stay_responsive() { + let (capture, _guard) = capture_traces(); + let entered = Arc::new(Notify::new()); + let release = Arc::new(Notify::new()); + let handler = Arc::new(GatedApproveHandler { + entered: entered.clone(), + release: release.clone(), + }); + let (session, mut server) = + create_session_pair_with_config(move |cfg| cfg.with_permission_handler(handler)).await; + let mut subscription = session.subscribe(); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-write-error", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + let permission_event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(permission_event.event_type, "permission.requested"); + timeout(TIMEOUT, entered.notified()).await.unwrap(); + + let FakeServer { + read, + mut write, + session_id, + } = server; + drop(read); + release.notify_one(); + + let idle_event = serde_json::json!({ + "jsonrpc": "2.0", + "method": "session.event", + "params": { + "sessionId": session_id, + "event": { + "id": "evt-after-write-error", + "timestamp": "2025-01-01T00:00:00Z", + "type": "session.idle", + "data": {}, + }, + }, + }); + write_framed(&mut write, &serde_json::to_vec(&idle_event).unwrap()).await; + let event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(event.event_type, "session.idle"); + + let outcome = capture + .wait_for_permission_outcome("perm-write-error") + .await; + assert!(outcome.message_contains("failed to deliver permission decision back to the runtime")); + assert!(outcome.field_is("session_id", &session_id)); + assert!(outcome.field_is("request_id", "perm-write-error")); + assert!(outcome.field_is("method", PERMISSION_CONFIRMATION_METHOD)); +} + +#[tokio::test] +async fn permission_confirmation_without_response_does_not_block_events_or_other_rpcs() { + let (capture, _guard) = capture_traces(); + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + let session = Arc::new(session); + let mut subscription = session.subscribe(); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-no-response", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + let permission_event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(permission_event.event_type, "permission.requested"); + let confirmation = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(confirmation["method"], PERMISSION_CONFIRMATION_METHOD); + + server + .send_event("session.idle", serde_json::json!({})) + .await; + let event = timeout(TIMEOUT, subscription.recv()) + .await + .unwrap() + .unwrap(); + assert_eq!(event.event_type, "session.idle"); + + let get_events = tokio::spawn({ + let session = session.clone(); + async move { session.get_events().await } + }); + let follow_up = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(follow_up["method"], "session.getMessages"); + server + .respond(&follow_up, serde_json::json!({ "events": [] })) + .await; + assert!(timeout(TIMEOUT, get_events).await.unwrap().unwrap().is_ok()); + assert!( + capture.permission_outcome("perm-no-response").is_none(), + "the confirmation task should still be waiting silently for its response" + ); +} + +#[tokio::test] +async fn permission_confirmation_wait_is_cancelled_on_session_teardown() { + let (capture, _guard) = capture_traces(); + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_permission_handler(Arc::new(ApproveAllHandler)) + }) + .await; + let session_id = server.session_id.clone(); + + server + .send_event( + "permission.requested", + serde_json::json!({ + "requestId": "perm-teardown", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, + }), + ) + .await; + let confirmation = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(confirmation["method"], PERMISSION_CONFIRMATION_METHOD); + + drop(session); + + let outcome = capture.wait_for_permission_outcome("perm-teardown").await; + assert!(outcome.message_contains("permission confirmation acknowledgement wait cancelled")); + assert!(outcome.field_is("session_id", &session_id)); + assert!(outcome.field_is("request_id", "perm-teardown")); + assert!(outcome.field_is("method", PERMISSION_CONFIRMATION_METHOD)); } #[tokio::test] @@ -3169,10 +3647,10 @@ async fn send_and_wait_drop_clears_waiter() { } /// Cancel-safety regression: `Session::stop_event_loop` must NOT abort -/// the event-loop task mid-handler. An in-flight handler (here a slow -/// `userInput.request` callback) must run to completion before the loop -/// exits — the CLI receives the response on the wire before the session -/// tears down. +/// the event-loop task at an arbitrary await point. Requests are +/// dispatched to their own tasks, so a handler that is still running when +/// shutdown is signalled keeps going and its response still reaches the +/// wire rather than being lost mid-protocol. /// /// Closes RFD-400 review finding #3. #[tokio::test] @@ -3216,31 +3694,84 @@ async fn stop_event_loop_completes_in_flight_handler() { // Give the loop a moment to dispatch into the handler. tokio::time::sleep(Duration::from_millis(20)).await; - // Now request shutdown. The loop is parked in handle_request awaiting - // the slow handler. `notify_one()` buffers the signal until the loop - // re-enters its select, which can only happen after the handler - // returns and the response is sent on the wire. + // Now request shutdown while the spawned handler is still sleeping. let stop_handle = tokio::spawn({ let session = session.clone(); async move { session.stop_event_loop().await } }); - // Verify the handler's response lands on the wire BEFORE the loop - // exits — i.e. stop_event_loop did not abort mid-handler. + // The handler task is independent of the loop, so its response still + // lands on the wire instead of being lost to an aborted task. let response = timeout(Duration::from_secs(2), server.read_response()) .await .unwrap(); assert_eq!(response["id"], 900); assert_eq!(response["result"]["answer"], "completed"); - // stop_event_loop completes after the handler returns and the loop - // observes the buffered shutdown signal on its next select iteration. timeout(Duration::from_secs(2), stop_handle) .await .unwrap() .unwrap(); } +/// A panicking request handler must still answer its request id. Tokio +/// isolates the panic to the spawned handler task, so without an explicit +/// reply the caller would wait out its own timeout on a request that can +/// never complete. +#[tokio::test] +async fn panicking_request_handler_responds_with_internal_error() { + struct PanickingHandler; + #[async_trait] + impl UserInputHandler for PanickingHandler { + async fn handle( + &self, + _session_id: SessionId, + _question: String, + _choices: Option>, + _allow_freeform: Option, + ) -> Option { + panic!("handler blew up"); + } + } + + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_user_input_handler(Arc::new(PanickingHandler)) + }) + .await; + + server + .send_request( + 901, + "userInput.request", + serde_json::json!({ + "sessionId": server.session_id, + "question": "boom", + "choices": null, + "allowFreeform": true, + }), + ) + .await; + + let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(response["id"], 901); + assert_eq!(response["error"]["code"], -32603); + assert!(response.get("result").is_none()); + + // The loop survives the panicking handler and keeps serving requests. + server + .send_request( + 902, + "unknown.method", + serde_json::json!({ "sessionId": server.session_id }), + ) + .await; + let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); + assert_eq!(response["id"], 902); + assert_eq!(response["error"]["code"], -32601); + + session.stop_event_loop().await; +} + /// Cancel-safety regression: dropping a Session does NOT abort the event /// loop mid-handler. The loop sees the buffered shutdown signal on its /// next select iteration and exits cleanly. This is the Drop equivalent @@ -3482,6 +4013,229 @@ async fn external_tool_requested_dispatches_to_handler_and_responds() { assert_eq!(rpc_call["params"]["result"], "all tests passed"); } +#[tokio::test] +async fn external_tool_completed_cancels_blocked_handler() { + struct DropProbe(Option>); + + impl Drop for DropProbe { + fn drop(&mut self) { + if let Some(sender) = self.0.take() { + let _ = sender.send(()); + } + } + } + + struct BlockingTool { + started: parking_lot::Mutex>>, + cancelled: parking_lot::Mutex>>, + } + + #[async_trait] + impl tool::ToolHandler for BlockingTool { + async fn call( + &self, + _invocation: ToolInvocation, + ) -> Result { + if let Some(sender) = self.started.lock().take() { + let _ = sender.send(()); + } + let _probe = DropProbe(self.cancelled.lock().take()); + std::future::pending().await + } + } + + let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let (cancelled_tx, cancelled_rx) = tokio::sync::oneshot::channel(); + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_tools(vec![ + Tool::new("blocked_tool") + .with_description("Blocks") + .with_parameters(serde_json::json!({"type":"object"})) + .with_handler(Arc::new(BlockingTool { + started: parking_lot::Mutex::new(Some(started_tx)), + cancelled: parking_lot::Mutex::new(Some(cancelled_tx)), + })), + ]) + }) + .await; + + server + .send_event( + "external_tool.requested", + serde_json::json!({ + "requestId": "request-cancel-1", + "sessionId": server.session_id, + "toolCallId": "tool-call-cancel-1", + "toolName": "blocked_tool", + "arguments": {}, + }), + ) + .await; + timeout(TIMEOUT, started_rx).await.unwrap().unwrap(); + + server + .send_event( + "external_tool.completed", + serde_json::json!({ "requestId": "request-cancel-1" }), + ) + .await; + + timeout(TIMEOUT, cancelled_rx).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn connection_close_cancels_blocked_external_tool() { + struct DropProbe(Option>); + + impl Drop for DropProbe { + fn drop(&mut self) { + if let Some(sender) = self.0.take() { + let _ = sender.send(()); + } + } + } + + struct BlockingTool { + started: parking_lot::Mutex>>, + cancelled: parking_lot::Mutex>>, + } + + #[async_trait] + impl tool::ToolHandler for BlockingTool { + async fn call( + &self, + _invocation: ToolInvocation, + ) -> Result { + if let Some(sender) = self.started.lock().take() { + let _ = sender.send(()); + } + let _probe = DropProbe(self.cancelled.lock().take()); + std::future::pending().await + } + } + + let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let (cancelled_tx, cancelled_rx) = tokio::sync::oneshot::channel(); + let (_session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_tools(vec![ + Tool::new("blocked_tool") + .with_description("Blocks") + .with_parameters(serde_json::json!({"type":"object"})) + .with_handler(Arc::new(BlockingTool { + started: parking_lot::Mutex::new(Some(started_tx)), + cancelled: parking_lot::Mutex::new(Some(cancelled_tx)), + })), + ]) + }) + .await; + + server + .send_event( + "external_tool.requested", + serde_json::json!({ + "requestId": "request-connection-close", + "sessionId": server.session_id, + "toolCallId": "tool-call-connection-close", + "toolName": "blocked_tool", + "arguments": {}, + }), + ) + .await; + timeout(TIMEOUT, started_rx).await.unwrap().unwrap(); + + drop(server); + + timeout(TIMEOUT, cancelled_rx).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn disconnect_cancels_external_tools_before_stopping_session() { + struct DropProbe(Option>); + + impl Drop for DropProbe { + fn drop(&mut self) { + if let Some(sender) = self.0.take() { + let _ = sender.send(()); + } + } + } + + struct BlockingTool { + started: parking_lot::Mutex>>, + cancelled: parking_lot::Mutex>>, + } + + #[async_trait] + impl tool::ToolHandler for BlockingTool { + async fn call( + &self, + _invocation: ToolInvocation, + ) -> Result { + if let Some(sender) = self.started.lock().take() { + let _ = sender.send(()); + } + let _probe = DropProbe(self.cancelled.lock().take()); + std::future::pending().await + } + } + + let (started_tx, started_rx) = tokio::sync::oneshot::channel(); + let (cancelled_tx, mut cancelled_rx) = tokio::sync::oneshot::channel(); + let (session, mut server) = create_session_pair_with_config(|cfg| { + cfg.with_tools(vec![ + Tool::new("blocked_tool") + .with_description("Blocks") + .with_parameters(serde_json::json!({"type":"object"})) + .with_handler(Arc::new(BlockingTool { + started: parking_lot::Mutex::new(Some(started_tx)), + cancelled: parking_lot::Mutex::new(Some(cancelled_tx)), + })), + ]) + }) + .await; + let session = Arc::new(session); + let lifetime = session.cancellation_token(); + + server + .send_event( + "external_tool.requested", + serde_json::json!({ + "requestId": "request-disconnect", + "sessionId": server.session_id, + "toolCallId": "tool-call-disconnect", + "toolName": "blocked_tool", + "arguments": {}, + }), + ) + .await; + timeout(TIMEOUT, started_rx).await.unwrap().unwrap(); + + let disconnect = tokio::spawn({ + let session = session.clone(); + async move { session.disconnect().await } + }); + + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!(request["method"], "session.detach"); + assert!(!lifetime.is_cancelled()); + assert!( + timeout(Duration::from_millis(50), &mut cancelled_rx) + .await + .is_err() + ); + + server + .respond(&request, serde_json::json!({"success": true})) + .await; + timeout(TIMEOUT, cancelled_rx).await.unwrap().unwrap(); + timeout(TIMEOUT, disconnect) + .await + .unwrap() + .unwrap() + .unwrap(); + assert!(lifetime.is_cancelled()); +} + #[tokio::test] async fn external_tool_broadcast_for_unknown_tool_is_not_responded_to() { // Phase H multi-client safety: a handler that doesn't claim the @@ -4413,7 +5167,7 @@ async fn rpc_namespace_client_models_list_dispatches_correctly() { #[tokio::test] async fn client_stop_sends_session_destroy_for_each_active_session() { // One client, two registered sessions. Client::stop must send - // session.destroy for each before returning Ok. + // session.detach for each before returning Ok. let (client, server_read, server_write) = make_client(); let mut server = FakeServer { @@ -4463,31 +5217,33 @@ async fn client_stop_sends_session_destroy_for_each_active_session() { .await; let _session_b = timeout(TIMEOUT, create_b).await.unwrap(); - // Drive Client::stop and respond to each destroy in turn. + // Drive Client::stop and respond to each detach in turn. let stop_handle = tokio::spawn({ let client = client.clone(); async move { client.stop().await } }); - let mut destroyed = Vec::new(); + let mut detached = Vec::new(); for _ in 0..2 { let req = server.read_request().await; - assert_eq!(req["method"], "session.destroy"); - destroyed.push(req["params"]["sessionId"].as_str().unwrap().to_string()); - server.respond(&req, serde_json::json!(null)).await; + assert_eq!(req["method"], "session.detach"); + detached.push(req["params"]["sessionId"].as_str().unwrap().to_string()); + server + .respond(&req, serde_json::json!({ "success": true })) + .await; } - destroyed.sort(); + detached.sort(); let mut expected = [session_id_a.clone(), session_id_b.clone()]; expected.sort(); - assert_eq!(destroyed, expected); + assert_eq!(detached, expected); let stop_result = timeout(TIMEOUT, stop_handle).await.unwrap().unwrap(); assert!(stop_result.is_ok(), "stop returned errors: {stop_result:?}"); } #[tokio::test] -async fn client_stop_aggregates_session_destroy_errors() { - // session.destroy fails on the wire — Client::stop returns +async fn client_stop_aggregates_session_detach_errors() { + // session.detach fails on the wire — Client::stop returns // StopErrors carrying the failure rather than short-circuiting. let (session, mut server) = create_session_pair().await; let client = session.client().clone(); @@ -4495,7 +5251,7 @@ async fn client_stop_aggregates_session_destroy_errors() { let stop_handle = tokio::spawn(async move { client.stop().await }); let req = server.read_request().await; - assert_eq!(req["method"], "session.destroy"); + assert_eq!(req["method"], "session.detach"); let id = req["id"].as_u64().unwrap(); let response = serde_json::json!({ "jsonrpc": "2.0", diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index acdea09727..f609009320 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -84,6 +84,11 @@ const goIdentifierCasingOverrides = new Map([ ]); const goCommentTextWrapLength = 90; const wrapGoCommentText = wordwrap(goCommentTextWrapLength); +const optionalNullableGoProperties = new Set([ + "ModelSwitchToRequest.autoTier", + "TaskClientUpdateProgress.percentage", + "TaskClientUpdateProgress.phase", +]); function goIdentifierWord(word: string, normalizeRest = false): string { const lower = word.toLowerCase(); @@ -396,6 +401,10 @@ function goJSONTag(jsonName: string, required: boolean, goType: string): string return `json:"${jsonName}${goJSONOmitSuffix(required, goType)}"`; } +function preserveOptionalNullableGoProperty(typeName: string, propName: string, goType: string): string { + return optionalNullableGoProperties.has(`${typeName}.${propName}`) ? `*${goType}` : goType; +} + async function formatGoFile(filePath: string): Promise { try { await execFileAsync("go", ["fmt", filePath]); @@ -1221,7 +1230,11 @@ function emitGoStruct( const prop = propSchema as JSONSchema7; const isReq = required.has(propName); const goName = toGoFieldName(propName); - const goType = resolveGoPropertyType(prop, typeName, propName, isReq, ctx); + const goType = preserveOptionalNullableGoProperty( + typeName, + propName, + resolveGoPropertyType(prop, typeName, propName, isReq, ctx) + ); if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); @@ -1947,7 +1960,11 @@ function emitGoFlatDiscriminatedUnion( continue; } const goName = toGoFieldName(propName); - const goType = resolveGoPropertyType(prop, variantTypeName, propName, required.has(propName), ctx); + const goType = preserveOptionalNullableGoProperty( + variantTypeName, + propName, + resolveGoPropertyType(prop, variantTypeName, propName, required.has(propName), ctx) + ); if (prop.description) { pushGoCommentForContext(lines, prop.description, ctx, "\t"); } @@ -3967,7 +3984,10 @@ async function generateRpc(schemaPath?: string): Promise { if (generatedTypeCode.includes("time.Time")) { imports.push(`"time"`); } - if (schema.clientSession || schema.clientGlobal) { + const publicClientSession = schema.clientSession + ? filterNodeByVisibility(schema.clientSession, "public") + : null; + if (publicClientSession || schema.clientGlobal) { imports.push(`"errors"`, `"fmt"`); } imports.push(`"github.com/github/copilot-sdk/go/internal/jsonrpc2"`); @@ -4268,8 +4288,9 @@ function clientHandlerMethodName(rpcMethod: string): string { return toPascalCase(rpcMethod.split(".").at(-1)!); } -function emitClientSessionApiRegistration(lines: string[], clientSchema: Record, resolveType: (name: string) => string, unionInfos: Map): void { - const groups = collectClientGroups(clientSchema); +export function emitClientSessionApiRegistration(lines: string[], clientSchema: Record, resolveType: (name: string) => string, unionInfos: Map): void { + const publicClientSchema = filterNodeByVisibility(clientSchema, "public") ?? {}; + const groups = collectClientGroups(publicClientSchema); for (const { groupName, groupNode, methods } of groups) { const interfaceName = clientHandlerInterfaceName(groupName); @@ -4324,17 +4345,19 @@ function emitClientSessionApiRegistration(lines: string[], clientSchema: Record< lines.push(`}`); lines.push(``); - lines.push(`func clientSessionHandlerError(err error) *jsonrpc2.Error {`); - lines.push(`\tif err == nil {`); - lines.push(`\t\treturn nil`); - lines.push(`\t}`); - lines.push(`\tvar rpcErr *jsonrpc2.Error`); - lines.push(`\tif errors.As(err, &rpcErr) {`); - lines.push(`\t\treturn rpcErr`); - lines.push(`\t}`); - lines.push(`\treturn &jsonrpc2.Error{Code: -32603, Message: err.Error()}`); - lines.push(`}`); - lines.push(``); + if (groups.length > 0) { + lines.push(`func clientSessionHandlerError(err error) *jsonrpc2.Error {`); + lines.push(`\tif err == nil {`); + lines.push(`\t\treturn nil`); + lines.push(`\t}`); + lines.push(`\tvar rpcErr *jsonrpc2.Error`); + lines.push(`\tif errors.As(err, &rpcErr) {`); + lines.push(`\t\treturn rpcErr`); + lines.push(`\t}`); + lines.push(`\treturn &jsonrpc2.Error{Code: -32603, Message: err.Error()}`); + lines.push(`}`); + lines.push(``); + } lines.push(`// RegisterClientSessionAPIHandlers registers handlers for server-to-client session API calls.`); lines.push(`func RegisterClientSessionAPIHandlers(client *jsonrpc2.Client, getHandlers func(sessionID string) *ClientSessionAPIHandlers) {`); diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index d4be520fe7..b3bfcf8bc9 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -3773,12 +3773,13 @@ function clientSessionHandlerMethodName(rpcMethod: string): string { return toSnakeCase(parts[parts.length - 1]); } -function emitClientSessionApiRegistration( +export function emitClientSessionApiRegistration( lines: string[], node: Record, resolveType: (name: string) => string ): void { - const groups = Object.entries(node).filter(([, value]) => typeof value === "object" && value !== null && !isRpcMethod(value)); + const publicNode = filterNodeByVisibility(node, "public") ?? {}; + const groups = Object.entries(publicNode).filter(([, value]) => typeof value === "object" && value !== null && !isRpcMethod(value)); for (const [groupName, groupNode] of groups) { const handlerName = `${toPascalCase(groupName)}Handler`; diff --git a/scripts/codegen/rust.ts b/scripts/codegen/rust.ts index 0feec5e98a..b3cc5d5753 100644 --- a/scripts/codegen/rust.ts +++ b/scripts/codegen/rust.ts @@ -378,9 +378,7 @@ function tryEmitRustUnion( const lines: string[] = []; if (schema.description) { - for (const line of schema.description.split(/\r?\n/)) { - lines.push(`/// ${line}`); - } + pushRustDoc(lines, schema.description); } pushRustExperimentalDocs(lines, isSchemaExperimental(schema) || ctx.experimentalTypeNames.has(enumName)); lines.push("#[derive(Debug, Clone, Serialize, Deserialize)]"); @@ -468,7 +466,8 @@ function pushRustExperimentalDocs( function pushRustDoc(lines: string[], text: string | undefined, indent = ""): void { if (!text) return; - for (const paragraph of text.trim().split(/\r?\n/)) { + const sanitized = text.replace(/\[::\]/g, "`[::]`"); + for (const paragraph of sanitized.trim().split(/\r?\n/)) { if (paragraph.trim().length === 0) { lines.push(`${indent}///`); } else { @@ -971,9 +970,7 @@ function emitRustStruct( for (const { propName, prop, isReq, rustField, rustType } of fields) { if (prop.description) { - for (const line of prop.description.split(/\r?\n/)) { - lines.push(` /// ${line}`); - } + pushRustDoc(lines, prop.description, " "); } pushRustExperimentalDocs(lines, isSchemaExperimental(prop), " "); const propIsInternal = isSchemaInternal(prop); diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 4984816d8d..f5e8acb146 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -25,6 +25,7 @@ import { collectExperimentalOnlyRpcReferencedDefinitionNames, collectReachableDefinitionNames, collectRpcMethodReferencedDefinitionNames, + filterNodeByVisibility, findSharedSchemaDefinitions, hasSchemaPayload, parseExternalSchemaRef, @@ -1076,15 +1077,16 @@ function handlerMethodName(rpcMethod: string): string { * `getHandler` callback that resolves a sessionId to a handler object. * Param types include sessionId — handler code can simply ignore it. */ -function emitClientSessionApiRegistration(clientSchema: Record): string[] { +export function emitClientSessionApiRegistration(clientSchema: Record): string[] { const lines: string[] = []; - const groups = collectClientGroups(clientSchema); + const publicClientSchema = filterNodeByVisibility(clientSchema, "public") ?? {}; + const groups = collectClientGroups(publicClientSchema); // Emit a handler interface per group for (const [groupName, methods] of groups) { const interfaceName = toPascalCase(groupName) + "Handler"; - const groupDeprecated = isNodeFullyDeprecated(clientSchema[groupName] as Record); - const groupExperimental = isNodeFullyExperimental(clientSchema[groupName] as Record); + const groupDeprecated = isNodeFullyDeprecated(publicClientSchema[groupName] as Record); + const groupExperimental = isNodeFullyExperimental(publicClientSchema[groupName] as Record); if (groupDeprecated) { lines.push(`/** @deprecated Handler for \`${groupName}\` client session API methods. */`); } else if (groupExperimental) { diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 590213d72b..bad31a7675 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -12,6 +12,8 @@ import type { JSONSchema7, JSONSchema7Definition } from "json-schema"; import path from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; +import { COPILOT_CLI_VERSION } from "../../nodejs/src/cliVersion.js"; +import { ensureCopilotPackage } from "../../nodejs/scripts/releaseArtifacts.js"; export const execFileAsync = promisify(execFile); @@ -45,59 +47,23 @@ export type SchemaWithSharedDefinitions = T }; // ── Schema paths ──────────────────────────────────────────────────────────── -const SDK_NODE_MODULES = path.join(REPO_ROOT, "nodejs/node_modules"); - /** - * Resolve a JSON schema shipped by the `@github/copilot` CLI package. - * - * The CLI package layout changed in 1.0.64-1: the umbrella `@github/copilot` - * package became a thin loader and its bundled assets (including the JSON - * schemas) moved into the platform-specific packages installed as optional - * dependencies, e.g. `@github/copilot-linux-x64` or `@github/copilot-win32-x64`. - * - * To support both layouts we look in the umbrella package first (older - * versions) and then in whichever platform package was installed for the - * current host. + * Resolve a JSON schema from the pinned Copilot CLI GitHub Release. */ -async function resolveCopilotSchemaPath(nodeModulesDir: string, fileName: string): Promise { - const candidates = [path.join(nodeModulesDir, "@github/copilot/schemas", fileName)]; - - const githubScopeDir = path.join(nodeModulesDir, "@github"); - try { - for (const entry of await fs.readdir(githubScopeDir)) { - if (entry.startsWith("copilot-")) { - candidates.push(path.join(githubScopeDir, entry, "schemas", fileName)); - } - } - } catch (err) { - const code = (err as NodeJS.ErrnoException).code; - if (code !== "ENOENT" && code !== "ENOTDIR") { - throw err; - } - // @github scope directory may not exist yet; fall through to the error below. - } - - for (const candidate of candidates) { - try { - await fs.access(candidate); - return candidate; - } catch { - // Try the next candidate. - } - } - - throw new Error( - `${fileName} not found under ${githubScopeDir}. Run 'npm ci' in nodejs/ first.` - ); +async function resolveCopilotSchemaPath(fileName: string): Promise { + const packageRoot = await ensureCopilotPackage(COPILOT_CLI_VERSION); + const schemaPath = path.join(packageRoot, "schemas", fileName); + await fs.access(schemaPath); + return schemaPath; } export async function getSessionEventsSchemaPath(): Promise { - return resolveCopilotSchemaPath(SDK_NODE_MODULES, "session-events.schema.json"); + return resolveCopilotSchemaPath("session-events.schema.json"); } export async function getApiSchemaPath(cliArg?: string): Promise { if (cliArg) return cliArg; - return resolveCopilotSchemaPath(SDK_NODE_MODULES, "api.schema.json"); + return resolveCopilotSchemaPath("api.schema.json"); } // ── Brand casing normalization ────────────────────────────────────────────── diff --git a/test/harness/capturingHttpProxy.test.ts b/test/harness/capturingHttpProxy.test.ts index f434d3e67c..cba4d45e53 100644 --- a/test/harness/capturingHttpProxy.test.ts +++ b/test/harness/capturingHttpProxy.test.ts @@ -10,9 +10,21 @@ describe("Capturing HTTP Proxy", () => { let proxy: CapturingHttpProxy; let testServer: http.Server; let testServerAddress: string; + let onHangingRequest: (() => void) | undefined; + let onStreamingResponse: (() => void) | undefined; beforeEach(async () => { testServer = http.createServer((req, res) => { + if (req.url === "/hang") { + onHangingRequest?.(); + return; + } + if (req.url === "/stream") { + res.writeHead(200, { "content-type": "text/plain" }); + res.write("started"); + onStreamingResponse?.(); + return; + } res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ message: "Hello", path: req.url })); }); @@ -71,4 +83,34 @@ describe("Capturing HTTP Proxy", () => { } as CapturedExchange, ]); }); + + test("stops while a proxied request is still active", async () => { + proxy = new CapturingHttpProxy(testServerAddress); + const proxyUrl = await proxy.start(); + const requestStarted = new Promise((resolve) => { + onHangingRequest = resolve; + }); + const request = fetch(`${proxyUrl}/hang`).catch(() => undefined); + await requestStarted; + + await proxy.stop(); + + await request; + }); + + test("stops while a proxied response is still streaming", async () => { + proxy = new CapturingHttpProxy(testServerAddress); + const proxyUrl = await proxy.start(); + const responseStarted = new Promise((resolve) => { + onStreamingResponse = resolve; + }); + const responsePromise = fetch(`${proxyUrl}/stream`); + await responseStarted; + const response = await responsePromise; + const body = response.text().catch(() => undefined); + + await proxy.stop(); + + await body; + }); }); diff --git a/test/harness/capturingHttpProxy.ts b/test/harness/capturingHttpProxy.ts index edccca4ead..fdc1fc46c1 100644 --- a/test/harness/capturingHttpProxy.ts +++ b/test/harness/capturingHttpProxy.ts @@ -10,7 +10,10 @@ import https from "https"; */ export class CapturingHttpProxy { private readonly capturedExchanges: CapturedExchange[] = []; + private readonly activeRequests = new Set(); + private readonly activeResponses = new Set(); private server?: http.Server; + private stopPromise?: Promise; constructor(private targetUrl: string) {} @@ -90,6 +93,10 @@ export class CapturingHttpProxy { res.end(); }, onError: (err) => { + if (!this.server) { + res.destroy(); + return; + } console.error("Error in proxying request:", err); const endTime = Date.now(); const formattedError = @@ -130,9 +137,19 @@ export class CapturingHttpProxy { } async stop(): Promise { - if (this.server) { - return new Promise((resolve, reject) => { - this.server!.close((err) => { + if (this.stopPromise) { + return this.stopPromise; + } + + const server = this.server; + if (!server) { + return; + } + + this.server = undefined; + this.stopPromise = (async () => { + const closed = new Promise((resolve, reject) => { + server.close((err) => { if (err) { reject(err); } else { @@ -140,14 +157,38 @@ export class CapturingHttpProxy { } }); }); - } + + // server.close() waits for active connections. A replayed streaming request + // can otherwise wedge fixture teardown after its test has already passed. + server.closeAllConnections(); + for (const response of this.activeResponses) { + response.destroy(); + } + this.activeResponses.clear(); + for (const request of this.activeRequests) { + request.destroy(); + } + this.activeRequests.clear(); + + await closed; + })(); + return this.stopPromise; } performRequest(options: PerformRequestOptions): void { + if (this.stopPromise) { + options.onError(new Error("Proxy is stopping")); + return; + } + const protocol = options.isHttps ? https : http; const upstreamRequest = protocol.request( options.requestOptions, (upstreamResponse) => { + this.activeResponses.add(upstreamResponse); + upstreamResponse.once("close", () => { + this.activeResponses.delete(upstreamResponse); + }); options.onResponseStart( upstreamResponse.statusCode || 500, upstreamResponse.headers, @@ -157,6 +198,10 @@ export class CapturingHttpProxy { }, ); + this.activeRequests.add(upstreamRequest); + upstreamRequest.once("close", () => { + this.activeRequests.delete(upstreamRequest); + }); upstreamRequest.on("error", options.onError); if (options.body) { diff --git a/test/harness/connectProxy.test.ts b/test/harness/connectProxy.test.ts index 86d205dd39..ea2c8a27f5 100644 --- a/test/harness/connectProxy.test.ts +++ b/test/harness/connectProxy.test.ts @@ -60,6 +60,32 @@ describe("ConnectProxy", () => { await proxy.stop(); }); + test("stops with an active forward-proxy response", async () => { + let requestStarted!: () => void; + const started = new Promise((resolve) => { + requestStarted = resolve; + }); + const proxy = new ConnectProxy((_req, res) => { + res.writeHead(200); + res.write("partial"); + requestStarted(); + return true; + }); + await proxy.start(); + + const proxyUrl = new URL(proxy.proxyUrl); + const request = http.request({ + host: proxyUrl.hostname, + port: Number(proxyUrl.port), + path: "http://example.com/stream", + }); + request.on("error", () => {}); + request.end(); + await started; + + await proxy.stop(); + }); + test("intercepts HTTPS requests to configured domains", async () => { const requests: Array<{ host: string; url: string }> = []; const handler: RequestHandler = (req, res, targetHost) => { diff --git a/test/harness/connectProxy.ts b/test/harness/connectProxy.ts index d5aade0872..1de0ab8816 100644 --- a/test/harness/connectProxy.ts +++ b/test/harness/connectProxy.ts @@ -49,6 +49,8 @@ export class ConnectProxy { private passthroughDomains: Set; private onBlockedConnection?: (host: string, port: string) => void; private openSockets = new Set(); + private stopping = false; + private stopPromise?: Promise; constructor( private handler: RequestHandler, @@ -86,6 +88,8 @@ export class ConnectProxy { } async start(): Promise { + this.stopping = false; + this.stopPromise = undefined; this.ca = generateCA(); const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "e2e-proxy-ca-")); fs.writeFileSync(path.join(tmpDir, "test-ca.pem"), this.ca.certPem); @@ -137,35 +141,53 @@ export class ConnectProxy { } async stop(): Promise { - for (const socket of this.openSockets) { - socket.destroy(); + if (this.stopPromise) { + return this.stopPromise; } - this.openSockets.clear(); - const closeServer = (server?: http.Server) => - new Promise((resolve) => { - if (!server) { - resolve(); - return; - } - server.close(() => resolve()); - }); - - await Promise.all([ - closeServer(this.proxyServer), - closeServer(this.internalServer), - ]); - - if (this._caFilePath) { - try { - fs.rmSync(path.dirname(this._caFilePath), { - recursive: true, - force: true, + this.stopping = true; + const proxyServer = this.proxyServer; + const internalServer = this.internalServer; + const caFilePath = this._caFilePath; + this.proxyServer = undefined; + this.internalServer = undefined; + this._caFilePath = undefined; + this._proxyUrl = undefined; + + this.stopPromise = (async () => { + const closeServer = (server?: http.Server) => + new Promise((resolve) => { + if (!server) { + resolve(); + return; + } + server.close(() => resolve()); + server.closeAllConnections(); }); - } catch { - // Best-effort cleanup. + + for (const socket of this.openSockets) { + socket.destroy(); } - } + this.openSockets.clear(); + + await Promise.all([ + closeServer(proxyServer), + closeServer(internalServer), + ]); + + if (caFilePath) { + try { + fs.rmSync(path.dirname(caFilePath), { + recursive: true, + force: true, + }); + } catch { + // Best-effort cleanup. + } + } + })(); + + return this.stopPromise; } private handleConnect( @@ -173,6 +195,11 @@ export class ConnectProxy { clientSocket: net.Socket, head: Buffer, ) { + if (this.stopping) { + clientSocket.end("HTTP/1.1 503 Proxy Stopping\r\n\r\n"); + return; + } + const { host, port } = parseConnectTarget(req.url ?? ""); debugLog(`CONNECT ${host}:${port}`); if (!host) { @@ -244,6 +271,12 @@ export class ConnectProxy { req: http.IncomingMessage, res: http.ServerResponse, ) { + if (this.stopping) { + res.writeHead(503, { "content-type": "text/plain" }); + res.end("E2E proxy: stopping"); + return; + } + let targetHost: string; try { const url = new URL(req.url ?? ""); diff --git a/test/harness/modelProtocolAdapters.test.ts b/test/harness/modelProtocolAdapters.test.ts index c0ed084150..add59ae77f 100644 --- a/test/harness/modelProtocolAdapters.test.ts +++ b/test/harness/modelProtocolAdapters.test.ts @@ -42,7 +42,7 @@ const endpoints: Record = { const models: Record = { capi: "gpt-4.1", - "anthropic-messages": "claude-sonnet-4.5", + "anthropic-messages": "claude-sonnet-5", "openai-responses": "gpt-4.1", "openai-completions": "gpt-4.1", }; diff --git a/test/harness/package-lock.json b/test/harness/package-lock.json index a83d73d0a9..547e0a6ec9 100644 --- a/test/harness/package-lock.json +++ b/test/harness/package-lock.json @@ -9,7 +9,6 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@github/copilot": "^1.0.83-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", @@ -26,7 +25,7 @@ }, "node_modules/@emnapi/core": { "version": "1.10.0", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "integrity": "sha1-OAzMjyQS6iLR2XLff47iOjucdGc=", "dev": true, "license": "MIT", "optional": true, @@ -37,7 +36,7 @@ }, "node_modules/@emnapi/runtime": { "version": "1.10.0", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "integrity": "sha1-SyYMDTU0IE6YxhELjbGph9JuyHw=", "dev": true, "license": "MIT", "optional": true, @@ -47,7 +46,7 @@ }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "integrity": "sha1-KP7SGhuhznl8RKBwq8lNQvOuhUg=", "dev": true, "license": "MIT", "optional": true, @@ -57,7 +56,7 @@ }, "node_modules/@esbuild/aix-ppc64": { "version": "0.28.1", - "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "integrity": "sha1-egGo0uwvuy2seK2tCbD6eB5Agr4=", "cpu": [ "ppc64" ], @@ -73,7 +72,7 @@ }, "node_modules/@esbuild/android-arm": { "version": "0.28.1", - "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "integrity": "sha1-cEvSl95tdi3lTqu+r79V9nVqvi8=", "cpu": [ "arm" ], @@ -89,7 +88,7 @@ }, "node_modules/@esbuild/android-arm64": { "version": "0.28.1", - "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "integrity": "sha1-tUCifRTkr9BYSWpNvsTT9BTbEQo=", "cpu": [ "arm64" ], @@ -105,7 +104,7 @@ }, "node_modules/@esbuild/android-x64": { "version": "0.28.1", - "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "integrity": "sha1-0csWbTSw+/D+irRgpVlPJKN4cB4=", "cpu": [ "x64" ], @@ -121,7 +120,6 @@ }, "node_modules/@esbuild/darwin-arm64": { "version": "0.28.1", - "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -137,7 +135,7 @@ }, "node_modules/@esbuild/darwin-x64": { "version": "0.28.1", - "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "integrity": "sha1-ZVVqQyoeTXIDLYIYwZMvzKGkl3I=", "cpu": [ "x64" ], @@ -153,7 +151,7 @@ }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.28.1", - "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "integrity": "sha1-LmHgWS+QMNfj2uGO4l68U1kYrvY=", "cpu": [ "arm64" ], @@ -169,7 +167,7 @@ }, "node_modules/@esbuild/freebsd-x64": { "version": "0.28.1", - "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "integrity": "sha1-yV7CiZWe+AecTcqBeh4sS+Zrm9M=", "cpu": [ "x64" ], @@ -185,7 +183,7 @@ }, "node_modules/@esbuild/linux-arm": { "version": "0.28.1", - "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "integrity": "sha1-wJoPZ5F1kqwN6JKpvk04FN69Kmw=", "cpu": [ "arm" ], @@ -201,7 +199,7 @@ }, "node_modules/@esbuild/linux-arm64": { "version": "0.28.1", - "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "integrity": "sha1-QLIhdd2gYYLz7oFBGGxf8wTEpxc=", "cpu": [ "arm64" ], @@ -217,7 +215,7 @@ }, "node_modules/@esbuild/linux-ia32": { "version": "0.28.1", - "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "integrity": "sha1-pYD5xnZ5eDOJHlGfx6EzfIr9jbM=", "cpu": [ "ia32" ], @@ -233,7 +231,7 @@ }, "node_modules/@esbuild/linux-loong64": { "version": "0.28.1", - "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "integrity": "sha1-RkUs8yHcf56Rwvp4Cla7Vuec1os=", "cpu": [ "loong64" ], @@ -249,7 +247,7 @@ }, "node_modules/@esbuild/linux-mips64el": { "version": "0.28.1", - "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "integrity": "sha1-QhGzGE3WYI9T3LIuOfXTTuCIUsg=", "cpu": [ "mips64el" ], @@ -265,7 +263,7 @@ }, "node_modules/@esbuild/linux-ppc64": { "version": "0.28.1", - "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "integrity": "sha1-aXhXwqYcubC2u2ZS5AwdxeHKjl0=", "cpu": [ "ppc64" ], @@ -281,7 +279,7 @@ }, "node_modules/@esbuild/linux-riscv64": { "version": "0.28.1", - "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "integrity": "sha1-0ZKUPrFGpArExkl9DPe+NbmGvwg=", "cpu": [ "riscv64" ], @@ -297,7 +295,7 @@ }, "node_modules/@esbuild/linux-s390x": { "version": "0.28.1", - "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "integrity": "sha1-rOoDVtoODrwI+Xz3ucLkAeHmSNw=", "cpu": [ "s390x" ], @@ -313,7 +311,7 @@ }, "node_modules/@esbuild/linux-x64": { "version": "0.28.1", - "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "integrity": "sha1-bww84MtkxTS3DExF7LLBbTTjXf0=", "cpu": [ "x64" ], @@ -329,7 +327,7 @@ }, "node_modules/@esbuild/netbsd-arm64": { "version": "0.28.1", - "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "integrity": "sha1-i813B3oNzjN4tXT+2ybSolO3PTY=", "cpu": [ "arm64" ], @@ -345,7 +343,7 @@ }, "node_modules/@esbuild/netbsd-x64": { "version": "0.28.1", - "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "integrity": "sha1-5/sqAemcgwyU5mI82f77TI+1g0c=", "cpu": [ "x64" ], @@ -361,7 +359,7 @@ }, "node_modules/@esbuild/openbsd-arm64": { "version": "0.28.1", - "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "integrity": "sha1-xSkJNy24uG4sVeBaiUADO1Zgo7I=", "cpu": [ "arm64" ], @@ -377,7 +375,7 @@ }, "node_modules/@esbuild/openbsd-x64": { "version": "0.28.1", - "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "integrity": "sha1-xCe5vlpkwmL/mn63C1+7qt9EbGw=", "cpu": [ "x64" ], @@ -393,7 +391,7 @@ }, "node_modules/@esbuild/openharmony-arm64": { "version": "0.28.1", - "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "integrity": "sha1-3JsUe6yi5sSzyFVxdB70hgpIkJc=", "cpu": [ "arm64" ], @@ -409,7 +407,7 @@ }, "node_modules/@esbuild/sunos-x64": { "version": "0.28.1", - "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "integrity": "sha1-zoZtEt8TwV5MmfBzo9Rm9uBkmzo=", "cpu": [ "x64" ], @@ -425,7 +423,7 @@ }, "node_modules/@esbuild/win32-arm64": { "version": "0.28.1", - "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "integrity": "sha1-dGjjaS0B1inVlB5dg4F7uA+eObQ=", "cpu": [ "arm64" ], @@ -441,7 +439,7 @@ }, "node_modules/@esbuild/win32-ia32": { "version": "0.28.1", - "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "integrity": "sha1-pbwAY/sryrbQ7WPyoVN5WLwmnsY=", "cpu": [ "ia32" ], @@ -457,7 +455,7 @@ }, "node_modules/@esbuild/win32-x64": { "version": "0.28.1", - "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "integrity": "sha1-EAZO5E9DR7kMmgK0Rrv4CpFjKxI=", "cpu": [ "x64" ], @@ -471,159 +469,8 @@ "node": ">=18" } }, - "node_modules/@github/copilot": { - "version": "1.0.83-0", - "integrity": "sha512-Nv4IsqsveMgghwaBhgvSBZyIyvsqNBZTqnbVnv69+9+Suyq20vJcv6aB74UcJ7VPCMxIGJJUaJkugEtkMNv6wA==", - "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.83-0", - "@github/copilot-darwin-x64": "1.0.83-0", - "@github/copilot-linux-arm64": "1.0.83-0", - "@github/copilot-linux-x64": "1.0.83-0", - "@github/copilot-linuxmusl-arm64": "1.0.83-0", - "@github/copilot-linuxmusl-x64": "1.0.83-0", - "@github/copilot-win32-arm64": "1.0.83-0", - "@github/copilot-win32-x64": "1.0.83-0" - } - }, - "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-0KQjKS9vd4QGxLAbFJcvyv/zsC5kivrtDe0UZhHt/43nUGqoS61DFcsM596/kg75vNE6c9J4gmZ5fUPYef+0hw==", - "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.83-0", - "integrity": "sha512-fiyW+hy4c8AI7ONxN623f9cmJGRpbqTztc0jSVXc9z9WwzcWi39X0nxUprRM2l2Dq6YQ3guPCqGl/g1T5bQfQg==", - "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.83-0", - "integrity": "sha512-RWbRU+KgEmtAdKp1GQVTqfdwg4Ti/OVmgZGkXq4lMYj3wnBBQcayFpSLHg5ShzDSS0RglD4b8Z27NjPrm7bXxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linux-x64": { - "version": "1.0.83-0", - "integrity": "sha512-5COXUNT+jDfkeyqrymZMvhTogkBYUXt+wuRwKrK6ol5vaw5SoDP1DYbI2hIEfoUj4g7XTHLUCD1s3lw8eicqUA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linux-x64": "copilot" - } - }, - "node_modules/@github/copilot-linuxmusl-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-7sYf364iz6s97ClviBRQusTKz3S3TgoKniyYv8+aRi5f5w6TL8NTPnGX1bXMeU0VZmk5VKQTlxVRO2yA4uFwpg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-arm64": "copilot" - } - }, - "node_modules/@github/copilot-linuxmusl-x64": { - "version": "1.0.83-0", - "integrity": "sha512-jze/f6Yd3Y83kxUa88kXUiwHlZmHDwAqudswdHT6f6q+K1ZEELFGEzbB6Ku4i0L8M6wHXO1EF/zaiSFWQaM4Tw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "linux" - ], - "bin": { - "copilot-linuxmusl-x64": "copilot" - } - }, - "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.83-0", - "integrity": "sha512-93jln98UAJpslMQ7n+wAmCpoOWGEV5lXxV/DaEajySvYrCU33D2yj7d9kl8X2CgaUVBas6sNWSWKtqy+rKJxXQ==", - "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.83-0", - "integrity": "sha512-+4Htk3CixO1qcOtYegjn33/8bSDdx8QXDpgVBak2D4Y5hzBWPO5IuQoICwvjaW5VOIW+I7Q62RK2pupSjxB38Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "SEE LICENSE IN LICENSE.md", - "optional": true, - "os": [ - "win32" - ], - "bin": { - "copilot-win32-x64": "copilot.exe" - } - }, "node_modules/@hono/node-server": { "version": "1.19.14", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "dev": true, "license": "MIT", "engines": { @@ -635,13 +482,11 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.26.0", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "dev": true, "license": "MIT", "dependencies": { @@ -680,26 +525,28 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.4", - "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "version": "1.2.3", + "integrity": "sha1-l+PUXXQk3F2h1OMvO/OykvbBtEw=", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.1" + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@oxc-project/types": { "version": "0.133.0", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", "funding": { @@ -708,7 +555,7 @@ }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.3", - "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "integrity": "sha1-VM6Pg4IhP0oxSgwve6g/gf/q5ZI=", "cpu": [ "arm64" ], @@ -724,7 +571,6 @@ }, "node_modules/@rolldown/binding-darwin-arm64": { "version": "1.0.3", - "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -740,7 +586,7 @@ }, "node_modules/@rolldown/binding-darwin-x64": { "version": "1.0.3", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "integrity": "sha1-U/V94fWZ7PHbE4I8/IjBj7gJVK0=", "cpu": [ "x64" ], @@ -756,7 +602,7 @@ }, "node_modules/@rolldown/binding-freebsd-x64": { "version": "1.0.3", - "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "integrity": "sha1-bz/dobeuqsnSaKUmgEtPuW5ONfE=", "cpu": [ "x64" ], @@ -772,7 +618,7 @@ }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { "version": "1.0.3", - "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "integrity": "sha1-2HpFS/WFzJZ2hJN36R1uN1KXMm8=", "cpu": [ "arm" ], @@ -788,7 +634,7 @@ }, "node_modules/@rolldown/binding-linux-arm64-gnu": { "version": "1.0.3", - "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "integrity": "sha1-QZ/Wv2Es80jxBSjLzZTrq5YH2NE=", "cpu": [ "arm64" ], @@ -804,7 +650,7 @@ }, "node_modules/@rolldown/binding-linux-arm64-musl": { "version": "1.0.3", - "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "integrity": "sha1-/MaRhpa7doRId+HkkwoY/Q03QGk=", "cpu": [ "arm64" ], @@ -820,7 +666,7 @@ }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { "version": "1.0.3", - "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "integrity": "sha1-Mq7LfI2uXU8qjN5XoFjshpkVQvg=", "cpu": [ "ppc64" ], @@ -836,7 +682,7 @@ }, "node_modules/@rolldown/binding-linux-s390x-gnu": { "version": "1.0.3", - "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "integrity": "sha1-vtk0bqgea7i5PPEfXYi3fbiQt2M=", "cpu": [ "s390x" ], @@ -852,7 +698,7 @@ }, "node_modules/@rolldown/binding-linux-x64-gnu": { "version": "1.0.3", - "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "integrity": "sha1-ZMLSb3Xf/ZtaH5dVegCudyUMjLc=", "cpu": [ "x64" ], @@ -868,7 +714,7 @@ }, "node_modules/@rolldown/binding-linux-x64-musl": { "version": "1.0.3", - "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "integrity": "sha1-WkUTLopHZZ7qrztUDClUqXyGD/M=", "cpu": [ "x64" ], @@ -884,7 +730,7 @@ }, "node_modules/@rolldown/binding-openharmony-arm64": { "version": "1.0.3", - "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "integrity": "sha1-KQUTBoxV6EnchFejKv7h17Csswk=", "cpu": [ "arm64" ], @@ -900,7 +746,7 @@ }, "node_modules/@rolldown/binding-wasm32-wasi": { "version": "1.0.3", - "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "integrity": "sha1-PZly2/GpU9PHr6pKDyDvKy458xs=", "cpu": [ "wasm32" ], @@ -918,7 +764,7 @@ }, "node_modules/@rolldown/binding-win32-arm64-msvc": { "version": "1.0.3", - "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "integrity": "sha1-oASrYHoW1vA7y1VXKP+IivdXc60=", "cpu": [ "arm64" ], @@ -934,7 +780,7 @@ }, "node_modules/@rolldown/binding-win32-x64-msvc": { "version": "1.0.3", - "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "integrity": "sha1-4qJbNGkaHMihIJ195wkGMCbdDNs=", "cpu": [ "x64" ], @@ -950,19 +796,17 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "dev": true, "license": "MIT" }, "node_modules/@standard-schema/spec": { "version": "1.1.0", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "version": "0.10.3", + "integrity": "sha1-AVy6np3UfOFNA9KoxdVHv7FpZl0=", "dev": true, "license": "MIT", "optional": true, @@ -972,7 +816,6 @@ }, "node_modules/@types/chai": { "version": "5.2.3", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { @@ -982,19 +825,16 @@ }, "node_modules/@types/deep-eql": { "version": "4.0.2", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.9", - "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, "node_modules/@types/node": { "version": "25.3.3", - "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1003,7 +843,6 @@ }, "node_modules/@types/node-forge": { "version": "1.3.14", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1012,7 +851,6 @@ }, "node_modules/@vitest/expect": { "version": "4.1.8", - "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1029,7 +867,6 @@ }, "node_modules/@vitest/mocker": { "version": "4.1.8", - "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { @@ -1055,7 +892,6 @@ }, "node_modules/@vitest/pretty-format": { "version": "4.1.8", - "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { @@ -1067,7 +903,6 @@ }, "node_modules/@vitest/runner": { "version": "4.1.8", - "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1080,7 +915,6 @@ }, "node_modules/@vitest/snapshot": { "version": "4.1.8", - "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1095,7 +929,6 @@ }, "node_modules/@vitest/spy": { "version": "4.1.8", - "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", "funding": { @@ -1104,7 +937,6 @@ }, "node_modules/@vitest/utils": { "version": "4.1.8", - "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -1118,7 +950,6 @@ }, "node_modules/accepts": { "version": "2.0.0", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "dev": true, "license": "MIT", "dependencies": { @@ -1131,7 +962,6 @@ }, "node_modules/ajv": { "version": "8.18.0", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -1147,7 +977,6 @@ }, "node_modules/ajv-formats": { "version": "3.0.1", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1164,7 +993,6 @@ }, "node_modules/assertion-error": { "version": "2.0.1", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { @@ -1173,7 +1001,6 @@ }, "node_modules/body-parser": { "version": "2.2.2", - "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "license": "MIT", "dependencies": { @@ -1197,7 +1024,6 @@ }, "node_modules/bytes": { "version": "3.1.2", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, "license": "MIT", "engines": { @@ -1206,7 +1032,6 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1219,7 +1044,6 @@ }, "node_modules/call-bound": { "version": "1.0.4", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, "license": "MIT", "dependencies": { @@ -1235,7 +1059,6 @@ }, "node_modules/chai": { "version": "6.2.2", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { @@ -1244,7 +1067,6 @@ }, "node_modules/content-disposition": { "version": "1.0.1", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "dev": true, "license": "MIT", "engines": { @@ -1257,7 +1079,6 @@ }, "node_modules/content-type": { "version": "1.0.5", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "license": "MIT", "engines": { @@ -1266,13 +1087,11 @@ }, "node_modules/convert-source-map": { "version": "2.0.0", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/cookie": { "version": "0.7.2", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, "license": "MIT", "engines": { @@ -1281,7 +1100,6 @@ }, "node_modules/cookie-signature": { "version": "1.2.2", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "dev": true, "license": "MIT", "engines": { @@ -1290,7 +1108,6 @@ }, "node_modules/cors": { "version": "2.8.6", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", "dev": true, "license": "MIT", "dependencies": { @@ -1307,7 +1124,6 @@ }, "node_modules/cross-spawn": { "version": "7.0.6", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -1321,7 +1137,6 @@ }, "node_modules/debug": { "version": "4.4.3", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1338,7 +1153,6 @@ }, "node_modules/depd": { "version": "2.0.0", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", "engines": { @@ -1347,7 +1161,6 @@ }, "node_modules/detect-libc": { "version": "2.1.2", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1356,7 +1169,6 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", "dependencies": { @@ -1370,13 +1182,11 @@ }, "node_modules/ee-first": { "version": "1.1.1", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true, "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -1385,7 +1195,6 @@ }, "node_modules/es-define-property": { "version": "1.0.1", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", "engines": { @@ -1394,7 +1203,6 @@ }, "node_modules/es-errors": { "version": "1.3.0", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", "engines": { @@ -1403,13 +1211,11 @@ }, "node_modules/es-module-lexer": { "version": "2.1.0", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { "version": "1.1.1", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", "dependencies": { @@ -1421,7 +1227,6 @@ }, "node_modules/esbuild": { "version": "0.28.1", - "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1462,13 +1267,11 @@ }, "node_modules/escape-html": { "version": "1.0.3", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true, "license": "MIT" }, "node_modules/estree-walker": { "version": "3.0.3", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", "dependencies": { @@ -1477,7 +1280,6 @@ }, "node_modules/etag": { "version": "1.8.1", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, "license": "MIT", "engines": { @@ -1486,7 +1288,6 @@ }, "node_modules/eventsource": { "version": "3.0.7", - "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", "dev": true, "license": "MIT", "dependencies": { @@ -1498,7 +1299,6 @@ }, "node_modules/eventsource-parser": { "version": "3.0.6", - "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", "dev": true, "license": "MIT", "engines": { @@ -1507,7 +1307,6 @@ }, "node_modules/expect-type": { "version": "1.3.0", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1516,7 +1315,6 @@ }, "node_modules/express": { "version": "5.2.1", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", "dependencies": { @@ -1559,7 +1357,6 @@ }, "node_modules/express-rate-limit": { "version": "8.5.2", - "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "dev": true, "license": "MIT", "dependencies": { @@ -1577,13 +1374,12 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.5", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.7", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "dev": true, "funding": [ { @@ -1599,7 +1395,6 @@ }, "node_modules/fdir": { "version": "6.5.0", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -1616,7 +1411,6 @@ }, "node_modules/finalhandler": { "version": "2.1.1", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { @@ -1637,7 +1431,6 @@ }, "node_modules/forwarded": { "version": "0.2.0", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, "license": "MIT", "engines": { @@ -1646,7 +1439,6 @@ }, "node_modules/fresh": { "version": "2.0.0", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "dev": true, "license": "MIT", "engines": { @@ -1655,9 +1447,7 @@ }, "node_modules/fsevents": { "version": "2.3.3", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "hasInstallScript": true, "license": "MIT", "optional": true, "os": [ @@ -1669,7 +1459,6 @@ }, "node_modules/function-bind": { "version": "1.1.2", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", "funding": { @@ -1678,7 +1467,6 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1702,7 +1490,6 @@ }, "node_modules/get-proto": { "version": "1.0.1", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", "dependencies": { @@ -1715,7 +1502,6 @@ }, "node_modules/gopd": { "version": "1.2.0", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", "engines": { @@ -1727,7 +1513,6 @@ }, "node_modules/has-symbols": { "version": "1.1.0", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -1739,7 +1524,6 @@ }, "node_modules/hasown": { "version": "2.0.2", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1751,7 +1535,6 @@ }, "node_modules/hono": { "version": "4.13.1", - "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", "dev": true, "license": "MIT", "engines": { @@ -1760,7 +1543,6 @@ }, "node_modules/http-errors": { "version": "2.0.1", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1780,7 +1562,6 @@ }, "node_modules/iconv-lite": { "version": "0.7.2", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -1796,13 +1577,11 @@ }, "node_modules/inherits": { "version": "2.0.4", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, "license": "ISC" }, "node_modules/ip-address": { "version": "10.4.0", - "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", "dev": true, "license": "MIT", "engines": { @@ -1811,7 +1590,6 @@ }, "node_modules/ipaddr.js": { "version": "1.9.1", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, "license": "MIT", "engines": { @@ -1820,19 +1598,16 @@ }, "node_modules/is-promise": { "version": "4.0.0", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "dev": true, "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, "license": "ISC" }, "node_modules/jose": { "version": "6.1.3", - "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", "dev": true, "license": "MIT", "funding": { @@ -1841,19 +1616,16 @@ }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, "node_modules/json-schema-typed": { "version": "8.0.2", - "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "dev": true, "license": "BSD-2-Clause" }, "node_modules/lightningcss": { "version": "1.32.0", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -1882,7 +1654,7 @@ }, "node_modules/lightningcss-android-arm64": { "version": "1.32.0", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "integrity": "sha1-8DOIURbf79nG9UeHUj41FLYeGWg=", "cpu": [ "arm64" ], @@ -1902,7 +1674,6 @@ }, "node_modules/lightningcss-darwin-arm64": { "version": "1.32.0", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -1922,7 +1693,7 @@ }, "node_modules/lightningcss-darwin-x64": { "version": "1.32.0", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "integrity": "sha1-NfPpczLRMLnKGB4RtWje1q68bV4=", "cpu": [ "x64" ], @@ -1942,7 +1713,7 @@ }, "node_modules/lightningcss-freebsd-x64": { "version": "1.32.0", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "integrity": "sha1-l3enZHK2Ttb/lDQq1kx7r9eUpXU=", "cpu": [ "x64" ], @@ -1962,7 +1733,7 @@ }, "node_modules/lightningcss-linux-arm-gnueabihf": { "version": "1.32.0", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "integrity": "sha1-E65lLhq3O5E117faFy9mbEEK1T0=", "cpu": [ "arm" ], @@ -1982,7 +1753,7 @@ }, "node_modules/lightningcss-linux-arm64-gnu": { "version": "1.32.0", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "integrity": "sha1-QXhYeVqUWS9oASOhsfnaig4e8zU=", "cpu": [ "arm64" ], @@ -2002,7 +1773,7 @@ }, "node_modules/lightningcss-linux-arm64-musl": { "version": "1.32.0", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "integrity": "sha1-a+NmkugQtxgECAL9gJYjz/5zITM=", "cpu": [ "arm64" ], @@ -2022,7 +1793,7 @@ }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.32.0", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "integrity": "sha1-C3gDr06yHP043Tn+Kru1PH3QkfY=", "cpu": [ "x64" ], @@ -2042,7 +1813,7 @@ }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.32.0", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "integrity": "sha1-iNyLqGXd3bGsXvBLDxYYBEGMFjs=", "cpu": [ "x64" ], @@ -2062,7 +1833,7 @@ }, "node_modules/lightningcss-win32-arm64-msvc": { "version": "1.32.0", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "integrity": "sha1-TzC6P6XpJfW3n5RejMDRdsOxqzg=", "cpu": [ "arm64" ], @@ -2082,7 +1853,7 @@ }, "node_modules/lightningcss-win32-x64-msvc": { "version": "1.32.0", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "integrity": "sha1-FBqlYFZFBkkokCu0rwRfp9n0Igo=", "cpu": [ "x64" ], @@ -2102,7 +1873,6 @@ }, "node_modules/magic-string": { "version": "0.30.21", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2111,7 +1881,6 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", "engines": { @@ -2120,7 +1889,6 @@ }, "node_modules/media-typer": { "version": "1.1.0", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "dev": true, "license": "MIT", "engines": { @@ -2129,7 +1897,6 @@ }, "node_modules/merge-descriptors": { "version": "2.0.0", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", "dev": true, "license": "MIT", "engines": { @@ -2141,7 +1908,6 @@ }, "node_modules/mime-db": { "version": "1.54.0", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true, "license": "MIT", "engines": { @@ -2150,7 +1916,6 @@ }, "node_modules/mime-types": { "version": "3.0.2", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { @@ -2166,13 +1931,11 @@ }, "node_modules/ms": { "version": "2.1.3", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.17", - "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "dev": true, "funding": [ { @@ -2190,7 +1953,6 @@ }, "node_modules/negotiator": { "version": "1.0.0", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, "license": "MIT", "engines": { @@ -2199,7 +1961,6 @@ }, "node_modules/node-forge": { "version": "1.4.0", - "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { @@ -2208,7 +1969,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, "license": "MIT", "engines": { @@ -2217,7 +1977,6 @@ }, "node_modules/object-inspect": { "version": "1.13.4", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, "license": "MIT", "engines": { @@ -2229,7 +1988,6 @@ }, "node_modules/obug": { "version": "2.1.1", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -2239,7 +1997,6 @@ }, "node_modules/on-finished": { "version": "2.4.1", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -2251,7 +2008,6 @@ }, "node_modules/once": { "version": "1.4.0", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", "dependencies": { @@ -2260,7 +2016,6 @@ }, "node_modules/openai": { "version": "6.17.0", - "integrity": "sha512-NHRpPEUPzAvFOAFs9+9pC6+HCw/iWsYsKCMPXH5Kw7BpMxqd8g/A07/1o7Gx2TWtCnzevVRyKMRFqyiHyAlqcA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2281,7 +2036,6 @@ }, "node_modules/parseurl": { "version": "1.3.3", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, "license": "MIT", "engines": { @@ -2290,7 +2044,6 @@ }, "node_modules/path-key": { "version": "3.1.1", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -2299,7 +2052,6 @@ }, "node_modules/path-to-regexp": { "version": "8.4.2", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "dev": true, "license": "MIT", "funding": { @@ -2309,19 +2061,16 @@ }, "node_modules/pathe": { "version": "2.0.3", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.4", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -2333,7 +2082,6 @@ }, "node_modules/pkce-challenge": { "version": "5.0.1", - "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", "dev": true, "license": "MIT", "engines": { @@ -2342,7 +2090,6 @@ }, "node_modules/postcss": { "version": "8.5.25", - "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -2370,7 +2117,6 @@ }, "node_modules/proxy-addr": { "version": "2.0.7", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "license": "MIT", "dependencies": { @@ -2383,7 +2129,6 @@ }, "node_modules/qs": { "version": "6.15.2", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2398,7 +2143,6 @@ }, "node_modules/range-parser": { "version": "1.2.1", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, "license": "MIT", "engines": { @@ -2407,7 +2151,6 @@ }, "node_modules/raw-body": { "version": "3.0.2", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { @@ -2422,7 +2165,6 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { @@ -2431,7 +2173,6 @@ }, "node_modules/rolldown": { "version": "1.0.3", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { @@ -2464,7 +2205,6 @@ }, "node_modules/router": { "version": "2.2.0", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2480,13 +2220,11 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true, "license": "MIT" }, "node_modules/send": { "version": "1.2.1", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2512,7 +2250,6 @@ }, "node_modules/serve-static": { "version": "2.2.1", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { @@ -2531,13 +2268,11 @@ }, "node_modules/setprototypeof": { "version": "1.2.0", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true, "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -2549,7 +2284,6 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -2558,7 +2292,6 @@ }, "node_modules/side-channel": { "version": "1.1.0", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", "dependencies": { @@ -2577,7 +2310,6 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, "license": "MIT", "dependencies": { @@ -2593,7 +2325,6 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, "license": "MIT", "dependencies": { @@ -2611,7 +2342,6 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "license": "MIT", "dependencies": { @@ -2630,13 +2360,11 @@ }, "node_modules/siginfo": { "version": "2.0.0", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, "license": "ISC" }, "node_modules/source-map-js": { "version": "1.2.1", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -2645,13 +2373,11 @@ }, "node_modules/stackback": { "version": "0.0.2", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -2660,19 +2386,16 @@ }, "node_modules/std-env": { "version": "4.1.0", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, "node_modules/tinybench": { "version": "2.9.0", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, "node_modules/tinyexec": { "version": "1.0.2", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, "license": "MIT", "engines": { @@ -2681,7 +2404,6 @@ }, "node_modules/tinyglobby": { "version": "0.2.17", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -2697,7 +2419,6 @@ }, "node_modules/tinyrainbow": { "version": "3.1.0", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -2706,7 +2427,6 @@ }, "node_modules/toidentifier": { "version": "1.0.1", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true, "license": "MIT", "engines": { @@ -2715,14 +2435,13 @@ }, "node_modules/tslib": { "version": "2.8.1", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", "dev": true, "license": "0BSD", "optional": true }, "node_modules/tsx": { "version": "4.22.4", - "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, "license": "MIT", "dependencies": { @@ -2740,7 +2459,6 @@ }, "node_modules/type-is": { "version": "2.0.1", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, "license": "MIT", "dependencies": { @@ -2754,7 +2472,6 @@ }, "node_modules/typescript": { "version": "5.9.3", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2767,13 +2484,11 @@ }, "node_modules/undici-types": { "version": "7.18.2", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, "license": "MIT", "engines": { @@ -2782,7 +2497,6 @@ }, "node_modules/vary": { "version": "1.1.2", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, "license": "MIT", "engines": { @@ -2791,7 +2505,6 @@ }, "node_modules/vite": { "version": "8.0.16", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { @@ -2868,7 +2581,6 @@ }, "node_modules/vitest": { "version": "4.1.8", - "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "dev": true, "license": "MIT", "dependencies": { @@ -2957,7 +2669,6 @@ }, "node_modules/which": { "version": "2.0.2", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -2972,7 +2683,6 @@ }, "node_modules/why-is-node-running": { "version": "2.3.0", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "license": "MIT", "dependencies": { @@ -2988,13 +2698,11 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, "license": "ISC" }, "node_modules/yaml": { "version": "2.9.0", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { @@ -3009,7 +2717,6 @@ }, "node_modules/zod": { "version": "4.3.6", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "funding": { @@ -3018,7 +2725,6 @@ }, "node_modules/zod-to-json-schema": { "version": "3.25.1", - "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "dev": true, "license": "ISC", "peerDependencies": { diff --git a/test/harness/package.json b/test/harness/package.json index c2081214ab..9b37dfb9d0 100644 --- a/test/harness/package.json +++ b/test/harness/package.json @@ -14,7 +14,6 @@ "node": "^20.19.0 || >=22.12.0" }, "devDependencies": { - "@github/copilot": "^1.0.83-0", "@modelcontextprotocol/sdk": "^1.26.0", "@types/node": "^25.3.3", "@types/node-forge": "^1.3.14", diff --git a/test/harness/replayingCapiProxy.test.ts b/test/harness/replayingCapiProxy.test.ts index 245b035c5c..062fe89ae9 100644 --- a/test/harness/replayingCapiProxy.test.ts +++ b/test/harness/replayingCapiProxy.test.ts @@ -24,13 +24,21 @@ import { ShellConfig } from "./util"; describe("ReplayingCapiProxy", () => { let tempDir: string; let workDir: string; + let githubActions: string | undefined; beforeEach(async () => { + githubActions = process.env.GITHUB_ACTIONS; + delete process.env.GITHUB_ACTIONS; tempDir = await mkdtemp(path.join(os.tmpdir(), "capi-proxy-test-")); workDir = path.join(tempDir, "work"); }); afterEach(async () => { + if (githubActions === undefined) { + delete process.env.GITHUB_ACTIONS; + } else { + process.env.GITHUB_ACTIONS = githubActions; + } await rm(tempDir, { recursive: true, force: true }); }); @@ -569,6 +577,14 @@ Always include PINEAPPLE_COCONUT_42. arguments: '{"command":"sleep 100"}', }, }, + { + id: "tc3", + type: "function", + function: { + name: "bash", + arguments: '{"command":"sleep 100"}', + }, + }, ], }, { @@ -582,6 +598,11 @@ Always include PINEAPPLE_COCONUT_42. tool_call_id: "tc2", content: "", }, + { + role: "tool", + tool_call_id: "tc3", + content: "unknown attachedShellSession handle 9", + }, ], }); const responseBody = JSON.stringify({ @@ -599,6 +620,7 @@ Always include PINEAPPLE_COCONUT_42. expect(toolMessages.map((message) => message.content)).toEqual([ "The execution of this tool, or a previous tool was interrupted.", "The execution of this tool, or a previous tool was interrupted.", + "The execution of this tool, or a previous tool was interrupted.", ]); }); @@ -959,7 +981,7 @@ Always include PINEAPPLE_COCONUT_42. } }); - test("matches semantically equivalent interrupted shell results", async () => { + test("matches semantically equivalent interrupted tool results", async () => { const originalShellConfig = process.platform === "win32" ? ShellConfig.powerShell @@ -1034,8 +1056,7 @@ Always include PINEAPPLE_COCONUT_42. { role: "tool", tool_call_id: "runtime-call-id", - content: - "", + content: "Session aborted", }, ], }, @@ -1048,6 +1069,25 @@ Always include PINEAPPLE_COCONUT_42. .message.content, ).toBe("Ready for another request."); + const unknownHandleResponse = await makeRequest( + proxyUrl, + "/chat/completions", + { + body: { + model: "test-model", + messages: [ + ...messages, + { + role: "tool", + tool_call_id: "runtime-call-id", + content: "unknown attachedShellSession handle 9", + }, + ], + }, + }, + ); + expect(unknownHandleResponse.status).toBe(200); + const meaningfulErrorResponse = await makeRequest( proxyUrl, "/chat/completions", @@ -1591,6 +1631,42 @@ Always include PINEAPPLE_COCONUT_42. } }); + test.each([false, true])( + "defaults to Sonnet 5 without stored models (capture exists: %s)", + async (captureExists) => { + const cachePath = path.join(tempDir, "cache.yaml"); + if (captureExists) { + await writeFile( + cachePath, + yaml.stringify({ + models: [], + conversations: [], + } satisfies NormalizedData), + ); + } + + const proxy = new ReplayingCapiProxy( + "http://localhost:9999", + cachePath, + workDir, + ); + const proxyUrl = await proxy.start(); + + try { + const response = await makeRequest(proxyUrl, "/models", { + method: "GET", + }); + expect(response.status).toBe(200); + const parsed = JSON.parse(response.body) as { + data: Array<{ id: string }>; + }; + expect(parsed.data.map((model) => model.id)).toEqual(["claude-sonnet-5"]); + } finally { + await proxy.stop(); + } + }, + ); + test("returns cached models for /models endpoint", async () => { const cachePath = path.join(tempDir, "cache.yaml"); const cacheContent = yaml.stringify({ diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index f30a9fbd77..9bcafcdad4 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -107,7 +107,7 @@ const normalizedToolNames: Record = { * Default model to use when no stored data is available for a given test. * This enables responding to /models without needing to have a capture file. */ -const defaultModel = "claude-sonnet-4.5"; +const defaultModel = "claude-sonnet-5"; /** * An HTTP proxy that not only captures HTTP exchanges, but also stores them in a file on disk and @@ -1556,7 +1556,7 @@ function normalizeAvailableToolNames(result: string): string { function normalizeInterruptedToolResult(result: string): string { return result.replace( - /^(?:Failed to execute `[^`]+` tool(?: with arguments: [\s\S]*?)? due to error: (?:Error: )?Session aborted|)$/, + /^(?:Session aborted|Failed to execute `[^`]+` tool(?: with arguments: [\s\S]*?)? due to error: (?:Error: )?Session aborted||unknown attachedShellSession handle \d+)$/, "The execution of this tool, or a previous tool was interrupted.", ); } diff --git a/test/harness/test-mcp-oauth-server.mjs b/test/harness/test-mcp-oauth-server.mjs index eacd35f304..1ab7260eb0 100644 --- a/test/harness/test-mcp-oauth-server.mjs +++ b/test/harness/test-mcp-oauth-server.mjs @@ -53,10 +53,7 @@ export async function startOAuthMcpServer({ return; } - if ( - req.method === "GET" && - url.pathname === PROTECTED_RESOURCE_PATH - ) { + if (req.method === "GET" && url.pathname === PROTECTED_RESOURCE_PATH) { respondJson(res, 200, { resource: `${baseUrl}/mcp`, authorization_servers: [baseUrl], @@ -165,9 +162,10 @@ export async function startOAuthMcpServer({ url: `http://${host}:${address.port}`, requests, close: () => - new Promise((resolve, reject) => - server.close((err) => (err ? reject(err) : resolve())), - ), + new Promise((resolve, reject) => { + server.close((err) => (err ? reject(err) : resolve())); + server.closeAllConnections(); + }), }; } @@ -313,7 +311,10 @@ function respondJson(res, statusCode, body) { res.end(data); } -if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { const server = await startOAuthMcpServer({ expectedToken: process.env.EXPECTED_TOKEN ?? DEFAULT_EXPECTED_TOKEN, }); diff --git a/test/snapshots/abort/should_abort_during_active_streaming.yaml b/test/snapshots/abort/should_abort_during_active_streaming.yaml index 70981ee597..8556fec349 100644 --- a/test/snapshots/abort/should_abort_during_active_streaming.yaml +++ b/test/snapshots/abort/should_abort_during_active_streaming.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/abort/should_abort_during_active_tool_execution.yaml b/test/snapshots/abort/should_abort_during_active_tool_execution.yaml index 99ea89f7b0..a975cae284 100644 --- a/test/snapshots/abort/should_abort_during_active_tool_execution.yaml +++ b/test/snapshots/abort/should_abort_during_active_tool_execution.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml b/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml index ac5cc94336..498bfebad0 100644 --- a/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml +++ b/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml b/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml index 4ba16d4d81..1624cb5317 100644 --- a/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml +++ b/test/snapshots/ask_user/ask_user_does_not_block_sibling_tool_call_in_same_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml b/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml index 49944c9732..c33ce7e8f2 100644 --- a/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml +++ b/test/snapshots/ask_user/should_handle_freeform_user_input_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml b/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml index 4549b99dc1..417f0b3446 100644 --- a/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml +++ b/test/snapshots/ask_user/should_invoke_user_input_handler_when_model_uses_ask_user_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml b/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml index 705378061f..97e08e5852 100644 --- a/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml +++ b/test/snapshots/ask_user/should_receive_choices_in_user_input_request.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/auto_tier/should_preserve_auto_tier_when_set_model_omits_it.yaml b/test/snapshots/auto_tier/should_preserve_auto_tier_when_set_model_omits_it.yaml new file mode 100644 index 0000000000..b287603f41 --- /dev/null +++ b/test/snapshots/auto_tier/should_preserve_auto_tier_when_set_model_omits_it.yaml @@ -0,0 +1,4 @@ +models: + - auto + - claude-sonnet-5 +conversations: [] diff --git a/test/snapshots/auto_tier/should_stage_and_reset_auto_tier_preference.yaml b/test/snapshots/auto_tier/should_stage_and_reset_auto_tier_preference.yaml new file mode 100644 index 0000000000..b287603f41 --- /dev/null +++ b/test/snapshots/auto_tier/should_stage_and_reset_auto_tier_preference.yaml @@ -0,0 +1,4 @@ +models: + - auto + - claude-sonnet-5 +conversations: [] diff --git a/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml b/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml index 01cf1298d3..5c3c638d50 100644 --- a/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml +++ b/test/snapshots/builtin_tools/should_capture_exit_code_in_output.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_capture_stderr_output.yaml b/test/snapshots/builtin_tools/should_capture_stderr_output.yaml index 0ba318148d..ba0bd164ea 100644 --- a/test/snapshots/builtin_tools/should_capture_stderr_output.yaml +++ b/test/snapshots/builtin_tools/should_capture_stderr_output.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_create_a_new_file.yaml b/test/snapshots/builtin_tools/should_create_a_new_file.yaml index 8afe8b38b6..869777e9a4 100644 --- a/test/snapshots/builtin_tools/should_create_a_new_file.yaml +++ b/test/snapshots/builtin_tools/should_create_a_new_file.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml b/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml index 3f4e986906..922d7751dd 100644 --- a/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml +++ b/test/snapshots/builtin_tools/should_edit_a_file_successfully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml b/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml index 6cf85ea51d..338dd03ef2 100644 --- a/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml +++ b/test/snapshots/builtin_tools/should_find_files_by_pattern.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml b/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml index c5c00fb65c..410da4e089 100644 --- a/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml +++ b/test/snapshots/builtin_tools/should_handle_nonexistent_file_gracefully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml b/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml index 601ae0f04c..23a7fec7a1 100644 --- a/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml +++ b/test/snapshots/builtin_tools/should_read_file_with_line_range.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml b/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml index f0af500b6e..615b9ae39e 100644 --- a/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml +++ b/test/snapshots/builtin_tools/should_search_for_patterns_in_files.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml b/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml +++ b/test/snapshots/canvas/canvas_list_discovers_declared_canvases.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml b/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml +++ b/test/snapshots/client/listmodels_withcustomhandler_callshandler.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_force_stop_client.yaml b/test/snapshots/client/should_force_stop_client.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_force_stop_client.yaml +++ b/test/snapshots/client/should_force_stop_client.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_get_authenticated_status.yaml b/test/snapshots/client/should_get_authenticated_status.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_get_authenticated_status.yaml +++ b/test/snapshots/client/should_get_authenticated_status.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_get_status.yaml b/test/snapshots/client/should_get_status.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_get_status.yaml +++ b/test/snapshots/client/should_get_status.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_list_models_when_authenticated.yaml b/test/snapshots/client/should_list_models_when_authenticated.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_list_models_when_authenticated.yaml +++ b/test/snapshots/client/should_list_models_when_authenticated.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml b/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml +++ b/test/snapshots/client/should_start_ping_and_stop_stdio_client.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml b/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml +++ b/test/snapshots/client/should_start_ping_and_stop_tcp_client.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client/should_stop_client_with_active_session.yaml b/test/snapshots/client/should_stop_client_with_active_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client/should_stop_client_with_active_session.yaml +++ b/test/snapshots/client/should_stop_client_with_active_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client_api/should_delete_session_by_id.yaml b/test/snapshots/client_api/should_delete_session_by_id.yaml index 0981462bf6..bfeaca5f6d 100644 --- a/test/snapshots/client_api/should_delete_session_by_id.yaml +++ b/test/snapshots/client_api/should_delete_session_by_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml b/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml index 8486832a46..8e3aa9d94c 100644 --- a/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml +++ b/test/snapshots/client_api/should_track_last_session_id_after_session_created.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml b/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml index beb8b443d2..3569a8ca8e 100644 --- a/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml +++ b/test/snapshots/client_lifecycle/should_emit_session_lifecycle_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml b/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml index 4419c5854e..bb4a148072 100644 --- a/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml +++ b/test/snapshots/client_lifecycle/should_receive_session_deleted_lifecycle_event_when_deleted.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml b/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml index 3b9da534c2..62da2b03a2 100644 --- a/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml +++ b/test/snapshots/client_lifecycle/should_return_last_session_id_after_sending_a_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml b/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml +++ b/test/snapshots/client_options/should_listen_on_configured_tcp_port.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml b/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml index 469d091288..c87d0cb124 100644 --- a/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml +++ b/test/snapshots/client_options/should_use_client_cwd_for_default_workingdirectory.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml b/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml index 60d1eadeaf..51716b5c80 100644 --- a/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/combinedconfiguration/accept_mcp_servers_and_custom_agents.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/commands/session_with_commands_creates_successfully.yaml b/test/snapshots/commands/session_with_commands_creates_successfully.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/commands/session_with_commands_creates_successfully.yaml +++ b/test/snapshots/commands/session_with_commands_creates_successfully.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/commands/session_with_commands_resumes_successfully.yaml b/test/snapshots/commands/session_with_commands_resumes_successfully.yaml index 0981462bf6..bfeaca5f6d 100644 --- a/test/snapshots/commands/session_with_commands_resumes_successfully.yaml +++ b/test/snapshots/commands/session_with_commands_resumes_successfully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml b/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml +++ b/test/snapshots/commands/session_with_no_commands_creates_successfully.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml b/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml index 9773a132f5..6d966efe2f 100644 --- a/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml +++ b/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml b/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml index 9deca12228..7d476ec66c 100644 --- a/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml +++ b/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml b/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml +++ b/test/snapshots/customagents/accept_custom_agent_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml b/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml index 16db486e88..e454b1e96c 100644 --- a/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml +++ b/test/snapshots/customagents/accept_custom_agent_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml b/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml +++ b/test/snapshots/elicitation/confirm_returns_false_when_handler_declines.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml b/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml +++ b/test/snapshots/elicitation/confirm_returns_true_when_handler_accepts.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml b/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml +++ b/test/snapshots/elicitation/defaults_capabilities_when_not_provided.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml b/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml +++ b/test/snapshots/elicitation/elicitation_returns_all_action_shapes.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml b/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml +++ b/test/snapshots/elicitation/elicitation_throws_when_capability_is_missing.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/input_returns_freeform_value.yaml b/test/snapshots/elicitation/input_returns_freeform_value.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/input_returns_freeform_value.yaml +++ b/test/snapshots/elicitation/input_returns_freeform_value.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/select_returns_selected_option.yaml b/test/snapshots/elicitation/select_returns_selected_option.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/select_returns_selected_option.yaml +++ b/test/snapshots/elicitation/select_returns_selected_option.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml b/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml +++ b/test/snapshots/elicitation/sends_requestelicitation_when_handler_provided.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml b/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml +++ b/test/snapshots/elicitation/session_without_elicitationhandler_creates_successfully.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml b/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml +++ b/test/snapshots/elicitation/should_report_elicitation_capability_based_on_handler_presence.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml b/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml index caac261e2a..1499c083ca 100644 --- a/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml +++ b/test/snapshots/event_fidelity/should_emit_assistant_message_with_messageid.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml b/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml index 48667da723..af30539431 100644 --- a/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml +++ b/test/snapshots/event_fidelity/should_emit_assistant_usage_event_after_model_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml b/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml index c8f272e6b9..726cbf9322 100644 --- a/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml +++ b/test/snapshots/event_fidelity/should_emit_events_in_correct_order_for_tool_using_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml b/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml index ecc10bdbd6..2b24bf8a50 100644 --- a/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml +++ b/test/snapshots/event_fidelity/should_emit_pending_messages_modified_event_when_message_queue_changes.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml b/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml index 48667da723..af30539431 100644 --- a/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml +++ b/test/snapshots/event_fidelity/should_emit_session_usage_info_event_after_model_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml b/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml index 46fd7715ab..717f09ea24 100644 --- a/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml +++ b/test/snapshots/event_fidelity/should_emit_tool_execution_events_with_correct_fields.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml b/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml index 48667da723..af30539431 100644 --- a/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml +++ b/test/snapshots/event_fidelity/should_include_valid_fields_on_all_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml b/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml index 1797cc16b1..6fcaebc6a0 100644 --- a/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml +++ b/test/snapshots/event_fidelity/should_preserve_message_order_in_getmessages_after_tool_use.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/external_tool_cancellation/should_cancel_tool_handler_when_session_disconnects.yaml b/test/snapshots/external_tool_cancellation/should_cancel_tool_handler_when_session_disconnects.yaml new file mode 100644 index 0000000000..aa37004d64 --- /dev/null +++ b/test/snapshots/external_tool_cancellation/should_cancel_tool_handler_when_session_disconnects.yaml @@ -0,0 +1,15 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Use slow_analysis with value 'test_abort'. Wait for the result. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: slow_analysis + arguments: '{"value":"test_abort"}' diff --git a/test/snapshots/external_tool_cancellation/should_cancel_tool_handler_when_session_disposes.yaml b/test/snapshots/external_tool_cancellation/should_cancel_tool_handler_when_session_disposes.yaml new file mode 100644 index 0000000000..028b44e73f --- /dev/null +++ b/test/snapshots/external_tool_cancellation/should_cancel_tool_handler_when_session_disposes.yaml @@ -0,0 +1,18 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Use slow_analysis with value 'test_abort'. Wait for the result. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: slow_analysis + arguments: '{"value":"test_abort"}' + - role: tool + tool_call_id: toolcall_0 + content: The execution of this tool, or a previous tool was interrupted. diff --git a/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml b/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml index 10bca8e4b0..296ee0db9a 100644 --- a/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml +++ b/test/snapshots/hooks/deny_tool_execution_when_pre_tool_use_returns_deny.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml b/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml index 9ed9431545..10e3a06189 100644 --- a/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml +++ b/test/snapshots/hooks/invoke_both_hooks_for_single_tool_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml b/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml index 2a5f1ae446..3bf5ee1f4d 100644 --- a/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/invoke_post_tool_use_hook_after_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml b/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml index f695c60f3d..86c2865756 100644 --- a/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/invoke_pre_tool_use_hook_when_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml b/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml index 2860f52c15..a1ecaef30e 100644 --- a/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml +++ b/test/snapshots/hooks/should_deny_tool_execution_when_pretooluse_returns_deny.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml index 9ed9431545..10e3a06189 100644 --- a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml +++ b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_a_single_tool_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml index 9ed9431545..10e3a06189 100644 --- a/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml +++ b/test/snapshots/hooks/should_invoke_both_pretooluse_and_posttooluse_hooks_for_single_tool_call.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml b/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml index 2a5f1ae446..3bf5ee1f4d 100644 --- a/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/should_invoke_posttooluse_hook_after_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml b/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml index f695c60f3d..86c2865756 100644 --- a/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml +++ b/test/snapshots/hooks/should_invoke_pretooluse_hook_when_model_runs_a_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml b/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml index a340e9326c..f5cbaff9c6 100644 --- a/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml +++ b/test/snapshots/hooks_extended/should_allow_posttooluse_to_return_modifiedresult.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml b/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml index 8415fe771b..89c3ee9234 100644 --- a/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml +++ b/test/snapshots/hooks_extended/should_allow_pretooluse_to_return_modifiedargs_and_suppressoutput.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml b/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml index 6485670a1c..6c1a055c04 100644 --- a/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml +++ b/test/snapshots/hooks_extended/should_invoke_agentstop_hook_and_apply_block_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml b/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml index dcd2f32be2..2a17b8a0a3 100644 --- a/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onerroroccurred_hook_when_error_occurs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml b/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml index bde2373cc2..bd61da81cb 100644 --- a/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onsessionend_hook_when_session_is_disconnected.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml b/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml index 0d2da93e50..e200c7f68f 100644 --- a/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onsessionstart_hook_on_new_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml b/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml index beb8b443d2..3569a8ca8e 100644 --- a/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml +++ b/test/snapshots/hooks_extended/should_invoke_onuserpromptsubmitted_hook_when_sending_a_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml b/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml index bbe815735c..a1c3f4fb2d 100644 --- a/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml +++ b/test/snapshots/hooks_extended/should_invoke_posttoolusefailure_hook_for_failed_tool_result.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml b/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml index 75fccc4e1f..017000cfba 100644 --- a/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml +++ b/test/snapshots/hooks_extended/should_invoke_sessionend_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml b/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml index c1d643b4c3..725222bfd3 100644 --- a/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml +++ b/test/snapshots/hooks_extended/should_invoke_sessionstart_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -13,7 +13,7 @@ conversations: Hi! 👋 - I'm GitHub Copilot CLI, powered by claude-sonnet-4.5. I'm here to help you with software engineering tasks + I'm GitHub Copilot CLI, powered by claude-sonnet-5. I'm here to help you with software engineering tasks like exploring codebases, running commands, making code changes, and more. diff --git a/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml b/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml index ceb291c9dd..688dc01e1e 100644 --- a/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml +++ b/test/snapshots/hooks_extended/should_invoke_userpromptsubmitted_hook_and_modify_prompt.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml b/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml index db2b029680..1fdf62b876 100644 --- a/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml +++ b/test/snapshots/hooks_extended/should_invoke_userprompttransformed_hook_and_modify_transformed_prompt.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml b/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml index 2f02a0570d..ef34d886b1 100644 --- a/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml +++ b/test/snapshots/hooks_extended/should_register_erroroccurred_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml b/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_both_mcp_servers_and_custom_agents.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml index 9703495c66..dbc06f70b1 100644 --- a/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_custom_agent_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml index 82c9917c34..96321ffe3a 100644 --- a/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp-and-agents/should_accept_mcp_server_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml +++ b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml index 16db486e88..e454b1e96c 100644 --- a/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml +++ b/test/snapshots/mcp_and_agents/accept_custom_agent_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_server_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml b/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_server_config_without_args.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml b/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml index 60d1eadeaf..51716b5c80 100644 --- a/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/mcp_and_agents/accept_mcp_servers_and_custom_agents.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml b/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml index 60d1eadeaf..51716b5c80 100644 --- a/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_both_mcp_servers_and_custom_agents.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml index 56da15bae1..dbf50977bc 100644 --- a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml index 9703495c66..dbc06f70b1 100644 --- a/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_custom_agent_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml b/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml index 65fe6664e6..5fab1b6334 100644 --- a/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_defaultagent_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml +++ b/test/snapshots/mcp_and_agents/should_accept_mcp_server_configuration_without_args.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_mcp_servers.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_custom_agent_with_tools_configuration.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml b/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_multiple_custom_agents.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml b/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml +++ b/test/snapshots/mcp_and_agents/should_handle_multiple_mcp_servers.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml b/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml index f5506bb184..bf8c7b4d33 100644 --- a/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml +++ b/test/snapshots/mcp_and_agents/should_hide_excluded_tools_from_default_agent.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml b/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml index 29ba0fc68b..b3eab15f66 100644 --- a/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml +++ b/test/snapshots/mcp_and_agents/should_pass_literal_env_values_to_mcp_server_subprocess.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml b/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml index c1df8e8023..9aa12434df 100644 --- a/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml +++ b/test/snapshots/mcp_and_agents/should_round_trip_mcp_server_elicitation_request.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml b/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml +++ b/test/snapshots/mcpservers/accept_mcp_server_config_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml b/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml index 82c9917c34..96321ffe3a 100644 --- a/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml +++ b/test/snapshots/mcpservers/accept_mcp_server_config_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml b/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml index fac88270d5..6193c536b4 100644 --- a/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml +++ b/test/snapshots/mode_empty/empty_mode_append_caller_instruction_takes_effect_and_env_context_stripped.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml b/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml index decf64bc37..b6606842af 100644 --- a/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml +++ b/test/snapshots/mode_empty/empty_mode_builtin_star_exposes_all_built_in_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml b/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml index decf64bc37..b6606842af 100644 --- a/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml +++ b/test/snapshots/mode_empty/empty_mode_excluded_tools_subtracts_from_available_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml b/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml index decf64bc37..b6606842af 100644 --- a/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml +++ b/test/snapshots/mode_empty/empty_mode_isolated_set_shell_tool_is_not_exposed.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml b/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml index 6f23714d94..58ec8e370a 100644 --- a/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml +++ b/test/snapshots/mode_empty/empty_mode_strips_environment_context_from_the_system_message_by_default.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml b/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml index 5d63a94018..cfa604adbe 100644 --- a/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml +++ b/test/snapshots/mode_empty/empty_mode_system_message_replace_llm_follows_caller_content_verbatim.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml b/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml index 19c271b4f1..30fee89306 100644 --- a/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml +++ b/test/snapshots/mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited.yaml @@ -1,8 +1,8 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 - auto errors: - - model: claude-sonnet-4.5 + - model: claude-sonnet-5 status: 429 code: user_weekly_rate_limited message: You've reached your weekly rate limit. diff --git a/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml b/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml index 078ba05483..9ee28ad083 100644 --- a/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml +++ b/test/snapshots/mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml b/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml index 62f0d004a0..89f28fcbe1 100644 --- a/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml +++ b/test/snapshots/multi_client/both_clients_see_tool_request_and_completion_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml b/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml index b6410e0d1b..925e8f3076 100644 --- a/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml +++ b/test/snapshots/multi_client/disconnecting_client_removes_its_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml b/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml index 2397bfdc01..fce177c1c3 100644 --- a/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml +++ b/test/snapshots/multi_client/one_client_approves_permission_and_both_see_the_result.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml b/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml index ba9db87d08..105a472f9c 100644 --- a/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml +++ b/test/snapshots/multi_client/one_client_rejects_permission_and_both_see_the_result.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml b/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml index c04864d827..39d56792d5 100644 --- a/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml +++ b/test/snapshots/multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml b/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml +++ b/test/snapshots/multi_provider_registry/should_register_multiple_providers_with_custom_agents_bound_to_their_models.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml b/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml index 583366363a..7d991ce94e 100644 --- a/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml +++ b/test/snapshots/multi_turn/should_handle_file_creation_then_reading_across_turns.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml b/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml index 96dc365c65..3ea91fec16 100644 --- a/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml +++ b/test/snapshots/multi_turn/should_use_tool_results_from_previous_turns.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml b/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml index acae9a8b24..29574ef20b 100644 --- a/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml +++ b/test/snapshots/pending_work_resume/should_continue_parallel_pending_external_tool_requests_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml b/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml index f9fcc188a7..c39ad4f6e4 100644 --- a/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml +++ b/test/snapshots/pending_work_resume/should_continue_pending_external_tool_request_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml b/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml index 4856cdc4c6..4e001d5cf2 100644 --- a/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml +++ b/test/snapshots/pending_work_resume/should_continue_pending_permission_request_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml index 8a32e431a7..d8ffe5df61 100644 --- a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml +++ b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_cold_resume_when_continuependingwork_is_false.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml index 4ae08f8a80..85098c636c 100644 --- a/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml +++ b/test/snapshots/pending_work_resume/should_keep_pending_external_tool_handleable_on_warm_resume_when_continuependingwork_is_false.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml b/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml index 3b5c7dfe41..bef3a92876 100644 --- a/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml +++ b/test/snapshots/pending_work_resume/should_report_continuependingwork_true_in_resume_event.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml b/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml index d7117cee65..de0c2634b1 100644 --- a/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml +++ b/test/snapshots/pending_work_resume/should_resume_successfully_when_no_pending_work_exists.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml b/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml +++ b/test/snapshots/per-session-auth/session_auth_status_is_unauthenticated_without_token.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml b/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml +++ b/test/snapshots/per-session-auth/session_fails_with_invalid_token.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml b/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml +++ b/test/snapshots/per-session-auth/session_token_overrides_client_token.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml b/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml +++ b/test/snapshots/per-session-auth/session_uses_client_token_when_no_session_token_is_supplied.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/permissions/async_permission_handler.yaml b/test/snapshots/permissions/async_permission_handler.yaml index 1d46c38a41..bf3431703c 100644 --- a/test/snapshots/permissions/async_permission_handler.yaml +++ b/test/snapshots/permissions/async_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/deny_permission.yaml b/test/snapshots/permissions/deny_permission.yaml index 480b640fb3..63ed54f671 100644 --- a/test/snapshots/permissions/deny_permission.yaml +++ b/test/snapshots/permissions/deny_permission.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/permission_handler_errors.yaml b/test/snapshots/permissions/permission_handler_errors.yaml index cee78a0929..f8f274d66e 100644 --- a/test/snapshots/permissions/permission_handler_errors.yaml +++ b/test/snapshots/permissions/permission_handler_errors.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/permission_handler_for_shell_commands.yaml b/test/snapshots/permissions/permission_handler_for_shell_commands.yaml index 1d46c38a41..bf3431703c 100644 --- a/test/snapshots/permissions/permission_handler_for_shell_commands.yaml +++ b/test/snapshots/permissions/permission_handler_for_shell_commands.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/permission_handler_for_write_operations.yaml b/test/snapshots/permissions/permission_handler_for_write_operations.yaml index 3f05a8c6de..9811c4a6ab 100644 --- a/test/snapshots/permissions/permission_handler_for_write_operations.yaml +++ b/test/snapshots/permissions/permission_handler_for_write_operations.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/resume_session_with_permission_handler.yaml b/test/snapshots/permissions/resume_session_with_permission_handler.yaml index 6296a0d73e..ade442a927 100644 --- a/test/snapshots/permissions/resume_session_with_permission_handler.yaml +++ b/test/snapshots/permissions/resume_session_with_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml b/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml index ef6f60dbed..8e28d8156d 100644 --- a/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml +++ b/test/snapshots/permissions/should_deny_permission_when_handler_returns_denied.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml b/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml index 0dfbd9e6b2..5f9a98ac80 100644 --- a/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml +++ b/test/snapshots/permissions/should_deny_permission_with_noresult_kind.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml index 0d25979c7c..0f7c4782cc 100644 --- a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml +++ b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml index 9a23c55f0a..fc802f16da 100644 --- a/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml +++ b/test/snapshots/permissions/should_deny_tool_operations_when_handler_explicitly_denies_after_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_handle_async_permission_handler.yaml b/test/snapshots/permissions/should_handle_async_permission_handler.yaml index 1d46c38a41..bf3431703c 100644 --- a/test/snapshots/permissions/should_handle_async_permission_handler.yaml +++ b/test/snapshots/permissions/should_handle_async_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml b/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml index 924123536c..7432c4caf9 100644 --- a/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml +++ b/test/snapshots/permissions/should_handle_concurrent_permission_requests_from_parallel_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml b/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml index 2a2db62101..17b75a4925 100644 --- a/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml +++ b/test/snapshots/permissions/should_handle_permission_handler_errors_gracefully.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml b/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml index ef6f60dbed..8e28d8156d 100644 --- a/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml +++ b/test/snapshots/permissions/should_honor_a_decision_annotated_with_decisioncontext.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml b/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml index 3f05a8c6de..9811c4a6ab 100644 --- a/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml +++ b/test/snapshots/permissions/should_invoke_permission_handler_for_write_operations.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml b/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml index 90407df6fc..cd73f7e165 100644 --- a/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml +++ b/test/snapshots/permissions/should_receive_toolcallid_in_permission_requests.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml b/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml index 6296a0d73e..ade442a927 100644 --- a/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml +++ b/test/snapshots/permissions/should_resume_session_with_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml b/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml index 3a6d66dc8d..1c33d19a98 100644 --- a/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml +++ b/test/snapshots/permissions/should_short_circuit_permission_handler_when_set_approve_all_enabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml b/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml index 19398ce5d6..9ecbdb6a51 100644 --- a/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml +++ b/test/snapshots/permissions/should_wait_for_slow_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml b/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml index 9199977dba..554b4b1f70 100644 --- a/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml +++ b/test/snapshots/permissions/should_work_with_approve_all_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml b/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml index 90407df6fc..cd73f7e165 100644 --- a/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml +++ b/test/snapshots/permissions/tool_call_id_in_permission_requests.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml b/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml index c771647842..5e1970247a 100644 --- a/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml +++ b/test/snapshots/pre_mcp_tool_call_hook/should_remove_meta_via_premcptoolcall_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml b/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml index d7ff876a6c..b8d0e30d19 100644 --- a/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml +++ b/test/snapshots/pre_mcp_tool_call_hook/should_replace_meta_via_premcptoolcall_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml b/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml index 1d92fe8eed..cf77c42735 100644 --- a/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml +++ b/test/snapshots/pre_mcp_tool_call_hook/should_set_meta_via_premcptoolcall_hook.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml b/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml +++ b/test/snapshots/resume_mcp_oauth/should_resume_a_persisted_session_with_mcp_auth_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml b/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml index 8a099ec46a..31753aed11 100644 --- a/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml +++ b/test/snapshots/rewind/should_restore_tracked_file_and_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml b/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml +++ b/test/snapshots/rpc_additional_edge_cases/mode_set_to_same_value_multiple_times_stays_stable.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/name_set_with_unicode_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml b/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml +++ b/test/snapshots/rpc_additional_edge_cases/permissions_reset_session_approvals_on_fresh_session_is_noop.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/permissions_set_approve_all_toggle_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml b/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml +++ b/test/snapshots/rpc_additional_edge_cases/plan_delete_when_none_exists_is_idempotent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml b/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml +++ b/test/snapshots/rpc_additional_edge_cases/plan_update_with_empty_content_then_read_returns_empty.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml b/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml +++ b/test/snapshots/rpc_additional_edge_cases/shell_exec_with_zero_timeout_does_not_kill_long_running_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml b/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml +++ b/test/snapshots/rpc_additional_edge_cases/usage_get_metrics_on_fresh_session_returns_zero_tokens.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_empty_content_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_large_content_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_create_file_with_unicode_content_round_trips.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_createfile_then_listfiles_returns_sorted_or_stable_order.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml b/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml +++ b/test/snapshots/rpc_additional_edge_cases/workspaces_getworkspace_returns_stable_result_across_calls.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_call_agent_reload.yaml b/test/snapshots/rpc_agents/should_call_agent_reload.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_call_agent_reload.yaml +++ b/test/snapshots/rpc_agents/should_call_agent_reload.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_deselect_current_agent.yaml b/test/snapshots/rpc_agents/should_deselect_current_agent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_deselect_current_agent.yaml +++ b/test/snapshots/rpc_agents/should_deselect_current_agent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml b/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml +++ b/test/snapshots/rpc_agents/should_emit_subagent_selected_and_deselected_events.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml b/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml +++ b/test/snapshots/rpc_agents/should_list_available_custom_agents.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml b/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml +++ b/test/snapshots/rpc_agents/should_return_empty_list_when_no_custom_agents_configured.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml b/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml +++ b/test/snapshots/rpc_agents/should_return_null_when_no_agent_is_selected.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml b/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml +++ b/test/snapshots/rpc_agents/should_select_and_get_current_agent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml b/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml index 7c58a8da96..200cc28dba 100644 --- a/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml +++ b/test/snapshots/rpc_event_side_effects/should_allow_session_use_after_truncate.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml b/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_mode_changed_event_when_mode_set.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_event_for_update_and_delete.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_plan_changed_update_operation_on_second_update.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml b/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml index 7c58a8da96..200cc28dba 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_snapshot_rewind_event_and_remove_events_on_truncate.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml b/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_title_changed_event_when_name_set.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml b/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml +++ b/test/snapshots/rpc_event_side_effects/should_emit_workspace_file_changed_event_when_file_created.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_and_toggle_session_skills.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_extensions.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_mcp_servers_with_configured_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml b/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_list_plugins.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml b/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_reload_session_skills.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_extensions_are_not_available.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_host_is_not_initialized.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_configured.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml +++ b/test/snapshots/rpc_mcp_and_skills/should_report_error_when_mcp_oauth_server_is_not_remote.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml b/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml +++ b/test/snapshots/rpc_mcp_config/should_call_server_mcp_config_rpcs.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml b/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml +++ b/test/snapshots/rpc_mcp_config/should_round_trip_http_mcp_oauth_config_rpc.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_configure_github_mcp_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_list_tools_and_report_running_status_for_connected_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml b/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_reload_mcp_servers_with_config.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_start_and_restart_mcp_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_stop_running_mcp_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml b/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml +++ b/test/snapshots/rpc_mcp_lifecycle/should_throw_when_listing_tools_for_unconnected_server.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml b/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_account_get_quota_when_authenticated.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml b/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_models_list_with_typed_result.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml b/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_ping_with_typed_params_and_result.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml b/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml +++ b/test/snapshots/rpc_server/should_call_rpc_tools_list_with_typed_result.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml b/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml +++ b/test/snapshots/rpc_server/should_discover_server_mcp_and_skills.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml b/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml +++ b/test/snapshots/rpc_server_misc/should_get_set_and_clear_user_settings.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml b/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml +++ b/test/snapshots/rpc_server_misc/should_login_list_getcurrentauth_and_logout_account.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml b/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml +++ b/test/snapshots/rpc_server_misc/should_reject_send_attachments_from_non_extension_connection.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml b/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml +++ b/test/snapshots/rpc_server_misc/should_reload_user_settings.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml b/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml +++ b/test/snapshots/rpc_server_misc/should_report_agent_registry_spawn_gate_closed.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml b/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml +++ b/test/snapshots/rpc_server_misc/should_report_not_found_when_opening_session_without_context.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml b/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml +++ b/test/snapshots/rpc_server_misc/should_shut_down_owned_runtime.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml b/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml +++ b/test/snapshots/rpc_server_plugins/should_enable_and_disable_marketplace_plugin.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml b/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml +++ b/test/snapshots/rpc_server_plugins/should_install_direct_local_plugin_with_deprecation_warning.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml b/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml +++ b/test/snapshots/rpc_server_plugins/should_install_list_and_uninstall_plugin_from_local_marketplace.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml b/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml +++ b/test/snapshots/rpc_server_plugins/should_list_browse_refresh_and_remove_local_marketplace.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml b/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml +++ b/test/snapshots/rpc_server_plugins/should_reload_mcp_config_cache.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml b/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml +++ b/test/snapshots/rpc_server_plugins/should_update_all_installed_plugins.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml b/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml +++ b/test/snapshots/rpc_server_plugins/should_update_single_marketplace_plugin.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml b/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml +++ b/test/snapshots/rpc_server_remote_control/should_reach_runtime_when_starting_remote_control_for_unknown_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml b/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml +++ b/test/snapshots/rpc_server_remote_control/should_reject_transfer_when_off_with_compare_and_swap.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml b/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml +++ b/test/snapshots/rpc_server_remote_control/should_report_not_stopped_when_remote_control_is_off.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml b/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml +++ b/test/snapshots/rpc_server_remote_control/should_report_remote_control_status_as_off.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml b/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml +++ b/test/snapshots/rpc_server_remote_control/should_treat_set_steering_as_no_op_when_off.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml b/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml +++ b/test/snapshots/rpc_session_state/should_call_session_rpc_model_getcurrent.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml b/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml index b276b6a398..9011c931e7 100644 --- a/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml +++ b/test/snapshots/rpc_session_state/should_call_session_rpc_model_switchto.yaml @@ -1,4 +1,4 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 - gpt-5.4 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml b/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml +++ b/test/snapshots/rpc_session_state/should_call_session_usage_and_permission_rpcs.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml b/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml +++ b/test/snapshots/rpc_session_state/should_call_workspace_file_rpc_methods.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml b/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml index 001e828461..ad474d2472 100644 --- a/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml +++ b/test/snapshots/rpc_session_state/should_compact_session_history_after_messages.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml b/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml +++ b/test/snapshots/rpc_session_state/should_create_workspace_file_with_nested_path_auto_creating_dirs.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml b/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml +++ b/test/snapshots/rpc_session_state/should_emit_title_changed_event_each_time_name_set_is_called.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml b/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml index 76ba212c5e..c1223c8a20 100644 --- a/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml +++ b/test/snapshots/rpc_session_state/should_fork_session_to_event_id_excluding_boundary_event.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml b/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml index 2313bd1483..f6fc12e544 100644 --- a/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml +++ b/test/snapshots/rpc_session_state/should_fork_session_with_persisted_messages.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml b/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml +++ b/test/snapshots/rpc_session_state/should_get_and_set_session_metadata.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml b/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml +++ b/test/snapshots/rpc_session_state/should_get_and_set_session_mode.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml b/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml +++ b/test/snapshots/rpc_session_state/should_handle_forking_session_without_persisted_events.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml b/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml +++ b/test/snapshots/rpc_session_state/should_read_update_and_delete_plan.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml b/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml +++ b/test/snapshots/rpc_session_state/should_reject_empty_or_whitespace_session_name.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml b/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml +++ b/test/snapshots/rpc_session_state/should_reject_workspace_file_path_traversal.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml b/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml +++ b/test/snapshots/rpc_session_state/should_report_error_reading_nonexistent_workspace_file.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml b/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml index 788c5b75f2..e59f571b4a 100644 --- a/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml +++ b/test/snapshots/rpc_session_state/should_report_error_when_forking_session_to_unknown_event_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml b/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml +++ b/test/snapshots/rpc_session_state/should_report_implemented_errors_for_unsupported_session_rpc_paths.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml b/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml index d79fd22384..6ea625da4d 100644 --- a/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml +++ b/test/snapshots/rpc_session_state/should_report_processing_and_context_metadata.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml b/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml +++ b/test/snapshots/rpc_session_state/should_set_and_get_each_session_mode_value.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml b/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml +++ b/test/snapshots/rpc_session_state/should_update_existing_workspace_file_with_update_operation.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml b/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml +++ b/test/snapshots/rpc_session_state_extras/should_add_byok_provider_and_model_at_runtime.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml b/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_and_set_allowall_permissions.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml b/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml index c4798dc83d..24c4d6bbf2 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_context_attribution_and_heaviest_messages_after_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml b/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml index 73f0499002..33bb479064 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_current_tool_metadata_after_initialization.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml b/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml +++ b/test/snapshots/rpc_session_state_extras/should_get_telemetry_engagement_id.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml b/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml +++ b/test/snapshots/rpc_session_state_extras/should_list_models_for_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml b/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml +++ b/test/snapshots/rpc_session_state_extras/should_read_empty_sql_todos_for_fresh_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml b/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml +++ b/test/snapshots/rpc_session_state_extras/should_reload_session_plugins.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml b/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml +++ b/test/snapshots/rpc_session_state_extras/should_report_session_activity_when_idle.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml b/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml +++ b/test/snapshots/rpc_session_state_extras/should_report_visibility_as_unsynced_for_local_session.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml b/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml +++ b/test/snapshots/rpc_session_state_extras/should_return_empty_completions_when_host_does_not_provide_them.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml b/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml +++ b/test/snapshots/rpc_session_state_extras/should_update_and_clear_live_subagent_settings.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml b/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml +++ b/test/snapshots/rpc_shell_and_fleet/should_execute_shell_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml b/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml +++ b/test/snapshots/rpc_shell_and_fleet/should_kill_shell_process.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml b/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml index 65ced1e366..d2d8a272c1 100644 --- a/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml +++ b/test/snapshots/rpc_shell_and_fleet/should_start_fleet_and_complete_custom_tool_task.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_custom_cwd_honors_override.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_large_stdout_cleans_up.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_nonexistent_command_returns_processid_and_cleans_up.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_stderr_output_cleans_up.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_exec_with_timeout_kills_long_running_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml b/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_kill_cleans_up_after_terminating_signal.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml b/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml +++ b/test/snapshots/rpc_shell_edge_cases/shell_kill_unknown_processid_returns_false.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml b/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml +++ b/test/snapshots/rpc_shell_user_requested/should_cancel_user_requested_shell_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml b/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml +++ b/test/snapshots/rpc_shell_user_requested/should_execute_user_requested_shell_command.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml b/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_list_task_state_and_return_false_for_missing_task_operations.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_invalid_task_agent_model.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_report_implemented_error_for_missing_task_agent_type.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml b/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_return_expected_results_for_missing_pending_handler_requestids.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml b/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml index 41bbe583d2..a9dbdd3751 100644 --- a/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml +++ b/test/snapshots/rpc_tasks_and_handlers/should_start_background_agent_and_report_task_details.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml b/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml index fec44be1fe..c5c1ce1c11 100644 --- a/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml +++ b/test/snapshots/rpc_ui_ephemeral_query/should_answer_ephemeral_query.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml b/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml index 20eefc57a9..8bfc5f5053 100644 --- a/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml +++ b/test/snapshots/rust_multi_client/both_clients_see_tool_request_and_completion_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml b/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml index 192105ac7a..476d5aeb1c 100644 --- a/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml +++ b/test/snapshots/rust_multi_client/disconnecting_client_removes_its_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml b/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml index c97e969df6..57bc48df32 100644 --- a/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml +++ b/test/snapshots/rust_multi_client/two_clients_register_different_tools_and_agent_uses_both.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/sandbox_bypass/approves_a_blocked_search_and_executes_it_outside_the_sandbox.yaml b/test/snapshots/sandbox_bypass/approves_a_blocked_search_and_executes_it_outside_the_sandbox.yaml new file mode 100644 index 0000000000..8098f1498c --- /dev/null +++ b/test/snapshots/sandbox_bypass/approves_a_blocked_search_and_executes_it_outside_the_sandbox.yaml @@ -0,0 +1,32 @@ +models: + - claude-sonnet-5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Search for OUTSIDE_MATCH_LINE in the vault directory. After the search succeeds, reply with exactly SANDBOX_BYPASS_APPROVED. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: grep + arguments: '{"pattern":"OUTSIDE_MATCH_LINE","path":"${workdir}/vault","output_mode":"content","-n":true}' + - messages: + - role: system + content: ${system} + - role: user + content: Search for OUTSIDE_MATCH_LINE in the vault directory. After the search succeeds, reply with exactly SANDBOX_BYPASS_APPROVED. + - role: assistant + tool_calls: + - id: toolcall_0 + type: function + function: + name: grep + arguments: '{"pattern":"OUTSIDE_MATCH_LINE","path":"${workdir}/vault","output_mode":"content","-n":true}' + - role: tool + tool_call_id: toolcall_0 + content: '${workdir}/vault/notes.txt:1:OUTSIDE_MATCH_LINE bypass-approved' + - role: assistant + content: SANDBOX_BYPASS_APPROVED diff --git a/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml b/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml +++ b/test/snapshots/session/disposeasync_from_handler_does_not_deadlock.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml b/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml +++ b/test/snapshots/session/handler_exception_does_not_halt_event_delivery.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml b/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml +++ b/test/snapshots/session/resumes_a_persisted_session_from_a_new_client_when_an_mcp_oauth_handler_is_configured.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml b/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml index c2e705ed2d..4ef08ab255 100644 --- a/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml +++ b/test/snapshots/session/send_returns_immediately_while_events_stream_in_background.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml b/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml +++ b/test/snapshots/session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/sendandwait_throws_on_timeout.yaml b/test/snapshots/session/sendandwait_throws_on_timeout.yaml index 0e019bdad7..fc736fe650 100644 --- a/test/snapshots/session/sendandwait_throws_on_timeout.yaml +++ b/test/snapshots/session/sendandwait_throws_on_timeout.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml b/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml index a03140fa17..7e1a256daa 100644 --- a/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml +++ b/test/snapshots/session/sendandwait_throws_operationcanceledexception_when_token_cancelled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_abort_a_session.yaml b/test/snapshots/session/should_abort_a_session.yaml index dbbbd32aa7..4ec09b51f3 100644 --- a/test/snapshots/session/should_abort_a_session.yaml +++ b/test/snapshots/session/should_abort_a_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_accept_blob_attachments.yaml b/test/snapshots/session/should_accept_blob_attachments.yaml index 1cca7142db..4caf7c8707 100644 --- a/test/snapshots/session/should_accept_blob_attachments.yaml +++ b/test/snapshots/session/should_accept_blob_attachments.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml index 3bf4a39f05..a0a9f1c391 100644 --- a/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml +++ b/test/snapshots/session/should_create_a_session_with_appended_systemmessage_config.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_availabletools.yaml b/test/snapshots/session/should_create_a_session_with_availabletools.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_a_session_with_availabletools.yaml +++ b/test/snapshots/session/should_create_a_session_with_availabletools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml index f3ce077a62..50d9cbecfa 100644 --- a/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml +++ b/test/snapshots/session/should_create_a_session_with_customized_systemmessage_config.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -8,8 +8,8 @@ conversations: content: Who are you? - role: assistant content: >- - I'm **GitHub Copilot CLI**, a terminal assistant built by GitHub. I'm powered by claude-sonnet-4.5 (model ID: - claude-sonnet-4.5). + I'm **GitHub Copilot CLI**, a terminal assistant built by GitHub. I'm powered by claude-sonnet-5 (model ID: + claude-sonnet-5). I'm here to help you with software engineering tasks, including: diff --git a/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml b/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml +++ b/test/snapshots/session/should_create_a_session_with_defaultagent_excludedtools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_excludedtools.yaml b/test/snapshots/session/should_create_a_session_with_excludedtools.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_a_session_with_excludedtools.yaml +++ b/test/snapshots/session/should_create_a_session_with_excludedtools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml b/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml index ebe0881b90..88d25dc817 100644 --- a/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml +++ b/test/snapshots/session/should_create_a_session_with_replaced_systemmessage_config.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_session_with_custom_config_dir.yaml b/test/snapshots/session/should_create_session_with_custom_config_dir.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session/should_create_session_with_custom_config_dir.yaml +++ b/test/snapshots/session/should_create_session_with_custom_config_dir.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_create_session_with_custom_tool.yaml b/test/snapshots/session/should_create_session_with_custom_tool.yaml index 4ae6dab721..3a08cbd9fe 100644 --- a/test/snapshots/session/should_create_session_with_custom_tool.yaml +++ b/test/snapshots/session/should_create_session_with_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_delete_session.yaml b/test/snapshots/session/should_delete_session.yaml index fb8249d325..72176060ce 100644 --- a/test/snapshots/session/should_delete_session.yaml +++ b/test/snapshots/session/should_delete_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_get_last_session_id.yaml b/test/snapshots/session/should_get_last_session_id.yaml index 3b9da534c2..62da2b03a2 100644 --- a/test/snapshots/session/should_get_last_session_id.yaml +++ b/test/snapshots/session/should_get_last_session_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_get_session_metadata.yaml b/test/snapshots/session/should_get_session_metadata.yaml index b326528e1d..f9b44b833a 100644 --- a/test/snapshots/session/should_get_session_metadata.yaml +++ b/test/snapshots/session/should_get_session_metadata.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_get_session_metadata_by_id.yaml b/test/snapshots/session/should_get_session_metadata_by_id.yaml index b326528e1d..f9b44b833a 100644 --- a/test/snapshots/session/should_get_session_metadata_by_id.yaml +++ b/test/snapshots/session/should_get_session_metadata_by_id.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_have_stateful_conversation.yaml b/test/snapshots/session/should_have_stateful_conversation.yaml index 39d3c5acc5..1309304fc4 100644 --- a/test/snapshots/session/should_have_stateful_conversation.yaml +++ b/test/snapshots/session/should_have_stateful_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_list_sessions.yaml b/test/snapshots/session/should_list_sessions.yaml index 4683506570..d2c3ebdd4f 100644 --- a/test/snapshots/session/should_list_sessions.yaml +++ b/test/snapshots/session/should_list_sessions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_list_sessions_with_context.yaml b/test/snapshots/session/should_list_sessions_with_context.yaml index 8486832a46..8e3aa9d94c 100644 --- a/test/snapshots/session/should_list_sessions_with_context.yaml +++ b/test/snapshots/session/should_list_sessions_with_context.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_log_messages_at_various_levels.yaml b/test/snapshots/session/should_log_messages_at_various_levels.yaml index 0e019bdad7..fc736fe650 100644 --- a/test/snapshots/session/should_log_messages_at_various_levels.yaml +++ b/test/snapshots/session/should_log_messages_at_various_levels.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_receive_session_events.yaml b/test/snapshots/session/should_receive_session_events.yaml index 229563a4cf..406c34f455 100644 --- a/test/snapshots/session/should_receive_session_events.yaml +++ b/test/snapshots/session/should_receive_session_events.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_recover_marker_after_cold_resume_with_explicit_session_id.yaml b/test/snapshots/session/should_recover_marker_after_cold_resume_with_explicit_session_id.yaml new file mode 100644 index 0000000000..f862aa80eb --- /dev/null +++ b/test/snapshots/session/should_recover_marker_after_cold_resume_with_explicit_session_id.yaml @@ -0,0 +1,16 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Please remember this exact secret marker for later - MARKER-7f3ac21e. Reply with only the single word + "Acknowledged". + - role: assistant + content: Acknowledged + - role: user + content: What was the exact secret marker I asked you to remember earlier? Reply with only that marker value and nothing + else. + - role: assistant + content: MARKER-7f3ac21e diff --git a/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml b/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml index bd02858372..667d0c3528 100644 --- a/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml +++ b/test/snapshots/session/should_resume_a_session_using_a_new_client.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml b/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml index b012e26ea8..ff54ee73da 100644 --- a/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml +++ b/test/snapshots/session/should_resume_a_session_using_the_same_client.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_custom_requestheaders.yaml b/test/snapshots/session/should_send_with_custom_requestheaders.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session/should_send_with_custom_requestheaders.yaml +++ b/test/snapshots/session/should_send_with_custom_requestheaders.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_directory_attachment.yaml b/test/snapshots/session/should_send_with_directory_attachment.yaml index aa410c9295..f2e8835c5d 100644 --- a/test/snapshots/session/should_send_with_directory_attachment.yaml +++ b/test/snapshots/session/should_send_with_directory_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_file_attachment.yaml b/test/snapshots/session/should_send_with_file_attachment.yaml index 2e8e4d1d2d..f2acea13fb 100644 --- a/test/snapshots/session/should_send_with_file_attachment.yaml +++ b/test/snapshots/session/should_send_with_file_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_github_reference_attachment.yaml b/test/snapshots/session/should_send_with_github_reference_attachment.yaml index 6e298de554..851f34c50f 100644 --- a/test/snapshots/session/should_send_with_github_reference_attachment.yaml +++ b/test/snapshots/session/should_send_with_github_reference_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_mode_property.yaml b/test/snapshots/session/should_send_with_mode_property.yaml index 4fec86c7f4..e6639acbdb 100644 --- a/test/snapshots/session/should_send_with_mode_property.yaml +++ b/test/snapshots/session/should_send_with_mode_property.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_send_with_selection_attachment.yaml b/test/snapshots/session/should_send_with_selection_attachment.yaml index ad6a2a28e8..c46f3d679b 100644 --- a/test/snapshots/session/should_send_with_selection_attachment.yaml +++ b/test/snapshots/session/should_send_with_selection_attachment.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_set_model_on_existing_session.yaml b/test/snapshots/session/should_set_model_on_existing_session.yaml index 0e019bdad7..fc736fe650 100644 --- a/test/snapshots/session/should_set_model_on_existing_session.yaml +++ b/test/snapshots/session/should_set_model_on_existing_session.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session/should_set_model_with_reasoningeffort.yaml b/test/snapshots/session/should_set_model_with_reasoningeffort.yaml index ccf204d2ae..db151e253a 100644 --- a/test/snapshots/session/should_set_model_with_reasoningeffort.yaml +++ b/test/snapshots/session/should_set_model_with_reasoningeffort.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 - gpt-5.4 conversations: - messages: diff --git a/test/snapshots/session_config/should_accept_blob_attachments.yaml b/test/snapshots/session_config/should_accept_blob_attachments.yaml index 672ca74d4e..71c7900348 100644 --- a/test/snapshots/session_config/should_accept_blob_attachments.yaml +++ b/test/snapshots/session_config/should_accept_blob_attachments.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_accept_message_attachments.yaml b/test/snapshots/session_config/should_accept_message_attachments.yaml index 5525d1fb04..188905752b 100644 --- a/test/snapshots/session_config/should_accept_message_attachments.yaml +++ b/test/snapshots/session_config/should_accept_message_attachments.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml b/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml +++ b/test/snapshots/session_config/should_apply_all_reasoning_effort_values_on_session_create.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml b/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_availabletools_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml index 3cbf86e981..4ef99a4d36 100644 --- a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml +++ b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_excluded_built_in_agents_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml b/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml +++ b/test/snapshots/session_config/should_apply_github_mcp_tool_config_on_create.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml b/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml +++ b/test/snapshots/session_config/should_apply_instruction_directories_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml b/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_instruction_directories_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml index 7c4d469970..dfead5f966 100644 --- a/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_instructiondirectories_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml b/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml +++ b/test/snapshots/session_config/should_apply_reasoning_effort_on_session_create.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_apply_session_limits_on_create.yaml b/test/snapshots/session_config/should_apply_session_limits_on_create.yaml index 904d69c872..142c4f6229 100644 --- a/test/snapshots/session_config/should_apply_session_limits_on_create.yaml +++ b/test/snapshots/session_config/should_apply_session_limits_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml b/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml index 904d69c872..142c4f6229 100644 --- a/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml +++ b/test/snapshots/session_config/should_apply_session_limits_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml b/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml index a455f6f6f3..a5111fd56a 100644 --- a/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_systemmessage_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml b/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml index 9d3dd78ff1..ec7d207e2e 100644 --- a/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml +++ b/test/snapshots/session_config/should_apply_workingdirectory_on_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml b/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml +++ b/test/snapshots/session_config/should_create_session_with_custom_provider_config.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml b/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml index f9918fa133..fe4181ccd2 100644 --- a/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml +++ b/test/snapshots/session_config/should_forward_clientname_in_user_agent.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml b/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml +++ b/test/snapshots/session_config/should_forward_clientname_in_useragent.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml b/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml +++ b/test/snapshots/session_config/should_forward_custom_provider_headers_on_create.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml b/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml index 9fe2fcd070..7cd9232226 100644 --- a/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml +++ b/test/snapshots/session_config/should_forward_custom_provider_headers_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_use_custom_session_id.yaml b/test/snapshots/session_config/should_use_custom_session_id.yaml index 056351ddb4..0c6b353c19 100644 --- a/test/snapshots/session_config/should_use_custom_session_id.yaml +++ b/test/snapshots/session_config/should_use_custom_session_id.yaml @@ -1,3 +1,3 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: [] diff --git a/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml b/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml index 250402101b..c8aa45cfff 100644 --- a/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml +++ b/test/snapshots/session_config/should_use_provider_model_id_as_wire_model.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml b/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml index 40000d491b..dd2b4592b3 100644 --- a/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml +++ b/test/snapshots/session_config/should_use_workingdirectory_for_tool_execution.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml b/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml index 1eb0acd729..a4000a80e6 100644 --- a/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml +++ b/test/snapshots/session_config/vision_disabled_then_enabled_via_setmodel.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml b/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml index b987a4e630..6afc03cb05 100644 --- a/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml +++ b/test/snapshots/session_config/vision_enabled_then_disabled_via_setmodel.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml b/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml index 4744667cd7..2d2d1e38ae 100644 --- a/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml +++ b/test/snapshots/session_fs/should_load_session_data_from_fs_provider_on_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml b/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml index e80ce51e64..9b8f1eb0d9 100644 --- a/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml +++ b/test/snapshots/session_fs/should_map_large_output_handling_into_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml b/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml index 5b0e81b22d..90ceee231a 100644 --- a/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml +++ b/test/snapshots/session_fs/should_persist_plan_md_via_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml b/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml index 269a80f11a..9aba0fefe1 100644 --- a/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml +++ b/test/snapshots/session_fs/should_reject_setprovider_when_sessions_already_exist.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml b/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml index 455652bfd8..086c69225d 100644 --- a/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml +++ b/test/snapshots/session_fs/should_route_file_operations_through_the_session_fs_provider.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml b/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml index 5e62634668..67f8e0121e 100644 --- a/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml +++ b/test/snapshots/session_fs/should_succeed_with_compaction_while_using_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml b/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml index 0a0325417c..59ed4624c7 100644 --- a/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml +++ b/test/snapshots/session_fs/should_write_workspace_metadata_via_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml b/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml index 32b8bc354f..b2b50348f5 100644 --- a/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml +++ b/test/snapshots/session_fs_sqlite/should_allow_subagents_to_use_sql_tool_via_inherited_sessionfs.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml b/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml index b26cfbfebe..e51bc5d247 100644 --- a/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml +++ b/test/snapshots/session_fs_sqlite/should_route_sql_queries_through_the_sessionfs_sqlite_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml b/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml index b302f117d4..6e38f93031 100644 --- a/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml +++ b/test/snapshots/session_lifecycle/should_delete_session_permanently.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml b/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml index f4add013a1..47bd9bc305 100644 --- a/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml +++ b/test/snapshots/session_lifecycle/should_isolate_events_between_concurrent_sessions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml b/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml index 274ab7d2fa..f39bcb95cd 100644 --- a/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml +++ b/test/snapshots/session_lifecycle/should_list_created_sessions_after_sending_a_message.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml b/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml index fd621f2b0b..005604fdd6 100644 --- a/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml +++ b/test/snapshots/session_lifecycle/should_return_events_via_getmessages_after_conversation.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml b/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml index fdb7ebca03..180fb52406 100644 --- a/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml +++ b/test/snapshots/session_lifecycle/should_support_multiple_concurrent_sessions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml b/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml index 425c772fce..0a4b41c9f2 100644 --- a/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml +++ b/test/snapshots/session_todos_changed/fires_session_todos_changed_and_exposes_rows_and_dependencies.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml b/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml index 007c5c1c53..fcecde4286 100644 --- a/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml +++ b/test/snapshots/skills/should_allow_agent_with_skills_to_invoke_skill.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml b/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml index 38b35946b0..ee9e6452d2 100644 --- a/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml +++ b/test/snapshots/skills/should_load_and_apply_skill_from_skilldirectories.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml b/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml index 0c678deab8..1cc5776a34 100644 --- a/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml +++ b/test/snapshots/skills/should_not_apply_skill_when_disabled_via_disabledskills.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml b/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml index 0c678deab8..1cc5776a34 100644 --- a/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml +++ b/test/snapshots/skills/should_not_provide_skills_to_agent_without_skills_field.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml b/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml index 3b18558220..c6d74510f4 100644 --- a/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml +++ b/test/snapshots/streaming_fidelity/should_emit_assistantmessagestart_before_deltas_with_matching_messageid.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml b/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml index 25e10c4b12..ca514e0135 100644 --- a/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml +++ b/test/snapshots/streaming_fidelity/should_not_produce_deltas_after_session_resume_with_streaming_disabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml b/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml index d210f22ea0..c9c9d1200f 100644 --- a/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml +++ b/test/snapshots/streaming_fidelity/should_not_produce_deltas_when_streaming_is_disabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml b/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml index 3b18558220..c6d74510f4 100644 --- a/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml +++ b/test/snapshots/streaming_fidelity/should_produce_delta_events_when_streaming_is_enabled.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml b/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml index 25e10c4b12..ca514e0135 100644 --- a/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml +++ b/test/snapshots/streaming_fidelity/should_produce_deltas_after_session_resume.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml b/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml index f920f8705b..4c5419a0ab 100644 --- a/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml +++ b/test/snapshots/subagent_hooks/should_invoke_pretooluse_and_posttooluse_hooks_for_sub_agent_tool_calls.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml b/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml index c033a6cba1..5230710868 100644 --- a/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml +++ b/test/snapshots/suspend/should_allow_resume_and_continue_conversation_after_suspend.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml b/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml index 97939357ca..54e64b3edb 100644 --- a/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml +++ b/test/snapshots/suspend/should_cancel_pending_permission_request_when_suspending.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml b/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml index 32e07aa5d5..3bf332978d 100644 --- a/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml +++ b/test/snapshots/suspend/should_reject_pending_external_tool_when_suspending.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml b/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml index a3a35bf25b..d853722e5b 100644 --- a/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml +++ b/test/snapshots/suspend/should_suspend_idle_session_without_throwing.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml b/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml index 5721955d27..e53acdf159 100644 --- a/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml +++ b/test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml b/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml index 9d2c688c18..f73112abb5 100644 --- a/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml +++ b/test/snapshots/system_message_sections/should_use_replaced_preamble_section_in_response.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system @@ -13,5 +13,5 @@ conversations: watering, pests, plant identification, or growing tips, I'm here to help! - I'm powered by claude-sonnet-4.5, but I focus specifically on gardening topics. What plant or gardening + I'm powered by claude-sonnet-5, but I focus specifically on gardening topics. What plant or gardening question can I help you with today? diff --git a/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml b/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml index 98e57919c6..4db03cac93 100644 --- a/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml +++ b/test/snapshots/system_message_transform/should_apply_transform_modifications_to_section_content.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml b/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml index c54f25e2aa..93b7221aa3 100644 --- a/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml +++ b/test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml b/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml index 32d6367390..94c50ff0bd 100644 --- a/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml +++ b/test/snapshots/system_message_transform/should_work_with_static_overrides_and_transforms_together.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml b/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml index f8342047b7..c919c8d0b1 100644 --- a/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml +++ b/test/snapshots/telemetry/should_export_file_telemetry_for_sdk_interactions.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml b/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml index 621dfc4e8d..0992eae919 100644 --- a/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml +++ b/test/snapshots/tool_results/should_handle_structured_toolresultobject_from_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml b/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml index 6a829fb23f..3658586343 100644 --- a/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml +++ b/test/snapshots/tool_results/should_handle_tool_result_with_denied_resulttype.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml b/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml index 3fddb1600b..b328558471 100644 --- a/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml +++ b/test/snapshots/tool_results/should_handle_tool_result_with_failure_resulttype.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml b/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml index 891f75cb55..d329be915e 100644 --- a/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml +++ b/test/snapshots/tool_results/should_handle_tool_result_with_rejected_resulttype.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml b/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml index 2388d7d8fd..6c8ee43bb9 100644 --- a/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml +++ b/test/snapshots/tool_results/should_pass_validated_zod_parameters_to_tool_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml b/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml index 71021d3b8d..bc65d05d6b 100644 --- a/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml +++ b/test/snapshots/tool_results/should_preserve_tooltelemetry_and_not_stringify_structured_results_for_llm.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/can_receive_and_return_complex_types.yaml b/test/snapshots/tools/can_receive_and_return_complex_types.yaml index be869484ea..982f66e3ad 100644 --- a/test/snapshots/tools/can_receive_and_return_complex_types.yaml +++ b/test/snapshots/tools/can_receive_and_return_complex_types.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml b/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml index 36d5adce4a..39d2bed2f0 100644 --- a/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml +++ b/test/snapshots/tools/clears_context_from_a_terminal_tool_and_starts_the_seeded_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml b/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml index 47f9286e0c..59acd092aa 100644 --- a/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml +++ b/test/snapshots/tools/denies_custom_tool_when_permission_denied.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/ergonomic_tool_arity0.yaml b/test/snapshots/tools/ergonomic_tool_arity0.yaml index a55f486816..16bbbf1cd6 100644 --- a/test/snapshots/tools/ergonomic_tool_arity0.yaml +++ b/test/snapshots/tools/ergonomic_tool_arity0.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/ergonomic_tool_arity2.yaml b/test/snapshots/tools/ergonomic_tool_arity2.yaml index e34c695bd4..b6cdd07614 100644 --- a/test/snapshots/tools/ergonomic_tool_arity2.yaml +++ b/test/snapshots/tools/ergonomic_tool_arity2.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/ergonomic_tool_definition.yaml b/test/snapshots/tools/ergonomic_tool_definition.yaml index ebb05ce1b9..e61e95f930 100644 --- a/test/snapshots/tools/ergonomic_tool_definition.yaml +++ b/test/snapshots/tools/ergonomic_tool_definition.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/handles_tool_calling_errors.yaml b/test/snapshots/tools/handles_tool_calling_errors.yaml index 33226722dd..3b85da9acd 100644 --- a/test/snapshots/tools/handles_tool_calling_errors.yaml +++ b/test/snapshots/tools/handles_tool_calling_errors.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/invokes_built_in_tools.yaml b/test/snapshots/tools/invokes_built_in_tools.yaml index 0fba134424..72d959be46 100644 --- a/test/snapshots/tools/invokes_built_in_tools.yaml +++ b/test/snapshots/tools/invokes_built_in_tools.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/invokes_custom_tool.yaml b/test/snapshots/tools/invokes_custom_tool.yaml index 6f212e4a79..6362a5e4f7 100644 --- a/test/snapshots/tools/invokes_custom_tool.yaml +++ b/test/snapshots/tools/invokes_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml b/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml index fcb6fa7266..2cfccba989 100644 --- a/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml +++ b/test/snapshots/tools/invokes_custom_tool_with_permission_handler.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/low_level_tool_definition.yaml b/test/snapshots/tools/low_level_tool_definition.yaml index 03cb0748a2..5921d061cb 100644 --- a/test/snapshots/tools/low_level_tool_definition.yaml +++ b/test/snapshots/tools/low_level_tool_definition.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml b/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml index 5410d3f295..ec2fa0a9a7 100644 --- a/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml +++ b/test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml b/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml index a9aae3aea5..949ffd18fa 100644 --- a/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml +++ b/test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml b/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml index cf0cf564da..50494f2324 100644 --- a/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml +++ b/test/snapshots/tools/should_respect_availabletools_and_excludedtools_combined.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system diff --git a/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml b/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml index dfdfa63fa7..fa7a5cfbe1 100644 --- a/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml +++ b/test/snapshots/tools/skippermission_sent_in_tool_definition.yaml @@ -1,5 +1,5 @@ models: - - claude-sonnet-4.5 + - claude-sonnet-5 conversations: - messages: - role: system